@neetru/cli 2.33.0 → 2.34.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +38 -3
  3. package/dist/commands/admin/sdk-credential.js +3 -1
  4. package/dist/commands/ai-models.d.ts +2 -1
  5. package/dist/commands/ai-models.js +4 -5
  6. package/dist/commands/ai.d.ts +12 -2
  7. package/dist/commands/ai.js +47 -38
  8. package/dist/commands/auth.d.ts +3 -2
  9. package/dist/commands/auth.js +96 -57
  10. package/dist/commands/build.js +35 -4
  11. package/dist/commands/chat.d.ts +24 -6
  12. package/dist/commands/chat.js +43 -7
  13. package/dist/commands/code.js +11 -2
  14. package/dist/commands/config.js +15 -13
  15. package/dist/commands/db-frontdoor.js +10 -5
  16. package/dist/commands/db.d.ts +8 -2
  17. package/dist/commands/db.js +13 -6
  18. package/dist/commands/deploy.d.ts +14 -5
  19. package/dist/commands/deploy.js +376 -55
  20. package/dist/commands/dev-context.js +1 -1
  21. package/dist/commands/environments-repoint.d.ts +49 -0
  22. package/dist/commands/environments-repoint.js +219 -0
  23. package/dist/commands/init.js +7 -1
  24. package/dist/commands/new.js +14 -0
  25. package/dist/commands/secrets.d.ts +6 -4
  26. package/dist/commands/secrets.js +14 -7
  27. package/dist/commands/ui.js +1 -1
  28. package/dist/commands/workspaces.js +16 -1
  29. package/dist/index.js +47 -7
  30. package/dist/lib/ai/auth-discovery.d.ts +9 -12
  31. package/dist/lib/ai/auth-discovery.js +43 -145
  32. package/dist/lib/ai/orchestrator.d.ts +13 -6
  33. package/dist/lib/ai/orchestrator.js +46 -246
  34. package/dist/lib/ai/provedores.d.ts +57 -0
  35. package/dist/lib/ai/provedores.js +66 -0
  36. package/dist/lib/build-targets.d.ts +48 -10
  37. package/dist/lib/build-targets.js +64 -10
  38. package/dist/lib/cli-write.js +6 -3
  39. package/dist/lib/code-agent-gateway.d.ts +2 -2
  40. package/dist/lib/code-agent-gateway.js +2 -2
  41. package/dist/lib/code-agent-settings.d.ts +2 -2
  42. package/dist/lib/code-agent-settings.js +2 -4
  43. package/dist/lib/code-agent-types.d.ts +3 -1
  44. package/dist/lib/config.d.ts +4 -7
  45. package/dist/lib/config.js +9 -6
  46. package/dist/lib/deploy-recusa-de-destino.d.ts +9 -0
  47. package/dist/lib/deploy-recusa-de-destino.js +98 -0
  48. package/dist/lib/deploy-render-probe.d.ts +117 -0
  49. package/dist/lib/deploy-render-probe.js +475 -0
  50. package/dist/lib/deploy-vm-um-app.d.ts +62 -0
  51. package/dist/lib/deploy-vm-um-app.js +83 -0
  52. package/dist/lib/segredo-produto.d.ts +25 -5
  53. package/dist/lib/segredo-produto.js +45 -10
  54. package/dist/lib/sse.d.ts +4 -5
  55. package/dist/lib/sse.js +4 -5
  56. package/package.json +1 -2
