@ar-agents/mercadopago 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/cookbook/18-usa-llc-self-incorporates-ar.ts +208 -0
- package/cookbook/19-forensic-compliance-dashboard.ts +320 -0
- package/cookbook/20-multi-tenant-marketplace.ts +274 -0
- package/cookbook/21-cross-jurisdictional-ap2.ts +298 -0
- package/cookbook/22-mp-webhook-afip-reconciliation.ts +374 -0
- package/cookbook/23-astro-arg-reference-customer.ts +187 -0
- package/cookbook/24-sociedad-ia-disaster-recovery.ts +350 -0
- package/cookbook/25-sociedad-ia-quarterly-compliance.ts +545 -0
- package/cookbook/26-certify-by-fetch.ts +536 -0
- package/cookbook/27-live-conformance-monitoring.ts +260 -0
- package/cookbook/28-operator-onboarding-checklist.ts +315 -0
- package/cookbook/29-publish-your-keys.ts +193 -0
- package/cookbook/30-submit-to-registry.ts +257 -0
- package/dist/index.cjs +27 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -5
- package/dist/index.d.ts +21 -5
- package/dist/index.js +27 -14
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`8c58aa0`](https://github.com/ar-agents/ar-agents/commit/8c58aa061a7579a2854ee4239ceb698c92148f28) Thanks [@naza00000](https://github.com/naza00000)! - `MercadoPagoError` now extends `ArAgentsError` from `@ar-agents/core`.
|
|
8
|
+
|
|
9
|
+
Brings family-coherence to the flagship integration. Every MP error
|
|
10
|
+
(`MercadoPagoAuthError`, `MercadoPagoRateLimitError`,
|
|
11
|
+
`MercadoPagoOverloadedError`, `MercadoPagoTimeoutError`,
|
|
12
|
+
`MercadoPagoPaymentRejectedError`, …) now exposes the uniform
|
|
13
|
+
`{ code, retryable, context }` contract so `@ar-agents/core`'s
|
|
14
|
+
`withRetry` middleware (and any external middleware) can switch on
|
|
15
|
+
the same fields used by every other `@ar-agents/*` package.
|
|
16
|
+
|
|
17
|
+
Codes assigned:
|
|
18
|
+
|
|
19
|
+
| Subclass | code | retryable |
|
|
20
|
+
| ------------------------------------------------------------------ | ----------------- | --------------------------------- |
|
|
21
|
+
| `MercadoPagoError` (generic) | `mp_api_error` | `true` for 5xx / 429 / `status:0` |
|
|
22
|
+
| `MercadoPagoAuthError` | `mp_auth_failed` | `false` |
|
|
23
|
+
| `MercadoPagoRateLimitError` | `mp_rate_limited` | `true` |
|
|
24
|
+
| `MercadoPagoOverloadedError` | `mp_overloaded` | `true` |
|
|
25
|
+
| `MercadoPagoTimeoutError` | `mp_timeout` | `true` |
|
|
26
|
+
| all 400 subclasses (back_url / self-payment / country / authorize) | `mp_api_error` | `false` |
|
|
27
|
+
|
|
28
|
+
Backward compatible: all existing public properties (`status`,
|
|
29
|
+
`endpoint`, `mpResponse`, `retryAfterSeconds`, `preapprovalId`, …) are
|
|
30
|
+
preserved on the instance AND mirrored into `context` for new code
|
|
31
|
+
that reads the `ArAgentsError` contract. `instanceof MercadoPagoError`
|
|
32
|
+
keeps working; `isArAgentsError(e)` now additionally returns `true`.
|
|
33
|
+
|
|
34
|
+
All 328 existing mercadopago tests pass with no changes.
|
|
35
|
+
|
|
3
36
|
## 0.17.2
|
|
4
37
|
|
|
5
38
|
### Patch Changes
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe 18 — USA-LLC agent self-incorporates an AR sociedad-IA.
|
|
3
|
+
*
|
|
4
|
+
* The end-to-end pattern that the headline of `/sociedades-ia` makes a
|
|
5
|
+
* concrete claim about: a USA-incorporated agent (ClawBank-formed
|
|
6
|
+
* Wyoming/Ohio LLC, doola Agentic LLC, Marshall Islands MIDAO) needs an
|
|
7
|
+
* AR-resident operating layer for some of its activity. Until now this
|
|
8
|
+
* was a manual escribano + contador job per spawn. Recipe 18 collapses
|
|
9
|
+
* the manual layer to one programmatic call:
|
|
10
|
+
*
|
|
11
|
+
* POST https://ar-agents.ar/api/auto-incorporate
|
|
12
|
+
*
|
|
13
|
+
* via the `@ar-agents/incorporate` client. The output is everything the
|
|
14
|
+
* USA-LLC agent's deploy pipeline needs to spin up the AR side:
|
|
15
|
+
*
|
|
16
|
+
* - package.json + lib/agent.ts + .env.example + README.md
|
|
17
|
+
* - Vercel one-click deploy URL pointing at apps/sociedad-ia-starter
|
|
18
|
+
* - Env-var manifest the agent must source from its secret store
|
|
19
|
+
* - Legal + operational checklist (ARCA cert, MP token, Meta verify,
|
|
20
|
+
* IGJ inscription via TAD)
|
|
21
|
+
* - Signed audit-log reference — the entire incorporation request is
|
|
22
|
+
* a forensic event under RFC-001 § 9.2, queryable later
|
|
23
|
+
*
|
|
24
|
+
* # Why this recipe matters
|
|
25
|
+
*
|
|
26
|
+
* RFC-001 § 7 sketches "cross-jurisdictional agent commerce". The USA
|
|
27
|
+
* side has its own corporate-form vehicles for AI-only entities (Wyoming
|
|
28
|
+
* DAO LLC, Marshall Islands MIDAO). What it doesn't have natively is a
|
|
29
|
+
* way to operate inside the AR jurisdiction — emit factura electrónica,
|
|
30
|
+
* receive Mercado Pago, monitor Boletín Oficial, pay monotributo. Recipe
|
|
31
|
+
* 18 is how that side fills.
|
|
32
|
+
*
|
|
33
|
+
* # The one-line pitch
|
|
34
|
+
*
|
|
35
|
+
* `pnpm add @ar-agents/incorporate` → `await incorporate({...})` →
|
|
36
|
+
* AR sociedad-IA spec ready in 100ms (then the human-pending bits:
|
|
37
|
+
* ARCA cert wait, IGJ inscription wait, Meta verification wait — all
|
|
38
|
+
* upstream timers we don't control).
|
|
39
|
+
*
|
|
40
|
+
* # Edge Runtime
|
|
41
|
+
*
|
|
42
|
+
* The client is fetch-only, zero dependencies. Runs on Vercel Edge,
|
|
43
|
+
* Cloudflare Workers, Deno, browsers. No `node:*` imports.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { incorporate, fetchAudit, type IncorporateInput } from "@ar-agents/incorporate";
|
|
47
|
+
|
|
48
|
+
// ─── 1. The USA-LLC agent gathers the AR sociedad's parameters ─────────────
|
|
49
|
+
//
|
|
50
|
+
// Often these come from the human deal-maker once (corporate name + objeto)
|
|
51
|
+
// and the rest is mechanical. The `sessionId` is what ties this incorporation
|
|
52
|
+
// request to subsequent operational events under one forensic timeline.
|
|
53
|
+
|
|
54
|
+
const sessionId = crypto.randomUUID();
|
|
55
|
+
|
|
56
|
+
const input: IncorporateInput = {
|
|
57
|
+
// Public corporate name. IGJ rejects reserved words (Nacional / Estatal /
|
|
58
|
+
// Gobierno / Estado / Oficial). The pre-flight at the server side will
|
|
59
|
+
// catch them; surface the validation findings to the human if any.
|
|
60
|
+
denominacion: "ClawBank-AR Operations SAS",
|
|
61
|
+
|
|
62
|
+
// SOCIEDAD-IA is the eventual target — but until the AR regime is
|
|
63
|
+
// sancionado (estimated H1 2027), use SAS so the operator runs under the
|
|
64
|
+
// RFC-001 § 3.1 three-layer liability framework with a human representante.
|
|
65
|
+
tipo: "SAS",
|
|
66
|
+
|
|
67
|
+
// Capital social in ARS. SAS minimum is 100k. Set higher if the operator
|
|
68
|
+
// expects to handle larger incoming flows (banks may scrutinize disparities
|
|
69
|
+
// between capital and transaction volume).
|
|
70
|
+
capitalSocial: 200_000,
|
|
71
|
+
|
|
72
|
+
// Objeto social. IGJ rejects generic phrasing; be specific. 20-2000 chars.
|
|
73
|
+
objeto:
|
|
74
|
+
"Operación de servicios digitales y desarrollo de software propio para clientes argentinos, en representación de la entidad madre USA-incorporada bajo el marco RFC-001 § 7 (cross-jurisdictional agent commerce).",
|
|
75
|
+
|
|
76
|
+
// Human representante for the AR-side legal facade per RFC-001 § 3.1. In
|
|
77
|
+
// production the orchestrator pulls this from its operator-of-record store
|
|
78
|
+
// (e.g., the escribano contracted as the AR-presence layer).
|
|
79
|
+
representante: {
|
|
80
|
+
nombre: "Pérez, Juan",
|
|
81
|
+
cuit: "20-12345678-9",
|
|
82
|
+
},
|
|
83
|
+
emailContacto: "ops+ar@usa-llc.example",
|
|
84
|
+
|
|
85
|
+
// Pieza selection. The auto-incorporate endpoint always merges in the
|
|
86
|
+
// required set (identity, gde-tad, mercadopago, banking, facturacion).
|
|
87
|
+
// Add what this particular operator needs:
|
|
88
|
+
piezas: [
|
|
89
|
+
"identity",
|
|
90
|
+
"gde-tad",
|
|
91
|
+
"mercadopago",
|
|
92
|
+
"banking",
|
|
93
|
+
"facturacion",
|
|
94
|
+
"boletin-oficial",
|
|
95
|
+
"igj",
|
|
96
|
+
"whatsapp",
|
|
97
|
+
"ap2", // for AP2 mandate verification on incoming agent commerce
|
|
98
|
+
"agentic-commerce-bridge", // to expose ACP-compliant checkout to ChatGPT/Claude buyers
|
|
99
|
+
],
|
|
100
|
+
|
|
101
|
+
// The session id chains every event under one forensic timeline.
|
|
102
|
+
sessionId,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// ─── 2. Call the endpoint ──────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
async function main() {
|
|
108
|
+
const result = await incorporate(input);
|
|
109
|
+
|
|
110
|
+
if (!result.ok) {
|
|
111
|
+
// Validation failure (HTTP 422). The server caught a structural issue
|
|
112
|
+
// (reserved word, capital below minimum, malformed CUIT, etc). The agent
|
|
113
|
+
// should surface findings to the human and retry with a fix.
|
|
114
|
+
console.error("Incorporation rejected at pre-flight:");
|
|
115
|
+
for (const f of result.validation.findings) {
|
|
116
|
+
console.error(` [${f.severity}] ${f.field}: ${f.message}`);
|
|
117
|
+
}
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── 3. Materialize the four generated files into the deploy repo ─────
|
|
122
|
+
//
|
|
123
|
+
// The output is plain strings — the USA-LLC's deploy pipeline writes them
|
|
124
|
+
// to a fresh GitHub repo, then triggers the Vercel deploy. Below we just
|
|
125
|
+
// print to stdout for the recipe's purposes.
|
|
126
|
+
|
|
127
|
+
console.log("Sociedad:", result.sociedad);
|
|
128
|
+
console.log();
|
|
129
|
+
console.log("Generated files:");
|
|
130
|
+
for (const path of Object.keys(result.config) as Array<
|
|
131
|
+
keyof typeof result.config
|
|
132
|
+
>) {
|
|
133
|
+
console.log(`\n--- ${path} (${result.config[path].length} chars) ---`);
|
|
134
|
+
console.log(result.config[path].slice(0, 200) + "…");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── 4. Surface the deploy URL to the human (or auto-deploy) ──────────
|
|
138
|
+
//
|
|
139
|
+
// The Vercel one-click clone URL is pre-filled with the right env-var
|
|
140
|
+
// slots. In production: pipe to the orchestrator's deploy runner. For
|
|
141
|
+
// a manual flow: print the URL and let the human click.
|
|
142
|
+
|
|
143
|
+
console.log("\nDeploy:", result.deploy.oneClickUrl);
|
|
144
|
+
|
|
145
|
+
// ─── 5. Hand off the operational checklist to the human ───────────────
|
|
146
|
+
//
|
|
147
|
+
// These are the human-pending steps: ARCA cert wait, MP creds, Meta
|
|
148
|
+
// business verify, IGJ inscription via TAD. The agent can't shortcut
|
|
149
|
+
// them — they're upstream gov/private-co timers — but it can emit them
|
|
150
|
+
// all at once so nothing falls through the cracks.
|
|
151
|
+
|
|
152
|
+
console.log("\nChecklist (human-pending):");
|
|
153
|
+
for (const [i, step] of result.checklist.entries()) {
|
|
154
|
+
console.log(` ${i + 1}. ${step}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── 6. Log + verify the forensic event ───────────────────────────────
|
|
158
|
+
//
|
|
159
|
+
// The incorporation request is itself a tool call recorded in the audit
|
|
160
|
+
// log. Anyone (regulator, journalist, downstream agent) can later hit
|
|
161
|
+
// /api/play/audit/{sessionId}?verify=1
|
|
162
|
+
// and confirm the entry is HMAC-clean. This is what makes RFC-001 § 9.2's
|
|
163
|
+
// claim that the log is "legally probative" mechanically true.
|
|
164
|
+
|
|
165
|
+
console.log("\nAudit:");
|
|
166
|
+
console.log(" sessionId:", result.audit.sessionId);
|
|
167
|
+
console.log(" backend:", result.audit.backend);
|
|
168
|
+
console.log(" hmac:", result.audit.entry.hmac?.slice(0, 30) + "…");
|
|
169
|
+
console.log(" dashboard:", result.audit.dashboardUrl);
|
|
170
|
+
|
|
171
|
+
// Optional: re-verify the entry the agent just wrote. Useful if the orchestrator
|
|
172
|
+
// wants to assert tamper-free state before proceeding to step 7+.
|
|
173
|
+
const audit = (await fetchAudit(sessionId, { verify: true })) as {
|
|
174
|
+
verification?: { tampered: number; verified: number; total: number };
|
|
175
|
+
};
|
|
176
|
+
if (audit.verification && audit.verification.tampered > 0) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Audit log shows ${audit.verification.tampered} tampered entries — abort`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
console.log("\nVerified clean:", audit.verification);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
main().catch((err) => {
|
|
185
|
+
console.error("Recipe 18 failed:", err);
|
|
186
|
+
process.exit(1);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// ─── 7. What happens after ─────────────────────────────────────────────────
|
|
190
|
+
//
|
|
191
|
+
// Once the AR side is provisioned, the USA-LLC agent and the AR sociedad-IA
|
|
192
|
+
// can transact under a single trust contract:
|
|
193
|
+
//
|
|
194
|
+
// - The USA agent calls the AR sociedad's /api/agent endpoint with mandate
|
|
195
|
+
// proof (AP2 § 4 — see @ar-agents/ap2 cookbook recipe 02 — multi-hop).
|
|
196
|
+
// - The AR sociedad executes within its jurisdiction (factura emission,
|
|
197
|
+
// MP cobro, BCRA credit checks, etc).
|
|
198
|
+
// - Every tool call lands in the same audit log (chain via the same
|
|
199
|
+
// sessionId).
|
|
200
|
+
// - At end-of-month, the USA-LLC's accountant + the AR sociedad's contador
|
|
201
|
+
// settle the inter-entity ledger off the audit log's signed events.
|
|
202
|
+
//
|
|
203
|
+
// This is the reference implementation of cross-jurisdictional agent
|
|
204
|
+
// commerce. The whole stack — from the npm package this recipe imports to
|
|
205
|
+
// the audit log it leaves behind — is MIT-licensed and SLSA-provenanced.
|
|
206
|
+
//
|
|
207
|
+
// `agent self-incorporates → operates → settles` in three contractual hops,
|
|
208
|
+
// no escribono in the loop after step 1.
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe 19 — Forensic-grade compliance dashboard powered by the
|
|
3
|
+
* `/api/play/audit/{sessionId}` endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Every operator running an Argentine sociedad-IA accumulates an
|
|
6
|
+
* append-only HMAC-signed audit log under a sessionId. RFC-001 § 9.2
|
|
7
|
+
* makes that log legally probative — but "legally probative" only
|
|
8
|
+
* matters if a regulator can actually inspect it. This recipe is the
|
|
9
|
+
* compliance-side companion: a Node.js process that ingests audit
|
|
10
|
+
* entries on a schedule, checks for tampering, and routes alerts to
|
|
11
|
+
* the operator's SOC + the contador's monthly summary.
|
|
12
|
+
*
|
|
13
|
+
* # Pattern
|
|
14
|
+
*
|
|
15
|
+
* 1. Pull the latest audit entries via `fetchAudit(sessionId, { verify: true })`
|
|
16
|
+
* from `@ar-agents/incorporate` — same primitives the incorporation flow uses.
|
|
17
|
+
* 2. Reconcile the verified count + tampered count + entry count against
|
|
18
|
+
* expected ranges. Tampering immediately escalates.
|
|
19
|
+
* 3. Bucket entries by tool, governance class, and durationMs to surface
|
|
20
|
+
* operational anomalies (e.g., a `crear_factura` tool started running
|
|
21
|
+
* in 12s instead of <1s — that's an AFIP slowdown worth noting).
|
|
22
|
+
* 4. Stream a daily digest to the contador (Slack, email, WhatsApp)
|
|
23
|
+
* summarizing volume + categories + any anomalies.
|
|
24
|
+
*
|
|
25
|
+
* # When to use
|
|
26
|
+
*
|
|
27
|
+
* - Multi-tenant marketplace operating many sociedades-IA, one audit log
|
|
28
|
+
* per tenant. Daily compliance roll-up scales linearly.
|
|
29
|
+
* - Regulated SaaS where the audit log is contractually required to be
|
|
30
|
+
* monitored, not just retained.
|
|
31
|
+
* - Periodic third-party audit cycles where the auditor wants a
|
|
32
|
+
* reproducible forensic report (the JSON returned by `?verify=1` is
|
|
33
|
+
* already that report).
|
|
34
|
+
*
|
|
35
|
+
* # Edge Runtime
|
|
36
|
+
*
|
|
37
|
+
* Yes. The client is fetch-based, zero deps. Schedule via Vercel Cron
|
|
38
|
+
* (`vercel.json → crons`) or Cloudflare Workers Cron Triggers — either
|
|
39
|
+
* works.
|
|
40
|
+
*
|
|
41
|
+
* # Production-only assertions
|
|
42
|
+
*
|
|
43
|
+
* The audit log is HMAC-signed with `AUDIT_HMAC_SECRET` server-side.
|
|
44
|
+
* The verifier here delegates to `?verify=1` (so the secret never
|
|
45
|
+
* leaves the server) but the consumer can independently re-verify by
|
|
46
|
+
* re-implementing the canonical-JSON + HMAC check. The agent endpoint
|
|
47
|
+
* uses constant-time comparison; the read endpoint exposes per-entry
|
|
48
|
+
* hmac so external libraries can recompute.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { fetchAudit } from "@ar-agents/incorporate";
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// Types
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
interface AuditEntry {
|
|
58
|
+
id: string;
|
|
59
|
+
sessionId: string;
|
|
60
|
+
ts: string; // ISO 8601
|
|
61
|
+
tool: string;
|
|
62
|
+
governance:
|
|
63
|
+
| "algorithm-only"
|
|
64
|
+
| "audit-logged"
|
|
65
|
+
| "mocked-upstream"
|
|
66
|
+
| "requires-confirmation";
|
|
67
|
+
input: unknown;
|
|
68
|
+
output?: unknown;
|
|
69
|
+
errored?: boolean;
|
|
70
|
+
durationMs?: number;
|
|
71
|
+
hmac: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface AuditEnvelope {
|
|
75
|
+
sessionId: string;
|
|
76
|
+
backend: "vercel-kv" | "in-memory";
|
|
77
|
+
count: number;
|
|
78
|
+
entries: AuditEntry[];
|
|
79
|
+
verification?: {
|
|
80
|
+
total: number;
|
|
81
|
+
verified: number;
|
|
82
|
+
tampered: number;
|
|
83
|
+
hmacWired: boolean;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface DailyDigest {
|
|
88
|
+
sessionId: string;
|
|
89
|
+
generatedAt: string;
|
|
90
|
+
rangeStart: string;
|
|
91
|
+
rangeEnd: string;
|
|
92
|
+
totals: { all: number; errored: number; byGovernance: Record<string, number> };
|
|
93
|
+
byTool: Record<string, { count: number; avgDurationMs: number; errors: number }>;
|
|
94
|
+
anomalies: string[];
|
|
95
|
+
/** Highest-priority issue. If null, the day is clean. */
|
|
96
|
+
alert: { severity: "tampered" | "performance" | "errors"; message: string } | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
// Core: pull + verify + bucket
|
|
101
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
const PERFORMANCE_THRESHOLDS_MS: Record<string, number> = {
|
|
104
|
+
// Per-tool latency expectations. If an entry's durationMs exceeds 4× this,
|
|
105
|
+
// the operator gets pinged. Numbers are heuristic — replace with your
|
|
106
|
+
// observed p95 from /api/play/audit/* over the last 30 days.
|
|
107
|
+
validate_cuit: 50,
|
|
108
|
+
validate_cbu: 50,
|
|
109
|
+
validate_solicitar_cae: 50,
|
|
110
|
+
validate_igj_inscription: 100,
|
|
111
|
+
lookup_cuit_afip: 1500,
|
|
112
|
+
lookup_credit_situation: 1200,
|
|
113
|
+
get_usd_oficial: 800,
|
|
114
|
+
bo_today: 2000,
|
|
115
|
+
igj_get_entity: 1200,
|
|
116
|
+
list_domicilio_inbox: 1500,
|
|
117
|
+
crear_factura: 3000,
|
|
118
|
+
send_whatsapp_text: 800,
|
|
119
|
+
mp_create_subscription: 1500,
|
|
120
|
+
auto_incorporate: 200,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export async function buildDailyDigest(
|
|
124
|
+
sessionId: string,
|
|
125
|
+
options: { rangeStart?: Date; rangeEnd?: Date; baseUrl?: string } = {},
|
|
126
|
+
): Promise<DailyDigest> {
|
|
127
|
+
const rangeEnd = options.rangeEnd ?? new Date();
|
|
128
|
+
const rangeStart = options.rangeStart ?? new Date(rangeEnd.getTime() - 86_400_000);
|
|
129
|
+
|
|
130
|
+
const raw = (await fetchAudit(sessionId, {
|
|
131
|
+
verify: true,
|
|
132
|
+
baseUrl: options.baseUrl,
|
|
133
|
+
})) as AuditEnvelope;
|
|
134
|
+
|
|
135
|
+
// Bucket entries to the requested range (24h default).
|
|
136
|
+
const inRange = raw.entries.filter((e) => {
|
|
137
|
+
const t = Date.parse(e.ts);
|
|
138
|
+
return t >= rangeStart.getTime() && t < rangeEnd.getTime();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const totals = {
|
|
142
|
+
all: inRange.length,
|
|
143
|
+
errored: inRange.filter((e) => e.errored).length,
|
|
144
|
+
byGovernance: groupCount(inRange, (e) => e.governance),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const byTool: Record<
|
|
148
|
+
string,
|
|
149
|
+
{ count: number; avgDurationMs: number; errors: number }
|
|
150
|
+
> = {};
|
|
151
|
+
for (const e of inRange) {
|
|
152
|
+
const slot = (byTool[e.tool] ??= { count: 0, avgDurationMs: 0, errors: 0 });
|
|
153
|
+
slot.count++;
|
|
154
|
+
if (e.errored) slot.errors++;
|
|
155
|
+
if (typeof e.durationMs === "number") {
|
|
156
|
+
// running mean
|
|
157
|
+
slot.avgDurationMs =
|
|
158
|
+
(slot.avgDurationMs * (slot.count - 1) + e.durationMs) / slot.count;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const anomalies: string[] = [];
|
|
163
|
+
|
|
164
|
+
// 1. Tampering — highest-severity escalation.
|
|
165
|
+
const tampered = raw.verification?.tampered ?? 0;
|
|
166
|
+
const hmacWired = raw.verification?.hmacWired ?? false;
|
|
167
|
+
if (!hmacWired) {
|
|
168
|
+
anomalies.push(
|
|
169
|
+
"AUDIT_HMAC_SECRET no está cableado en el deploy — el log no está firmado.",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
if (tampered > 0) {
|
|
173
|
+
anomalies.push(
|
|
174
|
+
`${tampered} entrada${tampered === 1 ? "" : "s"} con tampering detectado en la sesión completa.`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 2. Performance — flag tools whose avg latency is 4× threshold.
|
|
179
|
+
for (const [tool, slot] of Object.entries(byTool)) {
|
|
180
|
+
const threshold = PERFORMANCE_THRESHOLDS_MS[tool];
|
|
181
|
+
if (threshold && slot.avgDurationMs > threshold * 4 && slot.count > 1) {
|
|
182
|
+
anomalies.push(
|
|
183
|
+
`${tool}: avg ${Math.round(slot.avgDurationMs)}ms (esperado <${threshold * 4}ms) en ${slot.count} llamadas.`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 3. Error rate — flag tools above 5% error rate over 10+ calls.
|
|
189
|
+
for (const [tool, slot] of Object.entries(byTool)) {
|
|
190
|
+
if (slot.count >= 10 && slot.errors / slot.count > 0.05) {
|
|
191
|
+
anomalies.push(
|
|
192
|
+
`${tool}: ${slot.errors}/${slot.count} (${Math.round((slot.errors / slot.count) * 100)}%) errores.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Pick the highest-severity alert.
|
|
198
|
+
let alert: DailyDigest["alert"] = null;
|
|
199
|
+
if (tampered > 0) {
|
|
200
|
+
alert = {
|
|
201
|
+
severity: "tampered",
|
|
202
|
+
message: `URGENTE: ${tampered} entrada(s) con tampering detectado en la sesión ${sessionId}. Investigar acceso al audit log.`,
|
|
203
|
+
};
|
|
204
|
+
} else if (totals.errored / Math.max(totals.all, 1) > 0.1 && totals.all > 10) {
|
|
205
|
+
alert = {
|
|
206
|
+
severity: "errors",
|
|
207
|
+
message: `Tasa de error del día (${totals.errored}/${totals.all}) supera el 10%.`,
|
|
208
|
+
};
|
|
209
|
+
} else if (anomalies.some((a) => a.includes("avg"))) {
|
|
210
|
+
alert = {
|
|
211
|
+
severity: "performance",
|
|
212
|
+
message: anomalies.find((a) => a.includes("avg")) ?? "Performance anomaly",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
sessionId,
|
|
218
|
+
generatedAt: new Date().toISOString(),
|
|
219
|
+
rangeStart: rangeStart.toISOString(),
|
|
220
|
+
rangeEnd: rangeEnd.toISOString(),
|
|
221
|
+
totals,
|
|
222
|
+
byTool,
|
|
223
|
+
anomalies,
|
|
224
|
+
alert,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function groupCount<T>(arr: T[], key: (v: T) => string): Record<string, number> {
|
|
229
|
+
const out: Record<string, number> = {};
|
|
230
|
+
for (const v of arr) {
|
|
231
|
+
const k = key(v);
|
|
232
|
+
out[k] = (out[k] ?? 0) + 1;
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
// Sink: contador's monthly summary
|
|
239
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Render a digest as a readable Spanish-language summary the contador
|
|
243
|
+
* can paste into a monthly compliance report. Structurally similar to
|
|
244
|
+
* a balance summary — totals + observations.
|
|
245
|
+
*/
|
|
246
|
+
export function renderForContador(digest: DailyDigest): string {
|
|
247
|
+
const date = digest.rangeEnd.slice(0, 10);
|
|
248
|
+
const lines: string[] = [
|
|
249
|
+
`RESUMEN AUDITORÍA · ${date}`,
|
|
250
|
+
`Sesión: ${digest.sessionId}`,
|
|
251
|
+
"",
|
|
252
|
+
"TOTALES",
|
|
253
|
+
` Tool calls: ${digest.totals.all}`,
|
|
254
|
+
` Errores: ${digest.totals.errored}`,
|
|
255
|
+
"",
|
|
256
|
+
"POR CLASE DE GOVERNANCE",
|
|
257
|
+
];
|
|
258
|
+
for (const [k, v] of Object.entries(digest.totals.byGovernance)) {
|
|
259
|
+
lines.push(` ${k}: ${v}`);
|
|
260
|
+
}
|
|
261
|
+
lines.push("", "POR TOOL");
|
|
262
|
+
const sorted = Object.entries(digest.byTool).sort(
|
|
263
|
+
([, a], [, b]) => b.count - a.count,
|
|
264
|
+
);
|
|
265
|
+
for (const [tool, slot] of sorted) {
|
|
266
|
+
lines.push(
|
|
267
|
+
` ${tool}: ${slot.count} llamadas (avg ${Math.round(slot.avgDurationMs)}ms${slot.errors ? `, ${slot.errors} errores` : ""})`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (digest.anomalies.length > 0) {
|
|
271
|
+
lines.push("", "ANOMALÍAS");
|
|
272
|
+
for (const a of digest.anomalies) lines.push(` - ${a}`);
|
|
273
|
+
}
|
|
274
|
+
if (digest.alert) {
|
|
275
|
+
lines.push(
|
|
276
|
+
"",
|
|
277
|
+
`ALERTA [${digest.alert.severity.toUpperCase()}]`,
|
|
278
|
+
` ${digest.alert.message}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return lines.join("\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
285
|
+
// Cron entrypoint (drop in /api/cron/compliance-digest in your Next.js app)
|
|
286
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
async function main(sessionId: string) {
|
|
289
|
+
const digest = await buildDailyDigest(sessionId);
|
|
290
|
+
|
|
291
|
+
// 1. Always log the structured digest for the operator's metrics pipeline.
|
|
292
|
+
console.log(JSON.stringify(digest));
|
|
293
|
+
|
|
294
|
+
// 2. Render a contador-friendly summary for the monthly compliance report.
|
|
295
|
+
console.log(renderForContador(digest));
|
|
296
|
+
|
|
297
|
+
// 3. Escalate on tampering or high error rate. In production, replace
|
|
298
|
+
// these console.warn calls with WhatsApp template / email / PagerDuty.
|
|
299
|
+
if (digest.alert) {
|
|
300
|
+
console.warn(`ESCALATE [${digest.alert.severity}]: ${digest.alert.message}`);
|
|
301
|
+
// await sendWhatsAppTemplate({ to: process.env.SOC_WHATSAPP, template: "audit_alert", ... });
|
|
302
|
+
// await sendEmail({ to: process.env.CONTADOR_EMAIL, subject: "...", body: ... });
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return digest;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (typeof require !== "undefined" && require.main === module) {
|
|
309
|
+
const sid = process.argv[2];
|
|
310
|
+
if (!sid) {
|
|
311
|
+
console.error("usage: pnpm tsx 19-forensic-compliance-dashboard.ts <sessionId>");
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
main(sid).catch((err) => {
|
|
315
|
+
console.error(err);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export { main };
|