@microteia/console 2.10.0 → 2.19.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.
@@ -1,6 +1,7 @@
1
1
  import { access, cp, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
2
2
  import { basename, join } from 'node:path';
3
3
  import { callableFunctionNames, declaredParametersIn } from '../kernel/manifest.js';
4
+ import { callableDeclarations, triggerDeclarations, unscopedDeclarations, } from './functionGate.js';
4
5
  /**
5
6
  * As Cloud Functions do ESCRITÓRIO — as que ele escreve, não as do kernel.
6
7
  *
@@ -79,6 +80,33 @@ const readDependencies = async (path, label) => {
79
80
  }
80
81
  return parsed.dependencies;
81
82
  };
83
+ /**
84
+ * `microteia.serves` do `package.json` do diretório: nome de function -> os ids que ela serve.
85
+ *
86
+ * OPCIONAL, como as `dependencies` ao lado. A ausência dela é o comportamento anterior — a function comum
87
+ * sobe sempre e nunca é retirada —, e o provisionamento diz quais estão nessa situação em vez de calar.
88
+ */
89
+ const readServes = async (path, label) => {
90
+ const manifest = join(path, 'package.json');
91
+ if (!await exists(manifest))
92
+ return {};
93
+ const parsed = JSON.parse(await readFile(manifest, 'utf8'));
94
+ const declared = parsed.microteia?.serves;
95
+ if (declared === undefined)
96
+ return {};
97
+ if (typeof declared !== 'object' || declared === null || Array.isArray(declared)) {
98
+ throw new Error(`${label}/package.json has a "microteia.serves" that is not an object.`);
99
+ }
100
+ const serves = {};
101
+ for (const [name, ids] of Object.entries(declared)) {
102
+ if (!Array.isArray(ids) || ids.some((id) => typeof id !== 'string')) {
103
+ throw new Error(`${label}/package.json declares "microteia.serves.${name}" as something other than a list of `
104
+ + 'extension ids.');
105
+ }
106
+ serves[name] = [...new Set(ids)];
107
+ }
108
+ return serves;
109
+ };
82
110
  const readSource = async (path, label, slug, shared) => {
83
111
  const entry = join(path, 'index.ts');
84
112
  if (!await exists(entry)) {
@@ -88,11 +116,19 @@ const readSource = async (path, label, slug, shared) => {
88
116
  }
89
117
  const exported = exportedNames(await readFile(entry, 'utf8'));
90
118
  const callables = new Set();
119
+ const triggers = [];
120
+ const unscoped = [];
121
+ const callableGuards = [];
91
122
  const parameters = new Map();
92
123
  for (const file of await sourceFiles(path)) {
93
124
  const source = await readFile(file, 'utf8');
94
125
  for (const name of callableFunctionNames(source))
95
126
  callables.add(name);
127
+ // O gatilho é declarado no arquivo dele e reexportado pelo `index.ts` — é o que o molde escreve —,
128
+ // então a varredura é a mesma das callables: todo `.ts`, e o `index.ts` decide o que conta.
129
+ triggers.push(...triggerDeclarations(source));
130
+ unscoped.push(...unscopedDeclarations(source));
131
+ callableGuards.push(...callableDeclarations(source));
96
132
  // Um nome declarado em vários lugares é UM parâmetro, e ele é exigido se ALGUMA declaração o pede
97
133
  // sem `default`: é essa que recusa o deploy quando o valor falta, e a conservadora é a que avisa.
98
134
  for (const parameter of declaredParametersIn(source)) {
@@ -112,8 +148,12 @@ const readSource = async (path, label, slug, shared) => {
112
148
  // Só as que o `index.ts` de fato exporta: uma callable escrita e não exportada não é deployada, e
113
149
  // conferir o alcance dela mandaria procurar um erro que não existe.
114
150
  callables: exported.filter((name) => callables.has(name)),
151
+ triggers,
152
+ unscoped,
153
+ callableGuards,
115
154
  parameters: [...parameters.values()].sort((a, b) => a.name.localeCompare(b.name)),
116
155
  dependencies: await readDependencies(path, label),
156
+ serves: await readServes(path, label),
117
157
  };
118
158
  };
119
159
  /** Onde o escritório pode ter escrito function, na ordem em que as mensagens as citam. */
@@ -146,10 +186,21 @@ export const officeFunctionsOf = (sources) => {
146
186
  });
147
187
  }
148
188
  }
189
+ const shared = sources.filter((source) => source.shared);
190
+ const declared = new Map();
191
+ for (const source of shared) {
192
+ for (const [name, ids] of Object.entries(source.serves))
193
+ declared.set(name, ids);
194
+ }
195
+ const sharedNames = shared.flatMap((source) => source.exported);
149
196
  return {
150
197
  sources,
151
198
  all: sources.flatMap((source) => source.exported),
152
- shared: sources.filter((source) => source.shared).flatMap((source) => source.exported),
199
+ shared: sharedNames,
200
+ served: sharedNames
201
+ .filter((name) => declared.has(name))
202
+ .map((name) => ({ name, serves: declared.get(name) ?? [] })),
203
+ unserved: sharedNames.filter((name) => !declared.has(name)),
153
204
  callables: sources.flatMap((source) => source.callables),
154
205
  parameters: [...parameters.values()].sort((a, b) => a.name.localeCompare(b.name)),
155
206
  };
@@ -165,6 +216,41 @@ export const readOfficeFunctions = async (root) => officeFunctionsOf(await findF
165
216
  * silêncio — e substituir um gatilho do kernel é o tipo de defeito que só aparece quando algo deixa de
166
217
  * acontecer, sem erro em lugar nenhum.
167
218
  */
219
+ /**
220
+ * O que impede `microteia.serves` de ser levado a sério.
221
+ *
222
+ * As três recusas têm o mesmo motivo: a declaração é o que autoriza a plataforma a RETIRAR a function
223
+ * comum, e uma declaração que não fecha faria a retirada acontecer pelo motivo errado — ou não acontecer,
224
+ * e nada diria por quê.
225
+ */
226
+ export const servesProblems = (sources) => {
227
+ const problems = [];
228
+ for (const source of sources) {
229
+ const declared = Object.entries(source.serves);
230
+ if (declared.length === 0)
231
+ continue;
232
+ // A function de extensão já tem dono, e ele vem do LUGAR do arquivo. Declarar de novo, e num lugar
233
+ // que só a comum consulta, criaria duas autoridades sobre a mesma retirada.
234
+ if (!source.shared) {
235
+ problems.push(`${source.label}/package.json declares "microteia.serves", which only ${FUNCTIONS_DIR}/ uses — a `
236
+ + "function of an extension is already owned by the folder it is in, and is declared in that "
237
+ + 'extension\'s manifest.backend.functions');
238
+ continue;
239
+ }
240
+ const exported = new Set(source.exported);
241
+ for (const [name, ids] of declared) {
242
+ if (!exported.has(name)) {
243
+ problems.push(`${source.label}/package.json says "${name}" serves ${ids.join(', ') || 'nothing'}, but `
244
+ + `${source.label}/index.ts does not export it`);
245
+ }
246
+ if (ids.length === 0) {
247
+ problems.push(`${source.label}/package.json declares "${name}" as serving no extension at all — name the `
248
+ + 'extensions it exists for, or leave it out of microteia.serves');
249
+ }
250
+ }
251
+ }
252
+ return problems;
253
+ };
168
254
  export const functionProblems = (sources, kernelFunctions) => {
169
255
  const problems = [];
170
256
  const kernel = new Set(kernelFunctions);
@@ -186,6 +272,12 @@ export const functionProblems = (sources, kernelFunctions) => {
186
272
  }
187
273
  }
188
274
  }
275
+ problems.push(...servesProblems(sources));
276
+ // O portão de instalação NÃO entra aqui. Esta porta responde "dá para montar o conjunto?", e a
277
+ // resposta dela vale para toda fonte que existe no disco — inclusive a da extensão que foi
278
+ // desinstalada e cujo diretório ficou. O portão responde outra coisa: "esta function pode ir para o
279
+ // ar?", e ela só é devida de quem vai. Quem pergunta é `gateProblemsFor`, no ponto em que os nomes
280
+ // que sobem já estão decididos: `deploy` contra a baseline mais as comuns, `provision` contra a fila.
189
281
  return problems;
190
282
  };
