@riligar/contract 1.0.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/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@riligar/contract",
3
+ "version": "1.0.0",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "description": "O contrato de API da stack RiLiGar: envelope único, catálogo de códigos de erro e os helpers que tornam impossível responder fora dele.",
7
+ "main": "src/index.js",
8
+ "files": ["src", "README.md"],
9
+ "exports": {
10
+ ".": "./src/index.js",
11
+ "./codes": "./src/codes.js",
12
+ "./schemas": "./src/schemas.js"
13
+ },
14
+ "scripts": {
15
+ "test": "bun test tests/",
16
+ "release": "semantic-release"
17
+ },
18
+ "peerDependencies": { "zod": "^4.0.0" },
19
+ "peerDependenciesMeta": { "zod": { "optional": true } },
20
+ "devDependencies": {
21
+ "zod": "^4.5.4",
22
+ "semantic-release": "^24.0.0",
23
+ "@semantic-release/changelog": "^6.0.3",
24
+ "@semantic-release/git": "^10.0.1"
25
+ },
26
+ "publishConfig": { "access": "public" },
27
+ "repository": { "type": "git", "url": "git+https://github.com/riligar-applications/contract.git" }
28
+ }
package/src/codes.js ADDED
@@ -0,0 +1,212 @@
1
+ // ============================================================================
2
+ // CÓDIGOS DE ERRO — o vocabulário fechado da stack
3
+ // ============================================================================
4
+ //
5
+ // `code` é a parte do erro que o AGENTE lê. `message` é para o humano e pode
6
+ // mudar de redação a qualquer momento; `code` não pode — renomear um código é
7
+ // quebra de contrato, e por isso ele vive aqui, num enum fechado, e não solto
8
+ // em cada handler.
9
+ //
10
+ // Este vocabulário não foi inventado: é a consolidação dos `reason` que já
11
+ // existiam espalhados pelos sete produtos (`plan_limit`, `has_resources`,
12
+ // `taken`, `expired`, `duplicate_name`, `no_access`…). A stack já havia
13
+ // convergido sozinha — só não tinha escrito, e por isso cada produto grafava
14
+ // o mesmo conceito de um jeito.
15
+ //
16
+ // Adicionar um código é mudança deliberada: entra aqui, não no handler.
17
+
18
+ export const CODES = {
19
+ // ── Identidade e acesso ──────────────────────────────────────────────
20
+ /** Sem credencial, ou credencial ilegível. O cliente não se identificou. */
21
+ UNAUTHORIZED: 'UNAUTHORIZED',
22
+ /** Identificado, mas sem permissão para ESTE recurso. */
23
+ FORBIDDEN: 'FORBIDDEN',
24
+ /** Token válido, emitido para OUTRO servidor (RFC 8707). */
25
+ WRONG_AUDIENCE: 'WRONG_AUDIENCE',
26
+ /** A credencial não cobre a operação — falta escopo, não permissão. */
27
+ INSUFFICIENT_SCOPE: 'INSUFFICIENT_SCOPE',
28
+
29
+ // ── O recurso ────────────────────────────────────────────────────────
30
+ /** Não existe, ou não é visível para quem pergunta. */
31
+ NOT_FOUND: 'NOT_FOUND',
32
+ /** Já existe outro com a mesma chave natural (nome, slug, e-mail). */
33
+ CONFLICT: 'CONFLICT',
34
+ /** O nome pedido está tomado. É um CONFLICT com diagnóstico próprio. */
35
+ NAME_TAKEN: 'NAME_TAKEN',
36
+ /** Existiu e não vale mais — versão de deploy, token, link de convite. */
37
+ EXPIRED: 'EXPIRED',
38
+ /** A remoção esbarra em dependentes que precisam sair antes. */
39
+ HAS_RESOURCES: 'HAS_RESOURCES',
40
+
41
+ // ── O pedido ─────────────────────────────────────────────────────────
42
+ /** Corpo, query ou path malformado. `details` carrega o que falhou. */
43
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
44
+ /** Sintaticamente válido, mas impossível no estado atual do recurso. */
45
+ UNPROCESSABLE: 'UNPROCESSABLE',
46
+ /** Excedeu a janela de chamadas. `details.retryAfter` diz quando voltar. */
47
+ RATE_LIMITED: 'RATE_LIMITED',
48
+ /** Corpo maior que o teto aceito. */
49
+ PAYLOAD_TOO_LARGE: 'PAYLOAD_TOO_LARGE',
50
+
51
+ // ── Comercial ────────────────────────────────────────────────────────
52
+ /** O plano atual não comporta. `details` traz current/limit/requiredPlan. */
53
+ PLAN_LIMIT: 'PLAN_LIMIT',
54
+ /** A assinatura do dono não está ativa. Bloqueia a operação inteira. */
55
+ SUBSCRIPTION_REQUIRED: 'SUBSCRIPTION_REQUIRED',
56
+
57
+ // ── Nós ──────────────────────────────────────────────────────────────
58
+ /** Falha inesperada. NUNCA carrega mensagem de exceção interna. */
59
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
60
+ /** Dependência externa fora do ar. É transitório — vale repetir. */
61
+ SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
62
+ /** Um provedor de terceiros recusou (gateway, SES, Cloudflare). */
63
+ UPSTREAM_ERROR: 'UPSTREAM_ERROR',
64
+ }
65
+
66
+ /**
67
+ * O status HTTP de cada código.
68
+ *
69
+ * A tabela vive aqui para que status e código NUNCA discordem: um `NOT_FOUND`
70
+ * com 200 é o defeito que esta migração existe para eliminar, e a única forma
71
+ * de garantir isso é o chamador não escolher o número.
72
+ */
73
+ export const STATUS_BY_CODE = {
74
+ UNAUTHORIZED: 401,
75
+ FORBIDDEN: 403,
76
+ WRONG_AUDIENCE: 401,
77
+ INSUFFICIENT_SCOPE: 403,
78
+
79
+ NOT_FOUND: 404,
80
+ CONFLICT: 409,
81
+ NAME_TAKEN: 409,
82
+ EXPIRED: 410,
83
+ HAS_RESOURCES: 409,
84
+
85
+ VALIDATION_ERROR: 400,
86
+ UNPROCESSABLE: 422,
87
+ RATE_LIMITED: 429,
88
+ PAYLOAD_TOO_LARGE: 413,
89
+
90
+ PLAN_LIMIT: 402,
91
+ SUBSCRIPTION_REQUIRED: 402,
92
+
93
+ INTERNAL_ERROR: 500,
94
+ SERVICE_UNAVAILABLE: 503,
95
+ UPSTREAM_ERROR: 502,
96
+ }
97
+
98
+ /** Um erro transitório merece nova tentativa; os outros, não. */
99
+ export const RETRIABLE = new Set([CODES.RATE_LIMITED, CODES.SERVICE_UNAVAILABLE, CODES.UPSTREAM_ERROR])
100
+
101
+ /**
102
+ * Os `reason` antigos, e o código que cada um virou.
103
+ *
104
+ * POR QUE ISTO EXISTE, E POR QUE NÃO É DÍVIDA
105
+ *
106
+ * Antes deste pacote, cada produto tinha seu vocabulário de `reason` em
107
+ * snake_case: `invalid_request`, `plan_limit`, `not_found`. São centenas de
108
+ * chamadas espalhadas — só no Auth são 54. Trocá-las todas de uma vez seria
109
+ * uma refatoração ampla, com risco real e zero ganho visível: o que o cliente
110
+ * da API lê é o `code` da RESPOSTA, e ele já sai correto.
111
+ *
112
+ * A tradução acontece na borda, uma vez, aqui. `falha('invalid_request')`
113
+ * emite `VALIDATION_ERROR` — o handler continua legível na língua que já
114
+ * falava, e a resposta cumpre o contrato.
115
+ *
116
+ * O QUE ISTO NÃO É
117
+ *
118
+ * Não é um segundo vocabulário. Nada aqui é código válido de saída: o mapa só
119
+ * traduz PARA `CODES`, nunca a partir dele. Código novo escreve
120
+ * `CODES.VALIDATION_ERROR` direto; esta tabela só cobre o que já existia.
121
+ *
122
+ * O mapa é fechado de propósito. Um `reason` que não estiver aqui cai em
123
+ * `INTERNAL_ERROR` — barulhento, e é o certo: código desconhecido é bug de
124
+ * quem chamou, não algo para adivinhar.
125
+ */
126
+ export const ALIASES = {
127
+ // ── Validação ────────────────────────────────────────────────────────
128
+ validation_error: CODES.VALIDATION_ERROR,
129
+ invalid_request: CODES.VALIDATION_ERROR,
130
+ invalid_email: CODES.VALIDATION_ERROR,
131
+ bad_request: CODES.VALIDATION_ERROR,
132
+ missing_field: CODES.VALIDATION_ERROR,
133
+
134
+ // ── Identidade e acesso ──────────────────────────────────────────────
135
+ unauthorized: CODES.UNAUTHORIZED,
136
+ forbidden: CODES.FORBIDDEN,
137
+ access_denied: CODES.FORBIDDEN,
138
+ no_access: CODES.FORBIDDEN,
139
+ application_mismatch: CODES.FORBIDDEN,
140
+ wrong_audience: CODES.WRONG_AUDIENCE,
141
+ insufficient_scope: CODES.INSUFFICIENT_SCOPE,
142
+
143
+ // ── Recurso ──────────────────────────────────────────────────────────
144
+ not_found: CODES.NOT_FOUND,
145
+ conflict: CODES.CONFLICT,
146
+ taken: CODES.NAME_TAKEN,
147
+ name_taken: CODES.NAME_TAKEN,
148
+ duplicate_name: CODES.NAME_TAKEN,
149
+ expired: CODES.EXPIRED,
150
+ expired_token: CODES.EXPIRED,
151
+ has_resources: CODES.HAS_RESOURCES,
152
+ organization_not_empty: CODES.HAS_RESOURCES,
153
+
154
+ // ── Não processável ──────────────────────────────────────────────────
155
+ unprocessable: CODES.UNPROCESSABLE,
156
+ no_op: CODES.UNPROCESSABLE,
157
+ last_owner: CODES.UNPROCESSABLE,
158
+ // RFC 8628: o device flow espera o humano digitar o código. Não é erro de
159
+ // cliente nem falha do servidor — é "ainda não".
160
+ authorization_pending: CODES.UNPROCESSABLE,
161
+
162
+ // ── Plano e cobrança ─────────────────────────────────────────────────
163
+ plan_limit: CODES.PLAN_LIMIT,
164
+ subscription_required: CODES.SUBSCRIPTION_REQUIRED,
165
+
166
+ // ── Ritmo e tamanho ──────────────────────────────────────────────────
167
+ rate_limited: CODES.RATE_LIMITED,
168
+ slow_down: CODES.RATE_LIMITED,
169
+ payload_too_large: CODES.PAYLOAD_TOO_LARGE,
170
+
171
+ // ── Falha nossa ou de terceiro ───────────────────────────────────────
172
+ server_error: CODES.INTERNAL_ERROR,
173
+ internal_error: CODES.INTERNAL_ERROR,
174
+ service_unavailable: CODES.SERVICE_UNAVAILABLE,
175
+ upstream_error: CODES.UPSTREAM_ERROR,
176
+ email_delivery_failed: CODES.UPSTREAM_ERROR,
177
+ }
178
+
179
+ export const isCode = valor => Object.prototype.hasOwnProperty.call(STATUS_BY_CODE, valor)
180
+
181
+ /**
182
+ * O código canônico a partir do que o handler escreveu.
183
+ *
184
+ * Aceita o código já canônico (passa direto), um `reason` antigo (traduz), ou
185
+ * qualquer outra coisa (`INTERNAL_ERROR`). É o único ponto da stack que decide
186
+ * isso — antes, cada produto tinha a sua tabela, e elas divergiam.
187
+ */
188
+ export const toCode = valor => (isCode(valor) ? valor : ALIASES[valor] || CODES.INTERNAL_ERROR)
189
+
190
+ /**
191
+ * O código a partir de um status HTTP.
192
+ *
193
+ * Para os pontos que já tinham o número na mão — um `throw { status: 401 }`
194
+ * pego num catch, por exemplo — e não têm de onde tirar o código.
195
+ */
196
+ export const STATUS_TO_CODE = {
197
+ 400: CODES.VALIDATION_ERROR,
198
+ 401: CODES.UNAUTHORIZED,
199
+ 402: CODES.PLAN_LIMIT,
200
+ 403: CODES.FORBIDDEN,
201
+ 404: CODES.NOT_FOUND,
202
+ 409: CODES.CONFLICT,
203
+ 410: CODES.EXPIRED,
204
+ 413: CODES.PAYLOAD_TOO_LARGE,
205
+ 422: CODES.UNPROCESSABLE,
206
+ 429: CODES.RATE_LIMITED,
207
+ 500: CODES.INTERNAL_ERROR,
208
+ 502: CODES.UPSTREAM_ERROR,
209
+ 503: CODES.SERVICE_UNAVAILABLE,
210
+ }
211
+
212
+ export const codeFromStatus = status => STATUS_TO_CODE[status] || CODES.INTERNAL_ERROR
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ // ============================================================================
2
+ // @riligar/contract — o contrato de API dos sete produtos
3
+ // ============================================================================
4
+ //
5
+ // A norma escrita vive em `website/docs/2026-09-09-api-contract.md`. Este
6
+ // pacote é a norma EXECUTÁVEL: o que os produtos importam para responder, e o
7
+ // que os testes importam para conferir.
8
+ //
9
+ // A regra de ouro é a que este pacote torna mecânica: **um agente que aprendeu
10
+ // um produto da RiLiGar já sabe usar os outros seis.**
11
+ export { CODES, STATUS_BY_CODE, RETRIABLE, isCode, toCode, codeFromStatus, ALIASES } from './codes.js'
12
+ export { ok, criado, colecao, falha, falhaComStatus, corpoDeFalha, paginacao } from './respostas.js'
13
+ export { codeSchema, errorSchema, pageSchema, collectionSchema, resourceSchema, chavesCamelCase } from './schemas.js'
@@ -0,0 +1,135 @@
1
+ // ============================================================================
2
+ // RESPOSTAS — a única forma de sair do servidor
3
+ // ============================================================================
4
+ //
5
+ // Quatro funções. Se um handler dos sete produtos constrói `Response` na mão,
6
+ // é porque saiu do contrato.
7
+ //
8
+ // O ponto do arquivo não é economizar digitação — é tornar o defeito
9
+ // IMPOSSÍVEL. `falha()` deriva o status do código: não existe assinatura que
10
+ // permita devolver `NOT_FOUND` com 200. Era exatamente esse par discordante
11
+ // que fazia o agente ler 200, concluir sucesso e seguir com dado inexistente.
12
+ import { CODES, STATUS_BY_CODE, RETRIABLE, isCode, toCode, codeFromStatus } from './codes.js'
13
+
14
+ const JSON_HEADERS = { 'content-type': 'application/json; charset=utf-8' }
15
+
16
+ const responder = (corpo, status, headers) => new Response(JSON.stringify(corpo), { status, headers: { ...JSON_HEADERS, ...headers } })
17
+
18
+ /**
19
+ * Um recurso, na raiz.
20
+ *
21
+ * ok({ id: 'mon_a1', name: 'checkout' })
22
+ * → 200 {"id":"mon_a1","name":"checkout"}
23
+ *
24
+ * Sem `data`, sem `success`, sem `message`. O status HTTP já disse que deu
25
+ * certo; repetir isso no corpo é ruído que o agente precisa aprender a
26
+ * ignorar — por produto.
27
+ */
28
+ export const ok = (recurso, { status = 200, headers } = {}) => responder(recurso, status, headers)
29
+
30
+ /**
31
+ * Um recurso que acabou de NASCER.
32
+ *
33
+ * 201 é o sinal de criação, e só. Um upsert que atualizou devolve `ok()`;
34
+ * quem criou devolve `criado()`. É o que permite ao agente saber se a
35
+ * chamada dele foi a que criou — sem precisar comparar timestamps.
36
+ */
37
+ export const criado = (recurso, { headers } = {}) => responder(recurso, 201, headers)
38
+
39
+ /**
40
+ * Uma coleção, sempre com a mesma forma.
41
+ *
42
+ * colecao([a, b], { limit: 50, offset: 0, total: 128 })
43
+ * → {"items":[…],"page":{"limit":50,"offset":0,"total":128,"hasMore":true}}
44
+ *
45
+ * `items` é sempre um array — vazio é uma resposta legítima e tem a MESMA
46
+ * forma da cheia. A poda de lista vazia (que o Payments fazia) quebrava só no
47
+ * primeiro cliente, porque todo ambiente de teste tem dado semeado.
48
+ *
49
+ * `hasMore` é explícito de propósito: o consumidor não deveria precisar
50
+ * comparar `offset + limit` com `total` para saber se continua paginando.
51
+ * Quando `total` não é conhecido (contar custaria uma query a mais em coisa
52
+ * que ninguém pagina), `hasMore` sai do tamanho da página cheia.
53
+ */
54
+ export const colecao = (itens, pagina = {}, { status = 200, headers } = {}) => {
55
+ const items = Array.isArray(itens) ? itens : []
56
+ const limit = pagina.limit ?? items.length
57
+ const offset = pagina.offset ?? 0
58
+ const total = pagina.total ?? offset + items.length
59
+ const hasMore = pagina.hasMore ?? offset + items.length < total
60
+
61
+ return responder({ items, page: { limit, offset, total, hasMore } }, status, headers)
62
+ }
63
+
64
+ /**
65
+ * Uma falha. O status vem do CÓDIGO — quem chama não escolhe o número.
66
+ *
67
+ * falha(CODES.NOT_FOUND, 'Projeto não encontrado.')
68
+ * → 404 {"error":{"code":"NOT_FOUND","message":"Projeto não encontrado."}}
69
+ *
70
+ * `details` é para o que o agente pode AGIR: o limite que estourou, o campo
71
+ * que faltou, quantos segundos esperar. Nunca para stack trace.
72
+ *
73
+ * Um código desconhecido vira INTERNAL_ERROR em vez de 500 silencioso com
74
+ * corpo torto: se alguém escreveu um código fora do enum, o bug é nosso, e a
75
+ * resposta ao cliente continua bem formada.
76
+ */
77
+ export const falha = (code, message, { details, headers, status } = {}) => {
78
+ const codigo = toCode(code)
79
+ const corpo = { error: { code: codigo, message: String(message ?? '') } }
80
+
81
+ if (details && Object.keys(details).length) corpo.error.details = details
82
+ if (RETRIABLE.has(codigo)) corpo.error.retriable = true
83
+
84
+ return responder(corpo, status ?? STATUS_BY_CODE[codigo], headers)
85
+ }
86
+
87
+ /**
88
+ * O corpo do erro, sem a `Response` em volta.
89
+ *
90
+ * Para quem responde por um framework que monta a resposta sozinho — o Elysia
91
+ * do Storage e do Functions, onde o handler devolve objeto e o status vai por
92
+ * `set.status`. Devolve `{ corpo, status }` para os dois irem juntos e não
93
+ * haver como esquecer um.
94
+ */
95
+ export const corpoDeFalha = (code, message, details) => {
96
+ const codigo = toCode(code)
97
+ const error = { code: codigo, message: String(message ?? '') }
98
+
99
+ if (details && Object.keys(details).length) error.details = details
100
+ if (RETRIABLE.has(codigo)) error.retriable = true
101
+
102
+ return { corpo: { error }, status: STATUS_BY_CODE[codigo] }
103
+ }
104
+
105
+ /**
106
+ * Lê `limit`/`offset` de uma URL, com teto.
107
+ *
108
+ * Um `limit` sem teto é um jeito de derrubar o serviço com uma query; um teto
109
+ * sem `offset` é o que tornava o registro 101 do Messages inalcançável. Os
110
+ * dois andam juntos.
111
+ */
112
+ export const paginacao = (url, { limitPadrao = 50, limitMaximo = 200 } = {}) => {
113
+ const p = url instanceof URL ? url.searchParams : new URL(url, 'http://x').searchParams
114
+ const bruto = Number(p.get('limit'))
115
+ const limit = Number.isFinite(bruto) && bruto > 0 ? Math.min(Math.floor(bruto), limitMaximo) : limitPadrao
116
+ const off = Number(p.get('offset'))
117
+ const offset = Number.isFinite(off) && off > 0 ? Math.floor(off) : 0
118
+ return { limit, offset }
119
+ }
120
+
121
+ /**
122
+ * Uma falha de que só se sabe o status HTTP.
123
+ *
124
+ * Para o handler global de erro, onde chega um `throw { status, message }` de
125
+ * qualquer camada e não há código na mão. Deriva o `code` do número em vez de
126
+ * cair sempre em `INTERNAL_ERROR` — um 401 que vira 500 na borda esconde o
127
+ * problema real de quem depura.
128
+ *
129
+ * `status` é preservado: quem já tinha o número continua com ele, mesmo quando
130
+ * o código canônico mapeia para outro (401 de `UNAUTHORIZED`, 402 de
131
+ * `PLAN_LIMIT`).
132
+ */
133
+ export const falhaComStatus = (status, message, details, headers) => falha(codeFromStatus(status), message, { details, headers, status })
134
+
135
+ export { CODES, STATUS_BY_CODE }
package/src/schemas.js ADDED
@@ -0,0 +1,80 @@
1
+ // ============================================================================
2
+ // SCHEMAS — o contrato como coisa executável
3
+ // ============================================================================
4
+ //
5
+ // As mesmas regras de `respostas.js`, do lado de quem LÊ. Servem para o teste
6
+ // de contrato afirmar a forma sem reimplementar a asserção em cada suíte, e
7
+ // para um consumidor validar o que recebeu.
8
+ //
9
+ // Zod é peer: os sete produtos já o têm (v4), e embutir uma segunda cópia num
10
+ // worker significa pagar o bundle duas vezes.
11
+ import { z } from 'zod'
12
+
13
+ import { STATUS_BY_CODE } from './codes.js'
14
+
15
+ /** O código é o enum fechado — nada de string livre. */
16
+ export const codeSchema = z.enum(Object.keys(STATUS_BY_CODE))
17
+
18
+ export const errorSchema = z.object({
19
+ error: z.object({
20
+ code: codeSchema,
21
+ message: z.string().min(1),
22
+ details: z.record(z.string(), z.unknown()).optional(),
23
+ retriable: z.boolean().optional(),
24
+ }),
25
+ })
26
+
27
+ export const pageSchema = z.object({
28
+ limit: z.number().int().nonnegative(),
29
+ offset: z.number().int().nonnegative(),
30
+ total: z.number().int().nonnegative(),
31
+ hasMore: z.boolean(),
32
+ })
33
+
34
+ /** `{items, page}` — a forma de TODA coleção. */
35
+ export const collectionSchema = item =>
36
+ z.object({
37
+ items: z.array(item ?? z.unknown()),
38
+ page: pageSchema,
39
+ })
40
+
41
+ /**
42
+ * O recurso vai na raiz, então não há envelope a validar — o que se pode
43
+ * afirmar é o que ele NÃO pode ser.
44
+ *
45
+ * As três chaves proibidas são exatamente os envelopes que a stack usava. Um
46
+ * recurso que ainda tenha `data`, `success` ou `message` no topo é um produto
47
+ * que não migrou — e o teste de contrato precisa dizer isso com todas as
48
+ * letras, em vez de passar porque "veio um objeto".
49
+ */
50
+ export const resourceSchema = corpo =>
51
+ (corpo ?? z.object({}).loose()).refine(v => !v || typeof v !== 'object' || (!('data' in v) && !('success' in v) && !('message' in v)), {
52
+ message: 'recurso não pode vir envelopado em `data`, `success` ou `message`',
53
+ })
54
+
55
+ const CAMEL = /^[a-z][a-zA-Z0-9]*$/
56
+
57
+ /**
58
+ * Toda chave do corpo é camelCase, em qualquer profundidade.
59
+ *
60
+ * O Storage devolvia `tenant_id` no corpo e `tenantId` no path da MESMA rota.
61
+ * Um consumidor precisava saber, campo a campo, qual vocabulário usar.
62
+ *
63
+ * Chaves que são DADO do usuário (o nome de uma coleção, de uma variável de
64
+ * ambiente, de um cabeçalho) não seguem a regra — não são contrato nosso.
65
+ * Por isso `ignorar`.
66
+ */
67
+ export const chavesCamelCase = (valor, { ignorar = [] } = {}, caminho = '') => {
68
+ const fora = []
69
+ const anda = (v, path) => {
70
+ if (Array.isArray(v)) return v.forEach((x, i) => anda(x, `${path}[${i}]`))
71
+ if (!v || typeof v !== 'object') return
72
+ for (const [k, sub] of Object.entries(v)) {
73
+ const aqui = path ? `${path}.${k}` : k
74
+ if (!ignorar.includes(k) && !CAMEL.test(k)) fora.push(aqui)
75
+ anda(sub, aqui)
76
+ }
77
+ }
78
+ anda(valor, caminho)
79
+ return fora
80
+ }