@microteia/console 2.10.0 → 2.18.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.
@@ -0,0 +1,400 @@
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 (`functionProblems`): o defeito é
17
+ * invisível depois de instalado, e um aviso na saída de um deploy é lido uma vez.
18
+ *
19
+ * A análise é por texto, no idioma dos outros leitores de fonte do Console (`callableFunctionNames`,
20
+ * `declaredParametersIn`). Ela erra para o lado ALTO — o que ela não entende ela recusa —, e é por isso
21
+ * que o portão declarado num identificador ao lado é reconhecido: recusar código correto é o que ensina
22
+ * a desligar a verificação.
23
+ */
24
+ /**
25
+ * Os construtores de gatilho de Firestore.
26
+ *
27
+ * São os únicos eventos cujo caminho carrega `{tenantId}`, e portanto os únicos para os quais o portão
28
+ * existe. Um `onSchedule` ou um `onMessagePublished` não tem escritório de onde perguntar — envolvê-los
29
+ * no portão os deixaria permanentemente inertes, porque ele falha FECHADO. Eles ficam de fora desta
30
+ * verificação de propósito: exigir deles um portão que não funciona seria trocar um defeito por outro.
31
+ */
32
+ export const TENANT_TRIGGER_CONSTRUCTORS = [
33
+ 'onDocumentWritten',
34
+ 'onDocumentCreated',
35
+ 'onDocumentUpdated',
36
+ 'onDocumentDeleted',
37
+ 'onDocumentWrittenWithAuthContext',
38
+ 'onDocumentCreatedWithAuthContext',
39
+ 'onDocumentUpdatedWithAuthContext',
40
+ 'onDocumentDeletedWithAuthContext',
41
+ ];
42
+ /**
43
+ * Os construtores cujo evento NÃO carrega escritório no caminho.
44
+ *
45
+ * Para eles as duas saídas do autor eram erradas, e é isso que faz deles uma recusa e não um aviso: sem
46
+ * portão a function age sobre o projeto inteiro; COM portão ela nunca roda — nem enquanto a extensão está
47
+ * instalada —, porque `forInstalledExtension` falha FECHADO e nunca acha o `{tenantId}` que não existe.
48
+ * Um autor que seguisse a orientação genérica "envolva o handler no portão" produzia uma function que
49
+ * passava no deploy, não acusava nada e nunca fazia nada.
50
+ *
51
+ * A resposta é o laço por escritório (`forEachInstalledOffice`), e é por existir uma resposta que a
52
+ * recusa é honesta.
53
+ *
54
+ * 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.
55
+ * O RTDB fica de fora de propósito — o caminho dele TEM parâmetros, então um `{tenantId}` ali é possível
56
+ * —, e a plataforma não usa RTDB em lugar nenhum.
57
+ */
58
+ export const UNSCOPED_TRIGGER_CONSTRUCTORS = [
59
+ 'onSchedule',
60
+ 'onMessagePublished',
61
+ 'onRequest',
62
+ 'onObjectFinalized',
63
+ 'onObjectArchived',
64
+ 'onObjectDeleted',
65
+ 'onObjectMetadataUpdated',
66
+ 'onCustomEventPublished',
67
+ 'onTaskDispatched',
68
+ 'beforeUserCreated',
69
+ 'beforeUserSignedIn',
70
+ ];
71
+ const GATE = 'forInstalledExtension';
72
+ const OFFICE_LOOP = 'forEachInstalledOffice';
73
+ const ANY_GATE = 'forAnyInstalledExtension';
74
+ const CALLABLE_GUARD = 'requireInstalledExtension';
75
+ /** Onde uma string termina, a partir da abertura dela. Aspas não fechada não engole o resto do arquivo. */
76
+ const stringEnd = (source, start, quote) => {
77
+ let index = start + 1;
78
+ while (index < source.length) {
79
+ const char = source[index];
80
+ if (char === '\\') {
81
+ index += 2;
82
+ continue;
83
+ }
84
+ if (char === quote)
85
+ return index + 1;
86
+ if (quote !== '`' && char === '\n')
87
+ return index;
88
+ index += 1;
89
+ }
90
+ return source.length;
91
+ };
92
+ /**
93
+ * A fonte sem comentário nenhum, com as strings intactas.
94
+ *
95
+ * O JSDoc que o molde escreve CITA `forInstalledExtension` — é ali que ele explica o portão. Sem esta
96
+ * limpeza, um gatilho que perdesse a chamada e mantivesse o comentário passaria na verificação, o que é
97
+ * o caso exato que ela existe para pegar.
98
+ */
99
+ export const withoutComments = (source) => {
100
+ let out = '';
101
+ let index = 0;
102
+ while (index < source.length) {
103
+ const pair = source.slice(index, index + 2);
104
+ if (pair === '//') {
105
+ const end = source.indexOf('\n', index);
106
+ index = end === -1 ? source.length : end;
107
+ out += ' ';
108
+ continue;
109
+ }
110
+ if (pair === '/*') {
111
+ const end = source.indexOf('*/', index + 2);
112
+ index = end === -1 ? source.length : end + 2;
113
+ out += ' ';
114
+ continue;
115
+ }
116
+ const char = source[index];
117
+ if (char === "'" || char === '"' || char === '`') {
118
+ const end = stringEnd(source, index, char);
119
+ out += source.slice(index, end);
120
+ index = end;
121
+ continue;
122
+ }
123
+ out += char;
124
+ index += 1;
125
+ }
126
+ return out;
127
+ };
128
+ /**
129
+ * O texto entre os parênteses da chamada que abre em `open`. Conta os parênteses e pula as strings: um
130
+ * `console.log('(')` no corpo do handler desequilibraria a contagem, e o efeito seria recusar uma
131
+ * function correta.
132
+ */
133
+ const callArguments = (source, open) => {
134
+ let depth = 0;
135
+ let index = open;
136
+ while (index < source.length) {
137
+ const char = source[index];
138
+ if (char === "'" || char === '"' || char === '`') {
139
+ index = stringEnd(source, index, char);
140
+ continue;
141
+ }
142
+ if (char === '(')
143
+ depth += 1;
144
+ else if (char === ')') {
145
+ depth -= 1;
146
+ if (depth === 0)
147
+ return source.slice(open + 1, index);
148
+ }
149
+ index += 1;
150
+ }
151
+ return source.slice(open + 1);
152
+ };
153
+ /**
154
+ * O id que o portão daquele gatilho recebeu, ou `null` quando não há portão.
155
+ *
156
+ * Duas formas valem, e a segunda não é indulgência: `onDocumentWritten(opts, gated)` com
157
+ * `const gated = forInstalledExtension('id', handler)` ao lado é código correto e comum quando o handler
158
+ * cresce. Recusá-lo seria a verificação errando contra quem fez certo.
159
+ */
160
+ const gatedFor = (args, source) => {
161
+ const direct = args.match(new RegExp(String.raw `\b${GATE}\s*\(\s*['"]([^'"]*)['"]`));
162
+ if (direct !== null)
163
+ return direct[1];
164
+ for (const token of new Set(args.match(/[A-Za-z_$][\w$]*/g) ?? [])) {
165
+ const indirect = source.match(new RegExp(String.raw `(?:const|let|var)\s+${token}\s*(?::[^=;\n]*)?=\s*${GATE}\s*\(\s*['"]([^'"]*)['"]`));
166
+ if (indirect !== null)
167
+ return indirect[1];
168
+ }
169
+ return null;
170
+ };
171
+ /**
172
+ * Todos os ids que as guardas daquele gatilho recebem.
173
+ *
174
+ * A function COMUM usa `forAnyInstalledExtension(['a', 'b'], handler)`, cuja pergunta é outra — "alguma
175
+ * das que eu sirvo está instalada?" —, então ela devolve mais de um id. A de extensão continua respondida
176
+ * por `gatedFor`, que devolve um: ali um segundo id não significaria nada.
177
+ */
178
+ const guardedIds = (args, source) => {
179
+ const found = new Set();
180
+ const any = args.match(new RegExp(String.raw `\b${ANY_GATE}\s*\(\s*\[([^\]]*)\]`));
181
+ if (any !== null) {
182
+ for (const id of any[1].matchAll(/['"]([^'"]+)['"]/g))
183
+ found.add(id[1]);
184
+ }
185
+ const single = gatedFor(args, source);
186
+ if (single !== null)
187
+ found.add(single);
188
+ return [...found];
189
+ };
190
+ /** Onde cada `export const x = <construtor>(` começa, para os construtores pedidos. */
191
+ const declarationsOf = (source, constructors) => {
192
+ const pattern = new RegExp(String.raw `(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=;\n]*)?=\s*(`
193
+ + `${constructors.join('|')})\\s*(?:<[^>]*>)?\\s*\\(`, 'g');
194
+ return [...source.matchAll(pattern)].map((match) => ({
195
+ name: match[1],
196
+ trigger: match[2],
197
+ open: (match.index ?? 0) + match[0].length - 1,
198
+ }));
199
+ };
200
+ /** Os gatilhos de Firestore declarados numa fonte, com o estado do portão de cada um. */
201
+ export const triggerDeclarations = (rawSource) => {
202
+ const source = withoutComments(rawSource);
203
+ return declarationsOf(source, TENANT_TRIGGER_CONSTRUCTORS).map((declaration) => {
204
+ const args = callArguments(source, declaration.open);
205
+ return {
206
+ name: declaration.name,
207
+ trigger: declaration.trigger,
208
+ gatedFor: gatedFor(args, source),
209
+ guards: guardedIds(args, source),
210
+ };
211
+ });
212
+ };
213
+ /**
214
+ * As functions sem escritório no evento, e o id que o laço por escritório recebeu no ARQUIVO.
215
+ *
216
+ * O alcance é o arquivo, e não os parênteses da chamada — a diferença é deliberada. O portão de um
217
+ * gatilho de documento embrulha AQUELE handler, então ele tem de estar ali; o laço, ao contrário, roda
218
+ * DENTRO do handler e pode estar algumas chamadas abaixo, num `syncOffice(tenantId)` que o próprio autor
219
+ * escreveu. Exigi-lo nos parênteses recusaria código correto, e a verificação que erra assim é a que
220
+ * ensina a próxima pessoa a desligá-la.
221
+ *
222
+ * O preço é conhecido: dois agendados no mesmo arquivo, um com laço e outro sem, passam os dois. É menos
223
+ * grave que o inverso, e está dito.
224
+ */
225
+ export const unscopedDeclarations = (rawSource) => {
226
+ const source = withoutComments(rawSource);
227
+ const loop = source.match(new RegExp(String.raw `\b${OFFICE_LOOP}\s*\(\s*['"]([^'"]*)['"]`));
228
+ return declarationsOf(source, UNSCOPED_TRIGGER_CONSTRUCTORS).map((declaration) => ({
229
+ name: declaration.name,
230
+ trigger: declaration.trigger,
231
+ gatedFor: loop === null ? null : loop[1],
232
+ guards: loop === null ? [] : [loop[1]],
233
+ }));
234
+ };
235
+ /**
236
+ * As callables declaradas, e o id que a guarda delas recebeu no ARQUIVO.
237
+ *
238
+ * O alcance é o arquivo pela mesma razão do laço por escritório: a guarda roda DENTRO do handler (ela
239
+ * precisa do `request`), e o guia manda o autor manter o handler fino, com a decisão num arquivo ao
240
+ * lado. Exigi-la nos parênteses recusaria justamente o código que o guia pede.
241
+ */
242
+ export const callableDeclarations = (rawSource) => {
243
+ const source = withoutComments(rawSource);
244
+ const guard = source.match(new RegExp(String.raw `\b${CALLABLE_GUARD}\s*\(\s*['"]([^'"]*)['"]`));
245
+ return declarationsOf(source, ['onCall']).map((declaration) => ({
246
+ name: declaration.name,
247
+ trigger: declaration.trigger,
248
+ gatedFor: guard === null ? null : guard[1],
249
+ guards: guard === null ? [] : [guard[1]],
250
+ }));
251
+ };
252
+ /** Os gatilhos que o `index.ts` de fato exporta — um declarado e não exportado não é deployado. */
253
+ const exportedOnly = (subject, declarations) => {
254
+ const exported = new Set(subject.exported);
255
+ return declarations.filter((trigger) => exported.has(trigger.name));
256
+ };
257
+ const exportedTriggers = (subject) => exportedOnly(subject, subject.triggers);
258
+ const exportedUnscoped = (subject) => exportedOnly(subject, subject.unscoped);
259
+ const exportedCallables = (subject) => exportedOnly(subject, subject.callableGuards);
260
+ /**
261
+ * Gatilho de extensão sem portão.
262
+ *
263
+ * `functions/` fica de fora porque ali a pergunta não tem resposta: a function é comum a várias
264
+ * extensões, nenhuma a reivindica, e não há um id para o portão receber. É lacuna conhecida e tem
265
+ * endereço próprio — enquanto ela não fechar, exigir aqui um portão impossível seria recusar a única
266
+ * forma que a plataforma oferece para uma function compartilhada.
267
+ */
268
+ export const ungatedTriggerProblems = (subjects) => subjects
269
+ .filter((subject) => !subject.shared)
270
+ .flatMap((subject) => exportedTriggers(subject)
271
+ .filter((trigger) => trigger.gatedFor === null)
272
+ .map((trigger) => `${subject.label} exports "${trigger.name}", a ${trigger.trigger} trigger with no `
273
+ + `installation gate — wrap its handler in ${GATE}('<this extension's id>', …) or it fires for `
274
+ + 'every office in the project, including the ones that never installed the extension'));
275
+ /**
276
+ * Function de extensão cujo evento não tem escritório, e que não passa pelo laço.
277
+ *
278
+ * A recusa vem acompanhada do substituto porque sem ele ela seria só uma porta fechada: o laço por
279
+ * escritório (`forEachInstalledOffice`) existe justamente para este caso, e é ele que a mensagem nomeia.
280
+ *
281
+ * `functions/` fica de fora pelo mesmo motivo de sempre: a comum não tem um id de extensão para dar ao
282
+ * laço. Quem responde por ela é a declaração de quem ela serve.
283
+ */
284
+ export const unscopedTriggerProblems = (subjects) => subjects
285
+ .filter((subject) => !subject.shared)
286
+ .flatMap((subject) => exportedUnscoped(subject)
287
+ .filter((trigger) => trigger.gatedFor === null)
288
+ .map((trigger) => `${subject.label} exports "${trigger.name}", a ${trigger.trigger} function whose `
289
+ + `event carries no {tenantId} — ${GATE} cannot gate it (it fails closed, so the function would `
290
+ + `never run at all). Loop with ${OFFICE_LOOP}('<this extension's id>', …) inside the handler, or `
291
+ + 'it acts on every office in the project, including the ones that never installed the extension'));
292
+ /**
293
+ * Gatilho de uma function COMUM que declara quem serve, e não guarda ninguém.
294
+ *
295
+ * A comum ficava fora desta cobrança porque não havia id para dar à guarda: nenhuma extensão pode
296
+ * reivindicá-la. `microteia.serves` resolve exatamente isso — a partir dela existem ids, e a partir dos
297
+ * ids a cobrança passa a ser possível. Um gatilho comum sem guarda dispara para todo escritório do
298
+ * projeto, e é o mesmo defeito do gatilho de extensão sem portão, com um alcance maior.
299
+ *
300
+ * A comum SEM declaração continua de fora, e é deliberado: cobrar dela uma guarda sem ter ids a oferecer
301
+ * seria fechar a porta sem abrir nenhuma. O que ela recebe é o relato do provisionamento.
302
+ *
303
+ * A segunda recusa é a guarda que nomeia extensão que a function não declara servir: as duas listas
304
+ * respondem à mesma pergunta em dois lugares, e divergirem faz uma delas mentir — a `serves` decide a
305
+ * retirada, a guarda decide a execução, e a function passaria a agir onde não pode ser retirada.
306
+ */
307
+ export const unguardedSharedProblems = (subjects) => subjects
308
+ .filter((subject) => subject.shared)
309
+ .flatMap((subject) => exportedTriggers(subject).flatMap((trigger) => {
310
+ const serves = subject.serves[trigger.name];
311
+ if (serves === undefined || serves.length === 0)
312
+ return [];
313
+ if (trigger.guards.length === 0) {
314
+ return [`${subject.label} exports "${trigger.name}", a ${trigger.trigger} trigger declared as `
315
+ + `serving ${serves.join(', ')} — guard it with ${ANY_GATE}([${serves.map((id) => `'${id}'`).join(', ')}], …) `
316
+ + 'or it fires for every office in the project, including the ones that installed none of them'];
317
+ }
318
+ const stranger = trigger.guards.filter((id) => !serves.includes(id));
319
+ if (stranger.length === 0)
320
+ return [];
321
+ return [`${subject.label} guards "${trigger.name}" for ${stranger.join(', ')}, which it does not `
322
+ + `declare serving in microteia.serves (${serves.join(', ')}) — the two lists answer the same `
323
+ + 'question, and one of them is wrong'];
324
+ }));
325
+ /**
326
+ * Callable de extensão sem a guarda de instalação.
327
+ *
328
+ * "Uma callable órfã é inerte" era verdade de INTERFACE, e não de segurança — e era a frase que o guia
329
+ * dizia. O deploy concede a `allUsers` o papel `roles/run.invoker` em cada callable; o que sobra depois é
330
+ * a validação do token do Firebase. Então a tela ir embora com a extensão não fecha nada: o endpoint
331
+ * segue no ar aceitando o token de QUALQUER usuário do projeto, de qualquer escritório, tendo ou não a
332
+ * extensão.
333
+ *
334
+ * E o escritório chega no PAYLOAD, porque `useCallable` manda só o que a tela passou. Sem a guarda, um
335
+ * usuário do escritório A chama a function apontando para o B — e o molde gerado conferia apenas se
336
+ * havia `request.auth`.
337
+ *
338
+ * A comum fica de fora: `requireInstalledExtension` pede UM id, e a comum serve vários. É lacuna
339
+ * conhecida e está registrada — a guarda "alguma das que eu sirvo" existe para gatilho, não para
340
+ * callable.
341
+ */
342
+ export const unguardedCallableProblems = (subjects) => subjects
343
+ .filter((subject) => !subject.shared)
344
+ .flatMap((subject) => exportedCallables(subject)
345
+ .filter((callable) => callable.gatedFor === null)
346
+ .map((callable) => `${subject.label} exports "${callable.name}", a callable with no installation `
347
+ + `check — call ${CALLABLE_GUARD}('<this extension's id>', tenantId, request) before you read the `
348
+ + 'payload. The deploy makes every callable publicly invocable, so it answers any signed-in user '
349
+ + 'of this project, from any office, whether or not they installed the extension'));
350
+ /**
351
+ * Portão aberto para o id ERRADO.
352
+ *
353
+ * É o defeito do copiar-e-colar entre extensões, e o sintoma dele é o oposto do gatilho sem portão: a
354
+ * function sobe, o gatilho dispara, e o handler nunca roda em escritório nenhum. Nada falha — a extensão
355
+ * apenas não faz o que promete, e a causa está numa string.
356
+ *
357
+ * Devolve os casos, e não as frases, porque quem chama precisa dos dois: a mensagem para relatar e o id
358
+ * da extensão para pendurar. Deduzir o id de volta a partir do texto é o que faz um relatório recusar a
359
+ * extensão errada quando o nome de uma aparece dentro da mensagem de outra.
360
+ *
361
+ * Só se pergunta quando o nome tem UM reivindicante: com dois, `functionClaimProblems` já recusou antes,
362
+ * e apontar um deles como o dono certo seria inventar uma resposta.
363
+ */
364
+ export const misgatedTriggers = (subjects, claimants) => {
365
+ const wrong = (subject, declarations, via) => declarations.flatMap((trigger) => {
366
+ const owner = claimants.get(trigger.name);
367
+ if (trigger.gatedFor === null || owner === undefined || owner === trigger.gatedFor)
368
+ return [];
369
+ return [{ label: subject.label, name: trigger.name, gatedFor: trigger.gatedFor, owner, via }];
370
+ });
371
+ return subjects
372
+ .filter((subject) => !subject.shared)
373
+ .flatMap((subject) => [
374
+ ...wrong(subject, exportedTriggers(subject), GATE),
375
+ ...wrong(subject, exportedUnscoped(subject), OFFICE_LOOP),
376
+ ...wrong(subject, exportedCallables(subject), CALLABLE_GUARD),
377
+ ]);
378
+ };
379
+ /** Como uma guarda de id errado aparece num relatório. */
380
+ export const describeMisgated = (entry) => `${entry.label} passes "${entry.gatedFor}" to ${entry.via} for "${entry.name}", but the extension that `
381
+ + `claims it is "${entry.owner}" — the guard never opens, so nothing this function does ever happens`;
382
+ /**
383
+ * Quem reivindica cada nome, quando é um só. Duas extensões pedindo o mesmo nome é problema de outra
384
+ * verificação, e o nome sai daqui em vez de receber um dono escolhido a esmo.
385
+ */
386
+ export const singleClaimants = (claims) => {
387
+ const owners = new Map();
388
+ const disputed = new Set();
389
+ for (const claim of claims) {
390
+ for (const name of claim.backend.functions ?? []) {
391
+ const previous = owners.get(name);
392
+ if (previous !== undefined && previous !== claim.pluginId)
393
+ disputed.add(name);
394
+ owners.set(name, claim.pluginId);
395
+ }
396
+ }
397
+ for (const name of disputed)
398
+ owners.delete(name);
399
+ return owners;
400
+ };
@@ -8,6 +8,31 @@ export interface InstalledClaim extends BackendClaim {
8
8
  documentPath: string;
9
9
  applied: boolean;
10
10
  }
