@microteia/console 2.7.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 (85) hide show
  1. package/CONSOLE-VERSIONS.md +85 -0
  2. package/README.md +64 -0
  3. package/lib/console/backendEnv.d.ts +52 -0
  4. package/lib/console/backendEnv.js +118 -0
  5. package/lib/console/backendIndexes.d.ts +40 -0
  6. package/lib/console/backendIndexes.js +178 -0
  7. package/lib/console/backendParameters.d.ts +48 -0
  8. package/lib/console/backendParameters.js +100 -0
  9. package/lib/console/callableAccess.d.ts +39 -0
  10. package/lib/console/callableAccess.js +61 -0
  11. package/lib/console/channel.d.ts +49 -0
  12. package/lib/console/channel.js +145 -0
  13. package/lib/console/connect.d.ts +2 -0
  14. package/lib/console/connect.js +74 -0
  15. package/lib/console/customFunctions.d.ts +120 -0
  16. package/lib/console/customFunctions.js +298 -0
  17. package/lib/console/deploy.d.ts +2 -0
  18. package/lib/console/deploy.js +261 -0
  19. package/lib/console/deployFunctions.d.ts +45 -0
  20. package/lib/console/deployFunctions.js +204 -0
  21. package/lib/console/environment.d.ts +62 -0
  22. package/lib/console/environment.js +151 -0
  23. package/lib/console/firebaseCli.d.ts +15 -0
  24. package/lib/console/firebaseCli.js +90 -0
  25. package/lib/console/firestoreRest.d.ts +39 -0
  26. package/lib/console/firestoreRest.js +131 -0
  27. package/lib/console/hostingCheck.d.ts +29 -0
  28. package/lib/console/hostingCheck.js +44 -0
  29. package/lib/console/init.d.ts +2 -0
  30. package/lib/console/init.js +60 -0
  31. package/lib/console/pluginWorkspace.d.ts +48 -0
  32. package/lib/console/pluginWorkspace.js +148 -0
  33. package/lib/console/provision.d.ts +70 -0
  34. package/lib/console/provision.js +478 -0
  35. package/lib/console/regionOrphans.d.ts +43 -0
  36. package/lib/console/regionOrphans.js +99 -0
  37. package/lib/console/runtimeConfigShape.d.ts +25 -0
  38. package/lib/console/runtimeConfigShape.js +35 -0
  39. package/lib/console/unpack.d.ts +9 -0
  40. package/lib/console/unpack.js +333 -0
  41. package/lib/console/versions.d.ts +10 -0
  42. package/lib/console/versions.js +72 -0
  43. package/lib/console/workspace.d.ts +19 -0
  44. package/lib/console/workspace.js +33 -0
  45. package/lib/index.d.ts +2 -0
  46. package/lib/index.js +136 -0
  47. package/lib/kernel/backendShape.d.ts +74 -0
  48. package/lib/kernel/backendShape.js +91 -0
  49. package/lib/kernel/channel.d.ts +79 -0
  50. package/lib/kernel/channel.js +157 -0
  51. package/lib/kernel/manifest.d.ts +138 -0
  52. package/lib/kernel/manifest.js +145 -0
  53. package/lib/shared/args.d.ts +14 -0
  54. package/lib/shared/args.js +44 -0
  55. package/lib/shared/digest.d.ts +30 -0
  56. package/lib/shared/digest.js +34 -0
  57. package/lib/shared/paths.d.ts +90 -0
  58. package/lib/shared/paths.js +97 -0
  59. package/lib/shared/run.d.ts +28 -0
  60. package/lib/shared/run.js +49 -0
  61. package/lib/shared/ui.d.ts +23 -0
  62. package/lib/shared/ui.js +43 -0
  63. package/lib/vendor/authoringGuide.d.ts +19 -0
  64. package/lib/vendor/authoringGuide.js +284 -0
  65. package/lib/vendor/bundleAudit.d.ts +19 -0
  66. package/lib/vendor/bundleAudit.js +95 -0
  67. package/lib/vendor/changelogs.d.ts +62 -0
  68. package/lib/vendor/changelogs.js +131 -0
  69. package/lib/vendor/exampleSources.d.ts +29 -0
  70. package/lib/vendor/exampleSources.js +126 -0
  71. package/lib/vendor/installationDocs.d.ts +35 -0
  72. package/lib/vendor/installationDocs.js +243 -0
  73. package/lib/vendor/pack.d.ts +2 -0
  74. package/lib/vendor/pack.js +276 -0
  75. package/lib/vendor/packConsole.d.ts +13 -0
  76. package/lib/vendor/packConsole.js +63 -0
  77. package/lib/vendor/packageTemplates.d.ts +66 -0
  78. package/lib/vendor/packageTemplates.js +200 -0
  79. package/lib/vendor/pluginWorkspaceTemplates.d.ts +19 -0
  80. package/lib/vendor/pluginWorkspaceTemplates.js +29 -0
  81. package/lib/vendor/publish.d.ts +2 -0
  82. package/lib/vendor/publish.js +277 -0
  83. package/lib/vendor/repository.d.ts +9 -0
  84. package/lib/vendor/repository.js +26 -0
  85. package/package.json +21 -0