@@ -0,0 +1,475 @@
1
+ /**
2
+ * deploy-render-probe.ts — a página inicial do produto RENDERIZA no navegador?
3
+ * (bug_0d2e0087, lado da CLI).
4
+ *
5
+ * DEFEITO (caso Zelo, 22/10): `neetru deploy --target vm` imprimia "Live em
6
+ * https://zelo.neetru.com" com `health.verified:true`, e a página ficava EM
7
+ * BRANCO para todo visitante. O `next.config` do produto mandava
8
+ * `Content-Security-Policy: ... script-src 'self'` (sem `'unsafe-inline'`, sem
9
+ * nonce). O App Router do Next põe o payload RSC em `<script>` embutidos
10
+ * (`self.__next_f.push(...)`); o navegador bloqueia todos, a hidratação falha
11
+ * na raiz e o React 19 desmonta a árvore. A CLI não tinha como ver:
12
+ * - `GET /api/cli/v1/deploy/status` devolve o status do DEPLOYMENT (`success`),
13
+ * não o do ambiente;
14
+ * - a sonda de saúde da CLI (`probeDeployedHealth`) lê só o JSON de
15
+ * `/api/health`, que respondia 200 saudável.
16
+ *
17
+ * O QUE ESTE MÓDULO FAZ: depois que a saúde PASSA, a CLI abre a página inicial
18
+ * (`https://<domínio>/`), lê as políticas CSP que valem (cabeçalho
19
+ * `Content-Security-Policy`, o mesmo que o navegador recebe, e
20
+ * `<meta http-equiv>`) e confere, script embutido por script embutido, se a
21
+ * política deixa ele rodar (nonce, hash ou `'unsafe-inline'` — este último
22
+ * ignorado quando há nonce/hash/`'strict-dynamic'`, como no navegador). Algum
23
+ * bloqueado → `inline_scripts_blocked`, e o deploy NÃO sai como "Live".
24
+ *
25
+ * SÓ ACUSA O QUE MEDIU: página que não é HTML 2xx, rede caída, redirect pra
26
+ * outra origem → `unverifiable`, e o veredito da saúde fica como estava.
27
+ * `Content-Security-Policy-Report-Only` não bloqueia nada no navegador, então
28
+ * não conta.
29
+ *
30
+ * ESPELHO: a regra é a mesma de `functions/src/lib/deploy-render-probe.ts` do
31
+ * Core (core#793), que roda no portão de saúde das functions — mas ele roda
32
+ * DEPOIS do `success` do deployment, num gatilho assíncrono, e grava o
33
+ * resultado no ambiente, que a rota que a CLI consulta não devolve. Por isso a
34
+ * CLI confere por conta própria: não depende de deploy do Core nem das
35
+ * functions. Divergiu do Core? Corrija nos dois.
36
+ *
37
+ * Classificação e análise são PURAS; o `fetch` entra injetado em
38
+ * `runRenderProbe` (o deploy passa o `fetch` global).
39
+ */
40
+ import { createHash } from 'node:crypto';
41
+ /** Teto de bytes lidos do HTML (os scripts do Next aparecem já no começo do body). */
42
+ export const RENDER_BODY_MAX_BYTES = 512 * 1024;
43
+ /** Redirects seguidos dentro da MESMA origem (ex.: `/` → `/pt-BR`). */
44
+ export const RENDER_MAX_REDIRECTS = 2;
45
+ /** Teto do texto da diretiva guardada no resultado. */
46
+ const DIRECTIVE_MAX_CHARS = 200;
47
+ /**
48
+ * Lê UMA política serializada (`a b; c d`). Diretiva repetida: vale a primeira
49
+ * (CSP3 §2.2.1). Nome de diretiva inválido é ignorado. PURA.
50
+ */
51
+ export function parseCspPolicy(text) {
52
+ const directives = new Map();
53
+ for (const part of text.split(';')) {
54
+ const tokens = part.trim().split(/[\t\n\f\r ]+/).filter(Boolean);
55
+ if (tokens.length === 0)
56
+ continue;
57
+ const name = tokens[0].toLowerCase();
58
+ if (!/^[a-z0-9-]+$/.test(name))
59
+ continue;
60
+ if (directives.has(name))
61
+ continue;
62
+ directives.set(name, tokens.slice(1));
63
+ }
64
+ return { directives };
65
+ }
66
+ /**
67
+ * Lê o valor de um cabeçalho `Content-Security-Policy`: várias políticas vêm
68
+ * separadas por vírgula (cabeçalhos repetidos também chegam assim, juntados
69
+ * pelo fetch). Política vazia é descartada. PURA.
70
+ */
71
+ export function parseCspHeader(value) {
72
+ return value
73
+ .split(',')
74
+ .map((p) => parseCspPolicy(p))
75
+ .filter((p) => p.directives.size > 0);
76
+ }
77
+ /**
78
+ * Lista de fontes que governa um `<script>` embutido: `script-src-elem`, senão
79
+ * `script-src`, senão `default-src`. `undefined` = a política não restringe
80
+ * script. PURA.
81
+ */
82
+ export function scriptElementSources(policy) {
83
+ for (const name of ['script-src-elem', 'script-src', 'default-src']) {
84
+ const sources = policy.directives.get(name);
85
+ if (sources)
86
+ return { name, sources };
87
+ }
88
+ return undefined;
89
+ }
90
+ /** Normaliza base64/base64url pra comparar hash (`-`→`+`, `_`→`/`, sem `=`). */
91
+ function normB64(v) {
92
+ return v.replace(/-/g, '+').replace(/_/g, '/').replace(/=+$/, '');
93
+ }
94
+ /**
95
+ * Este `<script>` embutido roda sob esta lista de fontes? Regras do navegador
96
+ * (CSP3 "Does element match source list for type and source"):
97
+ * - nonce do elemento igual a um `'nonce-…'` da lista → roda;
98
+ * - hash do conteúdo igual a um `'sha256|384|512-…'` → roda;
99
+ * - `'unsafe-inline'` → roda, MAS só se a lista não tem nonce, hash nem
100
+ * `'strict-dynamic'` (com qualquer um deles o navegador ignora o unsafe-inline);
101
+ * - o resto (`'self'`, hosts, `'none'`, lista vazia) NÃO libera script embutido.
102
+ * PURA.
103
+ */
104
+ export function inlineScriptAllowed(script, sources) {
105
+ let hasNonceOrHash = false;
106
+ let unsafeInline = false;
107
+ let strictDynamic = false;
108
+ for (const raw of sources) {
109
+ const lower = raw.toLowerCase();
110
+ if (lower === "'unsafe-inline'") {
111
+ unsafeInline = true;
112
+ continue;
113
+ }
114
+ if (lower === "'strict-dynamic'") {
115
+ strictDynamic = true;
116
+ continue;
117
+ }
118
+ const nonce = /^'nonce-([^']+)'$/i.exec(raw);
119
+ if (nonce) {
120
+ hasNonceOrHash = true;
121
+ if (script.nonce !== null && script.nonce === nonce[1])
122
+ return true;
123
+ continue;
124
+ }
125
+ const hash = /^'(sha256|sha384|sha512)-([^']+)'$/i.exec(raw);
126
+ if (hash) {
127
+ hasNonceOrHash = true;
128
+ const digest = createHash(hash[1].toLowerCase()).update(script.content, 'utf8').digest('base64');
129
+ if (normB64(digest) === normB64(hash[2]))
130
+ return true;
131
+ }
132
+ }
133
+ return unsafeInline && !hasNonceOrHash && !strictDynamic;
134
+ }
135
+ // ─── HTML ────────────────────────────────────────────────────────────────────
136
+ function decodeEntities(v) {
137
+ return v.replace(/&(#x[0-9a-f]+|#\d+|quot|apos|amp|lt|gt);/gi, (m, code) => {
138
+ const c = code.toLowerCase();
139
+ if (c === 'quot')
140
+ return '"';
141
+ if (c === 'apos')
142
+ return "'";
143
+ if (c === 'amp')
144
+ return '&';
145
+ if (c === 'lt')
146
+ return '<';
147
+ if (c === 'gt')
148
+ return '>';
149
+ const n = c.startsWith('#x') ? parseInt(c.slice(2), 16) : parseInt(c.slice(1), 10);
150
+ return Number.isFinite(n) && n >= 0 && n <= 0x10ffff ? String.fromCodePoint(n) : m;
151
+ });
152
+ }
153
+ /** Atributos de uma tag (nome minúsculo → valor decodificado; sem valor → ''). */
154
+ function parseAttributes(attrText) {
155
+ const attrs = new Map();
156
+ const re = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
157
+ let m;
158
+ while ((m = re.exec(attrText)) !== null) {
159
+ const name = m[1].toLowerCase();
160
+ if (attrs.has(name))
161
+ continue;
162
+ attrs.set(name, decodeEntities(m[2] ?? m[3] ?? m[4] ?? ''));
163
+ }
164
+ return attrs;
165
+ }
166
+ /** MIME de JavaScript (HTML "JavaScript MIME type essence match"). */
167
+ const JS_MIME_ESSENCES = new Set([
168
+ 'application/ecmascript',
169
+ 'application/javascript',
170
+ 'application/x-ecmascript',
171
+ 'application/x-javascript',
172
+ 'text/ecmascript',
173
+ 'text/javascript',
174
+ 'text/javascript1.0',
175
+ 'text/javascript1.1',
176
+ 'text/javascript1.2',
177
+ 'text/javascript1.3',
178
+ 'text/javascript1.4',
179
+ 'text/javascript1.5',
180
+ 'text/jscript',
181
+ 'text/livescript',
182
+ 'text/x-ecmascript',
183
+ 'text/x-javascript',
184
+ ]);
185
+ /**
186
+ * O `type` deste `<script>` é executado (e por isso passa pela CSP)? Sem type,
187
+ * MIME de JS, `module` ou `importmap` → sim. `application/json`,
188
+ * `application/ld+json` e afins são blocos de dados: o navegador não executa
189
+ * nem confere na CSP. PURA.
190
+ */
191
+ function isExecutableScriptType(type) {
192
+ if (type === undefined)
193
+ return true;
194
+ const t = type.trim().toLowerCase();
195
+ if (t === '' || t === 'module' || t === 'importmap')
196
+ return true;
197
+ return JS_MIME_ESSENCES.has(t.split(';')[0].trim());
198
+ }
199
+ /** Scripts embutidos executáveis do HTML (sem `src`, conteúdo não vazio). PURA. */
200
+ export function extractInlineScripts(html) {
201
+ const noComments = html.replace(/<!--[\s\S]*?-->/g, '');
202
+ const out = [];
203
+ const re = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
204
+ let m;
205
+ while ((m = re.exec(noComments)) !== null) {
206
+ const attrs = parseAttributes(m[1]);
207
+ if (attrs.has('src'))
208
+ continue;
209
+ if (!isExecutableScriptType(attrs.get('type')))
210
+ continue;
211
+ const content = m[2];
212
+ if (content.trim() === '')
213
+ continue;
214
+ out.push({ nonce: attrs.has('nonce') ? attrs.get('nonce') : null, content });
215
+ }
216
+ return out;
217
+ }
218
+ /** Políticas de `<meta http-equiv="Content-Security-Policy" content="…">`. PURA. */
219
+ export function extractMetaCspPolicies(html) {
220
+ const noComments = html.replace(/<!--[\s\S]*?-->/g, '');
221
+ const out = [];
222
+ const re = /<meta\b([^>]*)>/gi;
223
+ let m;
224
+ while ((m = re.exec(noComments)) !== null) {
225
+ const attrs = parseAttributes(m[1]);
226
+ if ((attrs.get('http-equiv') ?? '').trim().toLowerCase() !== 'content-security-policy')
227
+ continue;
228
+ const content = attrs.get('content');
229
+ if (!content)
230
+ continue;
231
+ // Meta traz UMA política (CSP3 §3.3): vírgula não separa políticas aqui.
232
+ const policy = parseCspPolicy(content);
233
+ if (policy.directives.size > 0)
234
+ out.push(policy);
235
+ }
236
+ return out;
237
+ }
238
+ /**
239
+ * Quantos scripts embutidos a CSP efetiva bloqueia. Um script só roda se TODAS
240
+ * as políticas em vigor (cabeçalhos + meta) deixarem. PURA.
241
+ */
242
+ export function analyzeInlineScriptCsp(input) {
243
+ const policies = [
244
+ ...input.cspHeaderValues.flatMap((v) => parseCspHeader(v)),
245
+ ...extractMetaCspPolicies(input.html),
246
+ ];
247
+ const scripts = extractInlineScripts(input.html);
248
+ let blocked = 0;
249
+ let blockingDirective = null;
250
+ for (const script of scripts) {
251
+ for (const policy of policies) {
252
+ const governing = scriptElementSources(policy);
253
+ if (!governing)
254
+ continue;
255
+ if (inlineScriptAllowed(script, governing.sources))
256
+ continue;
257
+ blocked += 1;
258
+ if (blockingDirective === null) {
259
+ const text = [governing.name, ...governing.sources].join(' ');
260
+ blockingDirective =
261
+ text.length > DIRECTIVE_MAX_CHARS ? `${text.slice(0, DIRECTIVE_MAX_CHARS)}…` : text;
262
+ }
263
+ break;
264
+ }
265
+ }
266
+ return { inlineScripts: scripts.length, blockedInlineScripts: blocked, blockingDirective };
267
+ }
268
+ function looksLikeHtml(contentType, body) {
269
+ if (contentType) {
270
+ const essence = contentType.split(';')[0].trim().toLowerCase();
271
+ return essence === 'text/html' || essence === 'application/xhtml+xml';
272
+ }
273
+ return /^\s*(<!doctype html|<html[\s>])/i.test(body);
274
+ }
275
+ function unverifiable(url, httpStatus, reason) {
276
+ return {
277
+ verdict: 'unverifiable',
278
+ url,
279
+ httpStatus,
280
+ inlineScripts: 0,
281
+ blockedInlineScripts: 0,
282
+ blockingDirective: null,
283
+ reason,
284
+ };
285
+ }
286
+ /**
287
+ * Classifica a resposta da página inicial. PURA.
288
+ * - rede/não-2xx/não-HTML → `unverifiable` (não é prova de nada);
289
+ * - HTML com script embutido que a CSP bloqueia → `inline_scripts_blocked`;
290
+ * - o resto (sem CSP, CSP que libera, sem script embutido) → `ok`.
291
+ */
292
+ export function classifyRenderProbe(url, input) {
293
+ if (input.kind === 'network_error')
294
+ return unverifiable(url, null, input.error);
295
+ const { httpStatus } = input;
296
+ if (httpStatus < 200 || httpStatus >= 300)
297
+ return unverifiable(url, httpStatus, `HTTP ${httpStatus}`);
298
+ if (!looksLikeHtml(input.contentType, input.html)) {
299
+ return unverifiable(url, httpStatus, `não é HTML (${input.contentType ?? 'sem content-type'})`);
300
+ }
301
+ const analysis = analyzeInlineScriptCsp({ cspHeaderValues: input.cspHeaderValues, html: input.html });
302
+ return {
303
+ verdict: analysis.blockedInlineScripts > 0 ? 'inline_scripts_blocked' : 'ok',
304
+ url,
305
+ httpStatus,
306
+ inlineScripts: analysis.inlineScripts,
307
+ blockedInlineScripts: analysis.blockedInlineScripts,
308
+ blockingDirective: analysis.blockingDirective,
309
+ reason: null,
310
+ };
311
+ }
312
+ // ─── I/O (injetado) ──────────────────────────────────────────────────────────
313
+ /**
314
+ * Lê o corpo até `maxBytes` (cancela o stream acima disso). Mesmo leitor do
315
+ * Core (`readBodyTextCapped`). Lança se o stream falhar — quem chama trata.
316
+ */
317
+ async function readBodyTextCapped(body, maxBytes) {
318
+ if (!body)
319
+ return '';
320
+ const reader = body.getReader();
321
+ const parts = [];
322
+ let total = 0;
323
+ let truncated = false;
324
+ try {
325
+ for (;;) {
326
+ const { done, value } = await reader.read();
327
+ if (done)
328
+ break;
329
+ if (!value || value.byteLength === 0)
330
+ continue;
331
+ const room = maxBytes - total;
332
+ if (value.byteLength > room) {
333
+ if (room > 0)
334
+ parts.push(value.subarray(0, room));
335
+ total += Math.max(0, room);
336
+ truncated = true;
337
+ break;
338
+ }
339
+ parts.push(value);
340
+ total += value.byteLength;
341
+ }
342
+ }
343
+ finally {
344
+ if (truncated) {
345
+ try {
346
+ await reader.cancel();
347
+ }
348
+ catch {
349
+ // stream já encerrado — nada a fazer
350
+ }
351
+ }
352
+ }
353
+ const joined = new Uint8Array(total);
354
+ let offset = 0;
355
+ for (const part of parts) {
356
+ joined.set(part, offset);
357
+ offset += part.byteLength;
358
+ }
359
+ return new TextDecoder().decode(joined);
360
+ }
361
+ /** URL da página inicial do domínio publicado. `null` se o domínio não forma URL. PURA. */
362
+ export function buildRenderProbeUrl(domain) {
363
+ try {
364
+ const u = new URL(`https://${domain}/`);
365
+ return `${u.origin}/`;
366
+ }
367
+ catch {
368
+ return null;
369
+ }
370
+ }
371
+ /**
372
+ * Abre a página inicial e classifica. Uma tentativa por salto (a saúde acabou
373
+ * de passar — não é hora de esperar app subir). `redirect:'manual'`: segue no
374
+ * máximo `RENDER_MAX_REDIRECTS` saltos e só na MESMA origem; redirect pra fora
375
+ * (login de terceiro, outro host) → `unverifiable`. Nunca lança.
376
+ */
377
+ export async function runRenderProbe(url, deps) {
378
+ const maxRedirects = deps.maxRedirects ?? RENDER_MAX_REDIRECTS;
379
+ let current = url;
380
+ const origin = (() => {
381
+ try {
382
+ return new URL(url).origin;
383
+ }
384
+ catch {
385
+ return null;
386
+ }
387
+ })();
388
+ if (!origin)
389
+ return unverifiable(url, null, 'URL inválida');
390
+ for (let hop = 0; hop <= maxRedirects; hop += 1) {
391
+ // `null` = este salto foi redirect (segue em `location`).
392
+ let input = null;
393
+ let location = null;
394
+ try {
395
+ const controller = new AbortController();
396
+ const timer = setTimeout(() => controller.abort(), deps.timeoutMs);
397
+ try {
398
+ const res = await deps.fetchImpl(current, {
399
+ method: 'GET',
400
+ redirect: 'manual',
401
+ signal: controller.signal,
402
+ headers: {
403
+ 'cache-control': 'no-cache',
404
+ accept: 'text/html,application/xhtml+xml',
405
+ },
406
+ });
407
+ if (!res || typeof res.status !== 'number') {
408
+ return unverifiable(current, null, 'resposta vazia');
409
+ }
410
+ if (res.status >= 300 && res.status < 400) {
411
+ location = res.headers.get('location');
412
+ try {
413
+ await res.body?.cancel();
414
+ }
415
+ catch {
416
+ // corpo já fechado
417
+ }
418
+ if (!location)
419
+ return unverifiable(current, res.status, `HTTP ${res.status} sem Location`);
420
+ }
421
+ else {
422
+ const csp = res.headers.get('content-security-policy');
423
+ const html = await readBodyTextCapped(res.body, RENDER_BODY_MAX_BYTES);
424
+ input = {
425
+ kind: 'response',
426
+ httpStatus: res.status,
427
+ contentType: res.headers.get('content-type'),
428
+ cspHeaderValues: csp ? [csp] : [],
429
+ html,
430
+ };
431
+ }
432
+ }
433
+ finally {
434
+ clearTimeout(timer);
435
+ }
436
+ }
437
+ catch (err) {
438
+ const msg = err instanceof Error ? err.message : String(err);
439
+ return classifyRenderProbe(current, {
440
+ kind: 'network_error',
441
+ error: msg.toLowerCase().includes('abort') ? `timeout após ${deps.timeoutMs}ms` : msg,
442
+ });
443
+ }
444
+ if (input !== null)
445
+ return classifyRenderProbe(current, input);
446
+ if (location === null)
447
+ return unverifiable(current, null, 'resposta sem corpo nem Location');
448
+ let next;
449
+ try {
450
+ next = new URL(location, current);
451
+ }
452
+ catch {
453
+ return unverifiable(current, null, 'Location inválido');
454
+ }
455
+ if (next.origin !== origin)
456
+ return unverifiable(current, null, `redirect para outra origem (${next.origin})`);
457
+ current = next.toString();
458
+ }
459
+ return unverifiable(current, null, `mais de ${maxRedirects} redirects`);
460
+ }
461
+ // ─── texto ───────────────────────────────────────────────────────────────────
462
+ /** O que foi bloqueado, em uma frase (vai no `detail` do JSON e na tela). PURA. */
463
+ export function describeRenderBlock(render) {
464
+ return (`${render.blockedInlineScripts} de ${render.inlineScripts} script(s) embutido(s) da página ` +
465
+ `${render.url} bloqueado(s) pela Content-Security-Policy` +
466
+ (render.blockingDirective ? ` (${render.blockingDirective})` : ''));
467
+ }
468
+ /**
469
+ * O efeito e onde se conserta — mesma frase do alerta do Core
470
+ * (`renderBlockedExplanation`), sem o prefixo da saúde. PURA.
471
+ */
472
+ export const RENDER_BLOCKED_FIX = 'Sem esses scripts o Next não hidrata e a tela some para todo visitante. ' +
473
+ 'Conserto no produto: no cabeçalho Content-Security-Policy (next.config, headers()), o script-src ' +
474
+ "precisa de nonce por requisição (recomendado) ou 'unsafe-inline'.";
475
+ //# sourceMappingURL=deploy-render-probe.js.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Deploy na VM: um app por produto e ambiente — o que a CLI diz ao dev.
3
+ *
4
+ * ORIGEM — bug_6d92ef71. A descrição tinha duas metades:
5
+ *
6
+ * 1. `neetru deploy --target vm --port 3001` descartava a porta sem avisar.
7
+ * Consertado antes (P6, f3a9611, d327d30): a CLI avisa que o Core escolhe
8
+ * a porta.
9
+ * 2. "não fica claro como subir dois processos do mesmo produto (ex.: web e
10
+ * API em subdomínios diferentes) na mesma VM: dois deploys --product=X
11
+ * --env prod provavelmente se substituem."
12
+ *
13
+ * A metade 2 foi MEDIDA no Core (vistoria de 22/10) e é verdadeira:
14
+ * - `appKeyOf(productId, environmentId)` (src/lib/ports/port-registry.ts) é a
15
+ * identidade do app na VM — produto + ambiente, nada mais;
16
+ * - `ensureStableProxyPort` (src/lib/actions/_vm-deploy-core.ts) reusa a
17
+ * MESMA porta para o mesmo par, e o deploy novo troca o app que está nela;
18
+ * - `syncEnvironmentSubdomainPortCore` (src/lib/actions/_product-subdomains-cores.ts)
19
+ * guarda UM endereço por (produto, ambiente) e o aponta para a porta e o
20
+ * servidor do último deploy;
21
+ * - o deploy da CLI não tem flag para nomear um segundo app do mesmo produto.
22
+ *
23
+ * O aviso do conserto 1 confirmava a limitação e não dava saída: quem passa
24
+ * `--port` quase sempre quer subir um SEGUNDO app, e depois do aviso seguia
25
+ * sem caminho. Aqui fica a resposta, escrita uma vez e usada nos dois lugares
26
+ * onde o dev a encontra: o aviso do `--port` (`runDeploy`) e a ajuda do
27
+ * `neetru deploy --help` (`index.ts`).
28
+ *
29
+ * O caminho que existe hoje, medido no Core:
30
+ * - dois apps = dois produtos. `POST /api/cli/v1/products`
31
+ * (`neetru products create`) exige papel admin ou operator; cada produto tem
32
+ * endereço próprio (um por produto e ambiente), banco próprio (banco por
33
+ * produto) e segredos próprios;
34
+ * - o deploy do produto novo exige que a CHAVE da CLI o alcance. A rota de
35
+ * deploy (`/api/cli/v1/deploy`) devolve 403 `forbidden_product` quando
36
+ * `cliPrincipalCanAccessProduct` nega: admin passa sempre; as outras chaves
37
+ * só passam com `'*'` ou o produto em `allowedProducts`. Chave de operator
38
+ * nasce com escopo de produto EXPLÍCITO (`createCliApiKey` recusa operator
39
+ * sem `allowedProducts`), o `neetru auth login` só emite chave (`'*'`) para
40
+ * admin, e nenhuma ação de `cli-keys.ts` (criar, listar, revogar) muda o
41
+ * escopo de uma chave existente. Logo o texto diz o que é certo — admin
42
+ * alcança todos; chave com escopo, só os dela, e o deploy do novo volta 403
43
+ * e fica com um admin — sem prometer um jeito de ampliar a chave que o Core
44
+ * não tem. Sem esta linha, um operator criava o produto e batia no 403 no
45
+ * primeiro deploy dele, longe da causa (rodada 3, medido no Core dc45be28);
46
+ * - se os dois precisam do MESMO banco, separar em dois produtos não serve
47
+ * (o banco é isolado por produto): a API fica dentro do próprio app.
48
+ * - `--target cloud-run` NÃO é saída para o mesmo produto e ambiente: o
49
+ * ambiente tem um destino só (`_vm-deploy-core.ts` recusa ambiente cujo
50
+ * destino não é `vm`), então não aparece no texto.
51
+ *
52
+ * Módulo sem dependência: o `index.ts` o importa no topo para a ajuda, e o
53
+ * resto do deploy continua carregado sob demanda.
54
+ */
55
+ /**
56
+ * Aviso de `neetru deploy --target vm --port <n>`: a porta é descartada, e o
57
+ * dev fica sabendo por que e o que fazer se queria um segundo app.
58
+ * Continuação recuada para alinhar com o `⚠` do `log.warn`.
59
+ */
60
+ export declare const AVISO_PORTA_IGNORADA_NA_VM: string;
61
+ /** Bloco da ajuda de `neetru deploy --help` (`addHelpText('after', …)`). */
62
+ export declare const AJUDA_DEPLOY_NA_VM: string;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Deploy na VM: um app por produto e ambiente — o que a CLI diz ao dev.
3
+ *
4
+ * ORIGEM — bug_6d92ef71. A descrição tinha duas metades:
5
+ *
6
+ * 1. `neetru deploy --target vm --port 3001` descartava a porta sem avisar.
7
+ * Consertado antes (P6, f3a9611, d327d30): a CLI avisa que o Core escolhe
8
+ * a porta.
9
+ * 2. "não fica claro como subir dois processos do mesmo produto (ex.: web e
10
+ * API em subdomínios diferentes) na mesma VM: dois deploys --product=X
11
+ * --env prod provavelmente se substituem."
12
+ *
13
+ * A metade 2 foi MEDIDA no Core (vistoria de 22/10) e é verdadeira:
14
+ * - `appKeyOf(productId, environmentId)` (src/lib/ports/port-registry.ts) é a
15
+ * identidade do app na VM — produto + ambiente, nada mais;
16
+ * - `ensureStableProxyPort` (src/lib/actions/_vm-deploy-core.ts) reusa a
17
+ * MESMA porta para o mesmo par, e o deploy novo troca o app que está nela;
18
+ * - `syncEnvironmentSubdomainPortCore` (src/lib/actions/_product-subdomains-cores.ts)
19
+ * guarda UM endereço por (produto, ambiente) e o aponta para a porta e o
20
+ * servidor do último deploy;
21
+ * - o deploy da CLI não tem flag para nomear um segundo app do mesmo produto.
22
+ *
23
+ * O aviso do conserto 1 confirmava a limitação e não dava saída: quem passa
24
+ * `--port` quase sempre quer subir um SEGUNDO app, e depois do aviso seguia
25
+ * sem caminho. Aqui fica a resposta, escrita uma vez e usada nos dois lugares
26
+ * onde o dev a encontra: o aviso do `--port` (`runDeploy`) e a ajuda do
27
+ * `neetru deploy --help` (`index.ts`).
28
+ *
29
+ * O caminho que existe hoje, medido no Core:
30
+ * - dois apps = dois produtos. `POST /api/cli/v1/products`
31
+ * (`neetru products create`) exige papel admin ou operator; cada produto tem
32
+ * endereço próprio (um por produto e ambiente), banco próprio (banco por
33
+ * produto) e segredos próprios;
34
+ * - o deploy do produto novo exige que a CHAVE da CLI o alcance. A rota de
35
+ * deploy (`/api/cli/v1/deploy`) devolve 403 `forbidden_product` quando
36
+ * `cliPrincipalCanAccessProduct` nega: admin passa sempre; as outras chaves
37
+ * só passam com `'*'` ou o produto em `allowedProducts`. Chave de operator
38
+ * nasce com escopo de produto EXPLÍCITO (`createCliApiKey` recusa operator
39
+ * sem `allowedProducts`), o `neetru auth login` só emite chave (`'*'`) para
40
+ * admin, e nenhuma ação de `cli-keys.ts` (criar, listar, revogar) muda o
41
+ * escopo de uma chave existente. Logo o texto diz o que é certo — admin
42
+ * alcança todos; chave com escopo, só os dela, e o deploy do novo volta 403
43
+ * e fica com um admin — sem prometer um jeito de ampliar a chave que o Core
44
+ * não tem. Sem esta linha, um operator criava o produto e batia no 403 no
45
+ * primeiro deploy dele, longe da causa (rodada 3, medido no Core dc45be28);
46
+ * - se os dois precisam do MESMO banco, separar em dois produtos não serve
47
+ * (o banco é isolado por produto): a API fica dentro do próprio app.
48
+ * - `--target cloud-run` NÃO é saída para o mesmo produto e ambiente: o
49
+ * ambiente tem um destino só (`_vm-deploy-core.ts` recusa ambiente cujo
50
+ * destino não é `vm`), então não aparece no texto.
51
+ *
52
+ * Módulo sem dependência: o `index.ts` o importa no topo para a ajuda, e o
53
+ * resto do deploy continua carregado sob demanda.
54
+ */
55
+ /** A explicação do modelo e da saída, em linhas. Fonte única dos dois textos abaixo. */
56
+ const UM_APP_POR_AMBIENTE = [
57
+ 'Cada produto tem UM app por ambiente na VM: um novo deploy do mesmo produto no mesmo ambiente',
58
+ 'substitui o app que está no ar (o endereço é o mesmo), em vez de subir um segundo ao lado.',
59
+ 'Dois apps (ex.: site e API em endereços diferentes) são dois produtos: cadastre o segundo com',
60
+ '`neetru products create` (exige papel admin ou operator) e faça o deploy dele com --product <slug>.',
61
+ 'A chave da CLI precisa alcançar o produto novo: a de admin alcança todos; uma chave com escopo',
62
+ 'de produto, só os dela — o deploy do novo volta 403 (forbidden_product) e fica com um admin.',
63
+ 'Cada produto tem endereço, banco e segredos próprios. Se os dois precisam do mesmo banco,',
64
+ 'sirva a API pelo próprio app (ex.: rotas /api), no mesmo endereço.',
65
+ ];
66
+ /**
67
+ * Aviso de `neetru deploy --target vm --port <n>`: a porta é descartada, e o
68
+ * dev fica sabendo por que e o que fazer se queria um segundo app.
69
+ * Continuação recuada para alinhar com o `⚠` do `log.warn`.
70
+ */
71
+ export const AVISO_PORTA_IGNORADA_NA_VM = [
72
+ '--port é ignorado em deploy na VM: quem escolhe a porta é o Core.',
73
+ ...UM_APP_POR_AMBIENTE,
74
+ ].join('\n ');
75
+ /** Bloco da ajuda de `neetru deploy --help` (`addHelpText('after', …)`). */
76
+ export const AJUDA_DEPLOY_NA_VM = [
77
+ '',
78
+ 'Na VM (--target vm):',
79
+ ' A porta é escolhida pelo Core; --port é ignorado.',
80
+ ...UM_APP_POR_AMBIENTE.map((linha) => ` ${linha}`),
81
+ '',
82
+ ].join('\n');
83
+ //# sourceMappingURL=deploy-vm-um-app.js.map
@@ -1,10 +1,25 @@
1
1
  export { CARACTERE_DE_TROCA, CodificacaoInvalida, decodificarUtf8, semBomInicial, type OrigemDosBytes, } from './texto-utf8.js';
