@neetru/cli 2.29.0 → 2.32.0
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/CHANGELOG.md +141 -0
- package/README.md +63 -17
- package/dist/cli-kit/choices.d.ts +33 -0
- package/dist/cli-kit/choices.js +32 -0
- package/dist/cli-kit/command-context.d.ts +98 -0
- package/dist/cli-kit/command-context.js +108 -0
- package/dist/cli-kit/errors.d.ts +79 -0
- package/dist/cli-kit/errors.js +94 -0
- package/dist/cli-kit/index.d.ts +8 -0
- package/dist/cli-kit/index.js +8 -0
- package/dist/cli-kit/suggest.d.ts +74 -0
- package/dist/cli-kit/suggest.js +207 -0
- package/dist/commands/admin/oidc-login.js +1 -1
- package/dist/commands/admin/sdk-credential.js +1 -1
- package/dist/commands/autocomplete.d.ts +1 -1
- package/dist/commands/autocomplete.js +1 -0
- package/dist/commands/bug.d.ts +59 -0
- package/dist/commands/bug.js +114 -2
- package/dist/commands/build.d.ts +24 -1
- package/dist/commands/build.js +195 -73
- package/dist/commands/chat.d.ts +94 -1
- package/dist/commands/chat.js +258 -26
- package/dist/commands/customers.js +1 -1
- package/dist/commands/db-move.js +3 -3
- package/dist/commands/db.js +17 -24
- package/dist/commands/deploy.d.ts +136 -2
- package/dist/commands/deploy.js +547 -69
- package/dist/commands/deployments.js +11 -6
- package/dist/commands/dev-context.js +15 -5
- package/dist/commands/dev-up.d.ts +7 -5
- package/dist/commands/dev-up.js +24 -23
- package/dist/commands/docs-produto.d.ts +90 -0
- package/dist/commands/docs-produto.js +384 -0
- package/dist/commands/doctor.js +36 -1
- package/dist/commands/domains.js +85 -4
- package/dist/commands/env.d.ts +9 -3
- package/dist/commands/env.js +23 -3
- package/dist/commands/infra-read.js +2 -2
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +14 -1
- package/dist/commands/login.js +35 -8
- package/dist/commands/logs.js +12 -4
- package/dist/commands/mobile.js +33 -9
- package/dist/commands/new.d.ts +31 -1
- package/dist/commands/new.js +94 -4
- package/dist/commands/open.js +16 -6
- package/dist/commands/ops.js +1 -1
- package/dist/commands/products-db.d.ts +1 -0
- package/dist/commands/products-db.js +41 -8
- package/dist/commands/products.d.ts +17 -3
- package/dist/commands/products.js +84 -16
- package/dist/commands/promote.js +30 -5
- package/dist/commands/secrets.d.ts +91 -0
- package/dist/commands/secrets.js +471 -0
- package/dist/commands/servers.js +6 -4
- package/dist/commands/status.js +2 -2
- package/dist/commands/support.js +5 -3
- package/dist/commands/tenants.js +28 -16
- package/dist/commands/upgrade.d.ts +9 -0
- package/dist/commands/upgrade.js +50 -1
- package/dist/commands/vm-memory.js +1 -0
- package/dist/commands/webhooks.d.ts +2 -0
- package/dist/commands/webhooks.js +9 -3
- package/dist/commands/workspaces.js +42 -11
- package/dist/index.js +142 -20
- package/dist/lib/api-client.d.ts +22 -1
- package/dist/lib/api-client.js +79 -8
- package/dist/lib/api-error.d.ts +14 -0
- package/dist/lib/api-error.js +107 -46
- package/dist/lib/build-targets.d.ts +127 -0
- package/dist/lib/build-targets.js +348 -0
- package/dist/lib/cli-error.d.ts +13 -0
- package/dist/lib/cli-error.js +24 -0
- package/dist/lib/cli-write.d.ts +43 -9
- package/dist/lib/cli-write.js +154 -45
- package/dist/lib/compose-generator.d.ts +13 -9
- package/dist/lib/compose-generator.js +28 -60
- package/dist/lib/config-schema.d.ts +36 -2
- package/dist/lib/config-schema.js +11 -0
- package/dist/lib/db-local/db-json.d.ts +1 -1
- package/dist/lib/db-local/db-json.js +13 -5
- package/dist/lib/db-local/env.d.ts +15 -3
- package/dist/lib/db-local/env.js +37 -10
- package/dist/lib/db-pipeline/build-deps.d.ts +5 -1
- package/dist/lib/db-pipeline/build-deps.js +23 -3
- package/dist/lib/db-pipeline/types.d.ts +6 -2
- package/dist/lib/db-pipeline/types.js +0 -18
- package/dist/lib/deploy-target-contract.d.ts +152 -0
- package/dist/lib/deploy-target-contract.js +411 -0
- package/dist/lib/env-file-do-build.d.ts +19 -0
- package/dist/lib/env-file-do-build.js +44 -0
- package/dist/lib/envs.d.ts +108 -0
- package/dist/lib/envs.js +127 -0
- package/dist/lib/fs-seguro.d.ts +63 -0
- package/dist/lib/fs-seguro.js +108 -0
- package/dist/lib/mobile-local/mobile-json.js +10 -4
- package/dist/lib/pickers.d.ts +44 -7
- package/dist/lib/pickers.js +60 -4
- package/dist/lib/product-docs/check.d.ts +62 -0
- package/dist/lib/product-docs/check.js +489 -0
- package/dist/lib/product-docs/config.d.ts +66 -0
- package/dist/lib/product-docs/config.js +234 -0
- package/dist/lib/product-docs/dates.d.ts +17 -0
- package/dist/lib/product-docs/dates.js +41 -0
- package/dist/lib/product-docs/frontmatter.d.ts +43 -0
- package/dist/lib/product-docs/frontmatter.js +302 -0
- package/dist/lib/product-docs/git.d.ts +16 -0
- package/dist/lib/product-docs/git.js +92 -0
- package/dist/lib/product-docs/markdown.d.ts +27 -0
- package/dist/lib/product-docs/markdown.js +98 -0
- package/dist/lib/product-docs/ops.d.ts +133 -0
- package/dist/lib/product-docs/ops.js +975 -0
- package/dist/lib/product-docs/structure.d.ts +61 -0
- package/dist/lib/product-docs/structure.js +239 -0
- package/dist/lib/product-docs/sync.d.ts +107 -0
- package/dist/lib/product-docs/sync.js +322 -0
- package/dist/lib/product-docs/taxonomy.d.ts +46 -0
- package/dist/lib/product-docs/taxonomy.js +163 -0
- package/dist/lib/product-docs/templates.d.ts +53 -0
- package/dist/lib/product-docs/templates.js +245 -0
- package/dist/lib/public-secret-scan.d.ts +117 -0
- package/dist/lib/public-secret-scan.js +771 -0
- package/dist/lib/render.d.ts +16 -0
- package/dist/lib/render.js +34 -4
- package/dist/lib/segredo-produto.d.ts +124 -0
- package/dist/lib/segredo-produto.js +336 -0
- package/dist/lib/texto-utf8.d.ts +96 -0
- package/dist/lib/texto-utf8.js +196 -0
- package/package.json +2 -1
- package/dist/cli-kit/format.js.map +0 -1
- package/dist/cli-kit/glyphs.js.map +0 -1
- package/dist/cli-kit/index.js.map +0 -1
- package/dist/cli-kit/palette.js.map +0 -1
- package/dist/commands/add.js.map +0 -1
- package/dist/commands/admin/iam-check.js.map +0 -1
- package/dist/commands/admin/oidc-login.js.map +0 -1
- package/dist/commands/admin/sdk-credential.js.map +0 -1
- package/dist/commands/agent-release.js.map +0 -1
- package/dist/commands/agent-write.js.map +0 -1
- package/dist/commands/ai-models.js.map +0 -1
- package/dist/commands/ai.js.map +0 -1
- package/dist/commands/api-catalog.js.map +0 -1
- package/dist/commands/archive.js.map +0 -1
- package/dist/commands/artifact-registry.js.map +0 -1
- package/dist/commands/audit.js.map +0 -1
- package/dist/commands/auth.js.map +0 -1
- package/dist/commands/autocomplete.js.map +0 -1
- package/dist/commands/billing.js.map +0 -1
- package/dist/commands/bootstrap.js.map +0 -1
- package/dist/commands/bug.js.map +0 -1
- package/dist/commands/build.js.map +0 -1
- package/dist/commands/chat-menu.js.map +0 -1
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/cloud-run.js.map +0 -1
- package/dist/commands/code.js.map +0 -1
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/customers.js.map +0 -1
- package/dist/commands/db-move.js.map +0 -1
- package/dist/commands/db.js.map +0 -1
- package/dist/commands/deploy.js.map +0 -1
- package/dist/commands/deployments.js.map +0 -1
- package/dist/commands/dev-context.js.map +0 -1
- package/dist/commands/dev-up.js.map +0 -1
- package/dist/commands/dev.js.map +0 -1
- package/dist/commands/docs.js.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/domains.js.map +0 -1
- package/dist/commands/dr.js.map +0 -1
- package/dist/commands/env.js.map +0 -1
- package/dist/commands/hosting-write.js.map +0 -1
- package/dist/commands/infra-read.js.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/logs.js.map +0 -1
- package/dist/commands/mobile.js.map +0 -1
- package/dist/commands/mocks.js.map +0 -1
- package/dist/commands/new.js.map +0 -1
- package/dist/commands/open.js.map +0 -1
- package/dist/commands/ops.js.map +0 -1
- package/dist/commands/products-db.js.map +0 -1
- package/dist/commands/products.js.map +0 -1
- package/dist/commands/promote.js.map +0 -1
- package/dist/commands/publish.js.map +0 -1
- package/dist/commands/schema.js.map +0 -1
- package/dist/commands/servers.js.map +0 -1
- package/dist/commands/smoke.js.map +0 -1
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/support.js.map +0 -1
- package/dist/commands/surface-status.js.map +0 -1
- package/dist/commands/tenants.js.map +0 -1
- package/dist/commands/ui.js.map +0 -1
- package/dist/commands/upgrade.js.map +0 -1
- package/dist/commands/validate.js.map +0 -1
- package/dist/commands/vm-memory.js.map +0 -1
- package/dist/commands/vm.js.map +0 -1
- package/dist/commands/webhooks.js.map +0 -1
- package/dist/commands/whoami.js.map +0 -1
- package/dist/commands/workspaces.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/agent-artifact-url.js.map +0 -1
- package/dist/lib/agent-commands.js.map +0 -1
- package/dist/lib/ai/auth-discovery.js.map +0 -1
- package/dist/lib/ai/context.js.map +0 -1
- package/dist/lib/ai/orchestrator.js.map +0 -1
- package/dist/lib/api-client.js.map +0 -1
- package/dist/lib/api-error.js.map +0 -1
- package/dist/lib/auth.js.map +0 -1
- package/dist/lib/cli-read.js.map +0 -1
- package/dist/lib/cli-write.js.map +0 -1
- package/dist/lib/code-agent-budget.js.map +0 -1
- package/dist/lib/code-agent-files.js.map +0 -1
- package/dist/lib/code-agent-gateway.js.map +0 -1
- package/dist/lib/code-agent-git-tools.js.map +0 -1
- package/dist/lib/code-agent-protocol.js.map +0 -1
- package/dist/lib/code-agent-session.js.map +0 -1
- package/dist/lib/code-agent-settings.js.map +0 -1
- package/dist/lib/code-agent-skills.js.map +0 -1
- package/dist/lib/code-agent-subagents.js.map +0 -1
- package/dist/lib/code-agent-tools.js.map +0 -1
- package/dist/lib/code-agent-tui.js.map +0 -1
- package/dist/lib/code-agent-types.js.map +0 -1
- package/dist/lib/code-agent-ui.js.map +0 -1
- package/dist/lib/code-agent-web-tools.js.map +0 -1
- package/dist/lib/code-agent-write-tools.js.map +0 -1
- package/dist/lib/compose-generator.js.map +0 -1
- package/dist/lib/config-schema.js.map +0 -1
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/db-local/db-json.js.map +0 -1
- package/dist/lib/db-local/env.js.map +0 -1
- package/dist/lib/db-local/fingerprint.js.map +0 -1
- package/dist/lib/db-local/index.js.map +0 -1
- package/dist/lib/db-pipeline/build-deps.js.map +0 -1
- package/dist/lib/db-pipeline/errors.js.map +0 -1
- package/dist/lib/db-pipeline/index.js.map +0 -1
- package/dist/lib/db-pipeline/pipeline.js.map +0 -1
- package/dist/lib/db-pipeline/rehearse.js.map +0 -1
- package/dist/lib/db-pipeline/types.js.map +0 -1
- package/dist/lib/dev-local-context.js.map +0 -1
- package/dist/lib/iam-preflight.js.map +0 -1
- package/dist/lib/mobile-local/index.js.map +0 -1
- package/dist/lib/mobile-local/mobile-json.js.map +0 -1
- package/dist/lib/pickers.js.map +0 -1
- package/dist/lib/product-config.js.map +0 -1
- package/dist/lib/render.js.map +0 -1
- package/dist/lib/schema-cache.js.map +0 -1
- package/dist/lib/sse.js.map +0 -1
- package/dist/lib/telemetry.js.map +0 -1
- package/dist/lib/token-resolver.js.map +0 -1
- package/dist/lib/url-allowlist.js.map +0 -1
- package/dist/lib/version-check.js.map +0 -1
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/logo.js.map +0 -1
- package/dist/utils/safe-exit.js.map +0 -1
- package/dist/utils/spinner-registry.js.map +0 -1
- package/dist/version.js.map +0 -1
- package/templates/dev/Dockerfile.agent +0 -39
- package/templates/dev/agent-entrypoint.sh +0 -41
- package/templates/dev/docker-compose.dev.yml +0 -63
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Varredura de segredo no pacote PÚBLICO antes do `neetru deploy` (2.30.0).
|
|
3
|
+
*
|
|
4
|
+
* Decisão do dono (2026-10-03): a credencial `nrt_` do produto é SÓ DE
|
|
5
|
+
* SERVIDOR. Hoje produtos carregam a chave no bundle do navegador
|
|
6
|
+
* (`NEXT_PUBLIC_NEETRU_API_KEY`, `NEXT_PUBLIC_NEETRU_PUBLIC_KEY`) — qualquer
|
|
7
|
+
* visitante abre o DevTools e copia. Este gate impede que um segredo saia no
|
|
8
|
+
* artefato servido ao público.
|
|
9
|
+
*
|
|
10
|
+
* O que é "público" (servido ao navegador): `.next/static/**`, `public/**`,
|
|
11
|
+
* `out/**`, `dist/**`, `build/**` — e, na stack `static`, o pacote inteiro.
|
|
12
|
+
* `node_modules/**` nunca entra (código de terceiros, não servido como asset).
|
|
13
|
+
*
|
|
14
|
+
* O que procuramos:
|
|
15
|
+
* 1. `nrt_[A-Za-z0-9_-]{20,}` — credencial de API do produto (Neetru).
|
|
16
|
+
* 2. `npk_[A-Za-z0-9_-]{20,}` — chave de produto Neetru.
|
|
17
|
+
* 3. `-----BEGIN … PRIVATE KEY-----` + material base64 — chave privada PEM
|
|
18
|
+
* (RSA/EC/OPENSSH/PKCS8). Só o cabeçalho (código de lib) não conta.
|
|
19
|
+
* 4. VALOR literal de env pública (`NEXT_PUBLIC_*`, `EXPO_PUBLIC_*`,
|
|
20
|
+
* `VITE_*`, `REACT_APP_*`, `PUBLIC_*`) cujo NOME contém KEY/SECRET/TOKEN —
|
|
21
|
+
* é exatamente o que o bundler injeta no JS do navegador.
|
|
22
|
+
* Exceção por FORMATO de valor sabidamente público: Stripe publishable
|
|
23
|
+
* (`pk_live_`/`pk_test_`), chave web Google/Firebase (`AIza…`), reCAPTCHA
|
|
24
|
+
* site key (`6L…`). Nome não isenta nada (`NEXT_PUBLIC_NEETRU_PUBLIC_KEY`
|
|
25
|
+
* é justamente a chave `nrt_` no navegador).
|
|
26
|
+
*
|
|
27
|
+
* Saída: NUNCA imprime o segredo. Mostra no máximo os 8 primeiros caracteres
|
|
28
|
+
* (e nunca mais que 1/4 do valor), seguidos de máscara.
|
|
29
|
+
*
|
|
30
|
+
* Sem dependências: o .tar.gz é lido em streaming (zlib + parser ustar/GNU/pax).
|
|
31
|
+
*/
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import fsp from 'node:fs/promises';
|
|
34
|
+
import path from 'node:path';
|
|
35
|
+
import zlib from 'node:zlib';
|
|
36
|
+
import { problemaDeCodificacao, semBomInicial } from './texto-utf8.js';
|
|
37
|
+
import { variaveisDoEnvFile } from './env-file-do-build.js';
|
|
38
|
+
export const PUBLIC_SECRET_PATTERNS = [
|
|
39
|
+
{ id: 'nrt_key', label: 'credencial nrt_ do produto (só de servidor)', re: /nrt_[A-Za-z0-9_-]{20,}/g },
|
|
40
|
+
{ id: 'npk_key', label: 'chave npk_ do produto', re: /npk_[A-Za-z0-9_-]{20,}/g },
|
|
41
|
+
{
|
|
42
|
+
id: 'private_key',
|
|
43
|
+
label: 'chave privada PEM',
|
|
44
|
+
// Exige MATERIAL de chave depois do cabeçalho (base64, com quebra real ou
|
|
45
|
+
// `\n` escapado de string JS) — o cabeçalho sozinho aparece em código de
|
|
46
|
+
// biblioteca (ex.: validação de PEM do `jose`) e não é segredo.
|
|
47
|
+
re: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY-----(?:\\[nr]|\s)*[A-Za-z0-9+/=]{40,}/g,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
/** Prefixos de env que os bundlers injetam no código do navegador/app. */
|
|
51
|
+
export const PUBLIC_ENV_PREFIXES = ['NEXT_PUBLIC_', 'EXPO_PUBLIC_', 'VITE_', 'REACT_APP_', 'PUBLIC_'];
|
|
52
|
+
const SENSITIVE_ENV_NAME_RE = /KEY|SECRET|TOKEN/i;
|
|
53
|
+
/** Valores curtos demais geram falso positivo em massa (ex.: "true", "1"). */
|
|
54
|
+
const MIN_ENV_VALUE_LENGTH = 12;
|
|
55
|
+
/** Formatos de valor que são públicos POR DESENHO do provedor. */
|
|
56
|
+
const KNOWN_PUBLIC_VALUE_SHAPES = [
|
|
57
|
+
/^pk_(live|test)_[A-Za-z0-9]+$/, // Stripe publishable key
|
|
58
|
+
/^AIza[0-9A-Za-z_-]{35}$/, // Google/Firebase web API key (restrita por domínio)
|
|
59
|
+
/^6L[0-9A-Za-z_-]{38}$/, // reCAPTCHA site key
|
|
60
|
+
];
|
|
61
|
+
export function isKnownPublicValue(value) {
|
|
62
|
+
return KNOWN_PUBLIC_VALUE_SHAPES.some((re) => re.test(value));
|
|
63
|
+
}
|
|
64
|
+
export function isPublicSensitiveEnvName(name) {
|
|
65
|
+
return PUBLIC_ENV_PREFIXES.some((p) => name.startsWith(p)) && SENSITIVE_ENV_NAME_RE.test(name);
|
|
66
|
+
}
|
|
67
|
+
// ─── Máscara ────────────────────────────────────────────────────────────────
|
|
68
|
+
/** Até 8 caracteres iniciais (nunca mais que 1/4 do valor) + máscara. */
|
|
69
|
+
export function maskSecret(value) {
|
|
70
|
+
const shown = Math.min(8, Math.floor(value.length / 4));
|
|
71
|
+
return `${value.slice(0, shown)}…[mascarado, ${value.length} caracteres]`;
|
|
72
|
+
}
|
|
73
|
+
// ─── Caminhos públicos ──────────────────────────────────────────────────────
|
|
74
|
+
const PUBLIC_TOP_DIRS = new Set(['public', 'out', 'dist', 'build']);
|
|
75
|
+
/** Normaliza o nome de membro do tar: `\` → `/`, sem `./` nem `/` iniciais. */
|
|
76
|
+
export function normalizeEntryPath(p) {
|
|
77
|
+
let s = p.replace(/\\/g, '/');
|
|
78
|
+
while (s.startsWith('./'))
|
|
79
|
+
s = s.slice(2);
|
|
80
|
+
s = s.replace(/^\/+/, '');
|
|
81
|
+
return s;
|
|
82
|
+
}
|
|
83
|
+
export function isPublicArtifactPath(relPath, stack) {
|
|
84
|
+
const p = normalizeEntryPath(relPath);
|
|
85
|
+
if (!p)
|
|
86
|
+
return false;
|
|
87
|
+
const segs = p.split('/');
|
|
88
|
+
if (segs.includes('node_modules'))
|
|
89
|
+
return false;
|
|
90
|
+
if (stack === 'static')
|
|
91
|
+
return true;
|
|
92
|
+
// `.next/static/**` em qualquer profundidade (monorepo: apps/web/.next/static).
|
|
93
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
94
|
+
if (segs[i] === '.next' && segs[i + 1] === 'static')
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return PUBLIC_TOP_DIRS.has(segs[0]) && segs.length > 1;
|
|
98
|
+
}
|
|
99
|
+
const BINARY_EXT = new Set([
|
|
100
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.ico', '.bmp', '.tif', '.tiff',
|
|
101
|
+
'.woff', '.woff2', '.ttf', '.otf', '.eot',
|
|
102
|
+
'.mp4', '.webm', '.mov', '.mp3', '.wav', '.ogg', '.m4a',
|
|
103
|
+
'.pdf', '.zip', '.gz', '.br', '.zst', '.7z', '.wasm',
|
|
104
|
+
]);
|
|
105
|
+
/** Arquivos maiores que isso não são varridos (e são reportados como não varridos). */
|
|
106
|
+
export const MAX_SCAN_FILE_BYTES = 25 * 1024 * 1024;
|
|
107
|
+
export function isScannableFile(relPath) {
|
|
108
|
+
return !BINARY_EXT.has(path.extname(relPath).toLowerCase());
|
|
109
|
+
}
|
|
110
|
+
// ─── Formas do valor da env dentro do pacote ────────────────────────────────
|
|
111
|
+
const ESPECIAIS_DE_REGEX = /[.*+?^${}()|[\]\\]/g;
|
|
112
|
+
/** Fonte de regex que casa `s` ao pé da letra (o caractere de controle cru casa ele mesmo). */
|
|
113
|
+
function literalDeRegex(s) {
|
|
114
|
+
return s.replace(ESPECIAIS_DE_REGEX, '\\$&');
|
|
115
|
+
}
|
|
116
|
+
/** Hex de `n` com `digitos` no mínimo, cada letra casando nas duas caixas (`e1` → `[eE]1`). */
|
|
117
|
+
function hexNasDuasCaixas(n, digitos) {
|
|
118
|
+
return n
|
|
119
|
+
.toString(16)
|
|
120
|
+
.padStart(digitos, '0')
|
|
121
|
+
.replace(/[a-f]/g, (c) => `[${c}${c.toUpperCase()}]`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Escape curto que pode estar no pacote no lugar do caractere ASCII (texto do
|
|
125
|
+
* pacote, não da regex). Medido na rodada final 4 do cli#81, num `next build`
|
|
126
|
+
* real (Next 15.5.27) e no esbuild 0.28.1:
|
|
127
|
+
* - `\\` (barra invertida): os dois;
|
|
128
|
+
* - `\"` e `\'`: o SWC, quando o valor tem as duas aspas (o esbuild usa crase);
|
|
129
|
+
* - `\/`: os dois, em `<\/script>`;
|
|
130
|
+
* - `\n`: o SWC (o esbuild grava a quebra crua, dentro de crase);
|
|
131
|
+
* - `\r`, `\b`, `\f`, `\v` e `\0`: os dois;
|
|
132
|
+
* - `\$` (em `\${`) e a crase escapada: o esbuild (0.21.5 do Vite 5.4 e 0.28), quando
|
|
133
|
+
* o valor tem as duas aspas repetidas e ele escolhe a crase (rodada final
|
|
134
|
+
* 5, medido num `vite build` real). O SWC não escapa nenhum dos dois.
|
|
135
|
+
* A tabulação não aparece como `\t`: os dois a gravam crua.
|
|
136
|
+
*/
|
|
137
|
+
const ESCAPE_CURTO = new Map([
|
|
138
|
+
[0x5c, '\\\\'],
|
|
139
|
+
[0x22, '\\"'],
|
|
140
|
+
[0x27, "\\'"],
|
|
141
|
+
[0x2f, '\\/'],
|
|
142
|
+
[0x0a, '\\n'],
|
|
143
|
+
[0x0d, '\\r'],
|
|
144
|
+
[0x08, '\\b'],
|
|
145
|
+
[0x0c, '\\f'],
|
|
146
|
+
[0x0b, '\\v'],
|
|
147
|
+
[0x00, '\\0'],
|
|
148
|
+
[0x24, '\\$'],
|
|
149
|
+
[0x60, '\\`'],
|
|
150
|
+
]);
|
|
151
|
+
/**
|
|
152
|
+
* O minificador pode gravar este caractere de outro jeito? Sim para letra fora
|
|
153
|
+
* do ASCII, caractere de controle (incluindo U+007F) e `\ " ' / $` e a crase.
|
|
154
|
+
* As demais letras ASCII saem sempre cruas.
|
|
155
|
+
*/
|
|
156
|
+
function podeSairEscapado(cp) {
|
|
157
|
+
return cp >= 0x7f || cp < 0x20 || ESCAPE_CURTO.has(cp);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Fonte de regex com todas as formas em que UM caractere que pode sair
|
|
161
|
+
* escapado estaria numa string JS do pacote. Medido nas rodadas finais 3 e 4
|
|
162
|
+
* do cli#81:
|
|
163
|
+
* - cru: nos bytes UTF-8 (Vite 5.4, esbuild com `charset: "utf8"`, e o SWC
|
|
164
|
+
* do Next 15.5 de U+0100 a U+FFFF), e o controle cru (a tabulação nos dois,
|
|
165
|
+
* e a quebra dentro de crase no esbuild);
|
|
166
|
+
* - o escape curto de `ESCAPE_CURTO`;
|
|
167
|
+
* - `\xNN`, até U+00FF: o SWC do Next 15.5 (`\xe1`, `\x01`, `\x7f`) e o
|
|
168
|
+
* esbuild no padrão (`\xE1`, `\x1B`, `\x7F`);
|
|
169
|
+
* - `\uNNNN`, no resto do plano básico: o esbuild no padrão (`\u20AC`);
|
|
170
|
+
* - o par `\uD83D\uDE00`, acima do plano básico: o SWC do Next 15.5;
|
|
171
|
+
* - `\u{1F600}`: o esbuild no padrão com alvo ES2015 ou mais novo.
|
|
172
|
+
* O hex casa nas duas caixas, porque o SWC grava `\xe1` e `\uD83D`, e o
|
|
173
|
+
* esbuild grava `\xE1`.
|
|
174
|
+
*
|
|
175
|
+
* A barra invertida NÃO tem a forma crua aqui (rodada final 5): dentro de uma
|
|
176
|
+
* string JS uma barra crua sempre abre um escape, e a forma crua `\` é prefixo
|
|
177
|
+
* do escape `\\`. Com as duas, cada barra do valor casava uma ou duas barras do
|
|
178
|
+
* pacote: o tempo crescia 1,7 vez por barra (24 barras, 1,6 s) e um valor com
|
|
179
|
+
* duas barras casava o JS de uma. Sem ela, as formas de cada caractere
|
|
180
|
+
* começam diferentes, no máximo uma casa em cada posição e a busca é linear.
|
|
181
|
+
* A barra crua fora de JS e JSON é coberta à parte, em `valorApareceNoPacote`.
|
|
182
|
+
*/
|
|
183
|
+
function formasDaLetra(letra, emBytes) {
|
|
184
|
+
const cp = letra.codePointAt(0);
|
|
185
|
+
const crua = emBytes ? Buffer.from(letra, 'utf8').toString('latin1') : letra;
|
|
186
|
+
const formas = cp === 0x5c ? [] : [literalDeRegex(crua)];
|
|
187
|
+
const curto = ESCAPE_CURTO.get(cp);
|
|
188
|
+
if (curto)
|
|
189
|
+
formas.push(literalDeRegex(curto));
|
|
190
|
+
if (cp <= 0xff)
|
|
191
|
+
formas.push(`\\\\x${hexNasDuasCaixas(cp, 2)}`);
|
|
192
|
+
if (cp <= 0xffff)
|
|
193
|
+
formas.push(`\\\\u${hexNasDuasCaixas(cp, 4)}`);
|
|
194
|
+
if (cp > 0xffff) {
|
|
195
|
+
const alto = 0xd800 + ((cp - 0x10000) >> 10);
|
|
196
|
+
const baixo = 0xdc00 + ((cp - 0x10000) & 0x3ff);
|
|
197
|
+
formas.push(`\\\\u${hexNasDuasCaixas(alto, 4)}\\\\u${hexNasDuasCaixas(baixo, 4)}`);
|
|
198
|
+
}
|
|
199
|
+
formas.push(`\\\\u\\{0*${hexNasDuasCaixas(cp, 1)}\\}`);
|
|
200
|
+
return `(?:${formas.join('|')})`;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Regex que acha o valor no pacote com cada caractere que pode sair escapado
|
|
204
|
+
* em qualquer uma das suas formas, letra a letra: o SWC do Next mistura as
|
|
205
|
+
* formas dentro do MESMO valor (`chave-€-łódź` sai com `€`, `ł` e `ź` crus
|
|
206
|
+
* e `ó` como `\xf3`). `emBytes` diz se o pacote foi lido em latin1 (a forma
|
|
207
|
+
* crua é então a dos bytes UTF-8). Devolve `null` quando nenhum caractere do
|
|
208
|
+
* valor pode sair escapado: aí basta `includes`.
|
|
209
|
+
*/
|
|
210
|
+
export function padraoDoValorNoPacote(valor, emBytes) {
|
|
211
|
+
const letras = [...valor];
|
|
212
|
+
if (!letras.some((l) => podeSairEscapado(l.codePointAt(0))))
|
|
213
|
+
return null;
|
|
214
|
+
let fonte = '';
|
|
215
|
+
for (const letra of letras) {
|
|
216
|
+
fonte += podeSairEscapado(letra.codePointAt(0))
|
|
217
|
+
? formasDaLetra(letra, emBytes)
|
|
218
|
+
: literalDeRegex(letra);
|
|
219
|
+
}
|
|
220
|
+
return new RegExp(fonte);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Arquivo em que o valor só pode estar dentro de string JS ou JSON: ali a barra
|
|
224
|
+
* invertida nunca aparece crua (uma barra crua sempre abre um escape).
|
|
225
|
+
*/
|
|
226
|
+
const BARRA_SEMPRE_ESCAPADA = new Set(['.js', '.mjs', '.cjs', '.json', '.map']);
|
|
227
|
+
function valorApareceNoPacote(arquivo, texto, valor, emBytes) {
|
|
228
|
+
const padrao = padraoDoValorNoPacote(valor, emBytes);
|
|
229
|
+
if (!padrao)
|
|
230
|
+
return texto.includes(valor);
|
|
231
|
+
if (padrao.test(texto))
|
|
232
|
+
return true;
|
|
233
|
+
// Valor com barra invertida num arquivo que não é JS nem JSON (`.txt`,
|
|
234
|
+
// `.html`, `.css`…): o texto pode trazer a barra crua, que a regex não aceita.
|
|
235
|
+
if (!valor.includes('\\') || BARRA_SEMPRE_ESCAPADA.has(path.extname(arquivo).toLowerCase()))
|
|
236
|
+
return false;
|
|
237
|
+
return texto.includes(emBytes ? Buffer.from(valor, 'utf8').toString('latin1') : valor);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Varre um conteúdo de arquivo. Um achado por (arquivo, padrão): desde a
|
|
241
|
+
* rodada final 5 do cli#81 cada nome tem até três valores procurados (o do
|
|
242
|
+
* Next, o do Vite e o cru), e quando um é pedaço do outro os dois casavam. A
|
|
243
|
+
* busca do nome para no primeiro valor achado.
|
|
244
|
+
*/
|
|
245
|
+
export function scanContent(file, content, envSecrets) {
|
|
246
|
+
const text = typeof content === 'string' ? content : content.toString('latin1');
|
|
247
|
+
const out = [];
|
|
248
|
+
for (const pat of PUBLIC_SECRET_PATTERNS) {
|
|
249
|
+
pat.re.lastIndex = 0;
|
|
250
|
+
const m = pat.re.exec(text);
|
|
251
|
+
if (m)
|
|
252
|
+
out.push({ file, pattern: pat.id, label: pat.label, preview: maskSecret(m[0]) });
|
|
253
|
+
}
|
|
254
|
+
const nomesAchados = new Set();
|
|
255
|
+
for (const env of envSecrets) {
|
|
256
|
+
if (nomesAchados.has(env.name))
|
|
257
|
+
continue;
|
|
258
|
+
// Rodadas finais 2 a 4 do cli#81: o pacote vem em bytes e vira texto em
|
|
259
|
+
// latin1 (1 byte = 1 caractere), mas o valor da env é texto, e cada
|
|
260
|
+
// caractere fora do ASCII, de controle, barra invertida, aspa ou barra pode
|
|
261
|
+
// estar no pacote cru ou escapado — ver `padraoDoValorNoPacote`. Para valor
|
|
262
|
+
// só com letras ASCII comuns nada muda.
|
|
263
|
+
if (valorApareceNoPacote(file, text, env.value, typeof content !== 'string')) {
|
|
264
|
+
nomesAchados.add(env.name);
|
|
265
|
+
out.push({
|
|
266
|
+
file,
|
|
267
|
+
pattern: `public_env:${env.name}`,
|
|
268
|
+
label: `valor da env pública ${env.name} (nome com KEY/SECRET/TOKEN vai pro navegador)`,
|
|
269
|
+
preview: maskSecret(env.value),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
// ─── Env pública com cara de segredo ────────────────────────────────────────
|
|
276
|
+
/**
|
|
277
|
+
* Lê o .env com a regra do dotenv, a MESMA que o `@next/env` 15.5 e o Vite 5.4
|
|
278
|
+
* embutem (copiada dos dois no `node_modules` da core-deps; rodada final 4 do
|
|
279
|
+
* cli#81). A varredura procura no pacote o valor que o bundler leu, e o parser
|
|
280
|
+
* mínimo de antes divergia em quatro pontos, medidos num `next build` real:
|
|
281
|
+
* - `CHAVE=valor # nota`: o dotenv corta no `#`; o parser antigo levava a nota;
|
|
282
|
+
* - valor entre crases: o dotenv tira as crases; o antigo não;
|
|
283
|
+
* - `CHAVE="a\nb"`: entre aspas duplas, o dotenv troca `\n` e `\r` pela
|
|
284
|
+
* quebra de linha e pelo retorno; o antigo deixava a barra e a letra;
|
|
285
|
+
* - valor entre aspas em mais de uma linha: o dotenv junta; o antigo cortava.
|
|
286
|
+
*/
|
|
287
|
+
export function parseDotenv(content) {
|
|
288
|
+
const LINHA = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
|
|
289
|
+
const out = {};
|
|
290
|
+
const texto = content.replace(/\r\n?/gm, '\n');
|
|
291
|
+
let m;
|
|
292
|
+
while ((m = LINHA.exec(texto)) != null) {
|
|
293
|
+
let valor = (m[2] || '').trim();
|
|
294
|
+
const aspa = valor[0];
|
|
295
|
+
valor = valor.replace(/^(['"`])([\s\S]*)\1$/gm, '$2');
|
|
296
|
+
if (aspa === '"')
|
|
297
|
+
valor = valor.replace(/\\n/g, '\n').replace(/\\r/g, '\r');
|
|
298
|
+
out[m[1]] = valor;
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
/** Arquivos .env que o bundler lê num build de produção (o `.env*` NÃO viaja no tarball). */
|
|
303
|
+
export const BUILD_ENV_FILES = ['.env', '.env.production', '.env.local', '.env.production.local'];
|
|
304
|
+
// ─── Expansão de $VAR, que o bundler faz depois do parse ────────────────────
|
|
305
|
+
//
|
|
306
|
+
// Rodada final 5 do cli#81: o Next 15.5 (`@next/env`) e o Vite 5.4 (`loadEnv`)
|
|
307
|
+
// expandem `$VAR`, `${VAR}` e `${VAR:-padrão}` depois de ler o `.env`, e é o
|
|
308
|
+
// valor EXPANDIDO que vai ao JS do navegador. Medido num `next build` e num
|
|
309
|
+
// `vite build` reais: com a busca pelo valor cru, 15 de 16 e 12 de 14 segredos
|
|
310
|
+
// passavam. Os dois embutem versões DIFERENTES do dotenv-expand, com
|
|
311
|
+
// resultados diferentes (no Vite, `$A-fim` lê o `-fim` como padrão e o some;
|
|
312
|
+
// no Next, `${A:-x}` com o `A` só no mesmo arquivo dá `x`). As duas regras
|
|
313
|
+
// estão copiadas abaixo, cada uma do seu `node_modules` na core-deps, e a
|
|
314
|
+
// varredura procura os dois resultados, e o valor cru para bundler que não expande.
|
|
315
|
+
/**
|
|
316
|
+
* Orçamento de substituições por valor e teto de tamanho do valor expandido.
|
|
317
|
+
* Um ciclo (`A=$B`, `B=$A`) estoura a pilha do bundler e derruba o build; aqui
|
|
318
|
+
* a expansão só para. Sem o teto, o valor de um ciclo crescia a cada passo e a
|
|
319
|
+
* varredura ficava sem memória (visto no teste do ciclo).
|
|
320
|
+
*/
|
|
321
|
+
const ORCAMENTO_DE_SUBSTITUICOES = 200;
|
|
322
|
+
const TETO_DO_VALOR_EXPANDIDO = 32 * 1024;
|
|
323
|
+
/** `_resolveEscapeSequences`, igual nos dois: `\$` vira `$`. */
|
|
324
|
+
function resolverEscapeDoDolar(valor) {
|
|
325
|
+
return valor.replace(/\\\$/g, '$');
|
|
326
|
+
}
|
|
327
|
+
/** `_searchLast` do `@next/env` 15.5. */
|
|
328
|
+
function ultimaPosicao(texto, re) {
|
|
329
|
+
const achados = Array.from(texto.matchAll(re));
|
|
330
|
+
return achados.length > 0 ? achados[achados.length - 1].index : -1;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* `_interpolate` do `@next/env` 15.5, copiado: troca o `$` mais à direita sem
|
|
334
|
+
* barra antes, e repete. A ordem é a do Next, ambiente, depois padrão, depois
|
|
335
|
+
* o arquivo: com `${A:-x}`, um `A` que só existe no mesmo arquivo perde para o `x`.
|
|
336
|
+
*/
|
|
337
|
+
function interpolarComoONext(valor, ambiente, arquivo, orcamento = { resta: ORCAMENTO_DE_SUBSTITUICOES }) {
|
|
338
|
+
if (--orcamento.resta < 0 || valor.length > TETO_DO_VALOR_EXPANDIDO)
|
|
339
|
+
return valor;
|
|
340
|
+
const ultimo = ultimaPosicao(valor, /(?!(?<=\\))\$/g);
|
|
341
|
+
if (ultimo === -1)
|
|
342
|
+
return valor;
|
|
343
|
+
const m = valor.slice(ultimo).match(/((?!(?<=\\))\${?([\w]+)(?::-([^}\\]*))?}?)/);
|
|
344
|
+
if (m == null)
|
|
345
|
+
return valor;
|
|
346
|
+
const [, grupo, nome, padrao] = m;
|
|
347
|
+
return interpolarComoONext(valor.replace(grupo, ambiente[nome] || padrao || arquivo[nome] || ''), ambiente, arquivo, orcamento);
|
|
348
|
+
}
|
|
349
|
+
/** `expand` do `@next/env` 15.5: lê do ambiente, que é o `process.env` do build, e o altera ao fim de cada arquivo. */
|
|
350
|
+
function expandirComoONext(arquivo, ambiente) {
|
|
351
|
+
for (const chave in arquivo) {
|
|
352
|
+
const valor = Object.prototype.hasOwnProperty.call(ambiente, chave) ? ambiente[chave] : arquivo[chave];
|
|
353
|
+
arquivo[chave] = resolverEscapeDoDolar(interpolarComoONext(valor, ambiente, arquivo));
|
|
354
|
+
}
|
|
355
|
+
for (const chave in arquivo)
|
|
356
|
+
ambiente[chave] = arquivo[chave];
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* O env do build no Next 15.5 (`loadEnvConfig` → `processEnv`): os arquivos vêm
|
|
360
|
+
* do de maior prioridade para o de menor, cada um expandido com o ambiente, e
|
|
361
|
+
* a chave que já está no ambiente do build não é trocada por arquivo nenhum.
|
|
362
|
+
*/
|
|
363
|
+
function envComoONextCarrega(arquivos, ambienteDoBuild) {
|
|
364
|
+
const ambiente = { ...ambienteDoBuild };
|
|
365
|
+
const dosArquivos = {};
|
|
366
|
+
for (const lido of arquivos) {
|
|
367
|
+
const arquivo = { ...lido };
|
|
368
|
+
expandirComoONext(arquivo, ambiente);
|
|
369
|
+
for (const chave of Object.keys(arquivo)) {
|
|
370
|
+
if (dosArquivos[chave] === undefined && ambienteDoBuild[chave] === undefined)
|
|
371
|
+
dosArquivos[chave] = arquivo[chave];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return Object.assign(ambiente, dosArquivos);
|
|
375
|
+
}
|
|
376
|
+
/** `DOTENV_SUBSTITUTION_REGEX` do dotenv-expand que o Vite 5.4 embute, copiada. */
|
|
377
|
+
const SUBSTITUICAO_DO_VITE = /(\\)?(\$)(?!\()(\{?)([\w.]+)(?::?-((?:\$\{(?:\$\{(?:\$\{[^}]*\}|[^}])*}|[^}])*}|[^}])+))?(\}?)/gi;
|
|
378
|
+
/**
|
|
379
|
+
* `interpolate` do Vite 5.4, copiado. Difere do Next: troca da esquerda para a
|
|
380
|
+
* direita, aceita `-padrão` sem os dois-pontos (por isso `$A-fim` vira só o
|
|
381
|
+
* valor de `A`) e prefere o ambiente, depois o arquivo, depois o padrão.
|
|
382
|
+
*/
|
|
383
|
+
function interpolarComoOVite(valor, ambiente, arquivo, orcamento = { resta: ORCAMENTO_DE_SUBSTITUICOES }) {
|
|
384
|
+
if (--orcamento.resta < 0 || valor.length > TETO_DO_VALOR_EXPANDIDO)
|
|
385
|
+
return valor;
|
|
386
|
+
return valor.replace(SUBSTITUICAO_DO_VITE, (casado, escapado, _dolar, _abre, chave, padrao) => {
|
|
387
|
+
if (escapado === '\\')
|
|
388
|
+
return casado.slice(1);
|
|
389
|
+
if (ambiente[chave]) {
|
|
390
|
+
if (ambiente[chave] === arquivo[chave])
|
|
391
|
+
return ambiente[chave];
|
|
392
|
+
return interpolarComoOVite(ambiente[chave], ambiente, arquivo, orcamento);
|
|
393
|
+
}
|
|
394
|
+
if (arquivo[chave]) {
|
|
395
|
+
if (arquivo[chave] === valor)
|
|
396
|
+
return arquivo[chave];
|
|
397
|
+
return interpolarComoOVite(arquivo[chave], ambiente, arquivo, orcamento);
|
|
398
|
+
}
|
|
399
|
+
if (padrao) {
|
|
400
|
+
if (padrao.startsWith('$'))
|
|
401
|
+
return interpolarComoOVite(padrao, ambiente, arquivo, orcamento);
|
|
402
|
+
return padrao;
|
|
403
|
+
}
|
|
404
|
+
return '';
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/** `expand` do Vite 5.4: a chave que o ambiente já tem com OUTRO valor fica com o do ambiente, sem expandir. */
|
|
408
|
+
function expandirComoOVite(arquivo, ambiente) {
|
|
409
|
+
for (const chave in arquivo) {
|
|
410
|
+
let valor = arquivo[chave];
|
|
411
|
+
if (Object.prototype.hasOwnProperty.call(ambiente, chave)) {
|
|
412
|
+
valor = ambiente[chave] === arquivo[chave] ? interpolarComoOVite(valor, ambiente, arquivo) : ambiente[chave];
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
valor = interpolarComoOVite(valor, ambiente, arquivo);
|
|
416
|
+
}
|
|
417
|
+
arquivo[chave] = resolverEscapeDoDolar(valor);
|
|
418
|
+
}
|
|
419
|
+
for (const chave in arquivo)
|
|
420
|
+
ambiente[chave] = arquivo[chave];
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* O env que o Vite 5.4 expõe (`loadEnv`): junta os arquivos, o de maior
|
|
424
|
+
* prioridade por último; expande uma vez, com uma CÓPIA do ambiente do build;
|
|
425
|
+
* e a variável do ambiente do build vence a do arquivo.
|
|
426
|
+
*/
|
|
427
|
+
function envComoOViteCarrega(arquivos, ambienteDoBuild) {
|
|
428
|
+
const arquivo = Object.fromEntries(arquivos.flatMap((a) => Object.entries(a)));
|
|
429
|
+
expandirComoOVite(arquivo, { ...ambienteDoBuild });
|
|
430
|
+
return { ...arquivo, ...ambienteDoBuild };
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Coleta envs públicas com nome sensível a partir do ambiente do build:
|
|
434
|
+
* `.env*` do projeto, `--env-file` e `process.env` do CLI (o build roda com ele).
|
|
435
|
+
*
|
|
436
|
+
* Rodada final 2 do cli#81: cada arquivo é lido COMO O BUNDLER O LÊ, porque a
|
|
437
|
+
* varredura procura no pacote o valor que o bundler gravou lá. O `@next/env`
|
|
438
|
+
* (Next 15.5) lê o `.env` com `readFileSync(…, "utf8")` e o Vite 5.4 passa os
|
|
439
|
+
* bytes ao `dotenv`, que faz `toString()`: os dois trocam byte que não é UTF-8
|
|
440
|
+
* por U+FFFD, e é esse valor trocado que vai ao JS do navegador. Por isso aqui
|
|
441
|
+
* a leitura NÃO recusa (como faz o `--env-file`, que a CLI injeta ela mesma):
|
|
442
|
+
* recusar esconderia o valor que de fato está no pacote. Em vez de passar
|
|
443
|
+
* calado, `aoLerForaDeUtf8` recebe cada arquivo que não é UTF-8 limpo, para o
|
|
444
|
+
* deploy avisar que o app vai receber outro valor.
|
|
445
|
+
*
|
|
446
|
+
* Rodada final 5: o `--env-file` não é um `.env` que o bundler lê. O
|
|
447
|
+
* `neetru build` o injeta no ambiente do processo do build, com a regra
|
|
448
|
+
* própria da CLI (`variaveisDoEnvFile`). Por isso ele entra aqui como ambiente:
|
|
449
|
+
* vence os arquivos, serve de fonte para a expansão e não é expandido. Cada
|
|
450
|
+
* valor é procurado como o Next o deixa, como o Vite o deixa e cru.
|
|
451
|
+
*/
|
|
452
|
+
export async function collectPublicEnvSecrets(cwd, envFile, processEnv = process.env, aoLerForaDeUtf8) {
|
|
453
|
+
const lidos = new Map();
|
|
454
|
+
for (const nome of BUILD_ENV_FILES) {
|
|
455
|
+
let bruto;
|
|
456
|
+
try {
|
|
457
|
+
bruto = await fsp.readFile(path.join(cwd, nome));
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
continue; /* arquivo ausente/ilegível — ignora */
|
|
461
|
+
}
|
|
462
|
+
const problema = problemaDeCodificacao(bruto);
|
|
463
|
+
if (problema)
|
|
464
|
+
aoLerForaDeUtf8?.(nome, problema);
|
|
465
|
+
lidos.set(nome, parseDotenv(bruto.toString('utf8')));
|
|
466
|
+
}
|
|
467
|
+
// O ambiente do processo do build: o da CLI e, por cima, o `--env-file`.
|
|
468
|
+
const ambiente = {};
|
|
469
|
+
for (const [k, v] of Object.entries(processEnv)) {
|
|
470
|
+
if (typeof v === 'string')
|
|
471
|
+
ambiente[k] = v;
|
|
472
|
+
}
|
|
473
|
+
if (envFile) {
|
|
474
|
+
const caminho = path.resolve(cwd, envFile);
|
|
475
|
+
let bruto;
|
|
476
|
+
try {
|
|
477
|
+
bruto = await fsp.readFile(caminho);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
bruto = undefined; /* ausente/ilegível — ignora */
|
|
481
|
+
}
|
|
482
|
+
if (bruto) {
|
|
483
|
+
const problema = problemaDeCodificacao(bruto);
|
|
484
|
+
if (problema)
|
|
485
|
+
aoLerForaDeUtf8?.(path.relative(cwd, caminho) || caminho, problema);
|
|
486
|
+
Object.assign(ambiente, variaveisDoEnvFile(semBomInicial(bruto.toString('utf8'))));
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const emOrdem = (nomes) => nomes.flatMap((n) => {
|
|
490
|
+
const lido = lidos.get(n);
|
|
491
|
+
return lido ? [lido] : [];
|
|
492
|
+
});
|
|
493
|
+
// Cru: do de menor para o de maior prioridade (o `.env.local` vence o `.env.production`), e o ambiente por cima.
|
|
494
|
+
const cru = Object.assign({}, ...emOrdem(BUILD_ENV_FILES), ambiente);
|
|
495
|
+
const comoONext = envComoONextCarrega(emOrdem(['.env.production.local', '.env.local', '.env.production', '.env']), ambiente);
|
|
496
|
+
const comoOVite = envComoOViteCarrega(emOrdem(['.env', '.env.local', '.env.production', '.env.production.local']), ambiente);
|
|
497
|
+
const seen = new Set();
|
|
498
|
+
const out = [];
|
|
499
|
+
for (const fonte of [cru, comoONext, comoOVite]) {
|
|
500
|
+
for (const [name, value] of Object.entries(fonte)) {
|
|
501
|
+
if (!isPublicSensitiveEnvName(name))
|
|
502
|
+
continue;
|
|
503
|
+
if (value.length < MIN_ENV_VALUE_LENGTH || isKnownPublicValue(value))
|
|
504
|
+
continue;
|
|
505
|
+
const key = `${name}\u0000${value}`;
|
|
506
|
+
if (seen.has(key))
|
|
507
|
+
continue;
|
|
508
|
+
seen.add(key);
|
|
509
|
+
out.push({ name, value });
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
514
|
+
function readString(buf, start, len) {
|
|
515
|
+
const slice = buf.subarray(start, start + len);
|
|
516
|
+
const nul = slice.indexOf(0);
|
|
517
|
+
return slice.subarray(0, nul < 0 ? slice.length : nul).toString('utf8');
|
|
518
|
+
}
|
|
519
|
+
function readSize(buf) {
|
|
520
|
+
const field = buf.subarray(124, 136);
|
|
521
|
+
if (field[0] & 0x80) {
|
|
522
|
+
// base-256 (GNU, arquivos > 8 GiB)
|
|
523
|
+
let n = 0;
|
|
524
|
+
for (let i = 1; i < field.length; i++)
|
|
525
|
+
n = n * 256 + field[i];
|
|
526
|
+
return n;
|
|
527
|
+
}
|
|
528
|
+
const s = readString(buf, 124, 12).trim();
|
|
529
|
+
return s ? parseInt(s, 8) : 0;
|
|
530
|
+
}
|
|
531
|
+
function parseHeader(block) {
|
|
532
|
+
const name = readString(block, 0, 100);
|
|
533
|
+
const type = String.fromCharCode(block[156] || 48); // '\0' → '0'
|
|
534
|
+
const magic = readString(block, 257, 6);
|
|
535
|
+
const prefix = magic.startsWith('ustar') ? readString(block, 345, 155) : '';
|
|
536
|
+
return { name: prefix ? `${prefix}/${name}` : name, size: readSize(block), type };
|
|
537
|
+
}
|
|
538
|
+
function parsePaxPath(data) {
|
|
539
|
+
const text = data.toString('utf8');
|
|
540
|
+
let i = 0;
|
|
541
|
+
while (i < text.length) {
|
|
542
|
+
const sp = text.indexOf(' ', i);
|
|
543
|
+
if (sp < 0)
|
|
544
|
+
break;
|
|
545
|
+
const len = parseInt(text.slice(i, sp), 10);
|
|
546
|
+
if (!Number.isFinite(len) || len <= 0)
|
|
547
|
+
break;
|
|
548
|
+
const record = text.slice(sp + 1, i + len - 1); // sem o '\n' final
|
|
549
|
+
const eq = record.indexOf('=');
|
|
550
|
+
if (eq > 0 && record.slice(0, eq) === 'path')
|
|
551
|
+
return record.slice(eq + 1);
|
|
552
|
+
i += len;
|
|
553
|
+
}
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
const META_MAX_BYTES = 1024 * 1024;
|
|
557
|
+
/**
|
|
558
|
+
* Lê um .tar.gz em streaming e chama `onFile` para cada arquivo regular que
|
|
559
|
+
* `want(nome, tamanho)` aceitar. Arquivos não desejados são pulados sem buffer.
|
|
560
|
+
*/
|
|
561
|
+
export async function walkTarGz(tarPath, want, onFile) {
|
|
562
|
+
let buf = Buffer.alloc(0);
|
|
563
|
+
let state = 'header';
|
|
564
|
+
let remaining = 0;
|
|
565
|
+
let pad = 0;
|
|
566
|
+
let cur = null;
|
|
567
|
+
let longName = null;
|
|
568
|
+
let paxPath = null;
|
|
569
|
+
let ended = false;
|
|
570
|
+
const finish = () => {
|
|
571
|
+
if (!cur)
|
|
572
|
+
return;
|
|
573
|
+
const data = cur.chunks ? Buffer.concat(cur.chunks) : null;
|
|
574
|
+
if (cur.type === 'L' && data) {
|
|
575
|
+
longName = readString(data, 0, data.length);
|
|
576
|
+
}
|
|
577
|
+
else if (cur.type === 'x' && data) {
|
|
578
|
+
paxPath = parsePaxPath(data) ?? paxPath;
|
|
579
|
+
}
|
|
580
|
+
else if ((cur.type === '0' || cur.type === '7') && data) {
|
|
581
|
+
onFile(cur.name, data);
|
|
582
|
+
}
|
|
583
|
+
cur = null;
|
|
584
|
+
};
|
|
585
|
+
const consume = () => {
|
|
586
|
+
let off = 0;
|
|
587
|
+
for (;;) {
|
|
588
|
+
if (ended) {
|
|
589
|
+
off = buf.length;
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
if (state === 'header') {
|
|
593
|
+
if (buf.length - off < 512)
|
|
594
|
+
break;
|
|
595
|
+
const block = buf.subarray(off, off + 512);
|
|
596
|
+
off += 512;
|
|
597
|
+
if (block.every((b) => b === 0))
|
|
598
|
+
continue; // bloco zero (fim/padding)
|
|
599
|
+
const h = parseHeader(block);
|
|
600
|
+
let name = h.name;
|
|
601
|
+
const isMeta = h.type === 'L' || h.type === 'x' || h.type === 'g' || h.type === 'K';
|
|
602
|
+
if (!isMeta) {
|
|
603
|
+
name = paxPath ?? longName ?? name;
|
|
604
|
+
paxPath = null;
|
|
605
|
+
longName = null;
|
|
606
|
+
}
|
|
607
|
+
const regular = h.type === '0' || h.type === '7';
|
|
608
|
+
let collect = false;
|
|
609
|
+
if (h.type === 'L' || h.type === 'x')
|
|
610
|
+
collect = h.size <= META_MAX_BYTES;
|
|
611
|
+
else if (regular)
|
|
612
|
+
collect = want(name, h.size);
|
|
613
|
+
cur = { name, type: h.type, chunks: collect ? [] : null };
|
|
614
|
+
remaining = h.size;
|
|
615
|
+
pad = (512 - (h.size % 512)) % 512;
|
|
616
|
+
if (remaining === 0) {
|
|
617
|
+
finish();
|
|
618
|
+
state = pad ? 'pad' : 'header';
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
state = 'data';
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
else if (state === 'data') {
|
|
625
|
+
const avail = buf.length - off;
|
|
626
|
+
if (avail === 0)
|
|
627
|
+
break;
|
|
628
|
+
const take = Math.min(avail, remaining);
|
|
629
|
+
if (cur?.chunks)
|
|
630
|
+
cur.chunks.push(Buffer.from(buf.subarray(off, off + take)));
|
|
631
|
+
off += take;
|
|
632
|
+
remaining -= take;
|
|
633
|
+
if (remaining === 0) {
|
|
634
|
+
finish();
|
|
635
|
+
state = pad ? 'pad' : 'header';
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
const avail = buf.length - off;
|
|
640
|
+
if (avail === 0)
|
|
641
|
+
break;
|
|
642
|
+
const take = Math.min(avail, pad);
|
|
643
|
+
off += take;
|
|
644
|
+
pad -= take;
|
|
645
|
+
if (pad === 0)
|
|
646
|
+
state = 'header';
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
buf = buf.subarray(off);
|
|
650
|
+
};
|
|
651
|
+
await new Promise((resolve, reject) => {
|
|
652
|
+
const input = fs.createReadStream(tarPath);
|
|
653
|
+
const gunzip = zlib.createGunzip();
|
|
654
|
+
input.on('error', reject);
|
|
655
|
+
gunzip.on('error', (e) => reject(new Error(`pacote .tar.gz ilegível: ${e.message}`)));
|
|
656
|
+
gunzip.on('data', (chunk) => {
|
|
657
|
+
buf = buf.length ? Buffer.concat([buf, chunk]) : chunk;
|
|
658
|
+
try {
|
|
659
|
+
consume();
|
|
660
|
+
}
|
|
661
|
+
catch (e) {
|
|
662
|
+
ended = true;
|
|
663
|
+
reject(e);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
gunzip.on('end', () => resolve());
|
|
667
|
+
input.pipe(gunzip);
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
/** Varre o tarball de deploy (o que de fato vai pro GCS). */
|
|
671
|
+
export async function scanTarballForPublicSecrets(tarPath, ctx) {
|
|
672
|
+
const findings = [];
|
|
673
|
+
const skippedLarge = [];
|
|
674
|
+
let scannedFiles = 0;
|
|
675
|
+
await walkTarGz(tarPath, (name, size) => {
|
|
676
|
+
if (!isPublicArtifactPath(name, ctx.stack) || !isScannableFile(name))
|
|
677
|
+
return false;
|
|
678
|
+
if (size > MAX_SCAN_FILE_BYTES) {
|
|
679
|
+
skippedLarge.push(normalizeEntryPath(name));
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
return true;
|
|
683
|
+
}, (name, data) => {
|
|
684
|
+
scannedFiles++;
|
|
685
|
+
findings.push(...scanContent(normalizeEntryPath(name), data, ctx.envSecrets));
|
|
686
|
+
});
|
|
687
|
+
return { findings, scannedFiles, skippedLarge };
|
|
688
|
+
}
|
|
689
|
+
/** Varre as pastas públicas no disco (target=cloud-run, onde não há tarball). */
|
|
690
|
+
export async function scanDirectoryForPublicSecrets(root, ctx) {
|
|
691
|
+
const findings = [];
|
|
692
|
+
const skippedLarge = [];
|
|
693
|
+
let scannedFiles = 0;
|
|
694
|
+
const starts = ctx.stack === 'static'
|
|
695
|
+
? ['.']
|
|
696
|
+
: ['.next/static', ...PUBLIC_TOP_DIRS].filter((d) => {
|
|
697
|
+
try {
|
|
698
|
+
return fs.statSync(path.join(root, d)).isDirectory();
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
});
|
|
704
|
+
async function walk(rel) {
|
|
705
|
+
let entries;
|
|
706
|
+
try {
|
|
707
|
+
entries = await fsp.readdir(path.join(root, rel), { withFileTypes: true });
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
for (const e of entries) {
|
|
713
|
+
const childRel = rel === '.' ? e.name : `${rel}/${e.name}`;
|
|
714
|
+
if (e.isSymbolicLink())
|
|
715
|
+
continue;
|
|
716
|
+
if (e.isDirectory()) {
|
|
717
|
+
if (e.name === 'node_modules' || e.name === '.git')
|
|
718
|
+
continue;
|
|
719
|
+
await walk(childRel);
|
|
720
|
+
}
|
|
721
|
+
else if (e.isFile()) {
|
|
722
|
+
if (!isPublicArtifactPath(childRel, ctx.stack) || !isScannableFile(childRel))
|
|
723
|
+
continue;
|
|
724
|
+
const abs = path.join(root, childRel);
|
|
725
|
+
const st = await fsp.stat(abs);
|
|
726
|
+
if (st.size > MAX_SCAN_FILE_BYTES) {
|
|
727
|
+
skippedLarge.push(childRel);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
scannedFiles++;
|
|
731
|
+
findings.push(...scanContent(childRel, await fsp.readFile(abs), ctx.envSecrets));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
for (const s of starts)
|
|
736
|
+
await walk(s);
|
|
737
|
+
return { findings, scannedFiles, skippedLarge };
|
|
738
|
+
}
|
|
739
|
+
// ─── Mensagens ──────────────────────────────────────────────────────────────
|
|
740
|
+
export const ALLOW_PUBLIC_SECRET_FLAG = '--allow-public-secret';
|
|
741
|
+
/** Motivo mínimo do override (evita `--allow-public-secret x`). */
|
|
742
|
+
export const MIN_OVERRIDE_REASON_LENGTH = 10;
|
|
743
|
+
export function formatFindingsMessage(findings) {
|
|
744
|
+
const lines = findings
|
|
745
|
+
.slice(0, 20)
|
|
746
|
+
.map((f) => ` - ${f.file} — ${f.label} (${f.preview})`);
|
|
747
|
+
const more = findings.length > 20 ? [` … e mais ${findings.length - 20} achado(s).`] : [];
|
|
748
|
+
return [
|
|
749
|
+
`Segredo no pacote PÚBLICO (servido ao navegador): ${findings.length} achado(s). Deploy BLOQUEADO.`,
|
|
750
|
+
...lines,
|
|
751
|
+
...more,
|
|
752
|
+
'',
|
|
753
|
+
'Por quê: tudo em .next/static, public, out, dist e build é baixado por qualquer visitante — ',
|
|
754
|
+
'a chave vira pública na hora. A credencial nrt_ do produto é SÓ DE SERVIDOR.',
|
|
755
|
+
'Como corrigir:',
|
|
756
|
+
' 1. Tire a chave do código do navegador/app: nada de NEXT_PUBLIC_*_KEY/EXPO_PUBLIC_*_KEY com segredo.',
|
|
757
|
+
' 2. O navegador/app chama a API do SEU produto (/api/v1/...) com o token do usuário;',
|
|
758
|
+
' o backend usa @neetru/sdk com NEETRU_API_KEY (injetada no servidor pelo Core).',
|
|
759
|
+
' 3. Rode `neetru build` de novo e repita o deploy.',
|
|
760
|
+
`Emergência (fica registrado no deploy): ${ALLOW_PUBLIC_SECRET_FLAG} "<motivo>"`,
|
|
761
|
+
].join('\n');
|
|
762
|
+
}
|
|
763
|
+
/** Registro do override — só caminho e tipo de padrão, NUNCA o valor. */
|
|
764
|
+
export function buildOverrideRecord(reason, findings, now = new Date()) {
|
|
765
|
+
return {
|
|
766
|
+
reason,
|
|
767
|
+
findings: findings.map((f) => ({ file: f.file, pattern: f.pattern })),
|
|
768
|
+
at: now.toISOString(),
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
//# sourceMappingURL=public-secret-scan.js.map
|