11
+ /**
12
+ * A varredura inteira, e não só o que ela achou.
13
+ *
14
+ * As contagens estão aqui porque é delas que sai o direito de APAGAR: a Cloud Function é do PROJETO e a
15
+ * instalação é do ESCRITÓRIO, então "extensão nenhuma exige mais" é uma afirmação sobre todos os
16
+ * escritórios do projeto, não sobre o último que foi lido. A varredura é completa ou não existe —
17
+ * `listDocuments` pagina até o fim e qualquer leitura recusada derruba o comando —, e o que faltava era
18
+ * DIZER isso a quem lê a proposta de remoção.
19
+ */
20
+ export interface InstalledSurvey {
21
+ /** Escritórios lidos. Todos os do projeto, ou nada abaixo teria acontecido. */
22
+ tenants: number;
23
+ /** Cópias de extensão lidas, ativas ou não. */
24
+ extensions: number;
25
+ claims: InstalledClaim[];
26
+ /**
27
+ * Todo id instalado e ativo no projeto, exija ele backend ou não.
28
+ *
29
+ * `claims` não serve para esta pergunta, e a diferença é a que faz a function comum ser retirada pelo
30
+ * motivo certo: uma extensão sem `manifest.backend` nunca vira reivindicação, mas ela pode muito bem
31
+ * ser uma das que a function comum serve. Decidir pela lista de reivindicações apagaria a function de
32
+ * quem está instalado.
33
+ */
34
+ installedIds: Set<string>;
35
+ }
11
36
  /**
12
37
  * O que cada escritório do projeto instalou e exige do backend.
13
38
  *
@@ -15,8 +40,7 @@ export interface InstalledClaim extends BackendClaim {
15
40
  * contra `manifest.functions` antes de qualquer deploy. Nome de function fora do pacote é recusado com
16
41
  * explicação; não há injeção possível, porque o código é sempre o do pacote.
17
42
  */