2
2
  /**
3
- * Aviso de que o segredo registrado ainda não chega ao app na VM. Com data:
4
- * quando o agente for liberado, a frase fica visivelmente velha em vez de
5
- * mentir calada. Mesmo fato que o Core declara no `assertNoSecretEnvVars`.
3
+ * Primeira versão PUBLICADA do agente da VM que entrega os segredos do produto
4
+ * ao app (Parte 2 de bug_8b85353b, agent#87): com `productEnv: true` no
5
+ * `agent.deploy`, ele busca os valores no Core e grava
6
+ * `/etc/neetru/<slug>.product.env` (root, 0600), lido pelo unit do app.
7
+ *
8
+ * Por que 1.6.80 e não 1.6.79: o CHANGELOG do agente põe o recurso na 1.6.79,
9
+ * mas essa versão foi declarada e nunca publicada; `agent_releases/1.6.80`
10
+ * (stable) saiu em 2026-10-20 e as duas VMs operacionais (snee-002-multi e
11
+ * snee-003-db) reportam 1.6.80 desde 2026-10-22 (medido na vistoria de 22/10).
12
+ * Agente mais velho ignora `productEnv` calado — por isso a versão vai na nota.
13
+ */
14
+ export declare const AGENTE_MINIMO_SEGREDO_DO_PRODUTO = "1.6.80";
15
+ /** Nota depois de `neetru secrets set` gravar: o app só vê a gravação no PRÓXIMO deploy. */
16
+ export declare function notaEntregaDoSegredo(ambiente: string): string;
17
+ /**
18
+ * Nota do `neetru deploy --target vm` depois de descartar do `--env-file`
19
+ * chave(s) com cara de segredo que o registro aceita. `env` é o `--env` cru do
20
+ * deploy: sai no nome canônico que o Core aceita, ou `<ambiente>`.
6
21
  */
