codetac 0.1.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/LICENSE +21 -0
- package/package.json +60 -0
- package/readme.md +172 -0
- package/src/action-view.mjs +104 -0
- package/src/ai.mjs +504 -0
- package/src/boundaries.mjs +483 -0
- package/src/browser/bar.js +593 -0
- package/src/cli.mjs +556 -0
- package/src/detect.mjs +198 -0
- package/src/diagnose.mjs +121 -0
- package/src/digest.mjs +317 -0
- package/src/home.mjs +15 -0
- package/src/origins.mjs +182 -0
- package/src/page.mjs +220 -0
- package/src/panel.mjs +800 -0
- package/src/recording.mjs +124 -0
- package/src/redact.mjs +35 -0
- package/src/register.mjs +94 -0
- package/src/runtime.mjs +353 -0
- package/src/sentences.mjs +209 -0
- package/src/store.mjs +376 -0
- package/src/transform.mjs +255 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
// Boundaries: what the application does outside its own code (database,
|
|
2
|
+
// external HTTP, AI, email, payments, files, authentication). Detection is by
|
|
3
|
+
// library or protocol, never by framework. Values are never recorded: SQL is
|
|
4
|
+
// kept as its template, URLs without query values, headers only classify.
|
|
5
|
+
import diagnostics from 'node:diagnostics_channel';
|
|
6
|
+
import http from 'node:http';
|
|
7
|
+
import https from 'node:https';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import { relative, isAbsolute, sep } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { syncBuiltinESMExports } from 'node:module';
|
|
12
|
+
import { actionOf, handleOwnRoute, injectScript, wantsPage, INTERNAL_HEADER } from './page.mjs';
|
|
13
|
+
|
|
14
|
+
export const SKIP = Symbol('codetac.skip');
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Descriptions
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
// Long opaque path segments (tokens, ids) are hidden; readable words stay.
|
|
21
|
+
export function safePath(pathname) {
|
|
22
|
+
return String(pathname).split('/').map(segment => {
|
|
23
|
+
const plain = decodeURIComponentSafe(segment);
|
|
24
|
+
return plain.length >= 20 && /^[\w.~-]+$/.test(plain) && /\d/.test(plain) && /[a-z]/i.test(plain) ? '[REDACTED]' : segment;
|
|
25
|
+
}).join('/');
|
|
26
|
+
}
|
|
27
|
+
function decodeURIComponentSafe(value) {
|
|
28
|
+
try { return decodeURIComponent(value); } catch { return value; }
|
|
29
|
+
}
|
|
30
|
+
export function splitUrl(raw, base = 'http://localhost') {
|
|
31
|
+
let url;
|
|
32
|
+
try { url = new URL(raw, base); } catch { return { path: '[inválido]', queryKeys: [] }; }
|
|
33
|
+
return { url, path: safePath(url.pathname), queryKeys: [...new Set(url.searchParams.keys())] };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// SQL is recorded as a template: string literals are replaced, so values
|
|
37
|
+
// concatenated into the text do not reach the recording.
|
|
38
|
+
export function describeSql(sql) {
|
|
39
|
+
if (typeof sql !== 'string') return { operation: 'consulta' };
|
|
40
|
+
const text = sql.replace(/'(?:[^'\\]|\\.|'')*'/g, "'?'").replace(/\s+/g, ' ').trim();
|
|
41
|
+
const operation = (text.match(/^\s*(\w+)/)?.[1] ?? 'consulta').toUpperCase();
|
|
42
|
+
const tables = [...text.matchAll(/\b(?:from|into|update|join|table(?: if (?:not )?exists)?)\s+[`"[]?([\w.]+)/gi)]
|
|
43
|
+
.map(match => match[1]).filter(name => !/^(select|if|not|exists)$/i.test(name));
|
|
44
|
+
return { operation, tables: [...new Set(tables)].slice(0, 10), sql: text.slice(0, 2000) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function rowsOf(result) {
|
|
48
|
+
if (Array.isArray(result)) return { rows: result.length };
|
|
49
|
+
if (result && typeof result === 'object') {
|
|
50
|
+
if (typeof result.affectedRows === 'number') return { affectedRows: result.affectedRows };
|
|
51
|
+
if (typeof result.changes === 'number') return { affectedRows: result.changes };
|
|
52
|
+
if (typeof result.rowCount === 'number') return { rows: result.rowCount };
|
|
53
|
+
return { rows: 1 };
|
|
54
|
+
}
|
|
55
|
+
return { rows: result === undefined ? 0 : 1 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// HTTP classification
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
const AI = { 'api.openai.com': 'OpenAI', 'api.anthropic.com': 'Anthropic', 'generativelanguage.googleapis.com': 'Google',
|
|
63
|
+
'api.mistral.ai': 'Mistral', 'api.groq.com': 'Groq', 'openrouter.ai': 'OpenRouter', 'api.deepseek.com': 'DeepSeek',
|
|
64
|
+
'api.cohere.com': 'Cohere', 'api.together.xyz': 'Together' };
|
|
65
|
+
const MAIL = { 'api.resend.com': 'Resend', 'api.sendgrid.com': 'SendGrid', 'api.postmarkapp.com': 'Postmark',
|
|
66
|
+
'api.mailgun.net': 'Mailgun', 'api.eu.mailgun.net': 'Mailgun', 'api.brevo.com': 'Brevo', 'api.twilio.com': 'Twilio' };
|
|
67
|
+
|
|
68
|
+
function bodyText(body) {
|
|
69
|
+
if (typeof body === 'string') return body.length <= 262144 ? body : null;
|
|
70
|
+
if (body instanceof Uint8Array && body.byteLength <= 262144) return Buffer.from(body).toString('utf8');
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function jsonBody(body) {
|
|
74
|
+
const text = bodyText(body);
|
|
75
|
+
if (!text) return null;
|
|
76
|
+
try { return JSON.parse(text); } catch { return null; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function classifyHttp({ method, url, headers, body, client }) {
|
|
80
|
+
const { url: parsed, path, queryKeys } = splitUrl(url);
|
|
81
|
+
const host = parsed?.hostname ?? '';
|
|
82
|
+
const base = { kind: 'http', library: client, method, host, path, queryKeys };
|
|
83
|
+
if (!parsed) return base;
|
|
84
|
+
const local = /^(localhost|127\.0\.0\.1|\[?::1\]?)$/.test(host);
|
|
85
|
+
// Known providers by domain; local or self-hosted models by their API paths.
|
|
86
|
+
const compatible = /^\/(v1\/(chat\/completions|completions|responses|messages)|api\/(chat|generate))$/.test(parsed.pathname);
|
|
87
|
+
if (AI[host] || compatible) {
|
|
88
|
+
const json = jsonBody(body);
|
|
89
|
+
return { ...base, kind: 'ia', provider: AI[host] ?? (local ? 'modelo local' : `compatível (${host})`), operation: path,
|
|
90
|
+
local: local || undefined, model: typeof json?.model === 'string' ? json.model : undefined };
|
|
91
|
+
}
|
|
92
|
+
if (local) return { ...base, local: true };
|
|
93
|
+
if (host === 'api.stripe.com') {
|
|
94
|
+
const auth = String(headers.authorization ?? '');
|
|
95
|
+
const text = bodyText(body) ?? '';
|
|
96
|
+
const form = new URLSearchParams(text);
|
|
97
|
+
return { ...base, kind: 'pagamento', provider: 'Stripe', operation: `${method} ${path}`,
|
|
98
|
+
mode: /(sk|rk)_test_/.test(auth) ? 'teste' : /(sk|rk)_live_/.test(auth) ? 'produção' : 'desconhecido',
|
|
99
|
+
amount: form.get('amount') ?? undefined, currency: form.get('currency') ?? undefined };
|
|
100
|
+
}
|
|
101
|
+
if (MAIL[host]) {
|
|
102
|
+
const json = jsonBody(body);
|
|
103
|
+
const to = json?.to ?? json?.personalizations?.[0]?.to ?? json?.To;
|
|
104
|
+
return { ...base, kind: MAIL[host] === 'Twilio' ? 'mensagem' : 'email', provider: MAIL[host],
|
|
105
|
+
to: to === undefined ? undefined : [to].flat().map(item => typeof item === 'string' ? item : item?.email).filter(Boolean),
|
|
106
|
+
subject: typeof json?.subject === 'string' ? json.subject : typeof json?.Subject === 'string' ? json.Subject : undefined };
|
|
107
|
+
}
|
|
108
|
+
if (host.endsWith('.supabase.co')) {
|
|
109
|
+
const [, area, version, first, second] = parsed.pathname.split('/');
|
|
110
|
+
if (area === 'rest' && version === 'v1') {
|
|
111
|
+
const operation = { GET: 'SELECT', HEAD: 'SELECT', POST: 'INSERT', PATCH: 'UPDATE', PUT: 'UPSERT', DELETE: 'DELETE' }[method] ?? method;
|
|
112
|
+
return { ...base, kind: 'base-de-dados', provider: 'Supabase', operation, tables: first === 'rpc' ? [`rpc:${second}`] : [first] };
|
|
113
|
+
}
|
|
114
|
+
if (area === 'auth') return { ...base, kind: 'autenticação', provider: 'Supabase Auth', operation: first };
|
|
115
|
+
if (area === 'storage') return { ...base, kind: 'ficheiros', provider: 'Supabase Storage', operation: method, bucket: second === undefined ? first : second };
|
|
116
|
+
}
|
|
117
|
+
if (host === 'api.clerk.com' || host.endsWith('.clerk.accounts.dev')) return { ...base, kind: 'autenticação', provider: 'Clerk' };
|
|
118
|
+
// S3-compatible storage: signed header, or a presigned URL (signature in the query).
|
|
119
|
+
if (/^AWS4-HMAC-SHA256/.test(String(headers.authorization ?? '')) || headers['x-amz-content-sha256']
|
|
120
|
+
|| queryKeys.some(key => /^x-amz-(signature|algorithm)$/i.test(key))) {
|
|
121
|
+
const virtualHost = host.match(/^(.+?)\.s3[.-]/);
|
|
122
|
+
const bucket = virtualHost ? virtualHost[1] : parsed.pathname.split('/')[1];
|
|
123
|
+
const operation = { GET: 'leitura', HEAD: 'verificação', PUT: 'escrita', POST: 'escrita', DELETE: 'remoção' }[method] ?? method;
|
|
124
|
+
const size = Number(headers['content-length']);
|
|
125
|
+
return { ...base, kind: 'ficheiros', provider: 'S3', operation, bucket, bytes: Number.isFinite(size) ? size : undefined };
|
|
126
|
+
}
|
|
127
|
+
return base;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// AI calls: token usage and short excerpts, read from the JSON or SSE bodies.
|
|
131
|
+
const EXCERPT = 200;
|
|
132
|
+
const excerpt = text => typeof text === 'string' && text ? text.replace(/\s+/g, ' ').trim().slice(0, EXCERPT) : undefined;
|
|
133
|
+
function textOf(content) {
|
|
134
|
+
if (typeof content === 'string') return content;
|
|
135
|
+
if (Array.isArray(content)) return content.map(part => typeof part === 'string' ? part : part?.text ?? part?.input_text ?? '').join(' ');
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
export function aiRequestDetails(json) {
|
|
139
|
+
if (!json || typeof json !== 'object') return {};
|
|
140
|
+
const messages = json.messages ?? json.contents ?? (Array.isArray(json.input) ? json.input : null);
|
|
141
|
+
let prompt = typeof json.input === 'string' ? json.input : typeof json.prompt === 'string' ? json.prompt : undefined;
|
|
142
|
+
if (!prompt && Array.isArray(messages)) {
|
|
143
|
+
const last = [...messages].reverse().find(message => (message?.role ?? 'user') === 'user') ?? messages.at(-1);
|
|
144
|
+
prompt = textOf(last?.content) ?? textOf(last?.parts);
|
|
145
|
+
}
|
|
146
|
+
return { model: typeof json.model === 'string' ? json.model : undefined, promptExcerpt: excerpt(prompt), stream: json.stream === true || undefined };
|
|
147
|
+
}
|
|
148
|
+
function usageOf(value) {
|
|
149
|
+
const usage = value?.usage ?? value?.response?.usage ?? value?.message?.usage;
|
|
150
|
+
if (usage) {
|
|
151
|
+
const input = usage.input_tokens ?? usage.prompt_tokens;
|
|
152
|
+
const output = usage.output_tokens ?? usage.completion_tokens;
|
|
153
|
+
if (input != null || output != null) return { input, output };
|
|
154
|
+
}
|
|
155
|
+
const meta = value?.usageMetadata;
|
|
156
|
+
if (meta) return { input: meta.promptTokenCount, output: meta.candidatesTokenCount };
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function answerOf(value) {
|
|
160
|
+
return value?.output_text ?? value?.choices?.[0]?.message?.content ?? value?.choices?.[0]?.text
|
|
161
|
+
?? value?.output?.find?.(item => item?.type === 'message')?.content?.[0]?.text
|
|
162
|
+
?? value?.content?.find?.(item => item?.type === 'text')?.text ?? value?.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
163
|
+
}
|
|
164
|
+
export function aiResponseDetails(text) {
|
|
165
|
+
const result = {};
|
|
166
|
+
let answer = '';
|
|
167
|
+
const take = value => {
|
|
168
|
+
// Named "usage" (not "tokens") so the secret redaction keeps the counts.
|
|
169
|
+
const usage = usageOf(value);
|
|
170
|
+
if (usage) result.usage = { ...result.usage, ...Object.fromEntries(Object.entries(usage).filter(([, count]) => count != null)) };
|
|
171
|
+
const full = answerOf(value);
|
|
172
|
+
if (typeof full === 'string') answer = full;
|
|
173
|
+
const delta = value?.choices?.[0]?.delta?.content ?? (value?.type === 'response.output_text.delta' ? value.delta : undefined)
|
|
174
|
+
?? (value?.type === 'content_block_delta' ? value.delta?.text : undefined);
|
|
175
|
+
if (typeof delta === 'string' && answer.length < EXCERPT) answer += delta;
|
|
176
|
+
};
|
|
177
|
+
try { take(JSON.parse(text)); }
|
|
178
|
+
catch {
|
|
179
|
+
for (const line of text.split('\n')) {
|
|
180
|
+
if (!line.startsWith('data:')) continue;
|
|
181
|
+
try { take(JSON.parse(line.slice(5).trim())); } catch {}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (answer) result.answerExcerpt = excerpt(answer);
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function headerMap(raw) {
|
|
189
|
+
const map = {};
|
|
190
|
+
if (Array.isArray(raw)) {
|
|
191
|
+
for (let i = 0; i + 1 < raw.length; i += 2) map[String(raw[i]).toLowerCase()] = String(raw[i + 1]);
|
|
192
|
+
} else if (typeof raw === 'string') {
|
|
193
|
+
for (const line of raw.split('\r\n')) {
|
|
194
|
+
const index = line.indexOf(':');
|
|
195
|
+
if (index > 0) map[line.slice(0, index).trim().toLowerCase()] = line.slice(index + 1).trim();
|
|
196
|
+
}
|
|
197
|
+
} else if (raw && typeof raw === 'object') {
|
|
198
|
+
for (const [key, value] of Object.entries(raw)) map[key.toLowerCase()] = String(value);
|
|
199
|
+
}
|
|
200
|
+
return map;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Installation
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
export function installBoundaries(runtime, root, page = {}) {
|
|
208
|
+
installRequests(runtime, page);
|
|
209
|
+
installHttpClients(runtime);
|
|
210
|
+
installNodeSqlite(runtime);
|
|
211
|
+
installFiles(runtime, root);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Files: only operations made directly by project code during a request.
|
|
215
|
+
// The first caller frame outside Node and CodeTAC decides; library code
|
|
216
|
+
// (node_modules, bundled dependency chunks) is ignored.
|
|
217
|
+
const own = fileURLToPath(new URL('.', import.meta.url));
|
|
218
|
+
function callerIsProject(root) {
|
|
219
|
+
const holder = {};
|
|
220
|
+
const limit = Error.stackTraceLimit;
|
|
221
|
+
Error.stackTraceLimit = 12;
|
|
222
|
+
Error.captureStackTrace(holder, callerIsProject);
|
|
223
|
+
Error.stackTraceLimit = limit;
|
|
224
|
+
for (const line of String(holder.stack).split('\n').slice(1)) {
|
|
225
|
+
const match = line.match(/(webpack-internal:\S*?)(?::\d+)+\)?\s*$/)
|
|
226
|
+
?? line.match(/\(?((?:file:\/\/|\/|[A-Za-z]:\\)[^():]*?)(?::\d+)+\)?\s*$/);
|
|
227
|
+
if (!match) continue;
|
|
228
|
+
let file = match[1];
|
|
229
|
+
if (file.startsWith('file://')) { try { file = fileURLToPath(file); } catch { continue; } }
|
|
230
|
+
if (file.startsWith(own)) continue;
|
|
231
|
+
if (/node_modules[\\/]|[\\/]node_modules_|^webpack-internal:.*node_modules/.test(file)) return false;
|
|
232
|
+
if (file.startsWith('webpack-internal:')) return true;
|
|
233
|
+
const within = relative(root, file);
|
|
234
|
+
return !(within.startsWith(`..${sep}`) || within === '..' || isAbsolute(within));
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
function describePath(root, target) {
|
|
239
|
+
if (typeof target !== 'string' && !(target instanceof URL) && !Buffer.isBuffer(target)) return { path: '(descritor)' };
|
|
240
|
+
let path = target instanceof URL ? fileURLToPath(target) : String(target);
|
|
241
|
+
const within = relative(root, path);
|
|
242
|
+
path = within && !within.startsWith('..') && !isAbsolute(within) ? within : path;
|
|
243
|
+
return { path: safePath(path) };
|
|
244
|
+
}
|
|
245
|
+
const FILE_OPERATIONS = { readFile: 'leitura', writeFile: 'escrita', appendFile: 'escrita', unlink: 'remoção', rm: 'remoção',
|
|
246
|
+
rename: 'mudança de nome', copyFile: 'cópia', mkdir: 'criação de pasta', rmdir: 'remoção' };
|
|
247
|
+
function sizeOf(value) {
|
|
248
|
+
if (typeof value === 'string') return Buffer.byteLength(value);
|
|
249
|
+
if (value instanceof Uint8Array) return value.byteLength;
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
function installFiles(runtime, root) {
|
|
253
|
+
if (!root) return;
|
|
254
|
+
const start = (name, args) => {
|
|
255
|
+
if (!runtime.currentRequest() || !callerIsProject(root)) return null;
|
|
256
|
+
const bytes = name === 'writeFile' || name === 'appendFile' ? sizeOf(args[1]) : undefined;
|
|
257
|
+
return runtime.startBoundary({ kind: 'ficheiros', provider: 'sistema de ficheiros', library: 'fs', function: name,
|
|
258
|
+
operation: FILE_OPERATIONS[name], ...describePath(root, args[0]), bytes });
|
|
259
|
+
};
|
|
260
|
+
const finish = (end, name, value) => end?.(name === 'readFile' ? { bytes: sizeOf(value) } : {});
|
|
261
|
+
for (const name of Object.keys(FILE_OPERATIONS)) {
|
|
262
|
+
const promised = fs.promises[name];
|
|
263
|
+
if (typeof promised === 'function') {
|
|
264
|
+
fs.promises[name] = async function (...args) {
|
|
265
|
+
const end = start(name, args);
|
|
266
|
+
try { const value = await promised.apply(this, args); finish(end, name, value); return value; }
|
|
267
|
+
catch (error) { end?.({ error: true }); throw error; }
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const sync = fs[`${name}Sync`];
|
|
271
|
+
if (typeof sync === 'function') {
|
|
272
|
+
fs[`${name}Sync`] = function (...args) {
|
|
273
|
+
const end = start(name, args);
|
|
274
|
+
try { const value = sync.apply(this, args); finish(end, name, value); return value; }
|
|
275
|
+
catch (error) { end?.({ error: true }); throw error; }
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const callback = fs[name];
|
|
279
|
+
if (typeof callback === 'function') {
|
|
280
|
+
fs[name] = function (...args) {
|
|
281
|
+
const end = start(name, args);
|
|
282
|
+
const last = args.length - 1;
|
|
283
|
+
if (end && typeof args[last] === 'function') {
|
|
284
|
+
const done = args[last];
|
|
285
|
+
args[last] = function (error, value) { error ? end({ error: true }) : finish(end, name, value); return done.apply(this, arguments); };
|
|
286
|
+
}
|
|
287
|
+
return callback.apply(this, args);
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// ESM named imports of node:fs and node:fs/promises see the observed versions.
|
|
292
|
+
syncBuiltinESMExports();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Each incoming HTTP request becomes a unit: everything it causes carries its
|
|
296
|
+
// id, and the browser action that caused it when the page script marked it.
|
|
297
|
+
// CodeTAC's own routes (/__codetac/) never reach the application.
|
|
298
|
+
function installRequests(runtime, page) {
|
|
299
|
+
for (const prototype of [http.Server.prototype, https.Server.prototype]) {
|
|
300
|
+
const emit = prototype.emit;
|
|
301
|
+
prototype.emit = function codetacEmit(event, request, response) {
|
|
302
|
+
// The port each server listens on: the `codetac` command uses it to say
|
|
303
|
+
// where the application is.
|
|
304
|
+
if (event === 'listening') {
|
|
305
|
+
try {
|
|
306
|
+
const address = this.address();
|
|
307
|
+
if (address && typeof address === 'object') runtime.emit({ type: 'listening', port: address.port, address: address.address });
|
|
308
|
+
} catch {}
|
|
309
|
+
}
|
|
310
|
+
if (event !== 'request' || !request || !response) return emit.apply(this, arguments);
|
|
311
|
+
try {
|
|
312
|
+
if (handleOwnRoute(request, response, runtime, page)) return true;
|
|
313
|
+
if (request.headers[INTERNAL_HEADER]) return emit.apply(this, arguments);
|
|
314
|
+
if (page.inject !== false && wantsPage(request)) {
|
|
315
|
+
injectScript(request, response);
|
|
316
|
+
runtime.emit({ type: 'page', path: splitUrl(request.url).path });
|
|
317
|
+
}
|
|
318
|
+
} catch {}
|
|
319
|
+
return runtime.request(request, response, () => emit.apply(this, arguments), actionOf(request));
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function installHttpClients(runtime) {
|
|
325
|
+
const pending = new WeakMap();
|
|
326
|
+
diagnostics.subscribe('undici:request:create', ({ request }) => {
|
|
327
|
+
try {
|
|
328
|
+
const headers = headerMap(request.headers);
|
|
329
|
+
const details = classifyHttp({ method: request.method, url: `${request.origin}${request.path}`, headers, body: request.body, client: 'fetch' });
|
|
330
|
+
const entry = { end: runtime.startBoundary(details) };
|
|
331
|
+
// AI bodies are read (bounded) for usage and excerpts; nothing else is.
|
|
332
|
+
if (details.kind === 'ia') Object.assign(entry, { ai: true, sent: [], received: [], size: 0, request: aiRequestDetails(jsonBody(request.body)) });
|
|
333
|
+
pending.set(request, entry);
|
|
334
|
+
} catch {}
|
|
335
|
+
});
|
|
336
|
+
const collect = (list, entry, chunk) => {
|
|
337
|
+
if (!entry?.ai || entry.size > 1048576) return;
|
|
338
|
+
const buffer = Buffer.from(chunk);
|
|
339
|
+
entry.size += buffer.length;
|
|
340
|
+
list(entry).push(buffer);
|
|
341
|
+
};
|
|
342
|
+
diagnostics.subscribe('undici:request:bodyChunkSent', ({ request, chunk }) => collect(entry => entry.sent, pending.get(request), chunk));
|
|
343
|
+
diagnostics.subscribe('undici:request:bodyChunkReceived', ({ request, chunk }) => collect(entry => entry.received, pending.get(request), chunk));
|
|
344
|
+
const aiDetails = entry => {
|
|
345
|
+
if (!entry.ai) return {};
|
|
346
|
+
try {
|
|
347
|
+
const sent = entry.sent.length ? aiRequestDetails(JSON.parse(Buffer.concat(entry.sent).toString('utf8'))) : {};
|
|
348
|
+
const request = { ...sent, ...Object.fromEntries(Object.entries(entry.request).filter(([, value]) => value !== undefined)) };
|
|
349
|
+
return { ...request, ...aiResponseDetails(Buffer.concat(entry.received).toString('utf8')) };
|
|
350
|
+
} catch { return {}; }
|
|
351
|
+
};
|
|
352
|
+
diagnostics.subscribe('undici:request:headers', ({ request, response }) => {
|
|
353
|
+
const entry = pending.get(request);
|
|
354
|
+
if (entry) entry.status = response.statusCode;
|
|
355
|
+
});
|
|
356
|
+
diagnostics.subscribe('undici:request:trailers', ({ request }) => {
|
|
357
|
+
const entry = pending.get(request);
|
|
358
|
+
if (entry) { pending.delete(request); entry.end({ status: entry.status, ...aiDetails(entry) }); }
|
|
359
|
+
});
|
|
360
|
+
diagnostics.subscribe('undici:request:error', ({ request }) => {
|
|
361
|
+
const entry = pending.get(request);
|
|
362
|
+
if (entry) { pending.delete(request); entry.end({ status: entry.status, error: true, ...aiDetails(entry) }); }
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
diagnostics.subscribe('http.client.request.start', ({ request }) => {
|
|
366
|
+
try {
|
|
367
|
+
const headers = headerMap(request.getHeaders?.() ?? {});
|
|
368
|
+
const host = request.host ?? headers.host ?? 'localhost';
|
|
369
|
+
const protocol = request.protocol ?? 'http:';
|
|
370
|
+
pending.set(request, { end: runtime.startBoundary(classifyHttp({ method: request.method,
|
|
371
|
+
url: `${protocol}//${host}${request.path}`, headers, body: null, client: 'http' })) });
|
|
372
|
+
} catch {}
|
|
373
|
+
});
|
|
374
|
+
diagnostics.subscribe('http.client.response.finish', ({ request, response }) => {
|
|
375
|
+
const entry = pending.get(request);
|
|
376
|
+
if (entry) { pending.delete(request); entry.end({ status: response.statusCode }); }
|
|
377
|
+
});
|
|
378
|
+
diagnostics.subscribe('http.client.request.error', ({ request }) => {
|
|
379
|
+
const entry = pending.get(request);
|
|
380
|
+
if (entry) { pending.delete(request); entry.end({ error: true }); }
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function wrapSync(runtime, owner, name, describe, library) {
|
|
385
|
+
const original = owner[name];
|
|
386
|
+
if (typeof original !== 'function' || original.codetac) return;
|
|
387
|
+
const wrapped = function (...args) {
|
|
388
|
+
let end;
|
|
389
|
+
try { end = runtime.startBoundary({ kind: 'base-de-dados', library, ...describe(this, args) }); } catch {}
|
|
390
|
+
try {
|
|
391
|
+
const result = original.apply(this, args);
|
|
392
|
+
end?.(name === 'iterate' ? {} : rowsOf(result));
|
|
393
|
+
return result;
|
|
394
|
+
} catch (error) {
|
|
395
|
+
end?.({ error: true });
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
wrapped.codetac = true;
|
|
400
|
+
Object.defineProperty(wrapped, 'name', { value: original.name });
|
|
401
|
+
owner[name] = wrapped;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function patchSqliteStatement(runtime, prototype, sqlOf, library) {
|
|
405
|
+
for (const method of ['run', 'get', 'all', 'iterate']) {
|
|
406
|
+
wrapSync(runtime, prototype, method, statement => describeSql(sqlOf(statement)), library);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function installNodeSqlite(runtime) {
|
|
411
|
+
let sqlite;
|
|
412
|
+
try { sqlite = process.getBuiltinModule?.('node:sqlite'); } catch {}
|
|
413
|
+
if (!sqlite?.StatementSync) return;
|
|
414
|
+
patchSqliteStatement(runtime, sqlite.StatementSync.prototype, statement => statement.sourceSQL, 'node:sqlite');
|
|
415
|
+
wrapSync(runtime, sqlite.DatabaseSync.prototype, 'exec', (_db, [sql]) => describeSql(sql), 'node:sqlite');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
// Library points: functions inside libraries (loaded directly or found in a
|
|
420
|
+
// bundle through its source map) that are wrapped as boundaries.
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
function sqlArgument(value) {
|
|
424
|
+
return typeof value === 'string' ? value : typeof value?.sql === 'string' ? value.sql : typeof value?.text === 'string' ? value.text : undefined;
|
|
425
|
+
}
|
|
426
|
+
function settle(result, end, extract = rowsOf) {
|
|
427
|
+
if (result && typeof result.then === 'function') {
|
|
428
|
+
// The derived promise keeps the caller's view: rejections stay unhandled
|
|
429
|
+
// unless the application handles them.
|
|
430
|
+
return result.then(value => { end(extract(value)); return value; }, error => { end({ error: true }); throw error; });
|
|
431
|
+
}
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export const libraryPoints = [
|
|
436
|
+
{ file: /node_modules\/mysql2\/lib\/base\/connection\.js$/, names: ['query', 'execute'], kind: 'base-de-dados', library: 'mysql2',
|
|
437
|
+
before: args => describeSql(sqlArgument(args[0])),
|
|
438
|
+
after(command, end) {
|
|
439
|
+
if (!command || typeof command.once !== 'function') { end({}); return command; }
|
|
440
|
+
const onResult = command.onResult;
|
|
441
|
+
if (typeof onResult === 'function') {
|
|
442
|
+
command.onResult = function (error, rows, ...rest) {
|
|
443
|
+
end(error ? { error: true } : rowsOf(rows));
|
|
444
|
+
return onResult.call(this, error, rows, ...rest);
|
|
445
|
+
};
|
|
446
|
+
} else {
|
|
447
|
+
command.once('end', () => end(rowsOf(command._rows?.[0])));
|
|
448
|
+
}
|
|
449
|
+
return command;
|
|
450
|
+
} },
|
|
451
|
+
{ file: /node_modules\/pg\/lib\/client\.js$/, names: ['query'], kind: 'base-de-dados', library: 'pg', callbacks: [1, 2],
|
|
452
|
+
before: args => describeSql(sqlArgument(args[0])),
|
|
453
|
+
after: (result, end) => settle(result, end) },
|
|
454
|
+
{ file: /node_modules\/nodemailer\/lib\/mailer\/index\.js$/, names: ['sendMail'], kind: 'email', library: 'nodemailer', callbacks: [1],
|
|
455
|
+
before: ([data]) => ({ provider: 'SMTP/transporte', to: [data?.to].flat().filter(Boolean).map(String),
|
|
456
|
+
subject: typeof data?.subject === 'string' ? data.subject : undefined }),
|
|
457
|
+
after: (result, end) => settle(result, end, info => ({ accepted: info?.accepted?.length, rejected: info?.rejected?.length })) },
|
|
458
|
+
// Authentication: whether a session exists, never its content.
|
|
459
|
+
{ file: /node_modules\/next-auth\/next\/index\.js$/, names: ['getServerSession'], kind: 'autenticação', library: 'next-auth',
|
|
460
|
+
before: () => ({ provider: 'NextAuth', operation: 'verificação de sessão' }),
|
|
461
|
+
after: (result, end) => settle(result, end, session => ({ sessao: session ? 'presente' : 'ausente' })) },
|
|
462
|
+
{ file: /node_modules\/next-auth\/lib\/index\.js$/, names: ['getSession'], kind: 'autenticação', library: 'next-auth',
|
|
463
|
+
before: () => ({ provider: 'Auth.js', operation: 'verificação de sessão' }),
|
|
464
|
+
after: (result, end) => settle(result, end, response => ({ status: response?.status })) },
|
|
465
|
+
// better-sqlite3 statements are native: the first prepare() patches their prototype.
|
|
466
|
+
{ file: /node_modules\/better-sqlite3\/lib\/methods\/wrappers\.js$/, names: ['prepare', 'exec'], kind: 'base-de-dados', library: 'better-sqlite3',
|
|
467
|
+
before: (args, _self, name, runtime) => name === 'exec' ? describeSql(args[0]) : SKIP,
|
|
468
|
+
after(result, end, _self, name, runtime) {
|
|
469
|
+
if (name === 'prepare' && result && typeof result === 'object') {
|
|
470
|
+
patchSqliteStatement(runtime, Object.getPrototypeOf(result), statement => statement.source, 'better-sqlite3');
|
|
471
|
+
} else end({});
|
|
472
|
+
return result;
|
|
473
|
+
} },
|
|
474
|
+
];
|
|
475
|
+
|
|
476
|
+
export function libraryPointFor(file, name) {
|
|
477
|
+
const normalized = String(file).replaceAll('\\', '/');
|
|
478
|
+
return libraryPoints.findIndex(point => point.file.test(normalized) && point.names.includes(name));
|
|
479
|
+
}
|
|
480
|
+
export function isLibraryFile(file) {
|
|
481
|
+
const normalized = String(file).replaceAll('\\', '/');
|
|
482
|
+
return libraryPoints.some(point => point.file.test(normalized));
|
|
483
|
+
}
|