191
283
  const barrel = (sources) => `// Generated by "microteia deploy". Do not edit.
@@ -7,6 +7,7 @@ import { ENVIRONMENT_FILE, readEnvironment, writeEnvironment } from './environme
7
7
  import { FIREBASE_BIN } from './firebaseCli.js';
8
8
  import { reportFailures, reportNotices } from './deployFunctions.js';
9
9
  import { clearRegionOrphans, listDeployedFunctions } from './regionOrphans.js';
10
+ import { gateProblemsFor } from './functionGate.js';
10
11
  import { deployFunctionsThatChanged } from './functionsUpToDate.js';
11
12
  import { checkCallables, invokerGrantCommand } from './callableAccess.js';
12
13
  import { checkHostingServed, htmlIsRevalidated } from './hostingCheck.js';
@@ -125,17 +126,40 @@ export const deploy = async (args) => {
125
126
  // comuns) e `extensions/<plugin>/functions/` (as de cada extensão). O `predeploy` do pacote compila
126
127
  // só o `custom/`, então o que não estiver aqui não sobe.
127
128
  const gathered = await gatherCustomFunctions(root, install, manifest.functions);
128
- // O que sobe aqui é a baseline MAIS as functions comuns do escritório (`functions/`). As de
129
- // `extensions/<plugin>/functions/` têm dono e sobem pelo provisionamento, no fim deste comando:
130
- // é o mesmo "empacotar não é deployar" que vale para as do kernel reivindicadas por extensão.
131
- const own = [...baseline, ...gathered.shared];
129
+ /**
130
+ * O que sobe aqui é a baseline MAIS as functions comuns que NÃO dizem a quem servem.
131
+ *
132
+ * As de `extensions/<plugin>/functions/` têm dono e sobem pelo provisionamento, no fim deste
133
+ * comando — é o mesmo "empacotar não é deployar" que vale para as do kernel reivindicadas por
134
+ * extensão. E a comum que DECLARA quem serve passou a ter a mesma condição, pela mesma razão: ela
135
+ * é devida enquanto alguma das extensões servidas estiver instalada, e subi-la aqui de qualquer
136
+ * forma faria o `--prune` apagá-la e este passo repô-la, indefinidamente.
137
+ *
138
+ * A comum sem declaração continua subindo sempre. É o comportamento anterior, e mantê-lo é o que
139
+ * impede esta versão de tirar do ar function de uma instalação que já existe.
140
+ */
141
+ const own = [...baseline, ...gathered.unserved];
142
+ /**
143
+ * O portão de instalação, cobrado do que ESTE passo sobe — e não de toda fonte que existe no
144
+ * disco.
145
+ *
146
+ * Na prática ele quase nunca tem o que dizer aqui, e isso é a confirmação de que o lugar é este:
147
+ * o que sobe no `deploy` é a baseline (do kernel) e a comum sem declaração (de quem não há id a
148
+ * cobrar). É no `provision` que a function de extensão entra na fila, e é lá que a recusa morde.
149
+ * Cobrar aqui mantém a regra atada ao ato — se um dia esta lista crescer, ela vem coberta.
150
+ */
151
+ const refused = gateProblemsFor(gathered.sources, own);
152
+ if (refused.length > 0) {
153
+ throw new Error(`These Cloud Functions cannot go up as they are:\n${refused.map((line) => ` ${line}`).join('\n')}`);
154
+ }
132
155
  step(current, total, `Cloud Functions (${own.length}, one at a time)`);
133
156
  if (gathered.sources.length > 0) {
134
157
  success(`${gathered.functions.length} function(s) of your own, from `
135
158
  + `${gathered.sources.map((source) => `${source.label}/`).join(', ')}`);
136
- const owned = gathered.all.length - gathered.shared.length;
137
- if (owned > 0)
138
- info(` ${owned} of them belong to an extension and go up with "provision"`);
159
+ const provisioned = gathered.all.length - gathered.unserved.length;
160
+ if (provisioned > 0) {
161
+ info(` ${provisioned} of them go up with "provision", only where they are needed`);
162
+ }
139
163
  }
140
164
  if (Object.keys(gathered.dependencies.added).length > 0) {
141
165
  success(`${Object.keys(gathered.dependencies.added).length} npm dependency(ies) your functions declare: `
@@ -29,13 +29,14 @@ export interface Environment {
29
29
  deployed: string | null;
30
30
  };
31
31
  /**
32
- * Quais Cloud Functions de EXTENSÃO já foram deployadas neste projeto. Fica aqui, e não no banco,
33
- * porque é estado da instalação e não do escritório — e é o que permite ao `provision` deployar só o
34
- * delta e reconhecer o que ficou órfão depois de uma desinstalação.
32
+ * Quais Cloud Functions de EXTENSÃO já foram deployadas neste projeto, e de QUEM. Fica aqui, e não no
33
+ * banco, porque é estado da instalação e não do escritório — e é o que permite ao `provision`
34
+ * deployar só o delta e reconhecer o que ficou órfão depois de uma desinstalação.
35
35
  *
36
- * Uma lista num escalar separado por vírgula porque o leitor de YAML daqui entende mapas de dois
37
- * níveis e escalares, e trazer sequências para guardar meia dúzia de nomes seria pagar caro pela
38
- * generalidade.
36
+ * `nome@pluginId` separado por vírgula — ver `appliedFunctions.ts`, que o lê e o escreve. O escalar é
37
+ * porque o leitor de YAML daqui entende mapas de dois níveis e escalares, e trazer sequências para
38
+ * guardar meia dúzia de pares seria pagar caro pela generalidade. Uma entrada sem `@` é o que uma
39
+ * instalação anterior ao dono gravou, e continua valendo.
39
40
  */
40
41
  backend: {
41
42
  functions: string | null;
@@ -20,8 +20,26 @@ export type FirestoreTypedValue = {
20
20
  stringValue: string;
21
21
  } | {
22
22
  timestampValue: string;
23
+ } | {
24
+ integerValue: string;
25
+ } | {
26
+ arrayValue: {
27
+ values: FirestoreTypedValue[];
28
+ };
29
+ } | {
30
+ mapValue: {
31
+ fields: Record<string, FirestoreTypedValue>;
32
+ };
23
33
  };
24
34
  export declare const asString: (value: string) => FirestoreTypedValue;
35
+ /**
36
+ * Inteiro como INTEIRO. A REST o transporta em string — é assim que ela representa um int64 sem perder
37
+ * precisão em JSON —, mas o campo gravado é numérico, e é isso que faz o outro lado do produto poder
38
+ * compará-lo em vez de tentar convertê-lo.
39
+ */
40
+ export declare const asInteger: (value: number) => FirestoreTypedValue;
41
+ export declare const asArray: (values: FirestoreTypedValue[]) => FirestoreTypedValue;
42
+ export declare const asMap: (fields: Record<string, FirestoreTypedValue>) => FirestoreTypedValue;
25
43
  /**
26
44
  * Data como DATA, e não como texto que se parece com uma.
27
45
  *
@@ -37,3 +55,12 @@ export declare const asTimestamp: (value: Date) => FirestoreTypedValue;
37
55
  * Console carimbar o estado sem conhecer (nem arriscar reescrever) o pacote inteiro que vive ao lado.
38
56
  */
39
57
  export declare const patchMapField: (projectId: string, path: string, field: string, entries: Record<string, FirestoreTypedValue>) => Promise<void>;
58
+ /**
59
+ * Grava um documento inteiro, com `updateMask` listando cada campo escrito.
60
+ *
61
+ * A máscara é explícita e não omitida de propósito: sem ela o comportamento de um PATCH depende de o
62
+ * documento existir, e o que se quer aqui é convergência — o que é escrito passa a valer, e nada de uma
63
+ * execução anterior sobra. Como todos os campos são escritos a cada vez, a máscara completa é também o
64
+ * que apaga o que deixou de ser verdade.
65
+ */
66
+ export declare const patchFields: (projectId: string, path: string, fields: Record<string, FirestoreTypedValue>) => Promise<void>;
@@ -108,6 +108,14 @@ export const listDocuments = async (projectId, path) => {
108
108
  };
109
109
  export const documentId = (document) => document.name.slice(document.name.lastIndexOf('/') + 1);
110
110
  export const asString = (value) => ({ stringValue: value });
111
+ /**
112
+ * Inteiro como INTEIRO. A REST o transporta em string — é assim que ela representa um int64 sem perder
113
+ * precisão em JSON —, mas o campo gravado é numérico, e é isso que faz o outro lado do produto poder
114
+ * compará-lo em vez de tentar convertê-lo.
115
+ */
116
+ export const asInteger = (value) => ({ integerValue: String(value) });
117
+ export const asArray = (values) => ({ arrayValue: { values } });
118
+ export const asMap = (fields) => ({ mapValue: { fields } });
111
119
  /**
112
120
  * Data como DATA, e não como texto que se parece com uma.
113
121
  *
@@ -129,3 +137,18 @@ export const patchMapField = async (projectId, path, field, entries) => {
129
137
  body: { fields: { [field]: { mapValue: { fields: entries } } } },
130
138
  });
131
139
  };
140
+ /**
141
+ * Grava um documento inteiro, com `updateMask` listando cada campo escrito.
142
+ *
143
+ * A máscara é explícita e não omitida de propósito: sem ela o comportamento de um PATCH depende de o
144
+ * documento existir, e o que se quer aqui é convergência — o que é escrito passa a valer, e nada de uma
145
+ * execução anterior sobra. Como todos os campos são escritos a cada vez, a máscara completa é também o
146
+ * que apaga o que deixou de ser verdade.
147
+ */
148
+ export const patchFields = async (projectId, path, fields) => {
149
+ await request(projectId, path, {
150
+ method: 'PATCH',
151
+ query: { 'updateMask.fieldPaths': Object.keys(fields) },
152
+ body: { fields },
153
+ });
154
+ };
@@ -0,0 +1,228 @@
1
+ /**
2
+ * O portão de instalação, conferido na FONTE.
3
+ *
4
+ * `forInstalledExtension` é a metade da retirada que age no instante certo: a Cloud Function é do
5
+ * PROJETO e a instalação da extensão é do ESCRITÓRIO, então um `onDocumentWritten('tenants/{tenantId}/…')`
6
+ * declarado por uma extensão dispara para TODOS os escritórios do projeto — os que nunca a instalaram, e
7
+ * o que a desinstalou enquanto outro a mantém. Nesse último caso `--prune` não pode ajudar: a function
8
+ * tem de continuar no ar, porque alguém ainda precisa dela.
9
+ *
10
+ * Só que nada cobrava o portão. O `create function` o escrevia no molde, e era só isso: nenhuma
11
+ * verificação do empacotamento, do deploy ou do provisionamento olhava a fonte. Um gatilho sem portão
12
+ * passava por tudo e ia para o ar — e o sintoma dele é o pior que existe, porque não é uma falha: é
13
+ * código de uma extensão que ninguém tem mais rodando sobre os dados de quem nunca a pediu, sem erro em
14
+ * lugar nenhum.
15
+ *
16
+ * É recusa, e não aviso, pelo mesmo motivo do nome de function repetido: o defeito é invisível depois de
17
+ * instalado, e um aviso na saída de um deploy é lido uma vez. Mas ela é cobrada da function que VAI
18
+ * SUBIR, e não de toda fonte que existe no disco — ver `gateProblemsFor`. Toda origem é lida, sempre, e
19
+ * o que não vai subir é relatado pelo `provision`: quem tem a fonte parada ao lado do `microteia.yaml`
20
+ * fica sabendo dela, sem ter o comando trancado por ela.
21
+ *
22
+ * A análise é por texto, no idioma dos outros leitores de fonte do Console (`callableFunctionNames`,
23
+ * `declaredParametersIn`). Ela erra para o lado ALTO — o que ela não entende ela recusa —, e é por isso
24
+ * que o portão declarado num identificador ao lado é reconhecido: recusar código correto é o que ensina
25
+ * a desligar a verificação.
26
+ */
27
+ /**
28
+ * Os construtores de gatilho de Firestore.
29
+ *
30
+ * São os únicos eventos cujo caminho carrega `{tenantId}`, e portanto os únicos para os quais o portão
31
+ * existe. Um `onSchedule` ou um `onMessagePublished` não tem escritório de onde perguntar — envolvê-los
32
+ * no portão os deixaria permanentemente inertes, porque ele falha FECHADO. Eles ficam de fora desta
33
+ * verificação de propósito: exigir deles um portão que não funciona seria trocar um defeito por outro.
34
+ */
35
+ export declare const TENANT_TRIGGER_CONSTRUCTORS: string[];
36
+ /**
37
+ * Os construtores cujo evento NÃO carrega escritório no caminho.
38
+ *
39
+ * Para eles as duas saídas do autor eram erradas, e é isso que faz deles uma recusa e não um aviso: sem
40
+ * portão a function age sobre o projeto inteiro; COM portão ela nunca roda — nem enquanto a extensão está
41
+ * instalada —, porque `forInstalledExtension` falha FECHADO e nunca acha o `{tenantId}` que não existe.
42
+ * Um autor que seguisse a orientação genérica "envolva o handler no portão" produzia uma function que
43
+ * passava no deploy, não acusava nada e nunca fazia nada.
44
+ *
45
+ * A resposta é o laço por escritório (`forEachInstalledOffice`), e é por existir uma resposta que a
46
+ * recusa é honesta.
47
+ *
48
+ * A lista é EXPLÍCITA, e não "tudo o que não é gatilho de documento": o que não se conhece não se recusa.
49
+ * O RTDB fica de fora de propósito — o caminho dele TEM parâmetros, então um `{tenantId}` ali é possível
50
+ * —, e a plataforma não usa RTDB em lugar nenhum.
51
+ */
52
+ export declare const UNSCOPED_TRIGGER_CONSTRUCTORS: string[];
53
+ /**
54
+ * A fonte sem comentário nenhum, com as strings intactas.
55
+ *
56
+ * O JSDoc que o molde escreve CITA `forInstalledExtension` — é ali que ele explica o portão. Sem esta
57
+ * limpeza, um gatilho que perdesse a chamada e mantivesse o comentário passaria na verificação, o que é
58
+ * o caso exato que ela existe para pegar.
59
+ */
60
+ export declare const withoutComments: (source: string) => string;
61
+ export interface TriggerDeclaration {
62
+ /** O nome declarado — é por ele que se sabe se o `index.ts` o exporta. */
63
+ name: string;
64
+ /** O construtor usado. Aparece na mensagem: é ele que explica por que o portão é devido. */
65
+ trigger: string;
66
+ /** O id que `forInstalledExtension` recebeu, ou `null` quando o portão não está lá. */
67
+ gatedFor: string | null;
68
+ /** Todos os ids que as guardas daquele gatilho recebem — a comum guarda mais de um. */
69
+ guards: string[];
70
+ }
71
+ /** Os gatilhos de Firestore declarados numa fonte, com o estado do portão de cada um. */
72
+ export declare const triggerDeclarations: (rawSource: string) => TriggerDeclaration[];
73
+ /**
74
+ * As functions sem escritório no evento, e o id que o laço por escritório recebeu no ARQUIVO.
75
+ *
76
+ * O alcance é o arquivo, e não os parênteses da chamada — a diferença é deliberada. O portão de um
77
+ * gatilho de documento embrulha AQUELE handler, então ele tem de estar ali; o laço, ao contrário, roda
78
+ * DENTRO do handler e pode estar algumas chamadas abaixo, num `syncOffice(tenantId)` que o próprio autor
79
+ * escreveu. Exigi-lo nos parênteses recusaria código correto, e a verificação que erra assim é a que
80
+ * ensina a próxima pessoa a desligá-la.
81
+ *
82
+ * O preço é conhecido: dois agendados no mesmo arquivo, um com laço e outro sem, passam os dois. É menos
83
+ * grave que o inverso, e está dito.
84
+ */
85
+ export declare const unscopedDeclarations: (rawSource: string) => TriggerDeclaration[];
86
+ /**
87
+ * As callables declaradas, e o id que a guarda delas recebeu no ARQUIVO.
88
+ *
89
+ * O alcance é o arquivo pela mesma razão do laço por escritório: a guarda roda DENTRO do handler (ela
90
+ * precisa do `request`), e o guia manda o autor manter o handler fino, com a decisão num arquivo ao
91
+ * lado. Exigi-la nos parênteses recusaria justamente o código que o guia pede.
92
+ */
93
+ export declare const callableDeclarations: (rawSource: string) => TriggerDeclaration[];
94
+ /**
95
+ * O que uma origem de functions precisa ter para ser conferida. É a forma de `FunctionSource`, declarada
96
+ * aqui em vez de importada: assim este módulo se testa sozinho, sem montar diretório nenhum.
97
+ */
98
+ export interface GateSubject {
99
+ label: string;
100
+ /** `functions/` é comum a várias extensões, e não tem uma instalação de onde perguntar. */
101
+ shared: boolean;
102
+ exported: string[];
103
+ triggers: TriggerDeclaration[];
104
+ /** As functions cujo evento não carrega escritório: o agendado, a fila, o HTTP, o Storage. */
105
+ unscoped: TriggerDeclaration[];
106
+ /** Quem cada function comum declara servir. Vazio numa origem de extensão. */
107
+ serves: Record<string, string[]>;
108
+ /** As callables declaradas, com o estado da guarda de cada uma. */
109
+ callableGuards: TriggerDeclaration[];
110
+ }
111
+ /**
112
+ * Um problema de portão, e a function a que ele pertence.
113
+ *
114
+ * O nome está aqui porque é ele que decide o PESO do problema. A verificação lê toda fonte que existe no
115
+ * disco — é assim que ela acha o que precisa achar —, mas o que existe no disco não é o que vai subir:
116
+ * `extensions/<x>/functions/` de uma extensão que ninguém instalou é lida, copiada e nunca deployada.
117
+ * Sem o nome, quem chama só tem a frase, e a única coisa que dá para fazer com uma frase é recusar tudo.
118
+ * Foi o que acontecia: uma extensão desinstalada, com a fonte parada ao lado do `microteia.yaml`,
119
+ * trancava o `deploy` inteiro — hosting, rules e índices junto — por causa de uma function que nenhum
120
+ * comando teria subido.
121
+ */
122
+ export interface GateProblem {
123
+ /** A function a que o problema pertence. É por ela que se sabe se ela vai, de fato, subir. */
124
+ name: string;
125
+ message: string;
126
+ }
127
+ /**
128
+ * Gatilho de extensão sem portão.
129
+ *
130
+ * `functions/` fica de fora porque ali a pergunta não tem resposta: a function é comum a várias
131
+ * extensões, nenhuma a reivindica, e não há um id para o portão receber. É lacuna conhecida e tem
132
+ * endereço próprio — enquanto ela não fechar, exigir aqui um portão impossível seria recusar a única
133
+ * forma que a plataforma oferece para uma function compartilhada.
134
+ */
135
+ export declare const ungatedTriggerProblems: (subjects: GateSubject[]) => GateProblem[];
136
+ /**
137
+ * Function de extensão cujo evento não tem escritório, e que não passa pelo laço.
138
+ *
139
+ * A recusa vem acompanhada do substituto porque sem ele ela seria só uma porta fechada: o laço por
140
+ * escritório (`forEachInstalledOffice`) existe justamente para este caso, e é ele que a mensagem nomeia.
141
+ *
142
+ * `functions/` fica de fora pelo mesmo motivo de sempre: a comum não tem um id de extensão para dar ao
143
+ * laço. Quem responde por ela é a declaração de quem ela serve.
144
+ */
145
+ export declare const unscopedTriggerProblems: (subjects: GateSubject[]) => GateProblem[];
146
+ /**
147
+ * Gatilho de uma function COMUM que declara quem serve, e não guarda ninguém.
148
+ *
149
+ * A comum ficava fora desta cobrança porque não havia id para dar à guarda: nenhuma extensão pode
150
+ * reivindicá-la. `microteia.serves` resolve exatamente isso — a partir dela existem ids, e a partir dos
151
+ * ids a cobrança passa a ser possível. Um gatilho comum sem guarda dispara para todo escritório do
152
+ * projeto, e é o mesmo defeito do gatilho de extensão sem portão, com um alcance maior.
153
+ *
154
+ * A comum SEM declaração continua de fora, e é deliberado: cobrar dela uma guarda sem ter ids a oferecer
155
+ * seria fechar a porta sem abrir nenhuma. O que ela recebe é o relato do provisionamento.
156
+ *
157
+ * A segunda recusa é a guarda que nomeia extensão que a function não declara servir: as duas listas
158
+ * respondem à mesma pergunta em dois lugares, e divergirem faz uma delas mentir — a `serves` decide a
159
+ * retirada, a guarda decide a execução, e a function passaria a agir onde não pode ser retirada.
160
+ */
161
+ export declare const unguardedSharedProblems: (subjects: GateSubject[]) => GateProblem[];
162
+ /**
163
+ * Callable de extensão sem a guarda de instalação.
164
+ *
165
+ * "Uma callable órfã é inerte" era verdade de INTERFACE, e não de segurança — e era a frase que o guia
166
+ * dizia. O deploy concede a `allUsers` o papel `roles/run.invoker` em cada callable; o que sobra depois é
167
+ * a validação do token do Firebase. Então a tela ir embora com a extensão não fecha nada: o endpoint
168
+ * segue no ar aceitando o token de QUALQUER usuário do projeto, de qualquer escritório, tendo ou não a
169
+ * extensão.
170
+ *
171
+ * E o escritório chega no PAYLOAD, porque `useCallable` manda só o que a tela passou. Sem a guarda, um
172
+ * usuário do escritório A chama a function apontando para o B — e o molde gerado conferia apenas se
173
+ * havia `request.auth`.
174
+ *
175
+ * A comum fica de fora: `requireInstalledExtension` pede UM id, e a comum serve vários. É lacuna
176
+ * conhecida e está registrada — a guarda "alguma das que eu sirvo" existe para gatilho, não para
177
+ * callable.
178
+ */
179
+ export declare const unguardedCallableProblems: (subjects: GateSubject[]) => GateProblem[];
180
+ /** Todo problema de portão de um conjunto de origens, na ordem em que as mensagens os citam. */
181
+ export declare const gateProblems: (subjects: GateSubject[]) => GateProblem[];
182
+ /**
183
+ * Os problemas das functions que ESTÃO subindo — a lista que autoriza uma recusa.
184
+ *
185
+ * É a diferença entre "a fonte foi lida" e "a function vai para o ar", e ela não é sutil: a primeira
186
+ * inclui toda extensão cujo diretório ficou no disco depois de desinstalada, e recusar por causa dela é
187
+ * recusar um comando que não faria nada de errado. A segunda é o momento em que a recusa tem
188
+ * consequência — e é o único em que ela é devida.
189
+ */
190
+ export declare const gateProblemsFor: (subjects: GateSubject[], deploying: string[]) => string[];
191
+ export interface MisgatedTrigger {
192
+ /** A origem, como as mensagens a citam. */
193
+ label: string;
194
+ name: string;
195
+ /** O id que a guarda recebeu. */
196
+ gatedFor: string;
197
+ /** O id da extensão que reivindica a function. */
198
+ owner: string;
199
+ /** Qual guarda recebeu o id errado — é ela que o autor tem de corrigir. */
200
+ via: string;
201
+ }
202
+ /**
203
+ * Portão aberto para o id ERRADO.
204
+ *
205
+ * É o defeito do copiar-e-colar entre extensões, e o sintoma dele é o oposto do gatilho sem portão: a
206
+ * function sobe, o gatilho dispara, e o handler nunca roda em escritório nenhum. Nada falha — a extensão
207
+ * apenas não faz o que promete, e a causa está numa string.
208
+ *
209
+ * Devolve os casos, e não as frases, porque quem chama precisa dos dois: a mensagem para relatar e o id
210
+ * da extensão para pendurar. Deduzir o id de volta a partir do texto é o que faz um relatório recusar a
211
+ * extensão errada quando o nome de uma aparece dentro da mensagem de outra.
212
+ *
213
+ * Só se pergunta quando o nome tem UM reivindicante: com dois, `functionClaimProblems` já recusou antes,
214
+ * e apontar um deles como o dono certo seria inventar uma resposta.
215
+ */
216
+ export declare const misgatedTriggers: (subjects: GateSubject[], claimants: Map<string, string>) => MisgatedTrigger[];
217
+ /** Como uma guarda de id errado aparece num relatório. */
218
+ export declare const describeMisgated: (entry: MisgatedTrigger) => string;
219
+ /**
220
+ * Quem reivindica cada nome, quando é um só. Duas extensões pedindo o mesmo nome é problema de outra
221
+ * verificação, e o nome sai daqui em vez de receber um dono escolhido a esmo.
222
+ */
223
+ export declare const singleClaimants: (claims: {
224
+ pluginId: string;
225
+ backend: {
226
+ functions?: string[];
227
+ };
228
+ }[]) => Map<string, string>;