auroq-os 2.8.0 → 2.8.2
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/.auroq-core/como-funciona.md +5 -5
- package/.auroq-core/core-config.yaml +1 -1
- package/.claude/CLAUDE.md +2 -3
- package/.claude/rules/agent-authority.md +8 -5
- package/.claude/rules/mcp-usage.md +2 -2
- package/.claude/rules/memoria-inteligente.md +3 -2
- package/.claude/rules/operacao-autonoma.md +11 -2
- package/.claude/rules/puxar-e-entregar.md +7 -7
- package/.claude/settings.json +7 -0
- package/.synapse/agent-ops +1 -1
- package/AGENTS.md +49 -14
- package/CHANGELOG.md +92 -0
- package/README.md +48 -13
- package/agents/organizer/agents/organizer.md +4 -4
- package/bin/auroq-os.js +33 -5
- package/installer/install.ps1 +2 -2
- package/installer/install.sh +2 -2
- package/lib/cofre.js +1453 -0
- package/lib/infisical-api.js +670 -0
- package/lib/infisical-cli.js +869 -0
- package/lib/infisical.js +1271 -0
- package/lib/legacy-shims.json +2 -1
- package/lib/onepassword.js +1315 -22
- package/lib/runtime-integrity.json +23 -7
- package/package.json +3 -3
- package/seeds/docs/knowledge/expert-business/canais-e-numeros.md +21 -0
- package/seeds/docs/knowledge/expert-business/equipe-e-ferramentas.md +11 -0
- package/seeds/docs/knowledge/expert-business/metas.md +20 -0
- package/seeds/docs/knowledge/expert-business/provas.md +15 -0
- package/seeds/docs/knowledge/expert-business/referencias.md +11 -0
- package/seeds/docs/knowledge/expert-mind/vida-pessoal.md +20 -0
- package/seeds/docs/knowledge/mapa-do-negocio.md +40 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auroq OS - Cliente da API do Infisical (rota B, cofre gratis)
|
|
5
|
+
*
|
|
6
|
+
* So fala HTTP: sem estado, sem disco, sem CLI. Quem orquestra (lib/infisical.js) injeta o dominio, o token
|
|
7
|
+
* e, nos testes, o `fetch` e o `sleep`. Contrato em saida/desenho.md (secao 3); rotas e corpos conferidos
|
|
8
|
+
* contra pesquisa/resultado-api.md (e o codigo do backend citado la).
|
|
9
|
+
*
|
|
10
|
+
* Regras que valem para tudo aqui:
|
|
11
|
+
* - `client.request` NUNCA lanca: devolve `{ ok:true, status, data }` ou `{ ok:false, status, codigo, mensagem }`.
|
|
12
|
+
* - Nenhum segredo (JWT, client secret, token de acesso, `st.`/`ops_`) sai em `mensagem`: passa por `redact`
|
|
13
|
+
* e o proprio token do cliente e apagado por valor exato. O corpo bruto da resposta nunca e devolvido.
|
|
14
|
+
* - O client secret so existe na resposta de `createClientSecret` e no corpo do login. Nunca em URL/argumento.
|
|
15
|
+
*
|
|
16
|
+
* Assinatura das operacoes: `op(client, ...)`. A forma principal e posicional, como no desenho
|
|
17
|
+
* (`getProjectBySlug(client, 'auroq')`, `listProjectIdentities(client, projectId, { search })`);
|
|
18
|
+
* tambem aceita um objeto unico (`getProjectBySlug(client, { slug })`, `listProjectIdentities(client, { projectId, search })`).
|
|
19
|
+
*
|
|
20
|
+
* @module auroq-os/infisical-api
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const REGIONS = Object.freeze({
|
|
24
|
+
us: 'https://app.infisical.com',
|
|
25
|
+
eu: 'https://eu.infisical.com',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const MASK = '[oculto]';
|
|
29
|
+
const HIDDEN_VALUE = '<hidden-by-infisical>';
|
|
30
|
+
const MAX_MESSAGE = 300;
|
|
31
|
+
// Texto do servidor que entra no redact: o redact tem regex que custam N^2 em texto longo, e um servidor
|
|
32
|
+
// hostil (--dominio) poderia mandar uma mensagem gigante. Como a mensagem final tem MAX_MESSAGE, o resto nunca aparece.
|
|
33
|
+
const MAX_SCAN = 2000;
|
|
34
|
+
const REDACT_MAX_INPUT = 8000;
|
|
35
|
+
const BACKOFF_MS = [500, 1500];
|
|
36
|
+
const MAX_RETRY_AFTER_S = 15;
|
|
37
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);
|
|
38
|
+
|
|
39
|
+
// ─── Helpers puros ───────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Normaliza o dominio do Infisical: `us`/`eu` viram a regiao; URL https (ou http so em loopback, para
|
|
43
|
+
* teste) perde o `/api` e a `/` do final. Devolve `null` se for invalido.
|
|
44
|
+
*/
|
|
45
|
+
function normalizeDomain(input) {
|
|
46
|
+
if (typeof input !== 'string') return null;
|
|
47
|
+
const text = input.trim();
|
|
48
|
+
if (!text) return null;
|
|
49
|
+
const regiao = text.toLowerCase();
|
|
50
|
+
if (Object.prototype.hasOwnProperty.call(REGIONS, regiao)) return REGIONS[regiao];
|
|
51
|
+
let url;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(text);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
if (url.username || url.password || url.search || url.hash || !url.hostname) return null;
|
|
58
|
+
if (url.protocol === 'http:') {
|
|
59
|
+
if (!LOOPBACK_HOSTS.has(url.hostname)) return null;
|
|
60
|
+
} else if (url.protocol !== 'https:') {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const caminho = url.pathname.replace(/\/+$/, '').replace(/\/api$/i, '').replace(/\/+$/, '');
|
|
64
|
+
return url.origin + caminho;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const JWT_SHAPE_RE = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
68
|
+
const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
|
|
69
|
+
|
|
70
|
+
/** Tres partes base64url separadas por ponto. Nao prova que e um token valido. */
|
|
71
|
+
function isJwtShape(value) {
|
|
72
|
+
return typeof value === 'string' && JWT_SHAPE_RE.test(value.trim());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Client secret do Universal Auth: 64 caracteres hexadecimais minusculos. */
|
|
76
|
+
function isClientSecretShape(value) {
|
|
77
|
+
return typeof value === 'string' && /^[0-9a-f]{64}$/.test(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Le o payload do JWT (base64url, SEM verificar assinatura). Objeto ou `null`; nunca lanca. */
|
|
81
|
+
function decodeJwtPayload(jwt) {
|
|
82
|
+
try {
|
|
83
|
+
if (!isJwtShape(jwt)) return null;
|
|
84
|
+
const meio = jwt.trim().split('.')[1].replace(/=+$/, '');
|
|
85
|
+
if (!BASE64URL_RE.test(meio)) return null;
|
|
86
|
+
const payload = JSON.parse(Buffer.from(meio, 'base64url').toString('utf8'));
|
|
87
|
+
return payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const REDACT_RULES = [
|
|
94
|
+
[/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi, `Bearer ${MASK}`],
|
|
95
|
+
[/eyJ[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]{4,}(?:\.[A-Za-z0-9_-]*)?/g, MASK],
|
|
96
|
+
[/[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, MASK],
|
|
97
|
+
[/(?<![0-9A-Za-z])[0-9a-fA-F]{64}(?![0-9A-Za-z])/g, MASK],
|
|
98
|
+
[/\bst\.[A-Za-z0-9._-]{8,}/g, MASK],
|
|
99
|
+
[/\bops_[A-Za-z0-9_=+/.-]{20,}/g, MASK],
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Apaga JWTs, client secrets (64 hex), tokens `st.`/`ops_` e `Bearer ...` de um texto. As regex custam N^2 em
|
|
104
|
+
* texto longo (um servidor hostil poderia travar o programa com uma mensagem gigante), entao so os primeiros
|
|
105
|
+
* REDACT_MAX_INPUT caracteres entram: o resto e descartado, nunca devolvido sem limpar.
|
|
106
|
+
*/
|
|
107
|
+
function redact(text) {
|
|
108
|
+
let out = String(text === undefined || text === null ? '' : text).slice(0, REDACT_MAX_INPUT);
|
|
109
|
+
for (const [re, troca] of REDACT_RULES) out = out.replace(re, troca);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function oneLine(text) {
|
|
114
|
+
return String(text).replace(/\s+/g, ' ').trim();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function issueText(issue) {
|
|
118
|
+
if (typeof issue === 'string') return issue;
|
|
119
|
+
if (!issue || typeof issue.message !== 'string') return '';
|
|
120
|
+
const caminho = Array.isArray(issue.path) && issue.path.length ? `${issue.path.join('.')}: ` : '';
|
|
121
|
+
return caminho + issue.message;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Texto do `message` da API (string, ou lista de problemas do zod no 422). */
|
|
125
|
+
function messageOf(body) {
|
|
126
|
+
if (body === undefined || body === null) return '';
|
|
127
|
+
if (typeof body === 'string') return body;
|
|
128
|
+
if (typeof body !== 'object') return '';
|
|
129
|
+
if (typeof body.message === 'string') return body.message;
|
|
130
|
+
if (Array.isArray(body.message)) return body.message.map(issueText).filter(Boolean).join('; ');
|
|
131
|
+
if (typeof body.detail === 'string') return body.detail;
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 400 do plano: so o limite de VAGAS (identidades/membros, que dividem as 5 vagas do Free) vira 'limite-plano'.
|
|
136
|
+
// Qualquer outra recusa do plano (papel, faixa de IP, ambientes, projetos) vira 'plano-restringe': o aluno nao
|
|
137
|
+
// resolve apagando identidade, e a mensagem nao pode mandar fazer isso.
|
|
138
|
+
const RE_PLAN_ANY = /limit reached|plan limit|plan (?:\w+ )?restriction|rbac|custom role|upgrade (?:your )?plan|upgrade to /i;
|
|
139
|
+
const RE_PLAN_SEATS = /\b(?:identity|identities|member|members|user|users)\b[^.]{0,40}\blimit\b|\blimit\b[^.]{0,40}\b(?:identity|identities|members?|users?)\b/i;
|
|
140
|
+
const RE_EXISTS = /already exists|already a member|already configured/i;
|
|
141
|
+
const RE_SESSION = /re-?authenticate|log in again|no longer valid|no organization found|has been revoked/i;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Traduz status HTTP + corpo da API em um `codigo` fixo:
|
|
145
|
+
* rede/tempo (fetch lancou) sao decididos em `request`; aqui so o que veio de resposta.
|
|
146
|
+
*/
|
|
147
|
+
function classifyHttp(status, body) {
|
|
148
|
+
const code = Number(status);
|
|
149
|
+
const message = messageOf(body);
|
|
150
|
+
const error = body && typeof body === 'object' && typeof body.error === 'string' ? body.error : '';
|
|
151
|
+
const tokenError = error === 'TokenError' || error === 'InvalidToken';
|
|
152
|
+
if (!code) return 'rede';
|
|
153
|
+
if (code === 429) return 'limite';
|
|
154
|
+
if (code >= 500) return 'servidor';
|
|
155
|
+
if (code === 401) {
|
|
156
|
+
if (/invalid credentials/i.test(message)) return 'credencial-invalida';
|
|
157
|
+
if (/temporarily locked/i.test(message)) return 'bloqueada';
|
|
158
|
+
// 'no organization found' vem no campo `error` (o `message` fica 'You are not allowed to access this resource')
|
|
159
|
+
if (tokenError || RE_SESSION.test(message) || RE_SESSION.test(error)) return 'sessao-expirada';
|
|
160
|
+
return 'falha';
|
|
161
|
+
}
|
|
162
|
+
if (code === 403) return tokenError ? 'sessao-expirada' : 'sem-permissao';
|
|
163
|
+
if (code === 400) {
|
|
164
|
+
if (RE_PLAN_SEATS.test(message)) return 'limite-plano';
|
|
165
|
+
if (RE_PLAN_ANY.test(message)) return 'plano-restringe';
|
|
166
|
+
if (RE_EXISTS.test(message)) return 'ja-existe';
|
|
167
|
+
return 'falha';
|
|
168
|
+
}
|
|
169
|
+
if (code === 404) return 'nao-encontrado';
|
|
170
|
+
if (code === 422) return 'invalido';
|
|
171
|
+
return 'falha';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── Cliente HTTP ────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
177
|
+
|
|
178
|
+
function positive(value, fallback) {
|
|
179
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function queryString(query) {
|
|
183
|
+
if (!query || typeof query !== 'object') return '';
|
|
184
|
+
const params = new URLSearchParams();
|
|
185
|
+
for (const [key, value] of Object.entries(query)) {
|
|
186
|
+
if (value === undefined || value === null) continue;
|
|
187
|
+
params.append(key, String(value));
|
|
188
|
+
}
|
|
189
|
+
const text = params.toString();
|
|
190
|
+
return text ? `?${text}` : '';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function retryAfterOf(res, message) {
|
|
194
|
+
try {
|
|
195
|
+
const header = res && res.headers && typeof res.headers.get === 'function' ? res.headers.get('retry-after') : null;
|
|
196
|
+
if (header !== null && header !== undefined && /^\d+(?:\.\d+)?$/.test(String(header).trim())) return Number(header);
|
|
197
|
+
} catch {
|
|
198
|
+
// sem header: tenta a mensagem
|
|
199
|
+
}
|
|
200
|
+
const m = /try again in (\d+(?:\.\d+)?) seconds?/i.exec(message || '');
|
|
201
|
+
return m ? Number(m[1]) : undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function networkHint(err) {
|
|
205
|
+
const code = err && ((err.cause && err.cause.code) || err.code);
|
|
206
|
+
return typeof code === 'string' && /^[A-Z0-9_]{3,40}$/.test(code) ? ` (${code})` : '';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Cria o cliente HTTP. `baseUrl` e o dominio SEM `/api` (use `normalizeDomain`); as operacoes montam `/api/...`.
|
|
211
|
+
* `token` vai em `Authorization: Bearer` (a menos que a chamada peca `auth: 'none'`, como o login).
|
|
212
|
+
*/
|
|
213
|
+
function createClient(options = {}) {
|
|
214
|
+
const o = options && typeof options === 'object' ? options : {};
|
|
215
|
+
const baseUrl = typeof o.baseUrl === 'string' ? o.baseUrl.trim().replace(/\/+$/, '') : '';
|
|
216
|
+
const token = typeof o.token === 'string' && o.token ? o.token : null;
|
|
217
|
+
const fetchImpl = typeof o.fetchImpl === 'function' ? o.fetchImpl : globalThis.fetch;
|
|
218
|
+
const userAgent = typeof o.userAgent === 'string' && o.userAgent ? o.userAgent : 'auroq-os';
|
|
219
|
+
const sleep = typeof o.sleep === 'function' ? o.sleep : defaultSleep;
|
|
220
|
+
const defaultTimeout = positive(o.timeoutMs, 20000);
|
|
221
|
+
const maxRetries = typeof o.maxRetries === 'number' && o.maxRetries >= 0 ? Math.floor(o.maxRetries) : 2;
|
|
222
|
+
|
|
223
|
+
async function pause(ms) {
|
|
224
|
+
try {
|
|
225
|
+
await sleep(ms);
|
|
226
|
+
} catch {
|
|
227
|
+
// um sleep injetado que falha nao pode derrubar a chamada
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function request(method, path, opts) {
|
|
232
|
+
try {
|
|
233
|
+
return await run(method, path, opts && typeof opts === 'object' ? opts : {});
|
|
234
|
+
} catch {
|
|
235
|
+
return { ok: false, status: 0, codigo: 'falha', mensagem: 'erro interno no cliente da API' };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function run(method, path, opts) {
|
|
240
|
+
const verb = String(method || 'GET').toUpperCase();
|
|
241
|
+
const falha = (codigo, mensagem) => ({ ok: false, status: 0, codigo, mensagem });
|
|
242
|
+
if (!/^[A-Z]{3,7}$/.test(verb)) return falha('falha', 'metodo HTTP invalido');
|
|
243
|
+
if (typeof path !== 'string' || !path.startsWith('/')) return falha('falha', 'caminho da API invalido');
|
|
244
|
+
if (!/^https?:\/\/[^/\s]+/i.test(baseUrl)) return falha('falha', 'dominio do Infisical invalido');
|
|
245
|
+
if (typeof fetchImpl !== 'function') return falha('rede', 'esta versao do Node nao tem fetch (use Node 18 ou mais novo)');
|
|
246
|
+
|
|
247
|
+
const url = baseUrl + path + queryString(opts.query);
|
|
248
|
+
const headers = { Accept: 'application/json', 'User-Agent': userAgent };
|
|
249
|
+
if (opts.auth !== 'none' && token) headers.Authorization = `Bearer ${token}`;
|
|
250
|
+
let payload;
|
|
251
|
+
if (opts.body !== undefined && opts.body !== null) {
|
|
252
|
+
payload = JSON.stringify(opts.body);
|
|
253
|
+
headers['Content-Type'] = 'application/json';
|
|
254
|
+
}
|
|
255
|
+
const timeoutMs = positive(opts.timeoutMs, defaultTimeout);
|
|
256
|
+
|
|
257
|
+
// valores exatos que nunca podem aparecer em mensagem (alem dos padroes do redact)
|
|
258
|
+
const literais = [token, opts.body && opts.body.clientSecret, opts.body && opts.body.secretValue].filter(
|
|
259
|
+
(v) => typeof v === 'string' && v.length >= 8,
|
|
260
|
+
);
|
|
261
|
+
const limpar = (text) => {
|
|
262
|
+
let out = String(text);
|
|
263
|
+
// 1) valores exatos no texto INTEIRO (split/join e linear; o corte abaixo nao pode deixar parte do segredo de fora)
|
|
264
|
+
for (const literal of literais) out = out.split(literal).join(MASK);
|
|
265
|
+
// 2) corta ANTES do redact (regex quadratica em texto longo)
|
|
266
|
+
out = oneLine(out).slice(0, MAX_SCAN);
|
|
267
|
+
return oneLine(redact(out)).slice(0, MAX_MESSAGE);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
async function once() {
|
|
271
|
+
const controller = new AbortController();
|
|
272
|
+
let timedOut = false;
|
|
273
|
+
let timer;
|
|
274
|
+
const limite = new Promise((_, reject) => {
|
|
275
|
+
timer = setTimeout(() => {
|
|
276
|
+
timedOut = true;
|
|
277
|
+
controller.abort();
|
|
278
|
+
reject(new Error('tempo'));
|
|
279
|
+
}, timeoutMs);
|
|
280
|
+
});
|
|
281
|
+
const trabalho = (async () => {
|
|
282
|
+
const res = await fetchImpl(url, { method: verb, headers, body: payload, signal: controller.signal, redirect: 'error' });
|
|
283
|
+
const text = await res.text();
|
|
284
|
+
return { res, text };
|
|
285
|
+
})();
|
|
286
|
+
trabalho.catch(() => {});
|
|
287
|
+
let got;
|
|
288
|
+
try {
|
|
289
|
+
got = await Promise.race([trabalho, limite]);
|
|
290
|
+
} catch (err) {
|
|
291
|
+
if (timedOut) return { ok: false, status: 0, codigo: 'tempo', mensagem: 'o Infisical nao respondeu a tempo' };
|
|
292
|
+
return { ok: false, status: 0, codigo: 'rede', mensagem: `sem conexao com o Infisical${networkHint(err)}` };
|
|
293
|
+
} finally {
|
|
294
|
+
clearTimeout(timer);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const status = Number(got.res && got.res.status) || 0;
|
|
298
|
+
let parsed = null;
|
|
299
|
+
let isJson = false;
|
|
300
|
+
const text = typeof got.text === 'string' ? got.text : '';
|
|
301
|
+
if (text.trim()) {
|
|
302
|
+
try {
|
|
303
|
+
parsed = JSON.parse(text);
|
|
304
|
+
isJson = true;
|
|
305
|
+
} catch {
|
|
306
|
+
parsed = null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (status >= 200 && status < 300) {
|
|
310
|
+
if (!text.trim()) return { ok: true, status, data: {} };
|
|
311
|
+
if (!isJson) return { ok: false, status, codigo: 'falha', mensagem: 'a resposta do Infisical nao veio em JSON' };
|
|
312
|
+
return { ok: true, status, data: parsed };
|
|
313
|
+
}
|
|
314
|
+
const body = parsed && typeof parsed === 'object' ? parsed : null;
|
|
315
|
+
const bruto = messageOf(body);
|
|
316
|
+
const out = {
|
|
317
|
+
ok: false,
|
|
318
|
+
status,
|
|
319
|
+
codigo: classifyHttp(status, body),
|
|
320
|
+
mensagem: limpar(bruto) || `HTTP ${status}`,
|
|
321
|
+
};
|
|
322
|
+
const depois = status === 429 ? retryAfterOf(got.res, bruto) : undefined;
|
|
323
|
+
if (depois !== undefined) out.retryAfter = depois;
|
|
324
|
+
return out;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Repetir 5xx/erro de rede so e seguro quando repetir nao duplica nada: leituras (GET) e as chamadas que
|
|
328
|
+
// a operacao marca com `repetir: true` (login e revogacao). POST de criacao (projeto, identidade, client
|
|
329
|
+
// secret) NUNCA repete: o servidor pode ter gravado antes de o gateway devolver o erro. O 429 repete sempre,
|
|
330
|
+
// porque a requisicao foi recusada antes de ser processada.
|
|
331
|
+
const podeRepetir5xx = opts.repetir === true || verb === 'GET' || verb === 'HEAD';
|
|
332
|
+
let tentativas5xx = 0;
|
|
333
|
+
let usou429 = false;
|
|
334
|
+
for (;;) {
|
|
335
|
+
const r = await once();
|
|
336
|
+
if (r.ok) return r;
|
|
337
|
+
if ((r.codigo === 'servidor' || r.codigo === 'rede') && podeRepetir5xx && tentativas5xx < maxRetries) {
|
|
338
|
+
await pause(BACKOFF_MS[Math.min(tentativas5xx, BACKOFF_MS.length - 1)]);
|
|
339
|
+
tentativas5xx += 1;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (r.codigo === 'limite' && !usou429 && typeof r.retryAfter === 'number' && r.retryAfter <= MAX_RETRY_AFTER_S) {
|
|
343
|
+
usou429 = true;
|
|
344
|
+
await pause(Math.ceil(r.retryAfter * 1000) + 500);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
return r;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return { request };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ─── Operacoes ───────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
const enc = encodeURIComponent;
|
|
357
|
+
|
|
358
|
+
function opFail(r) {
|
|
359
|
+
const out = { ok: false, codigo: r.codigo || 'falha', mensagem: r.mensagem || '' };
|
|
360
|
+
if (r.status !== undefined) out.status = r.status;
|
|
361
|
+
if (r.retryAfter !== undefined) out.retryAfter = r.retryAfter;
|
|
362
|
+
return out;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const invalid = (mensagem) => ({ ok: false, codigo: 'invalido', mensagem });
|
|
366
|
+
const unexpected = (mensagem) => ({ ok: false, codigo: 'falha', mensagem });
|
|
367
|
+
|
|
368
|
+
/** Envolve a operacao: cliente ausente ou excecao inesperada viram `{ ok:false }`, nunca lancam. */
|
|
369
|
+
function op(fn) {
|
|
370
|
+
return async (client, ...args) => {
|
|
371
|
+
try {
|
|
372
|
+
if (!client || typeof client.request !== 'function') return unexpected('cliente da API ausente');
|
|
373
|
+
return await fn(client, ...args);
|
|
374
|
+
} catch {
|
|
375
|
+
return unexpected('erro inesperado ao falar com o Infisical');
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function str(value) {
|
|
381
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Aceita `(valor, extras)` ou um objeto unico `{ [chave]: valor, ...extras }`. */
|
|
385
|
+
function params(first, key, rest) {
|
|
386
|
+
const base = first && typeof first === 'object' ? { ...first } : { [key]: first };
|
|
387
|
+
if (rest && typeof rest === 'object') Object.assign(base, rest);
|
|
388
|
+
return base;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function normProject(raw) {
|
|
392
|
+
const p = raw && typeof raw === 'object' ? (raw.project && typeof raw.project === 'object' ? raw.project : raw) : null;
|
|
393
|
+
if (!p || typeof p.id !== 'string' || !p.id) return null;
|
|
394
|
+
const environments = Array.isArray(p.environments)
|
|
395
|
+
? p.environments
|
|
396
|
+
.filter((e) => e && typeof e.slug === 'string')
|
|
397
|
+
.map((e) => ({ slug: e.slug, name: typeof e.name === 'string' ? e.name : e.slug }))
|
|
398
|
+
: [];
|
|
399
|
+
return {
|
|
400
|
+
id: p.id,
|
|
401
|
+
name: typeof p.name === 'string' ? p.name : '',
|
|
402
|
+
slug: typeof p.slug === 'string' ? p.slug : '',
|
|
403
|
+
type: typeof p.type === 'string' ? p.type : '',
|
|
404
|
+
orgId: typeof p.orgId === 'string' && p.orgId ? p.orgId : null,
|
|
405
|
+
environments,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function normIdentity(raw) {
|
|
410
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
411
|
+
const wrapped = raw.identity && typeof raw.identity === 'object';
|
|
412
|
+
const base = wrapped ? raw.identity : raw;
|
|
413
|
+
const id = typeof base.id === 'string' && base.id ? base.id : typeof raw.identityId === 'string' ? raw.identityId : '';
|
|
414
|
+
if (!id) return null;
|
|
415
|
+
const projectId = base.projectId || raw.projectId;
|
|
416
|
+
return {
|
|
417
|
+
id,
|
|
418
|
+
name: typeof base.name === 'string' ? base.name : '',
|
|
419
|
+
projectId: typeof projectId === 'string' && projectId ? projectId : null,
|
|
420
|
+
authMethods: Array.isArray(base.authMethods) ? base.authMethods.filter((m) => typeof m === 'string') : [],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const listOrganizations = op(async (client) => {
|
|
425
|
+
const r = await client.request('GET', '/api/v1/organization');
|
|
426
|
+
if (!r.ok) return opFail(r);
|
|
427
|
+
const lista = r.data && Array.isArray(r.data.organizations) ? r.data.organizations : null;
|
|
428
|
+
if (!lista) return unexpected('resposta inesperada ao listar organizacoes');
|
|
429
|
+
const orgs = lista
|
|
430
|
+
.filter((x) => x && typeof x.id === 'string' && x.id)
|
|
431
|
+
.map((x) => ({ id: x.id, name: typeof x.name === 'string' ? x.name : '', userRole: typeof x.userRole === 'string' ? x.userRole : null }));
|
|
432
|
+
return { ok: true, orgs };
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const getProjectBySlug = op(async (client, first) => {
|
|
436
|
+
const slug = str(params(first, 'slug').slug);
|
|
437
|
+
if (!slug) return invalid('falta o slug do projeto');
|
|
438
|
+
const r = await client.request('GET', `/api/v1/projects/slug/${enc(slug)}`);
|
|
439
|
+
if (!r.ok) return opFail(r);
|
|
440
|
+
const project = normProject(r.data);
|
|
441
|
+
return project ? { ok: true, project } : unexpected('resposta inesperada ao buscar o projeto');
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const createProject = op(async (client, first) => {
|
|
445
|
+
const a = first && typeof first === 'object' ? first : {};
|
|
446
|
+
const name = str(a.name);
|
|
447
|
+
const slug = str(a.slug);
|
|
448
|
+
if (!name || !slug) return invalid('falta nome e slug do projeto');
|
|
449
|
+
const r = await client.request('POST', '/api/v1/projects', { body: { projectName: name, slug } });
|
|
450
|
+
if (!r.ok) return opFail(r);
|
|
451
|
+
const project = normProject(r.data);
|
|
452
|
+
return project ? { ok: true, project } : unexpected('resposta inesperada ao criar o projeto');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const listProjectIdentities = op(async (client, first, rest) => {
|
|
456
|
+
const a = params(first, 'projectId', rest);
|
|
457
|
+
const projectId = str(a.projectId);
|
|
458
|
+
if (!projectId) return invalid('falta o id do projeto');
|
|
459
|
+
const query = { limit: 50 };
|
|
460
|
+
const search = str(a.search);
|
|
461
|
+
if (search) query.search = search;
|
|
462
|
+
const r = await client.request('GET', `/api/v1/projects/${enc(projectId)}/identities`, { query });
|
|
463
|
+
if (!r.ok) return opFail(r);
|
|
464
|
+
const lista = r.data && Array.isArray(r.data.identities) ? r.data.identities : null;
|
|
465
|
+
if (!lista) return unexpected('resposta inesperada ao listar identidades');
|
|
466
|
+
return { ok: true, identities: lista.map(normIdentity).filter(Boolean) };
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
const createProjectIdentity = op(async (client, first, rest) => {
|
|
470
|
+
const a = params(first, 'projectId', rest);
|
|
471
|
+
const projectId = str(a.projectId);
|
|
472
|
+
const name = str(a.name);
|
|
473
|
+
const role = str(a.role) || 'member';
|
|
474
|
+
if (!projectId || !name) return invalid('falta o id do projeto e o nome da identidade');
|
|
475
|
+
const r = await client.request('POST', `/api/v1/projects/${enc(projectId)}/identities`, {
|
|
476
|
+
body: { name, roles: [{ role, isTemporary: false }] },
|
|
477
|
+
});
|
|
478
|
+
if (!r.ok) return opFail(r);
|
|
479
|
+
const identity = normIdentity(r.data && r.data.identity);
|
|
480
|
+
return identity ? { ok: true, identity } : unexpected('resposta inesperada ao criar a identidade');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const getUniversalAuth = op(async (client, first) => {
|
|
484
|
+
const identityId = str(params(first, 'identityId').identityId);
|
|
485
|
+
if (!identityId) return invalid('falta o id da identidade');
|
|
486
|
+
const r = await client.request('GET', `/api/v1/auth/universal-auth/identities/${enc(identityId)}`);
|
|
487
|
+
if (!r.ok) return opFail(r);
|
|
488
|
+
const clientId = r.data && r.data.identityUniversalAuth && r.data.identityUniversalAuth.clientId;
|
|
489
|
+
return typeof clientId === 'string' && clientId ? { ok: true, clientId } : unexpected('resposta inesperada ao ler o Universal Auth');
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const attachUniversalAuth = op(async (client, first, rest) => {
|
|
493
|
+
const a = params(first, 'identityId', rest);
|
|
494
|
+
const identityId = str(a.identityId);
|
|
495
|
+
if (!identityId) return invalid('falta o id da identidade');
|
|
496
|
+
const accessTokenTTL = Number.isInteger(a.accessTokenTTL) ? a.accessTokenTTL : 3600;
|
|
497
|
+
const accessTokenMaxTTL = Number.isInteger(a.accessTokenMaxTTL) ? a.accessTokenMaxTTL : 86400;
|
|
498
|
+
const r = await client.request('POST', `/api/v1/auth/universal-auth/identities/${enc(identityId)}`, {
|
|
499
|
+
body: { accessTokenTTL, accessTokenMaxTTL },
|
|
500
|
+
});
|
|
501
|
+
if (r.ok) {
|
|
502
|
+
const clientId = r.data && r.data.identityUniversalAuth && r.data.identityUniversalAuth.clientId;
|
|
503
|
+
return typeof clientId === 'string' && clientId ? { ok: true, clientId, jaExistia: false } : unexpected('resposta inesperada ao anexar o Universal Auth');
|
|
504
|
+
}
|
|
505
|
+
if (r.codigo === 'ja-existe') {
|
|
506
|
+
const atual = await getUniversalAuth(client, identityId);
|
|
507
|
+
return atual.ok ? { ok: true, clientId: atual.clientId, jaExistia: true } : atual;
|
|
508
|
+
}
|
|
509
|
+
return opFail(r);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const createClientSecret = op(async (client, first, rest) => {
|
|
513
|
+
const a = params(first, 'identityId', rest);
|
|
514
|
+
const identityId = str(a.identityId);
|
|
515
|
+
if (!identityId) return invalid('falta o id da identidade');
|
|
516
|
+
const description = typeof a.description === 'string' ? a.description : '';
|
|
517
|
+
const r = await client.request('POST', `/api/v1/auth/universal-auth/identities/${enc(identityId)}/client-secrets`, {
|
|
518
|
+
body: { description, numUsesLimit: 0, ttl: 0 },
|
|
519
|
+
});
|
|
520
|
+
if (!r.ok) return opFail(r);
|
|
521
|
+
const clientSecret = r.data && r.data.clientSecret;
|
|
522
|
+
if (typeof clientSecret !== 'string' || !clientSecret) return unexpected('resposta inesperada ao criar a chave');
|
|
523
|
+
const secretId = r.data.clientSecretData && typeof r.data.clientSecretData.id === 'string' ? r.data.clientSecretData.id : null;
|
|
524
|
+
return { ok: true, clientSecret, secretId };
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const listClientSecrets = op(async (client, first) => {
|
|
528
|
+
const identityId = str(params(first, 'identityId').identityId);
|
|
529
|
+
if (!identityId) return invalid('falta o id da identidade');
|
|
530
|
+
const r = await client.request('GET', `/api/v1/auth/universal-auth/identities/${enc(identityId)}/client-secrets`);
|
|
531
|
+
if (!r.ok) return opFail(r);
|
|
532
|
+
const lista = r.data && Array.isArray(r.data.clientSecretData) ? r.data.clientSecretData : null;
|
|
533
|
+
if (!lista) return unexpected('resposta inesperada ao listar as chaves');
|
|
534
|
+
const secrets = lista
|
|
535
|
+
.filter((s) => s && typeof s.id === 'string' && s.id)
|
|
536
|
+
.map((s) => ({
|
|
537
|
+
id: s.id,
|
|
538
|
+
description: typeof s.description === 'string' ? s.description : '',
|
|
539
|
+
// 4 primeiros caracteres da chave (o Infisical os mostra na lista): so para reconhecer a chave EM USO neste computador.
|
|
540
|
+
// Nunca vai para tela, log nem config.
|
|
541
|
+
prefix: typeof s.clientSecretPrefix === 'string' ? s.clientSecretPrefix.slice(0, 16) : '',
|
|
542
|
+
createdAt: typeof s.createdAt === 'string' ? s.createdAt : null,
|
|
543
|
+
revoked: s.isClientSecretRevoked === true,
|
|
544
|
+
}));
|
|
545
|
+
return { ok: true, secrets };
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const revokeClientSecret = op(async (client, first, second) => {
|
|
549
|
+
const a = first && typeof first === 'object' ? first : { identityId: first, secretId: second };
|
|
550
|
+
const identityId = str(a.identityId);
|
|
551
|
+
const secretId = str(a.secretId);
|
|
552
|
+
if (!identityId || !secretId) return invalid('falta o id da identidade e o id da chave');
|
|
553
|
+
const r = await client.request(
|
|
554
|
+
'POST',
|
|
555
|
+
`/api/v1/auth/universal-auth/identities/${enc(identityId)}/client-secrets/${enc(secretId)}/revoke`,
|
|
556
|
+
{ repetir: true }, // revogar de novo uma chave ja revogada nao muda nada
|
|
557
|
+
);
|
|
558
|
+
return r.ok ? { ok: true } : opFail(r);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const universalLogin = op(async (client, first) => {
|
|
562
|
+
const a = first && typeof first === 'object' ? first : {};
|
|
563
|
+
const clientId = str(a.clientId);
|
|
564
|
+
const clientSecret = str(a.clientSecret);
|
|
565
|
+
if (!clientId || !clientSecret) return invalid('falta o client id e o client secret');
|
|
566
|
+
const r = await client.request('POST', '/api/v1/auth/universal-auth/login', { auth: 'none', repetir: true, body: { clientId, clientSecret } });
|
|
567
|
+
if (!r.ok) return opFail(r);
|
|
568
|
+
const accessToken = r.data && r.data.accessToken;
|
|
569
|
+
if (typeof accessToken !== 'string' || !accessToken) return unexpected('resposta inesperada no login da identidade');
|
|
570
|
+
const expiresIn = Number(r.data.expiresIn);
|
|
571
|
+
return { ok: true, accessToken, expiresIn: Number.isFinite(expiresIn) ? expiresIn : null };
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
/** Valida `{ projectId, environment, name }` (e `value` quando pedido). Devolve texto de erro ou null. */
|
|
575
|
+
function secretArgsProblem(a, needValue) {
|
|
576
|
+
if (!str(a.projectId) || !str(a.environment)) return 'falta o projeto e o ambiente';
|
|
577
|
+
if (typeof a.name !== 'string' || !a.name) return 'falta o nome do segredo';
|
|
578
|
+
if (needValue && typeof a.value !== 'string') return 'o valor do segredo precisa ser texto';
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const readSecret = op(async (client, first) => {
|
|
583
|
+
const a = first && typeof first === 'object' ? first : {};
|
|
584
|
+
const problem = secretArgsProblem(a, false);
|
|
585
|
+
if (problem) return invalid(problem);
|
|
586
|
+
// expandSecretReferences=false: a v4 expande ${...} por padrao e devolveria um valor diferente do gravado
|
|
587
|
+
const r = await client.request('GET', `/api/v4/secrets/${enc(a.name)}`, {
|
|
588
|
+
query: { projectId: a.projectId, environment: a.environment, secretPath: '/', type: 'shared', expandSecretReferences: 'false' },
|
|
589
|
+
});
|
|
590
|
+
if (!r.ok) return opFail(r);
|
|
591
|
+
const secret = r.data && r.data.secret;
|
|
592
|
+
if (!secret || typeof secret !== 'object') return unexpected('resposta inesperada ao ler o segredo');
|
|
593
|
+
if (secret.secretValueHidden === true || secret.secretValue === HIDDEN_VALUE) {
|
|
594
|
+
return { ok: false, codigo: 'sem-permissao', mensagem: 'esta identidade nao pode ver o valor do segredo' };
|
|
595
|
+
}
|
|
596
|
+
if (typeof secret.secretValue !== 'string') return unexpected('a resposta nao trouxe o valor do segredo');
|
|
597
|
+
return { ok: true, value: secret.secretValue };
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const writeSecret = op(async (client, first) => {
|
|
601
|
+
const a = first && typeof first === 'object' ? first : {};
|
|
602
|
+
const problem = secretArgsProblem(a, true);
|
|
603
|
+
if (problem) return invalid(problem);
|
|
604
|
+
const caminho = `/api/v4/secrets/${enc(a.name)}`;
|
|
605
|
+
const corpo = () => ({ projectId: a.projectId, environment: a.environment, secretPath: '/', secretValue: a.value, type: 'shared' });
|
|
606
|
+
let r = await client.request('POST', caminho, { body: corpo() });
|
|
607
|
+
let criado = true;
|
|
608
|
+
if (!r.ok && r.codigo === 'ja-existe') {
|
|
609
|
+
criado = false;
|
|
610
|
+
r = await client.request('PATCH', caminho, { body: corpo() });
|
|
611
|
+
}
|
|
612
|
+
if (!r.ok) return opFail(r);
|
|
613
|
+
if (r.data && r.data.approval && !r.data.secret) return unexpected('o projeto exige aprovacao para gravar segredos');
|
|
614
|
+
return { ok: true, criado };
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const listSecretNames = op(async (client, first) => {
|
|
618
|
+
const a = first && typeof first === 'object' ? first : {};
|
|
619
|
+
if (!str(a.projectId) || !str(a.environment)) return invalid('falta o projeto e o ambiente');
|
|
620
|
+
const r = await client.request('GET', '/api/v4/secrets', {
|
|
621
|
+
query: { projectId: a.projectId, environment: a.environment, secretPath: '/', viewSecretValue: 'false' },
|
|
622
|
+
});
|
|
623
|
+
if (!r.ok) return opFail(r);
|
|
624
|
+
const lista = r.data && Array.isArray(r.data.secrets) ? r.data.secrets : null;
|
|
625
|
+
if (!lista) return unexpected('resposta inesperada ao listar segredos');
|
|
626
|
+
const names = [];
|
|
627
|
+
for (const s of lista) {
|
|
628
|
+
// so a chave interessa: se a API mandou valor, ele e descartado aqui
|
|
629
|
+
if (s && typeof s.secretKey === 'string' && s.secretKey && !names.includes(s.secretKey)) names.push(s.secretKey);
|
|
630
|
+
}
|
|
631
|
+
return { ok: true, names };
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
const deleteSecret = op(async (client, first) => {
|
|
635
|
+
const a = first && typeof first === 'object' ? first : {};
|
|
636
|
+
const problem = secretArgsProblem(a, false);
|
|
637
|
+
if (problem) return invalid(problem);
|
|
638
|
+
const r = await client.request('DELETE', `/api/v4/secrets/${enc(a.name)}`, {
|
|
639
|
+
body: { projectId: a.projectId, environment: a.environment, secretPath: '/' },
|
|
640
|
+
});
|
|
641
|
+
return r.ok ? { ok: true } : opFail(r);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
module.exports = {
|
|
645
|
+
REGIONS,
|
|
646
|
+
normalizeDomain,
|
|
647
|
+
decodeJwtPayload,
|
|
648
|
+
isJwtShape,
|
|
649
|
+
isClientSecretShape,
|
|
650
|
+
redact,
|
|
651
|
+
createClient,
|
|
652
|
+
classifyHttp,
|
|
653
|
+
ops: {
|
|
654
|
+
listOrganizations,
|
|
655
|
+
getProjectBySlug,
|
|
656
|
+
createProject,
|
|
657
|
+
listProjectIdentities,
|
|
658
|
+
createProjectIdentity,
|
|
659
|
+
getUniversalAuth,
|
|
660
|
+
attachUniversalAuth,
|
|
661
|
+
createClientSecret,
|
|
662
|
+
listClientSecrets,
|
|
663
|
+
revokeClientSecret,
|
|
664
|
+
universalLogin,
|
|
665
|
+
readSecret,
|
|
666
|
+
writeSecret,
|
|
667
|
+
listSecretNames,
|
|
668
|
+
deleteSecret,
|
|
669
|
+
},
|
|
670
|
+
};
|