@baruk/core 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.baruk-provenance.json +4 -4
- package/dist/cjs/clients/ibge.js +14 -0
- package/dist/cjs/clients/ibge.js.map +1 -1
- package/dist/cjs/clients/registry/http.js +157 -0
- package/dist/cjs/clients/registry/http.js.map +1 -0
- package/dist/cjs/clients/registry/ibge-municipios.js +28 -0
- package/dist/cjs/clients/registry/ibge-municipios.js.map +1 -0
- package/dist/cjs/clients/registry/ibge.js +63 -0
- package/dist/cjs/clients/registry/ibge.js.map +1 -0
- package/dist/cjs/clients/registry/lookup.js +191 -0
- package/dist/cjs/clients/registry/lookup.js.map +1 -0
- package/dist/cjs/clients/registry/provider.js +34 -0
- package/dist/cjs/clients/registry/provider.js.map +1 -0
- package/dist/cjs/clients/registry/resolve.js +47 -0
- package/dist/cjs/clients/registry/resolve.js.map +1 -0
- package/dist/cjs/clients/registry/serpro.js +149 -0
- package/dist/cjs/clients/registry/serpro.js.map +1 -0
- package/dist/cjs/clients/registry/service.js +230 -0
- package/dist/cjs/clients/registry/service.js.map +1 -0
- package/dist/cjs/clients/registry/synthetic.js +165 -0
- package/dist/cjs/clients/registry/synthetic.js.map +1 -0
- package/dist/cjs/clients/repository.js +6 -2
- package/dist/cjs/clients/repository.js.map +1 -1
- package/dist/cjs/db/provision-e2e-identity.js +8 -3
- package/dist/cjs/db/provision-e2e-identity.js.map +1 -1
- package/dist/cjs/fiscal/collection/authorization.js +186 -0
- package/dist/cjs/fiscal/collection/authorization.js.map +1 -0
- package/dist/cjs/fiscal/collection/competence.js +97 -0
- package/dist/cjs/fiscal/collection/competence.js.map +1 -0
- package/dist/cjs/fiscal/collection/first-contact.js +204 -0
- package/dist/cjs/fiscal/collection/first-contact.js.map +1 -0
- package/dist/cjs/fiscal/collection/plan.js +122 -0
- package/dist/cjs/fiscal/collection/plan.js.map +1 -0
- package/dist/cjs/fiscal/coverage/model.js +11 -1
- package/dist/cjs/fiscal/coverage/model.js.map +1 -1
- package/dist/cjs/fiscal/credentials/inspect.js +182 -0
- package/dist/cjs/fiscal/credentials/inspect.js.map +1 -0
- package/dist/cjs/fiscal/credentials/material.js +1 -1
- package/dist/cjs/fiscal/credentials/material.js.map +1 -1
- package/dist/cjs/fiscal/credentials/onboarding.js +266 -0
- package/dist/cjs/fiscal/credentials/onboarding.js.map +1 -0
- package/dist/cjs/fiscal/credentials/readiness.js +29 -2
- package/dist/cjs/fiscal/credentials/readiness.js.map +1 -1
- package/dist/cjs/fiscal/enrollment.js +134 -0
- package/dist/cjs/fiscal/enrollment.js.map +1 -0
- package/dist/cjs/fiscal/transport/synthetic-tls.js +52 -0
- package/dist/cjs/fiscal/transport/synthetic-tls.js.map +1 -1
- package/dist/cjs/index.js +196 -113
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/legal/documents.js +64 -4
- package/dist/cjs/legal/documents.js.map +1 -1
- package/dist/cjs/legal/types.js.map +1 -1
- package/dist/cjs/platform/capabilities.js +29 -0
- package/dist/cjs/platform/capabilities.js.map +1 -1
- package/dist/cjs/rbac/catalog.js +78 -2
- package/dist/cjs/rbac/catalog.js.map +1 -1
- package/dist/cjs/subscription-billing/index.js +8 -1
- package/dist/cjs/subscription-billing/index.js.map +1 -1
- package/dist/cjs/subscription-billing/platform-exemption.js +121 -0
- package/dist/cjs/subscription-billing/platform-exemption.js.map +1 -0
- package/dist/cjs/subscription-billing/service.js +93 -1
- package/dist/cjs/subscription-billing/service.js.map +1 -1
- package/dist/cjs/vault/repository.js +92 -5
- package/dist/cjs/vault/repository.js.map +1 -1
- package/dist/clients/ibge.d.ts.map +1 -1
- package/dist/clients/ibge.js +14 -0
- package/dist/clients/ibge.js.map +1 -1
- package/dist/clients/registry/http.d.ts +61 -0
- package/dist/clients/registry/http.d.ts.map +1 -0
- package/dist/clients/registry/http.js +151 -0
- package/dist/clients/registry/http.js.map +1 -0
- package/dist/clients/registry/ibge-municipios.d.ts +25 -0
- package/dist/clients/registry/ibge-municipios.d.ts.map +1 -0
- package/dist/clients/registry/ibge-municipios.js +25 -0
- package/dist/clients/registry/ibge-municipios.js.map +1 -0
- package/dist/clients/registry/ibge.d.ts +62 -0
- package/dist/clients/registry/ibge.d.ts.map +1 -0
- package/dist/clients/registry/ibge.js +57 -0
- package/dist/clients/registry/ibge.js.map +1 -0
- package/dist/clients/registry/lookup.d.ts +94 -0
- package/dist/clients/registry/lookup.d.ts.map +1 -0
- package/dist/clients/registry/lookup.js +184 -0
- package/dist/clients/registry/lookup.js.map +1 -0
- package/dist/clients/registry/provider.d.ts +124 -0
- package/dist/clients/registry/provider.d.ts.map +1 -0
- package/dist/clients/registry/provider.js +39 -0
- package/dist/clients/registry/provider.js.map +1 -0
- package/dist/clients/registry/resolve.d.ts +36 -0
- package/dist/clients/registry/resolve.d.ts.map +1 -0
- package/dist/clients/registry/resolve.js +44 -0
- package/dist/clients/registry/resolve.js.map +1 -0
- package/dist/clients/registry/serpro.d.ts +57 -0
- package/dist/clients/registry/serpro.d.ts.map +1 -0
- package/dist/clients/registry/serpro.js +143 -0
- package/dist/clients/registry/serpro.js.map +1 -0
- package/dist/clients/registry/service.d.ts +59 -0
- package/dist/clients/registry/service.d.ts.map +1 -0
- package/dist/clients/registry/service.js +225 -0
- package/dist/clients/registry/service.js.map +1 -0
- package/dist/clients/registry/synthetic.d.ts +30 -0
- package/dist/clients/registry/synthetic.d.ts.map +1 -0
- package/dist/clients/registry/synthetic.js +161 -0
- package/dist/clients/registry/synthetic.js.map +1 -0
- package/dist/clients/repository.d.ts +4 -0
- package/dist/clients/repository.d.ts.map +1 -1
- package/dist/clients/repository.js +6 -2
- package/dist/clients/repository.js.map +1 -1
- package/dist/db/provision-e2e-identity.js +8 -3
- package/dist/db/provision-e2e-identity.js.map +1 -1
- package/dist/fiscal/collection/authorization.d.ts +152 -0
- package/dist/fiscal/collection/authorization.d.ts.map +1 -0
- package/dist/fiscal/collection/authorization.js +178 -0
- package/dist/fiscal/collection/authorization.js.map +1 -0
- package/dist/fiscal/collection/competence.d.ts +66 -0
- package/dist/fiscal/collection/competence.d.ts.map +1 -0
- package/dist/fiscal/collection/competence.js +90 -0
- package/dist/fiscal/collection/competence.js.map +1 -0
- package/dist/fiscal/collection/first-contact.d.ts +103 -0
- package/dist/fiscal/collection/first-contact.d.ts.map +1 -0
- package/dist/fiscal/collection/first-contact.js +201 -0
- package/dist/fiscal/collection/first-contact.js.map +1 -0
- package/dist/fiscal/collection/plan.d.ts +74 -0
- package/dist/fiscal/collection/plan.d.ts.map +1 -0
- package/dist/fiscal/collection/plan.js +119 -0
- package/dist/fiscal/collection/plan.js.map +1 -0
- package/dist/fiscal/coverage/model.d.ts +28 -0
- package/dist/fiscal/coverage/model.d.ts.map +1 -1
- package/dist/fiscal/coverage/model.js +11 -1
- package/dist/fiscal/coverage/model.js.map +1 -1
- package/dist/fiscal/credentials/inspect.d.ts +114 -0
- package/dist/fiscal/credentials/inspect.d.ts.map +1 -0
- package/dist/fiscal/credentials/inspect.js +178 -0
- package/dist/fiscal/credentials/inspect.js.map +1 -0
- package/dist/fiscal/credentials/material.js +2 -2
- package/dist/fiscal/credentials/material.js.map +1 -1
- package/dist/fiscal/credentials/onboarding.d.ts +119 -0
- package/dist/fiscal/credentials/onboarding.d.ts.map +1 -0
- package/dist/fiscal/credentials/onboarding.js +259 -0
- package/dist/fiscal/credentials/onboarding.js.map +1 -0
- package/dist/fiscal/credentials/readiness.d.ts +30 -3
- package/dist/fiscal/credentials/readiness.d.ts.map +1 -1
- package/dist/fiscal/credentials/readiness.js +29 -2
- package/dist/fiscal/credentials/readiness.js.map +1 -1
- package/dist/fiscal/enrollment.d.ts +91 -0
- package/dist/fiscal/enrollment.d.ts.map +1 -0
- package/dist/fiscal/enrollment.js +126 -0
- package/dist/fiscal/enrollment.js.map +1 -0
- package/dist/fiscal/transport/synthetic-tls.d.ts +19 -0
- package/dist/fiscal/transport/synthetic-tls.d.ts.map +1 -1
- package/dist/fiscal/transport/synthetic-tls.js +51 -0
- package/dist/fiscal/transport/synthetic-tls.js.map +1 -1
- package/dist/index.d.ts +32 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/legal/documents.d.ts.map +1 -1
- package/dist/legal/documents.js +64 -4
- package/dist/legal/documents.js.map +1 -1
- package/dist/legal/index.d.ts +1 -1
- package/dist/legal/index.d.ts.map +1 -1
- package/dist/legal/types.d.ts +77 -0
- package/dist/legal/types.d.ts.map +1 -1
- package/dist/legal/types.js.map +1 -1
- package/dist/platform/capabilities.d.ts.map +1 -1
- package/dist/platform/capabilities.js +29 -0
- package/dist/platform/capabilities.js.map +1 -1
- package/dist/rbac/catalog.d.ts.map +1 -1
- package/dist/rbac/catalog.js +78 -2
- package/dist/rbac/catalog.js.map +1 -1
- package/dist/subscription-billing/index.d.ts +3 -1
- package/dist/subscription-billing/index.d.ts.map +1 -1
- package/dist/subscription-billing/index.js +2 -1
- package/dist/subscription-billing/index.js.map +1 -1
- package/dist/subscription-billing/platform-exemption.d.ts +89 -0
- package/dist/subscription-billing/platform-exemption.d.ts.map +1 -0
- package/dist/subscription-billing/platform-exemption.js +113 -0
- package/dist/subscription-billing/platform-exemption.js.map +1 -0
- package/dist/subscription-billing/service.d.ts +53 -0
- package/dist/subscription-billing/service.d.ts.map +1 -1
- package/dist/subscription-billing/service.js +92 -1
- package/dist/subscription-billing/service.js.map +1 -1
- package/dist/vault/repository.d.ts +69 -1
- package/dist/vault/repository.d.ts.map +1 -1
- package/dist/vault/repository.js +88 -5
- package/dist/vault/repository.js.map +1 -1
- package/package.json +3 -2
- package/src/db/migrations/0060_fiscal_credential_vault_and_run_authorization.sql +321 -0
- package/src/db/migrations/0061_client_official_registry_provenance.sql +193 -0
- package/src/db/migrations/0062_first_contact_and_capability_truth.sql +119 -0
package/dist/vault/repository.js
CHANGED
|
@@ -7,6 +7,7 @@ function mapMeta(r) {
|
|
|
7
7
|
name: r.name,
|
|
8
8
|
category: r.category,
|
|
9
9
|
provider: r.provider,
|
|
10
|
+
exposure: r.exposure ?? 'revealable',
|
|
10
11
|
keyVersion: r.key_version,
|
|
11
12
|
metadata: r.metadata ?? {},
|
|
12
13
|
expiresAt: r.expires_at ? r.expires_at.toISOString() : null,
|
|
@@ -16,26 +17,43 @@ function mapMeta(r) {
|
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
const META_COLUMNS = `
|
|
19
|
-
id, client_id, name, category, provider, key_version, metadata,
|
|
20
|
+
id, client_id, name, category, provider, exposure, key_version, metadata,
|
|
20
21
|
expires_at, rotated_at, created_at, updated_at
|
|
21
22
|
`;
|
|
23
|
+
/**
|
|
24
|
+
* A classe de exposição de uma categoria.
|
|
25
|
+
*
|
|
26
|
+
* Material de certificado NUNCA é revelável, e a regra vive nos dois lugares de
|
|
27
|
+
* propósito: aqui, para que a gravação nasça certa, e no CHECK da 0060, para que
|
|
28
|
+
* um caminho que ninguém escreveu ainda também nasça certo.
|
|
29
|
+
*
|
|
30
|
+
* Sem esta função, `createCredential` — a porta genérica do cofre, usada por
|
|
31
|
+
* rotas e seeds — gravaria `revealable` e receberia uma violação de constraint.
|
|
32
|
+
* O comportamento seria seguro e a mensagem, incompreensível: um 500 de
|
|
33
|
+
* constraint onde deveria haver uma gravação correta.
|
|
34
|
+
*/
|
|
35
|
+
export function exposureFor(category) {
|
|
36
|
+
return category === 'certificate' ? 'write_only' : 'revealable';
|
|
37
|
+
}
|
|
22
38
|
export async function createCredential(tx, keyring, tenantId, input) {
|
|
23
39
|
const enc = encryptSecret(keyring, tenantId, input.secret);
|
|
40
|
+
const category = input.category ?? 'generic';
|
|
24
41
|
const rows = await tx `
|
|
25
42
|
INSERT INTO vault_credential (
|
|
26
43
|
id, tenant_id, client_id, name, category, provider,
|
|
27
|
-
ciphertext, iv, auth_tag, key_version, metadata, expires_at, created_by
|
|
44
|
+
ciphertext, iv, auth_tag, key_version, metadata, expires_at, created_by, exposure
|
|
28
45
|
) VALUES (
|
|
29
46
|
${uuidv7()},
|
|
30
47
|
current_setting('app.current_tenant')::uuid,
|
|
31
48
|
${input.clientId ?? null},
|
|
32
49
|
${input.name},
|
|
33
|
-
${
|
|
50
|
+
${category},
|
|
34
51
|
${input.provider ?? null},
|
|
35
52
|
${enc.ciphertext}, ${enc.iv}, ${enc.authTag}, ${enc.keyVersion},
|
|
36
53
|
${tx.json((input.metadata ?? {}))},
|
|
37
54
|
${input.expiresAt ?? null},
|
|
38
|
-
${input.createdBy ?? null}
|
|
55
|
+
${input.createdBy ?? null},
|
|
56
|
+
${exposureFor(category)}
|
|
39
57
|
)
|
|
40
58
|
RETURNING ${tx.unsafe(META_COLUMNS)}
|
|
41
59
|
`;
|
|
@@ -60,11 +78,76 @@ export async function getCredentialMeta(tx, id) {
|
|
|
60
78
|
throw new NotFoundError('Credencial não encontrada');
|
|
61
79
|
return mapMeta(rows[0]);
|
|
62
80
|
}
|
|
63
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Erro de quem pede para ver um segredo que não tem caminho de volta.
|
|
83
|
+
*
|
|
84
|
+
* Nomeado, e não um `NotFoundError`, porque a diferença importa para quem opera:
|
|
85
|
+
* "não existe" manda procurar; "não é revelável" manda substituir. Não carrega
|
|
86
|
+
* nome nem ponteiro — só a classe.
|
|
87
|
+
*/
|
|
88
|
+
export class SecretNotRevealableError extends Error {
|
|
89
|
+
constructor() {
|
|
90
|
+
super('Esta credencial é write-only: não existe caminho de leitura para humano.');
|
|
91
|
+
this.name = 'SecretNotRevealableError';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Decifra e retorna o segredo em claro. A ROTA deve exigir
|
|
96
|
+
* `vault.credential.decrypt` e auditar.
|
|
97
|
+
*
|
|
98
|
+
* ## O portão que faltava
|
|
99
|
+
*
|
|
100
|
+
* Esta função era o caminho ÚNICO de saída do cofre, e ela não perguntava que
|
|
101
|
+
* tipo de segredo estava devolvendo. Para uma chave de API isso é o
|
|
102
|
+
* comportamento desejado. Para o envelope de um certificado digital — que
|
|
103
|
+
* carrega a CHAVE PRIVADA do cliente em base64 — era a porta dos fundos: um
|
|
104
|
+
* `POST .../reveal` devolvia o material inteiro para a tela, e o papel
|
|
105
|
+
* `accountant` do catálogo já carregava a permissão.
|
|
106
|
+
*
|
|
107
|
+
* A recusa acontece DEPOIS de ler a linha e ANTES de decifrar: nem o custo da
|
|
108
|
+
* decifração é pago, e o texto em claro nunca chega a existir em memória.
|
|
109
|
+
*/
|
|
64
110
|
export async function revealSecret(tx, keyring, tenantId, id) {
|
|
65
111
|
const rows = await tx `
|
|
66
112
|
SELECT ${tx.unsafe(META_COLUMNS)}, ciphertext, iv, auth_tag
|
|
67
113
|
FROM vault_credential WHERE id = ${id} AND deleted_at IS NULL
|
|
114
|
+
`;
|
|
115
|
+
if (!rows[0])
|
|
116
|
+
throw new NotFoundError('Credencial não encontrada');
|
|
117
|
+
const r = rows[0];
|
|
118
|
+
if ((r.exposure ?? 'revealable') === 'write_only')
|
|
119
|
+
throw new SecretNotRevealableError();
|
|
120
|
+
const secret = decryptSecret(keyring, tenantId, {
|
|
121
|
+
ciphertext: r.ciphertext,
|
|
122
|
+
iv: r.iv,
|
|
123
|
+
authTag: r.auth_tag,
|
|
124
|
+
keyVersion: r.key_version,
|
|
125
|
+
});
|
|
126
|
+
return { meta: mapMeta(r), secret };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* O caminho de PROCESSO para um segredo write-only.
|
|
130
|
+
*
|
|
131
|
+
* ## Por que ela precisa existir, e por que não é exportada
|
|
132
|
+
*
|
|
133
|
+
* `revealSecret` passou a recusar `write_only` — o que fecha a rota humana e,
|
|
134
|
+
* sem esta função, quebraria o motor fiscal: `loadFiscalTlsMaterial` também
|
|
135
|
+
* decifra o mesmo envelope, e ele é justamente quem deve conseguir.
|
|
136
|
+
*
|
|
137
|
+
* A diferença entre os dois chamadores não é a permissão de quem pediu; é o
|
|
138
|
+
* DESTINO do texto em claro. Em `revealSecret` ele vira corpo de resposta HTTP e
|
|
139
|
+
* chega a uma tela. Aqui ele vira `Buffer` dentro de um `https.Agent`, é usado
|
|
140
|
+
* num handshake e é zerado — nunca atravessa a fronteira do processo.
|
|
141
|
+
*
|
|
142
|
+
* O nome grita o que a função faz porque é o único aviso que sobrevive a um
|
|
143
|
+
* autocomplete. E ela NÃO é reexportada pelo `index` do pacote: fora de
|
|
144
|
+
* `@baruk/core` não existe caminho para chamá-la, do mesmo modo que
|
|
145
|
+
* `grantFiscalSecretAccess` não existe fora daqui.
|
|
146
|
+
*/
|
|
147
|
+
export async function revealWriteOnlySecretForProcess(tx, keyring, tenantId, id) {
|
|
148
|
+
const rows = await tx `
|
|
149
|
+
SELECT ${tx.unsafe(META_COLUMNS)}, ciphertext, iv, auth_tag
|
|
150
|
+
FROM vault_credential WHERE id = ${id} AND deleted_at IS NULL
|
|
68
151
|
`;
|
|
69
152
|
if (!rows[0])
|
|
70
153
|
throw new NotFoundError('Credencial não encontrada');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repository.js","sourceRoot":"","sources":["../../src/vault/repository.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAE,aAAa,EAAE,aAAa,EAAgB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"repository.js","sourceRoot":"","sources":["../../src/vault/repository.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baruk/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/cjs/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"seed": "tsx src/db/seed.ts",
|
|
69
69
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
70
70
|
"seed:catalog": "tsx src/db/seed-catalog.ts",
|
|
71
|
-
"provision:e2e": "tsx src/db/provision-e2e-identity.ts"
|
|
71
|
+
"provision:e2e": "tsx src/db/provision-e2e-identity.ts",
|
|
72
|
+
"audit:fiscal-onboarding": "node scripts/audit-fiscal-credential-onboarding.mjs"
|
|
72
73
|
}
|
|
73
74
|
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
-- 0060 — o cofre fiscal ganha classe de exposição, o certificado ganha
|
|
2
|
+
-- identidade, e a primeira consulta real ganha um portão que se consome.
|
|
3
|
+
--
|
|
4
|
+
-- ═══ O defeito que a primeira seção existe para tornar indizível ══════════
|
|
5
|
+
--
|
|
6
|
+
-- `vault_credential` (0012) guarda todo segredo do escritório sob uma única
|
|
7
|
+
-- regra de leitura: quem tem `vault.credential.decrypt` chama
|
|
8
|
+
-- `POST /v1/vault/credentials/:id/reveal` e recebe o texto em claro. Para uma
|
|
9
|
+
-- chave de API isso é o comportamento desejado — o operador precisa colá-la
|
|
10
|
+
-- noutro sistema.
|
|
11
|
+
--
|
|
12
|
+
-- Para o material de um certificado digital, é a porta dos fundos do cofre. O
|
|
13
|
+
-- envelope que a Fase 2 definiu carrega a CHAVE PRIVADA do cliente em base64;
|
|
14
|
+
-- revelá-lo numa tela é entregá-la, e "não faça isso" não é um controle. Pior:
|
|
15
|
+
-- o papel `accountant` do catálogo já carrega `vault.credential.decrypt`, de
|
|
16
|
+
-- modo que a porta estava aberta para todo contador do escritório.
|
|
17
|
+
--
|
|
18
|
+
-- A correção não é tirar o botão da tela. É dizer no BANCO que existem duas
|
|
19
|
+
-- classes de segredo, e que a segunda não tem caminho de saída:
|
|
20
|
+
--
|
|
21
|
+
-- revealable pode voltar ao humano que o guardou (chave de API, token).
|
|
22
|
+
-- write_only entra, é usado pelo processo, e NUNCA volta.
|
|
23
|
+
--
|
|
24
|
+
-- E é uma RESTRIÇÃO, não uma convenção, que amarra `certificate` à segunda: um
|
|
25
|
+
-- certificado gravado por qualquer caminho — rota nova, script, seed — nasce
|
|
26
|
+
-- irrecuperável, inclusive num caminho que ainda não foi escrito.
|
|
27
|
+
|
|
28
|
+
ALTER TABLE vault_credential ADD COLUMN IF NOT EXISTS exposure text NOT NULL DEFAULT 'revealable';
|
|
29
|
+
|
|
30
|
+
DO $$
|
|
31
|
+
BEGIN
|
|
32
|
+
IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'vault_credential_exposure_values') THEN
|
|
33
|
+
ALTER TABLE vault_credential ADD CONSTRAINT vault_credential_exposure_values
|
|
34
|
+
CHECK (exposure IN ('revealable', 'write_only'));
|
|
35
|
+
END IF;
|
|
36
|
+
END $$;
|
|
37
|
+
|
|
38
|
+
-- Backfill ANTES da restrição de coerência: uma credencial de certificado já
|
|
39
|
+
-- gravada sob a regra antiga tornaria o CHECK inválido, e um `NOT VALID`
|
|
40
|
+
-- silencioso deixaria exatamente as linhas antigas — as que já correram risco —
|
|
41
|
+
-- fora da garantia.
|
|
42
|
+
UPDATE vault_credential SET exposure = 'write_only'
|
|
43
|
+
WHERE category = 'certificate' AND exposure <> 'write_only';
|
|
44
|
+
|
|
45
|
+
DO $$
|
|
46
|
+
BEGIN
|
|
47
|
+
IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'vault_credential_certificate_is_write_only') THEN
|
|
48
|
+
ALTER TABLE vault_credential ADD CONSTRAINT vault_credential_certificate_is_write_only
|
|
49
|
+
CHECK (category <> 'certificate' OR exposure = 'write_only');
|
|
50
|
+
END IF;
|
|
51
|
+
END $$;
|
|
52
|
+
|
|
53
|
+
COMMENT ON COLUMN vault_credential.exposure IS
|
|
54
|
+
'write_only = não existe caminho de leitura para humano, por nenhuma rota. Material de certificado é write_only por RESTRIÇÃO, não por disciplina da camada de rota.';
|
|
55
|
+
COMMENT ON CONSTRAINT vault_credential_certificate_is_write_only ON vault_credential IS
|
|
56
|
+
'Remover esta restrição é a decisão consciente de permitir que a chave privada de um cliente volte para uma tela.';
|
|
57
|
+
|
|
58
|
+
-- ═══ 2 · A autorização de execução única ═════════════════════════════════
|
|
59
|
+
--
|
|
60
|
+
-- O motor da Fase 2 tem três desligamentos (flag de ambiente, elegibilidade,
|
|
61
|
+
-- cadência) e todos são ESTADO: ligados, ficam ligados. Para a primeira
|
|
62
|
+
-- consulta real contra um ambiente oficial, em nome de um contribuinte que não
|
|
63
|
+
-- pediu nada, estado permanente é a forma errada de autorização — o titular
|
|
64
|
+
-- autorizou UMA leitura, não um regime.
|
|
65
|
+
--
|
|
66
|
+
-- Esta tabela é um portão que se gasta. Quatro propriedades, todas no schema:
|
|
67
|
+
--
|
|
68
|
+
-- `max_runs = 1` CHECK literal. "Uma execução" não é configuração.
|
|
69
|
+
-- `operation = 'READ_ONLY'` idem. Mutação é INGRAVÁVEL nesta fase.
|
|
70
|
+
-- `expires_at` uma autorização esquecida morre sozinha.
|
|
71
|
+
-- `runs_consumed` nunca ultrapassa `max_runs`, e o CHECK é quem diz.
|
|
72
|
+
--
|
|
73
|
+
-- O período vem por EXTENSO — `period_start` e `period_end` além do par
|
|
74
|
+
-- ano/mês. O relato humano que originou esta frente continha exemplos de datas
|
|
75
|
+
-- inconsistentes entre si, e transformar "mês anterior" em regra implícita
|
|
76
|
+
-- produziria uma coleta cuja janela ninguém confirmou. Aqui a janela é dado
|
|
77
|
+
-- gravado, e a tela mostra as duas datas antes de qualquer execução.
|
|
78
|
+
CREATE TABLE IF NOT EXISTS fiscal_run_authorization (
|
|
79
|
+
id uuid PRIMARY KEY,
|
|
80
|
+
tenant_id uuid NOT NULL REFERENCES tenant(id),
|
|
81
|
+
client_id uuid NOT NULL,
|
|
82
|
+
|
|
83
|
+
document_type text NOT NULL CHECK (document_type IN ('nfse', 'nfe', 'cte', 'nfce')),
|
|
84
|
+
environment text NOT NULL CHECK (environment IN ('synthetic', 'homologation', 'production')),
|
|
85
|
+
-- Slug da fonte, o mesmo vocabulário que governa cursor, disjuntor e
|
|
86
|
+
-- prontidão (`fiscal_sync_cursor.source`). Uma autorização emitida para uma
|
|
87
|
+
-- fonte não vale para outra.
|
|
88
|
+
source text NOT NULL,
|
|
89
|
+
credential_kind text NOT NULL DEFAULT 'certificate_a1'
|
|
90
|
+
CHECK (credential_kind IN ('certificate_a1', 'certificate_a3', 'municipal_portal')),
|
|
91
|
+
|
|
92
|
+
operation text NOT NULL DEFAULT 'READ_ONLY' CHECK (operation IN ('READ_ONLY', 'MUTATING')),
|
|
93
|
+
|
|
94
|
+
competence_year int NOT NULL CHECK (competence_year BETWEEN 2000 AND 2100),
|
|
95
|
+
competence_month int NOT NULL CHECK (competence_month BETWEEN 1 AND 12),
|
|
96
|
+
period_start date NOT NULL,
|
|
97
|
+
period_end date NOT NULL,
|
|
98
|
+
|
|
99
|
+
max_runs int NOT NULL DEFAULT 1,
|
|
100
|
+
runs_consumed int NOT NULL DEFAULT 0,
|
|
101
|
+
|
|
102
|
+
authorized_by uuid NOT NULL REFERENCES "user"(id),
|
|
103
|
+
authorized_at timestamptz NOT NULL DEFAULT now(),
|
|
104
|
+
-- Responsável contábil DECLARADO para esta execução. Texto, e não FK para
|
|
105
|
+
-- `user`: o profissional responsável pode não ter login no produto, e exigir
|
|
106
|
+
-- que tivesse forçaria a criação de uma identidade que ninguém pediu.
|
|
107
|
+
responsible_name text,
|
|
108
|
+
expires_at timestamptz NOT NULL,
|
|
109
|
+
|
|
110
|
+
consumed_at timestamptz,
|
|
111
|
+
consumed_by_run_id uuid,
|
|
112
|
+
revoked_at timestamptz,
|
|
113
|
+
revoked_by uuid REFERENCES "user"(id),
|
|
114
|
+
revoke_reason text,
|
|
115
|
+
|
|
116
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
117
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
118
|
+
|
|
119
|
+
CONSTRAINT fiscal_run_authorization_tenant_id_uq UNIQUE (tenant_id, id),
|
|
120
|
+
FOREIGN KEY (tenant_id, client_id) REFERENCES client (tenant_id, id),
|
|
121
|
+
|
|
122
|
+
CONSTRAINT fiscal_run_authorization_one_shot CHECK (max_runs = 1),
|
|
123
|
+
CONSTRAINT fiscal_run_authorization_read_only CHECK (operation = 'READ_ONLY'),
|
|
124
|
+
CONSTRAINT fiscal_run_authorization_budget CHECK (runs_consumed >= 0 AND runs_consumed <= max_runs),
|
|
125
|
+
CONSTRAINT fiscal_run_authorization_consumption_coherent
|
|
126
|
+
CHECK ((consumed_at IS NULL) = (runs_consumed = 0)),
|
|
127
|
+
CONSTRAINT fiscal_run_authorization_window CHECK (period_end >= period_start),
|
|
128
|
+
CONSTRAINT fiscal_run_authorization_expiry CHECK (expires_at > authorized_at),
|
|
129
|
+
CONSTRAINT fiscal_run_authorization_revocation_coherent
|
|
130
|
+
CHECK ((revoked_at IS NULL) = (revoked_by IS NULL))
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
COMMENT ON CONSTRAINT fiscal_run_authorization_one_shot ON fiscal_run_authorization IS
|
|
134
|
+
'Uma execução. Não é configuração: é restrição. Um segundo run real exige uma segunda autorização.';
|
|
135
|
+
COMMENT ON CONSTRAINT fiscal_run_authorization_read_only ON fiscal_run_authorization IS
|
|
136
|
+
'Mutação em sistema fiscal externo é ingravável nesta fase. Remover esta restrição é a decisão consciente de permitir escrita.';
|
|
137
|
+
|
|
138
|
+
-- Uma autorização VIVA por (cliente, fonte, ambiente, competência). Sem isto,
|
|
139
|
+
-- emitir duas e consumir as duas devolve na prática o regime permanente que a
|
|
140
|
+
-- tabela existe para impedir.
|
|
141
|
+
CREATE UNIQUE INDEX IF NOT EXISTS fiscal_run_authorization_live_uq
|
|
142
|
+
ON fiscal_run_authorization (tenant_id, client_id, source, environment,
|
|
143
|
+
competence_year, competence_month)
|
|
144
|
+
WHERE consumed_at IS NULL AND revoked_at IS NULL;
|
|
145
|
+
|
|
146
|
+
CREATE INDEX IF NOT EXISTS fiscal_run_authorization_client_idx
|
|
147
|
+
ON fiscal_run_authorization (tenant_id, client_id, authorized_at DESC);
|
|
148
|
+
|
|
149
|
+
ALTER TABLE fiscal_run_authorization ENABLE ROW LEVEL SECURITY;
|
|
150
|
+
ALTER TABLE fiscal_run_authorization FORCE ROW LEVEL SECURITY;
|
|
151
|
+
CREATE POLICY fiscal_run_authorization_tenant_isolation ON fiscal_run_authorization
|
|
152
|
+
USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid)
|
|
153
|
+
WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid);
|
|
154
|
+
|
|
155
|
+
-- ═══ 3 · A cobrança da PLATAFORMA, separada da cobrança do escritório ═════
|
|
156
|
+
--
|
|
157
|
+
-- Há duas cobranças neste produto e confundi-las custa caro nas duas direções:
|
|
158
|
+
--
|
|
159
|
+
-- plataforma Baruk → escritório (assinatura do Office). `saas_subscription`.
|
|
160
|
+
-- escritório escritório → clientes dele (honorários). `receivable`.
|
|
161
|
+
--
|
|
162
|
+
-- O escritório fundador não paga a primeira. A forma ERRADA de implementar isso
|
|
163
|
+
-- é desligar o financeiro dele — que apagaria a segunda, que é o produto
|
|
164
|
+
-- inteiro. A forma errada seguinte é `if (tenant.code === 'sc')`, que faz o
|
|
165
|
+
-- segundo escritório fundador virar um deploy.
|
|
166
|
+
--
|
|
167
|
+
-- Esta tabela é a forma certa: um fato do tenant, com quem concedeu, quando, e
|
|
168
|
+
-- por quê. O motor de billing lê a linha; ele não conhece nenhum código de
|
|
169
|
+
-- escritório.
|
|
170
|
+
--
|
|
171
|
+
-- ═══ Quem escreve não é quem é cobrado ═══════════════════════════════════
|
|
172
|
+
--
|
|
173
|
+
-- Um controle escrito pelo controlado não é controle. Se o tenant pudesse
|
|
174
|
+
-- gravar a própria isenção, todo escritório seria fundador na primeira tarde.
|
|
175
|
+
-- As políticas abaixo são POR COMANDO: o tenant LÊ a própria linha; só o
|
|
176
|
+
-- operador da plataforma (a GUC `app.platform_operator`, que apenas as rotas de
|
|
177
|
+
-- backoffice setam) INSERE, ATUALIZA ou APAGA.
|
|
178
|
+
CREATE TABLE IF NOT EXISTS tenant_platform_billing (
|
|
179
|
+
tenant_id uuid PRIMARY KEY REFERENCES tenant(id),
|
|
180
|
+
billing_mode text NOT NULL DEFAULT 'standard' CHECK (billing_mode IN ('standard', 'exempt')),
|
|
181
|
+
exemption_reason text CHECK (exemption_reason IS NULL OR
|
|
182
|
+
exemption_reason IN ('founding_tenant', 'internal', 'partner')),
|
|
183
|
+
founding_tenant boolean NOT NULL DEFAULT false,
|
|
184
|
+
granted_by uuid REFERENCES "user"(id),
|
|
185
|
+
granted_at timestamptz,
|
|
186
|
+
-- Isenção com prazo é possível; a do fundador não tem, e `NULL` diz isso.
|
|
187
|
+
expires_on date,
|
|
188
|
+
notes text,
|
|
189
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
190
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
191
|
+
|
|
192
|
+
-- Isentar custa motivo e data. `standard` é o estado gratuito — e o correto
|
|
193
|
+
-- para todo tenant sobre o qual ninguém decidiu nada.
|
|
194
|
+
CONSTRAINT tenant_platform_billing_exemption_needs_reason
|
|
195
|
+
CHECK (billing_mode = 'standard' OR (exemption_reason IS NOT NULL AND granted_at IS NOT NULL))
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
COMMENT ON TABLE tenant_platform_billing IS
|
|
199
|
+
'Cobrança da PLATAFORMA (Baruk → escritório). Não governa a cobrança do escritório aos clientes dele, que é outro domínio e continua ativa.';
|
|
200
|
+
COMMENT ON CONSTRAINT tenant_platform_billing_exemption_needs_reason ON tenant_platform_billing IS
|
|
201
|
+
'Isentar exige motivo e data. Ausência de linha significa cobrança padrão, nunca isenção por omissão.';
|
|
202
|
+
|
|
203
|
+
ALTER TABLE tenant_platform_billing ENABLE ROW LEVEL SECURITY;
|
|
204
|
+
ALTER TABLE tenant_platform_billing FORCE ROW LEVEL SECURITY;
|
|
205
|
+
|
|
206
|
+
-- Leitura: o próprio tenant, ou o operador.
|
|
207
|
+
CREATE POLICY tenant_platform_billing_read ON tenant_platform_billing
|
|
208
|
+
FOR SELECT
|
|
209
|
+
USING (tenant_id = NULLIF(current_setting('app.current_tenant', true), '')::uuid
|
|
210
|
+
OR current_setting('app.platform_operator', true) = 'true');
|
|
211
|
+
|
|
212
|
+
-- Escrita: SÓ o operador. Uma política por comando — `WITH CHECK` não governa
|
|
213
|
+
-- DELETE, e uma política única deixaria o apagamento da própria isenção (ou da
|
|
214
|
+
-- de outro) fora de qualquer regra.
|
|
215
|
+
CREATE POLICY tenant_platform_billing_insert ON tenant_platform_billing
|
|
216
|
+
FOR INSERT WITH CHECK (current_setting('app.platform_operator', true) = 'true');
|
|
217
|
+
CREATE POLICY tenant_platform_billing_update ON tenant_platform_billing
|
|
218
|
+
FOR UPDATE
|
|
219
|
+
USING (current_setting('app.platform_operator', true) = 'true')
|
|
220
|
+
WITH CHECK (current_setting('app.platform_operator', true) = 'true');
|
|
221
|
+
CREATE POLICY tenant_platform_billing_delete ON tenant_platform_billing
|
|
222
|
+
FOR DELETE USING (current_setting('app.platform_operator', true) = 'true');
|
|
223
|
+
|
|
224
|
+
-- ═══ 4 · O registry municipal deixa de responder só "não sabemos" ════════
|
|
225
|
+
--
|
|
226
|
+
-- `fiscal_coverage` (0058) já traz status, método, credencial exigida, fallback,
|
|
227
|
+
-- fonte e data. Faltavam duas respostas que a política API-first exige ANTES de
|
|
228
|
+
-- alguém cogitar automação de navegador:
|
|
229
|
+
--
|
|
230
|
+
-- `official_api_available` existe caminho estruturado oficial?
|
|
231
|
+
-- `supported_operations` o que ele faz — listar, baixar, consultar evento?
|
|
232
|
+
--
|
|
233
|
+
-- Sem a primeira, "não achei API" e "não procurei" são o mesmo registro, e o
|
|
234
|
+
-- segundo é o que costuma virar um Playwright logando num portal de prefeitura.
|
|
235
|
+
ALTER TABLE fiscal_coverage ADD COLUMN IF NOT EXISTS official_api_available text NOT NULL DEFAULT 'unknown';
|
|
236
|
+
ALTER TABLE fiscal_coverage ADD COLUMN IF NOT EXISTS supported_operations text[] NOT NULL DEFAULT '{}';
|
|
237
|
+
|
|
238
|
+
DO $$
|
|
239
|
+
BEGIN
|
|
240
|
+
IF NOT EXISTS (SELECT FROM pg_constraint WHERE conname = 'fiscal_coverage_official_api_values') THEN
|
|
241
|
+
ALTER TABLE fiscal_coverage ADD CONSTRAINT fiscal_coverage_official_api_values
|
|
242
|
+
CHECK (official_api_available IN ('yes', 'no', 'unknown'));
|
|
243
|
+
END IF;
|
|
244
|
+
END $$;
|
|
245
|
+
|
|
246
|
+
COMMENT ON COLUMN fiscal_coverage.official_api_available IS
|
|
247
|
+
'"no" é uma medição; "unknown" é a ausência dela. Só a primeira autoriza sequer discutir fallback por navegador.';
|
|
248
|
+
|
|
249
|
+
-- ═══ Backfill: as linhas que já afirmavam cobertura ══════════════════════
|
|
250
|
+
--
|
|
251
|
+
-- A 0058 criou quatro linhas `SUPPORTED_OFFICIAL` — NF-e, CT-e e NFS-e nacional
|
|
252
|
+
-- —, todas com a norma citada em `source_reference`. Com a coluna nova nascendo
|
|
253
|
+
-- `unknown`, elas passariam a AFIRMAR cobertura sem ter respondido "existe
|
|
254
|
+
-- caminho oficial?", que é justamente a pergunta que a coluna existe para fazer.
|
|
255
|
+
--
|
|
256
|
+
-- Uma coluna nova sem backfill deixa de fora exatamente as linhas antigas — as
|
|
257
|
+
-- que já estavam em uso. O mesmo raciocínio do `exposure` acima.
|
|
258
|
+
UPDATE fiscal_coverage SET
|
|
259
|
+
official_api_available = 'yes',
|
|
260
|
+
supported_operations = CASE
|
|
261
|
+
WHEN automation_method = 'dfe_nsu_soap' THEN ARRAY['dist_nsu', 'cons_nsu', 'cons_chave']
|
|
262
|
+
WHEN automation_method = 'dfe_nsu_rest' THEN ARRAY['list_by_nsu', 'fetch_document', 'fetch_events']
|
|
263
|
+
ELSE supported_operations
|
|
264
|
+
END
|
|
265
|
+
WHERE coverage_status LIKE 'SUPPORTED%'
|
|
266
|
+
AND official_api_available = 'unknown'
|
|
267
|
+
AND automation_method IN ('dfe_nsu_soap', 'dfe_nsu_rest');
|
|
268
|
+
|
|
269
|
+
-- As linhas dos três municípios do piloto. `SUPPORTED_OFFICIAL` aqui significa
|
|
270
|
+
-- o que já significava na linha nacional do ADN: existe caminho oficial
|
|
271
|
+
-- documentado — e não "já funcionou". Quem responde "já funcionou" é
|
|
272
|
+
-- `digital_certificate.verified_at` somado a um `fiscal_sync_run` completo, e
|
|
273
|
+
-- ambos continuam vazios.
|
|
274
|
+
INSERT INTO fiscal_coverage (
|
|
275
|
+
id, document_type, jurisdiction_scope, jurisdiction_code, provider, coverage_status,
|
|
276
|
+
automation_method, credential_requirement, environments, fallback_type,
|
|
277
|
+
official_api_available, supported_operations,
|
|
278
|
+
last_verified_at, source_reference, notes
|
|
279
|
+
) VALUES
|
|
280
|
+
(gen_random_uuid(), 'nfse', 'municipal', '5208707', 'nfse_adn', 'SUPPORTED_OFFICIAL',
|
|
281
|
+
'dfe_nsu_rest', 'icp_brasil_mtls', ARRAY['homologation', 'production'], 'manual_upload',
|
|
282
|
+
'yes', ARRAY['list_by_nsu', 'fetch_document', 'fetch_events'],
|
|
283
|
+
DATE '2026-09-08',
|
|
284
|
+
'Prefeitura de Goiânia — comunicado oficial de ajuste da NFS-e à Reforma Tributária (vigência 01/01/2026); LC 214/2025 art. 62; Manual dos Contribuintes — APIs do ADN v1.0',
|
|
285
|
+
'Goiânia MANTÉM emissor próprio (Sistema ISS Goiânia, provedor ISSNet) e o compartilhamento com o ADN é feito pela própria administração tributária. Logo o caminho de COLETA é o ADN nacional, por certificado — não o portal municipal. Nenhuma chamada autenticada foi feita a partir deste produto.'),
|
|
286
|
+
|
|
287
|
+
(gen_random_uuid(), 'nfse', 'municipal', '5201405', 'nfse_adn', 'SUPPORTED_OFFICIAL',
|
|
288
|
+
'dfe_nsu_rest', 'icp_brasil_mtls', ARRAY['homologation', 'production'], 'manual_upload',
|
|
289
|
+
'yes', ARRAY['list_by_nsu', 'fetch_document', 'fetch_events'],
|
|
290
|
+
DATE '2026-09-08',
|
|
291
|
+
'Portal oficial da Prefeitura de Aparecida de Goiânia (sistema NFS-e em issnetonline.com.br/aparecida); LC 214/2025 art. 62; Manual dos Contribuintes — APIs do ADN v1.0',
|
|
292
|
+
'Emissor municipal é ISSNet (mesmo provedor de Goiânia e Anápolis). Nenhuma API pública do ISSNet foi verificada; a via oficial de coleta é o ADN. Adesão específica do município ao compartilhamento NÃO foi confirmada em fonte municipal — apenas pela obrigação legal geral.'),
|
|
293
|
+
|
|
294
|
+
(gen_random_uuid(), 'nfse', 'municipal', '5201108', 'nfse_adn', 'SUPPORTED_OFFICIAL',
|
|
295
|
+
'dfe_nsu_rest', 'icp_brasil_mtls', ARRAY['homologation', 'production'], 'manual_upload',
|
|
296
|
+
'yes', ARRAY['list_by_nsu', 'fetch_document', 'fetch_events'],
|
|
297
|
+
DATE '2026-09-08',
|
|
298
|
+
'Prefeitura de Anápolis — Decreto nº 52.525 (03/03/2026, efeitos retroativos a 01/01/2026); LC 214/2025 art. 62; Manual dos Contribuintes — APIs do ADN v1.0',
|
|
299
|
+
'Anápolis mantém emissor municipal próprio (ISS.NET) com webservice local; o comunicado NÃO menciona o ADN. Simples Nacional migra ao emissor nacional a partir de 01/11/2026. A coleta pelo ADN depende do compartilhamento obrigatório, que este produto não verificou.'),
|
|
300
|
+
|
|
301
|
+
-- Estadual GO. A pergunta humana era "notas de entrada e saída"; a resposta
|
|
302
|
+
-- oficial separa as duas, e é essa separação que decide se um conector novo é
|
|
303
|
+
-- necessário — ou se o que falta não tem conector nenhum.
|
|
304
|
+
(gen_random_uuid(), 'nfe', 'state', 'GO', 'sefaz_dfe', 'SUPPORTED_OFFICIAL',
|
|
305
|
+
'dfe_nsu_soap', 'icp_brasil_mtls', ARRAY['homologation', 'production'], 'issuer_export',
|
|
306
|
+
'yes', ARRAY['dist_nsu', 'cons_nsu', 'cons_chave'],
|
|
307
|
+
DATE '2026-09-08',
|
|
308
|
+
'Secretaria da Economia de Goiás — Documentos Fiscais (nfeweb.sefaz.go.gov.br, arquivo XML por certificado digital); NT 2014.002 v1.02c',
|
|
309
|
+
'ENTRADAS: cobertas pela distribuição nacional NFeDistribuicaoDFe, autenticada por certificado ICP-Brasil — nenhuma credencial humana. SAÍDAS: o DF-e não distribui o que a própria empresa emitiu; a origem é o emissor do cliente. O login CPF/senha do portal da SEFAZ-GO NÃO é caminho de coleta estruturada e por isso não é modelado como credencial deste produto.')
|
|
310
|
+
ON CONFLICT DO NOTHING;
|
|
311
|
+
|
|
312
|
+
-- ═══ 5 · A capacidade ════════════════════════════════════════════════════
|
|
313
|
+
--
|
|
314
|
+
-- Chave NOVA, e não bump de `contract_version` — a versão do contrato é global
|
|
315
|
+
-- (0055) e subi-la para exigir schema novo obrigaria todo worker a reconhecer a
|
|
316
|
+
-- versão maior, inclusive os que nada têm a ver com cofre fiscal.
|
|
317
|
+
INSERT INTO platform_capability (key, label, contract_version, declared_by_migration) VALUES
|
|
318
|
+
('office.fiscal-credential-vault.v1',
|
|
319
|
+
'Cofre fiscal: certificado write-only, identidade conferida e autorização de execução única',
|
|
320
|
+
1, '0060')
|
|
321
|
+
ON CONFLICT (key) DO NOTHING;
|