@@ -0,0 +1,100 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * O valor de um parâmetro de backend é carimbado no deploy de CADA function: o `firebase-functions`
6
+ * resolve os `defineString` e grava o resultado como variável de ambiente da function que está
7
+ * subindo. Quem preenche o `.env.<projectId>` DEPOIS da instalação não alcança function nenhuma — ela
8
+ * continua rodando com a string vazia que o `ensureOfficeEnv` escreveu, e a falha aparece só no runtime
9
+ * da feature.
10
+ *
11
+ * O provisionamento não notava isso porque o estado que ele guardava era só QUAIS functions subiram,
12
+ * nunca COM QUAIS VALORES. Este módulo é a outra metade: uma impressão digital por parâmetro no
13
+ * `microteia.yaml`, comparada a cada execução. Parâmetro que mudou torna obsoletas as functions da
14
+ * extensão que o declara, e elas voltam para a fila de deploy mesmo já estando aplicadas.
15
+ */
16
+ /**
17
+ * Os parâmetros PREENCHIDOS do arquivo de ambiente. Linha sem valor é ausência: é assim que o
18
+ * `ensureOfficeEnv` grava o que ainda falta, e tratá-la como valor faria toda instalação nova nascer
19
+ * com uma mudança pendente.
20
+ */
21
+ export const parseBackendEnv = (content) => {
22
+ const values = new Map();
23
+ for (const line of content.split('\n')) {
24
+ const trimmed = line.trim();
25
+ if (trimmed === '' || trimmed.startsWith('#'))
26
+ continue;
27
+ const separator = trimmed.indexOf('=');
28
+ if (separator === -1)
29
+ continue;
30
+ const name = trimmed.slice(0, separator).trim();
31
+ const value = trimmed.slice(separator + 1).trim();
32
+ if (name === '' || value === '')
33
+ continue;
34
+ values.set(name, value);
35
+ }
36
+ return values;
37
+ };
38
+ /**
39
+ * Lê a FONTE: o `.env.<projectId>` ao lado do `microteia.yaml`. A cópia dentro de
40
+ * `.microteia/install/functions/` existe só porque é de lá que o `firebase-functions` lê — comparar
41
+ * contra ela seria comparar contra o que o último deploy carimbou, não contra o que a pessoa digitou.
42
+ */
43
+ export const readBackendEnv = async (root, projectId) => {
44
+ const content = await readFile(join(root, `.env.${projectId}`), 'utf8').catch(() => null);
45
+ return content === null ? new Map() : parseBackendEnv(content);
46
+ };
47
+ /**
48
+ * A impressão digital é SALGADA com o projeto e o nome do parâmetro. O `microteia.yaml` fica ao lado
49
+ * das Cloud Functions do escritório e não guarda credencial: um sha256 puro de um valor curto seria
50
+ * uma consulta de dicionário, e um salgado não é. Doze hexadecimais bastam para detectar mudança e
51
+ * não reconstroem valor nenhum.
52
+ */
53
+ export const parameterDigest = (projectId, name, value) => createHash('sha256').update(`${projectId} ${name} ${value}`).digest('hex').slice(0, 12);
54
+ export const parameterDigests = (projectId, values, tracked) => {
55
+ const digests = new Map();
56
+ for (const name of tracked) {
57
+ const value = values.get(name);
58
+ if (value === undefined)
59
+ continue;
60
+ digests.set(name, parameterDigest(projectId, name, value));
61
+ }
62
+ return digests;
63
+ };
64
+ /**
65
+ * `NOME:digest` separado por vírgula, no mesmo escalar de `backend.functions` e pelo mesmo motivo: o
66
+ * leitor de YAML daqui entende mapas de dois níveis e escalares, e trazer sequências para guardar meia
67
+ * dúzia de pares seria pagar caro pela generalidade.
68
+ */
69
+ export const parseParameterState = (raw) => {
70
+ const state = new Map();
71
+ for (const entry of (raw ?? '').split(',')) {
72
+ const trimmed = entry.trim();
73
+ if (trimmed === '')
74
+ continue;
75
+ const separator = trimmed.lastIndexOf(':');
76
+ if (separator <= 0)
77
+ continue;
78
+ state.set(trimmed.slice(0, separator), trimmed.slice(separator + 1));
79
+ }
80
+ return state;
81
+ };
82
+ export const serializeParameterState = (state) => {
83
+ const entries = [...state.entries()]
84
+ .sort(([left], [right]) => left.localeCompare(right))
85
+ .map(([name, digest]) => `${name}:${digest}`);
86
+ return entries.length === 0 ? null : entries.join(',');
87
+ };
88
+ /**
89
+ * O que mudou entre o que foi aplicado e o que está no arquivo agora — incluindo o parâmetro que
90
+ * ESVAZIOU, porque a function deployada continua carregando o valor antigo e convergir é reescrevê-la
91
+ * com o que o arquivo diz.
92
+ */
93
+ export const changedParameters = (recorded, current, tracked) => tracked.filter((name) => recorded.get(name) !== current.get(name)).sort();
94
+ /** As functions das extensões que declaram algum destes parâmetros. */
95
+ export const functionsDeclaringParameters = (claims, names) => {
96
+ const wanted = new Set(names);
97
+ return [...new Set(claims
98
+ .filter((claim) => (claim.backend.parameters ?? []).some((name) => wanted.has(name)))
99
+ .flatMap((claim) => claim.backend.functions ?? []))].sort();
100
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Uma callable no ar não é o mesmo que uma callable alcançável. O deploy concede a `allUsers` o papel
3
+ * `roles/run.invoker` em cada uma, mas a concessão pode ser RECUSADA — é o que uma política de
4
+ * Domain Restricted Sharing faz — e o `firebase deploy` termina com sucesso mesmo assim.
5
+ *
6
+ * O resultado é a pior forma de defeito: a aplicação abre, o login funciona, e toda chamada ao
7
+ * servidor falha no navegador como se fosse CORS. Parece problema de front, e manda procurar no lugar
8
+ * errado; o 403 do Cloud Run não carrega cabeçalho de CORS, e é só isso que o navegador reporta.
9
+ *
10
+ * Por isso a verificação é feita de fora, pelo mesmo caminho que o navegador usa: um preflight de
11
+ * verdade contra o endpoint da região. É a única prova que vale, porque é a única que reproduz a
12
+ * chamada que falharia.
13
+ */
14
+ export type CallableAccess = 'reachable' | 'blocked' | 'missing' | 'unknown';
15
+ export interface CallableCheck {
16
+ name: string;
17
+ access: CallableAccess;
18
+ status: number | null;
19
+ }
20
+ export interface CallableProbeOptions {
21
+ projectId: string;
22
+ region: string;
23
+ origin: string;
24
+ }
25
+ /** O host é por REGIÃO: fora da região do pacote, a callable simplesmente não existe neste endereço. */
26
+ export declare const callableEndpoint: (region: string, projectId: string, name: string) => string;
27
+ /**
28
+ * Uma function de 2ª geração é um serviço do Cloud Run, e lá ela aparece com o nome em MINÚSCULAS
29
+ * (`getUserInfo` → `getuserinfo`). Errar isso devolve "service not found", que parece indicar que a
30
+ * function não subiu.
31
+ */
32
+ export declare const invokerGrantCommand: (region: string, projectId: string, name: string) => string;
33
+ /**
34
+ * O 403 é a assinatura do IAM negando a invocação: o Cloud Run responde antes de a function existir
35
+ * para o mundo, e sem cabeçalho de CORS. Um 2xx sem `access-control-allow-origin` não conta como
36
+ * alcançável — é exatamente o caso em que o navegador recusa a resposta que o `curl` aceitaria.
37
+ */
38
+ export declare const accessFromPreflight: (status: number, allowOrigin: string | null) => CallableAccess;
39
+ export declare const checkCallables: (names: string[], options: CallableProbeOptions) => Promise<CallableCheck[]>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Uma callable no ar não é o mesmo que uma callable alcançável. O deploy concede a `allUsers` o papel
3
+ * `roles/run.invoker` em cada uma, mas a concessão pode ser RECUSADA — é o que uma política de
4
+ * Domain Restricted Sharing faz — e o `firebase deploy` termina com sucesso mesmo assim.
5
+ *
6
+ * O resultado é a pior forma de defeito: a aplicação abre, o login funciona, e toda chamada ao
7
+ * servidor falha no navegador como se fosse CORS. Parece problema de front, e manda procurar no lugar
8
+ * errado; o 403 do Cloud Run não carrega cabeçalho de CORS, e é só isso que o navegador reporta.
9
+ *
10
+ * Por isso a verificação é feita de fora, pelo mesmo caminho que o navegador usa: um preflight de
11
+ * verdade contra o endpoint da região. É a única prova que vale, porque é a única que reproduz a
12
+ * chamada que falharia.
13
+ */
14
+ const PROBE_TIMEOUT_MS = 10_000;
15
+ /** O host é por REGIÃO: fora da região do pacote, a callable simplesmente não existe neste endereço. */
16
+ export const callableEndpoint = (region, projectId, name) => `https://${region}-${projectId}.cloudfunctions.net/${name}`;
17
+ /**
18
+ * Uma function de 2ª geração é um serviço do Cloud Run, e lá ela aparece com o nome em MINÚSCULAS
19
+ * (`getUserInfo` → `getuserinfo`). Errar isso devolve "service not found", que parece indicar que a
20
+ * function não subiu.
21
+ */
22
+ export const invokerGrantCommand = (region, projectId, name) => `gcloud run services add-iam-policy-binding ${name.toLowerCase()}`
23
+ + ` --region=${region} --project=${projectId} --member=allUsers --role=roles/run.invoker`;
24
+ /**
25
+ * O 403 é a assinatura do IAM negando a invocação: o Cloud Run responde antes de a function existir
26
+ * para o mundo, e sem cabeçalho de CORS. Um 2xx sem `access-control-allow-origin` não conta como
27
+ * alcançável — é exatamente o caso em que o navegador recusa a resposta que o `curl` aceitaria.
28
+ */
29
+ export const accessFromPreflight = (status, allowOrigin) => {
30
+ if (status === 403)
31
+ return 'blocked';
32
+ if (status === 404)
33
+ return 'missing';
34
+ if (status >= 200 && status < 300)
35
+ return allowOrigin === null ? 'unknown' : 'reachable';
36
+ return 'unknown';
37
+ };
38
+ const probe = async (name, options) => {
39
+ try {
40
+ const response = await fetch(callableEndpoint(options.region, options.projectId, name), {
41
+ method: 'OPTIONS',
42
+ headers: {
43
+ Origin: options.origin,
44
+ 'Access-Control-Request-Method': 'POST',
45
+ 'Access-Control-Request-Headers': 'content-type',
46
+ },
47
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
48
+ });
49
+ return {
50
+ name,
51
+ access: accessFromPreflight(response.status, response.headers.get('access-control-allow-origin')),
52
+ status: response.status,
53
+ };
54
+ }
55
+ catch {
56
+ // Rede indisponível não é veredito: quem instala pode estar atrás de proxy, e acusar bloqueio
57
+ // aqui mandaria mexer em IAM que está correto.
58
+ return { name, access: 'unknown', status: null };
59
+ }
60
+ };
61
+ export const checkCallables = (names, options) => Promise.all(names.map((name) => probe(name, options)));
@@ -0,0 +1,49 @@
1
+ import type { ChannelEntry, ChannelIndex } from '../kernel/channel.js';
2
+ /**
3
+ * O lado que BAIXA. Uma responsabilidade: transformar "a versão que eu quero" num arquivo local que já
4
+ * foi verificado.
5
+ *
6
+ * Ele é opcional por construção. `microteia unpack --kernel <arquivo>.tgz` continua existindo e continua
7
+ * sendo o caminho de quem está numa rede fechada, ou de quem recebeu o pacote por outro meio — o canal
8
+ * não pode virar pré-requisito para instalar, porque a instalação já depende de coisas demais que não
9
+ * são nossas.
10
+ */
11
+ /**
12
+ * A URL do catálogo, e a razão de ela ser NOSSA e não a do host dos arquivos.
13
+ *
14
+ * Os tarballs ficam onde é barato hospedá-los, e as URLs deles são dado dentro do índice. Esta é a única
15
+ * que o Console conhece de cor, e é por isso que ela tem de estar num domínio que responda por si:
16
+ * mudar de hospedagem — ou passar a emitir URL assinada por escritório, que o host de hoje não faz —
17
+ * reescreve o índice, e um Console já instalado continua achando o catálogo no mesmo lugar.
18
+ */
19
+ export declare const DEFAULT_INDEX_URL = "https://dist.microteia.app/kernel/index.json";
20
+ export declare const indexUrl: () => string;
21
+ /**
22
+ * O cache fica FORA da instalação, de propósito: `.microteia/` é apagado por inteiro a cada troca de
23
+ * kernel, e um cache ali seria descartado exatamente no momento em que serve para algo (voltar à versão
24
+ * anterior sem baixá-la de novo).
25
+ */
26
+ export declare const cacheRoot: () => string;
27
+ export declare const fetchIndex: (url?: string) => Promise<ChannelIndex>;
28
+ export interface KernelDownload {
29
+ path: string;
30
+ entry: ChannelEntry;
31
+ /** `true` quando o arquivo já estava no cache e conferiu — o comando não usou a rede para baixá-lo. */
32
+ cached: boolean;
33
+ }
34
+ /**
35
+ * Resolve a versão pedida num arquivo local verificado, reusando o cache quando ele confere.
36
+ *
37
+ * Não há bandeira para ignorar o cache, e é deliberado: a entrada que não confere é apagada e baixada de
38
+ * novo aqui mesmo. Uma bandeira só serviria para quem já desconfia — e quem desconfia é justamente quem
39
+ * não vai lembrar de passá-la.
40
+ */
41
+ export declare const fetchKernel: (requested: string, url?: string) => Promise<KernelDownload>;
42
+ /**
43
+ * A versão mais nova que o canal oferece, ou `null` — e NUNCA uma exceção.
44
+ *
45
+ * Serve ao `--version`, que responde sobre a instalação e não sobre a rede: numa máquina sem acesso ao
46
+ * catálogo, perguntar a versão não pode falhar. O aviso é para o caso de alguém ter apontado
47
+ * `MICROTEIA_INDEX_URL` para um lugar errado e ficar sem entender por que nada é oferecido.
48
+ */
49
+ export declare const newerThan: (version: string | null) => Promise<ChannelEntry | null>;
@@ -0,0 +1,145 @@
1
+ import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { digestMismatch, digestOfFile } from '../shared/digest.js';
5
+ import { info, pending, success, warn } from '../shared/ui.js';
6
+ import { compareVersions } from '../kernel/manifest.js';
7
+ import { LATEST, kernelTarballName, parseIndex, resolveVersion } from '../kernel/channel.js';
8
+ /**
9
+ * O lado que BAIXA. Uma responsabilidade: transformar "a versão que eu quero" num arquivo local que já
10
+ * foi verificado.
11
+ *
12
+ * Ele é opcional por construção. `microteia unpack --kernel <arquivo>.tgz` continua existindo e continua
13
+ * sendo o caminho de quem está numa rede fechada, ou de quem recebeu o pacote por outro meio — o canal
14
+ * não pode virar pré-requisito para instalar, porque a instalação já depende de coisas demais que não
15
+ * são nossas.
16
+ */
17
+ /**
18
+ * A URL do catálogo, e a razão de ela ser NOSSA e não a do host dos arquivos.
19
+ *
20
+ * Os tarballs ficam onde é barato hospedá-los, e as URLs deles são dado dentro do índice. Esta é a única
21
+ * que o Console conhece de cor, e é por isso que ela tem de estar num domínio que responda por si:
22
+ * mudar de hospedagem — ou passar a emitir URL assinada por escritório, que o host de hoje não faz —
23
+ * reescreve o índice, e um Console já instalado continua achando o catálogo no mesmo lugar.
24
+ */
25
+ export const DEFAULT_INDEX_URL = 'https://dist.microteia.app/kernel/index.json';
26
+ export const indexUrl = () => {
27
+ const override = process.env.MICROTEIA_INDEX_URL;
28
+ return override === undefined || override === '' ? DEFAULT_INDEX_URL : override;
29
+ };
30
+ /**
31
+ * O cache fica FORA da instalação, de propósito: `.microteia/` é apagado por inteiro a cada troca de
32
+ * kernel, e um cache ali seria descartado exatamente no momento em que serve para algo (voltar à versão
33
+ * anterior sem baixá-la de novo).
34
+ */
35
+ export const cacheRoot = () => {
36
+ const override = process.env.MICROTEIA_CACHE;
37
+ return override === undefined || override === ''
38
+ ? join(homedir(), '.microteia', 'cache')
39
+ : override;
40
+ };
41
+ /** O catálogo é pequeno, e o timeout existe para que uma rede muda não pareça um comando travado. */
42
+ const INDEX_TIMEOUT_MS = 15_000;
43
+ const fetchText = async (url, timeoutMs) => {
44
+ const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
45
+ if (!response.ok) {
46
+ throw new Error(`${url} answered ${response.status} ${response.statusText}.`);
47
+ }
48
+ return response.text();
49
+ };
50
+ export const fetchIndex = async (url = indexUrl()) => {
51
+ const text = await fetchText(url, INDEX_TIMEOUT_MS).catch((error) => {
52
+ throw new Error(`Could not read the Microteia catalog at ${url}.\n`
53
+ + `${error instanceof Error ? error.message : String(error)}\n`
54
+ + 'If this machine has no access to it, install from a file: microteia unpack --kernel <package>.tgz');
55
+ });
56
+ return parseIndex(text, url);
57
+ };
58
+ const verified = async (path, entry) => {
59
+ const digest = await digestOfFile(path).catch(() => null);
60
+ if (digest === null)
61
+ return 'it is not there.';
62
+ return digestMismatch({ bytes: entry.bytes, sha256: entry.sha256 }, digest);
63
+ };
64
+ /**
65
+ * Baixa para um `.part` e só então renomeia.
66
+ *
67
+ * Escrever direto no nome final deixa, numa queda de rede, um arquivo com o nome certo e o conteúdo
68
+ * incompleto — e o `tar` recusa aquilo com uma mensagem sobre formato de arquivo, que manda procurar o
69
+ * defeito no pacote. Um `.part` que sobrou não é confundido com nada.
70
+ */
71
+ const download = async (entry, target) => {
72
+ const part = `${target}.part`;
73
+ await rm(part, { force: true });
74
+ const response = await fetch(entry.tarball);
75
+ if (!response.ok) {
76
+ throw new Error(`${entry.tarball} answered ${response.status} ${response.statusText}.\n`
77
+ + 'The catalog points at this file, so either the release was removed or the catalog is ahead of it.');
78
+ }
79
+ // Em memória, e não por stream: o artefato é de um dígito de MB, e o que se ganharia em bytes
80
+ // residentes se pagaria em duas conversões de tipo entre o corpo web e o stream do Node.
81
+ await writeFile(part, Buffer.from(await response.arrayBuffer()));
82
+ await rename(part, target);
83
+ };
84
+ /**
85
+ * Resolve a versão pedida num arquivo local verificado, reusando o cache quando ele confere.
86
+ *
87
+ * Não há bandeira para ignorar o cache, e é deliberado: a entrada que não confere é apagada e baixada de
88
+ * novo aqui mesmo. Uma bandeira só serviria para quem já desconfia — e quem desconfia é justamente quem
89
+ * não vai lembrar de passá-la.
90
+ */
91
+ export const fetchKernel = async (requested, url = indexUrl()) => {
92
+ const index = await fetchIndex(url);
93
+ const entry = resolveVersion(index, requested);
94
+ const directory = join(cacheRoot(), 'kernel', entry.version);
95
+ const target = join(directory, kernelTarballName(entry.version));
96
+ await mkdir(directory, { recursive: true });
97
+ const stale = await verified(target, entry);
98
+ if (stale === null) {
99
+ success(`kernel ${entry.version} is already in the cache, and it checks out`);
100
+ return { path: target, entry, cached: true };
101
+ }
102
+ pending(`downloading kernel ${entry.version} (${(entry.bytes / 1_000_000).toFixed(1)} MB)`);
103
+ await rm(target, { force: true });
104
+ await download(entry, target);
105
+ const problem = await verified(target, entry);
106
+ if (problem !== null) {
107
+ await rm(target, { force: true });
108
+ throw new Error(`What was downloaded is not the kernel ${entry.version} the catalog describes: ${problem}\n`
109
+ + `Source: ${entry.tarball}`);
110
+ }
111
+ success(`downloaded kernel ${entry.version} and verified its sha256`);
112
+ info(` cached in ${directory}`);
113
+ return { path: target, entry, cached: false };
114
+ };
115
+ /**
116
+ * A versão mais nova que o canal oferece, ou `null` — e NUNCA uma exceção.
117
+ *
118
+ * Serve ao `--version`, que responde sobre a instalação e não sobre a rede: numa máquina sem acesso ao
119
+ * catálogo, perguntar a versão não pode falhar. O aviso é para o caso de alguém ter apontado
120
+ * `MICROTEIA_INDEX_URL` para um lugar errado e ficar sem entender por que nada é oferecido.
121
+ */
122
+ export const newerThan = async (version) => {
123
+ const url = indexUrl();
124
+ const index = await fetchIndex(url).catch((error) => {
125
+ if (url !== DEFAULT_INDEX_URL) {
126
+ warn(`could not read the catalog at ${url}: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}`);
127
+ }
128
+ return null;
129
+ });
130
+ if (index === null)
131
+ return null;
132
+ const latest = (() => {
133
+ try {
134
+ return resolveVersion(index, LATEST);
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ })();
140
+ if (latest === null || version === null)
141
+ return latest;
142
+ // MAIOR, não "diferente": numa máquina de desenvolvimento a versão local costuma estar à frente do
143
+ // que foi publicado, e oferecer um downgrade como se fosse novidade é pior que não dizer nada.
144
+ return compareVersions(latest.version, version) > 0 ? latest : null;
145
+ };
@@ -0,0 +1,2 @@
1
+ import type { ParsedArgs } from '../shared/args.js';
2
+ export declare const connect: (args: ParsedArgs) => Promise<void>;
@@ -0,0 +1,74 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { hasFlag, requiredFlag } from '../shared/args.js';
4
+ import { blank, bullets, heading, info, step, success, warn } from '../shared/ui.js';
5
+ import { GOOGLE_DEFAULT_REGION, MANUAL_PREREQUISITES } from '../kernel/manifest.js';
6
+ import { ENVIRONMENT_FILE, readEnvironment, writeEnvironment } from './environment.js';
7
+ import { assertFirebaseCli, assertLoggedIn, createWebApp, listWebApps, webAppSdkConfig, } from './firebaseCli.js';
8
+ import { runtimeConfigFrom } from './runtimeConfigShape.js';
9
+ import { workspacePath, writeResolvedConfig } from './workspace.js';
10
+ export const connect = async (args) => {
11
+ const file = resolve(hasFlag(args, 'file') ? requiredFlag(args, 'file') : join(process.cwd(), ENVIRONMENT_FILE));
12
+ const root = dirname(file);
13
+ const projectId = requiredFlag(args, 'project');
14
+ const environment = await readEnvironment(file);
15
+ heading(`Connecting "${environment.project.name}" to Firebase project ${projectId}`);
16
+ const total = 4;
17
+ step(1, total, 'Checking the Firebase CLI');
18
+ const cliVersion = await assertFirebaseCli();
19
+ await assertLoggedIn();
20
+ success(`firebase-tools ${cliVersion}, signed in`);
21
+ step(2, total, 'Looking for a web app in the project');
22
+ const apps = await listWebApps(projectId);
23
+ let appId = apps[0]?.appId;
24
+ if (appId === undefined) {
25
+ if (hasFlag(args, 'no-create-web-app')) {
26
+ throw new Error(`Firebase project "${projectId}" has no web app, and --no-create-web-app was given.\n`
27
+ + 'Create one in the Firebase console, then run "microteia connect" again.');
28
+ }
29
+ appId = await createWebApp(projectId, environment.project.name);
30
+ success(`created a web app in ${projectId} (${appId})`);
31
+ }
32
+ else {
33
+ success(`using the existing web app ${appId}`);
34
+ if (apps.length > 1) {
35
+ warn(`the project has ${apps.length} web apps; the first one was used.`);
36
+ }
37
+ }
38
+ step(3, total, 'Reading the web SDK configuration');
39
+ const sdkConfig = await webAppSdkConfig(projectId, appId);
40
+ const runtimeConfig = runtimeConfigFrom(sdkConfig, environment.firebase.region);
41
+ await mkdir(workspacePath(root), { recursive: true });
42
+ await writeResolvedConfig(root, runtimeConfig);
43
+ success('configuration resolved');
44
+ step(4, total, `Recording the project in ${ENVIRONMENT_FILE}`);
45
+ await writeEnvironment(file, {
46
+ ...environment,
47
+ firebase: { ...environment.firebase, projectId },
48
+ });
49
+ success(`${environment.environment.id} → ${projectId}`);
50
+ blank();
51
+ info('Before deploying, make sure the Firebase project is prepared. These cannot be done from here:');
52
+ blank();
53
+ for (const prerequisite of MANUAL_PREREQUISITES) {
54
+ info(` ${prerequisite.optional ? '(optional) ' : ''}${prerequisite.title}`);
55
+ info(` ${prerequisite.detail.replace(/\n/g, '\n ')}`);
56
+ blank();
57
+ }
58
+ if (environment.firebase.region !== GOOGLE_DEFAULT_REGION) {
59
+ info(`This installation runs its functions in ${environment.firebase.region}, not in Google's`);
60
+ info(` ${GOOGLE_DEFAULT_REGION} default. The browser calls them at that region, so the permission to`);
61
+ info(' invoke them has to exist there — see "unauthenticated invocation" above. The deploy grants');
62
+ info(' it, then verifies it from outside, and prints the exact command per function if the grant');
63
+ info(' was refused.');
64
+ blank();
65
+ }
66
+ /**
67
+ * A linha aqui dizia `microteia deploy --kernel <…>.tgz`, e aquela bandeira saiu do `deploy` na
68
+ * `2.0.0` — o pacote passou a chegar pelo `unpack`. Desde então ela mandava quem acabou de conectar
69
+ * direto para uma recusa, no primeiro comando depois do que funcionou.
70
+ */
71
+ info('Then:');
72
+ bullets(['microteia unpack --version latest', 'microteia deploy']);
73
+ blank();
74
+ };
@@ -0,0 +1,120 @@
1
+ import type { BackendParameter } from '../kernel/manifest.js';
2
+ /**
3
+ * As Cloud Functions do ESCRITÓRIO — as que ele escreve, não as do kernel.
4
+ *
5
+ * Elas moravam em `.microteia/install/functions/custom/src/`, e isso tinha o mesmo defeito que a
6
+ * configuração de backend tinha: editar à mão dentro de um diretório que o `unpack` substitui por
7
+ * inteiro. Sobreviviam porque `functions/custom` estava numa lista de exceção — o que funciona até
8
+ * alguém mexer na lista, e não resolve o problema de o lugar certo não ser óbvio.
9
+ *
10
+ * Agora a fonte fica fora, ao lado do `microteia.yaml`, em dois lugares que respondem a duas perguntas
11
+ * diferentes:
12
+ *
13
+ * - `extensions/<plugin>/functions/` — a function é DAQUELA extensão. Fica junto do plugin que a usa,
14
+ * e quem lê a pasta da extensão vê tudo o que ela é.
15
+ * - `functions/` — a function é COMUM a várias. É o lugar de quem não pertence a uma só.
16
+ *
17
+ * O que vai para dentro de `install/` é uma cópia montada no deploy, com um barril gerado. É por isso
18
+ * que `functions/custom` saiu da lista de preservação do `unpack`: não há mais nada ali para preservar.
19
+ */
20
+ /** O nome do diretório, nos dois lugares. */
21
+ export declare const FUNCTIONS_DIR = "functions";
22
+ export declare const EXTENSIONS_DIR = "extensions";
23
+ export interface FunctionSource {
24
+ /** Como aparece nas mensagens: `functions` ou `extensions/<plugin>/functions`. */
25
+ label: string;
26
+ /** Diretório dentro de `custom/src/`, e o que o barril importa. */
27
+ slug: string;
28
+ path: string;
29
+ exported: string[];
30
+ /**
31
+ * `functions/` é COMUM a várias extensões, e por isso não tem dono que possa reivindicá-la: ela sobe
32
+ * no `deploy`, sempre. `extensions/<plugin>/functions/` tem dono, e sobe pelo provisionamento — só no
33
+ * escritório que instalou a extensão. A distinção sai do LUGAR do arquivo, e não de uma declaração:
34
+ * duas extensões não podem reivindicar o mesmo nome (`functionClaimProblems`), então uma function
35
+ * comum não teria como ser declarada.
36
+ */
37
+ shared: boolean;
38
+ /** As callables — as únicas que o navegador chama, e por isso as únicas com alcance a conferir. */
39
+ callables: string[];
40
+ /** Os `defineString` que ela declara. O `firebase-functions` os valida GLOBALMENTE (ver abaixo). */
41
+ parameters: BackendParameter[];
42
+ /** As dependências npm que ela declara no `package.json` do próprio diretório. */
43
+ dependencies: Record<string, string>;
44
+ }
45
+ /**
46
+ * Os nomes que um módulo de entrada exporta. Cobre as duas formas que se escreve de fato: o
47
+ * `export const x = onCall(...)` de quem declara no lugar, e o `export { a, b }` de quem reexporta.
48
+ */
49
+ export declare const exportedNames: (source: string) => string[];
50
+ /** Onde o escritório pode ter escrito function, na ordem em que as mensagens as citam. */
51
+ export declare const findFunctionSources: (root: string) => Promise<FunctionSource[]>;
52
+ /**
53
+ * O que o escritório escreveu, na forma em que os comandos precisam dele.
54
+ *
55
+ * `shared` e `owned` respondem a perguntas diferentes, e é por isso que são duas listas: a comum sobe
56
+ * no `deploy`, sempre; a de extensão sobe pelo provisionamento, e só onde a extensão está instalada.
57
+ */
58
+ export interface OfficeFunctions {
59
+ sources: FunctionSource[];
60
+ /** Todos os nomes, na ordem em que as origens são lidas. */
61
+ all: string[];
62
+ /** As de `functions/` — sem dono, e por isso deployadas sempre. */
63
+ shared: string[];
64
+ callables: string[];
65
+ parameters: BackendParameter[];
66
+ }
67
+ export declare const officeFunctionsOf: (sources: FunctionSource[]) => OfficeFunctions;
68
+ /**
69
+ * O que o escritório escreveu, lido de onde ele escreve. É a porta única: `deploy`, `provision` e
70
+ * `unpack` precisam da mesma resposta, e três leituras divergiriam no ponto em que ninguém olha.
71
+ */
72
+ export declare const readOfficeFunctions: (root: string) => Promise<OfficeFunctions>;
73
+ /**
74
+ * Os problemas que impedem montar o conjunto. São recusa e não aviso, pelo mesmo motivo do `pack`: o
75
+ * `index.js` do pacote espalha o customizado DEPOIS do kernel, então um nome repetido substitui em
76
+ * silêncio — e substituir um gatilho do kernel é o tipo de defeito que só aparece quando algo deixa de
77
+ * acontecer, sem erro em lugar nenhum.
78
+ */
79
+ export declare const functionProblems: (sources: FunctionSource[], kernelFunctions: string[]) => string[];
80
+ /**
81
+ * Duas declarações da MESMA dependência com faixas diferentes, entre diretórios do escritório.
82
+ *
83
+ * É recusa e não escolha porque não há dono: `install/functions/` é uma árvore de `node_modules` só, e
84
+ * eleger uma das faixas em silêncio é a mesma classe de defeito do nome de function repetido — funciona
85
+ * até a versão escolhida ser a errada, e aí a falha aparece longe da causa.
86
+ *
87
+ * Contra o KERNEL a regra é outra e é deliberada: a faixa do pacote VENCE, e o fato é relatado. O
88
+ * runtime das functions é escolha de quem publica o kernel, e uma extensão não pode movê-lo por baixo
89
+ * das outras.
90
+ */
91
+ export interface DependencyPlan {
92
+ /** O que acrescentar ao `package.json` do pacote. */
93
+ added: Record<string, string>;
94
+ /** Declarada pelo escritório e já no kernel, com faixa diferente — a do kernel fica. */
95
+ overridden: string[];
96
+ problems: string[];
97
+ }
98
+ export declare const dependencyPlan: (sources: FunctionSource[], kernelDependencies: Record<string, string>) => DependencyPlan;
99
+ /**
100
+ * Escreve no `package.json` do pacote as dependências que os diretórios do escritório declaram.
101
+ *
102
+ * Elas não podem viajar no pacote: o kernel não sabe o que o escritório vai escrever. E o `npm install`
103
+ * do deploy é o que as instala — o `package-lock.json` que veio do pacote deixa de cobrir tudo, e o npm
104
+ * resolve o que falta e reescreve o lock. É por isso que a primeira instalação depois de declarar uma
105
+ * dependência nova demora mais que as seguintes.
106
+ */
107
+ export declare const applyFunctionDependencies: (install: string, sources: FunctionSource[]) => Promise<DependencyPlan>;
108
+ export interface GatherResult extends OfficeFunctions {
109
+ /** Todos os nomes, ordenados — o que as mensagens citam. */
110
+ functions: string[];
111
+ dependencies: DependencyPlan;
112
+ }
113
+ /**
114
+ * Monta `install/functions/custom/src/` a partir das fontes de fora, escreve o barril e acrescenta ao
115
+ * `package.json` do pacote as dependências que essas fontes declaram.
116
+ *
117
+ * O diretório é apagado antes: ele é DERIVADO, e um resto da montagem anterior seria deployado sem que
118
+ * nada apontasse a origem dele.
119
+ */
120
+ export declare const gatherCustomFunctions: (root: string, install: string, kernelFunctions: string[]) => Promise<GatherResult>;