7
- export declare const AVISO_AGENTE_PENDENTE: string;
22
+ export declare function notaSegredoDescartadoNoDeploy(env: string | undefined): string;
8
23
  /**
9
24
  * O comando que registra `chave` no ambiente do deploy. `env` é o `--env` cru
10
25
  * do deploy (`dev`, `production`…): sai no nome canônico que o Core aceita, ou
@@ -57,8 +72,13 @@ export declare function problemaNaChaveReservada(chave: string): string | null;
57
72
  * Por que a chave é recusada, ou `null` se serve. NUNCA repete o que foi
58
73
  * digitado: `KEY=valor` e o valor colado no lugar do nome são os dois erros
59
74
  * mais prováveis, e ecoar a entrada imprimiria o segredo.
75
+ *
76
+ * `comoMandarOValor`: por onde o valor entra NO COMANDO que chamou (o `set`
77
+ * passa "por --stdin, --from-file ou pelo pedido na tela"). bug_89d2497e
78
+ * (etapa 1): o texto citava essas flags a todo chamador, e o `secrets archive`
79
+ * — que não recebe valor nem declara as flags — também o mostrava.
60
80
  */
61
- export declare function problemaNaChave(chave: string | undefined): string | null;
81
+ export declare function problemaNaChave(chave: string | undefined, comoMandarOValor?: string): string | null;
62
82
  /**
63
83
  * Por que o valor é recusado, ou `null` se serve. Mesma regra do Core e do
64
84
  * agente: o valor vira uma linha de `EnvironmentFile` do systemd, e o agente