18
- export declare const readInstalledClaims: (projectId: string) => Promise<InstalledClaim[]>;
19
- export declare const parseAppliedFunctions: (value: string | null) => string[];
43
+ export declare const surveyInstallations: (projectId: string) => Promise<InstalledSurvey>;
20
44
  export interface ProvisionOptions {
21
45
  projectId: string;
22
46
  region: string;
@@ -51,17 +75,6 @@ export interface ProvisionOutcome {
51
75
  * divergiriam exatamente no ponto em que ninguém olha.
52
76
  */
53
77
  export declare const provisionBackend: (options: ProvisionOptions) => Promise<ProvisionOutcome>;
54
- /**
55
- * A lista aplicada CONVERGE para o que ainda é exigido: o que já estava e continua sendo pedido, mais
56
- * o que subiu agora, mais o órfão que não foi removido. Reescrevê-la só com o que subiu NESTA execução
57
- * a esvaziava na segunda passada — e a terceira redeployava tudo, achando que nada havia sido feito.
58
- */
59
- export declare const appliedAfterRun: (input: {
60
- alreadyApplied: string[];
61
- wanted: string[];
62
- deployed: string[];
63
- keptOrphans: string[];
64
- }) => string[];
65
78
  /**
66
79
  * A impressão digital do parâmetro só avança quando TODAS as functions que o carregam subiram. Gravar
67
80
  * antes disso é o que transformaria uma falha de deploy em silêncio permanente: o valor novo ficaria