@ar-agents/mercadopago 0.17.1 → 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 +44 -0
- package/cookbook/16-acp-checkout-with-factura.ts +168 -0
- package/cookbook/17-usa-llc-companion.ts +117 -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/cookbook/README.md +2 -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/tools.manifest.json +1 -1
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe 25 — Sociedad-IA quarterly compliance report.
|
|
3
|
+
*
|
|
4
|
+
* # Pattern
|
|
5
|
+
*
|
|
6
|
+
* Every quarter, a sociedad-IA must produce — for AFIP/ARCA, for AAIP
|
|
7
|
+
* (data protection), and for whoever else asks — a self-audit summary
|
|
8
|
+
* covering the last 90 days of operational activity. Recipe 25 is the
|
|
9
|
+
* pure function that generates that report from the audit-log alone.
|
|
10
|
+
*
|
|
11
|
+
* Input: a list of sessionIds active in the quarter (e.g. one per
|
|
12
|
+
* customer interaction, or one per business day, depending on session
|
|
13
|
+
* granularity policy).
|
|
14
|
+
*
|
|
15
|
+
* Output: a single JSON document with:
|
|
16
|
+
*
|
|
17
|
+
* - Header (sociedad metadata, period, generation time, schema URL)
|
|
18
|
+
* - Per-session: full entry timeline + verification result
|
|
19
|
+
* - Aggregates: total entries, total verified, total tampered, total
|
|
20
|
+
* errored, governance breakdown (algorithm-only / audit-logged /
|
|
21
|
+
* mocked-upstream / requires-confirmation counts), p50/p95/p99
|
|
22
|
+
* duration per tool
|
|
23
|
+
* - Anomalies: late timestamps (clock skew > 5 min), governance
|
|
24
|
+
* class shifts mid-session (unusual), errored entries with
|
|
25
|
+
* governance "audit-logged" (LLM call failed; needed remediation)
|
|
26
|
+
* - Self-disclosure: any session with tampered entries is flagged
|
|
27
|
+
* and the report's HMAC over its own JSON makes the disclosure
|
|
28
|
+
* itself tamper-evident
|
|
29
|
+
*
|
|
30
|
+
* The report is shaped to be the answer to a single regulator question:
|
|
31
|
+
* "I want a complete picture of what your sociedad-IA did last quarter,
|
|
32
|
+
* verifiable end-to-end, with no expectation of me trusting your
|
|
33
|
+
* recollection." Hand them this JSON + the underlying live verify URLs;
|
|
34
|
+
* everything they need to forensically reconstruct is in the document.
|
|
35
|
+
*
|
|
36
|
+
* # Companion to RFC-004 § 9
|
|
37
|
+
*
|
|
38
|
+
* RFC-004 § 9 lists the four artifacts a regulator can demand without a
|
|
39
|
+
* court order: session inventory, full export, verification proof,
|
|
40
|
+
* operational narrative. Recipe 25 is the operational narrative — and
|
|
41
|
+
* it bundles in the session inventory + the verification proof, so the
|
|
42
|
+
* regulator gets one self-contained document instead of N HTTP fetches.
|
|
43
|
+
*
|
|
44
|
+
* # When to use
|
|
45
|
+
*
|
|
46
|
+
* - Quarterly self-audit cycle (calendar quarters Q1/Q2/Q3/Q4).
|
|
47
|
+
* - Regulator requests an ad-hoc window (parameterize start/end).
|
|
48
|
+
* - Customer requests a "what did your sociedad do for me?" extract
|
|
49
|
+
* (filter sessionIds to that customer's; reuse the same function).
|
|
50
|
+
* - Internal SOC/ops review (anomalies block on this report).
|
|
51
|
+
*
|
|
52
|
+
* # Edge Runtime
|
|
53
|
+
*
|
|
54
|
+
* Pure data shaping over the fetched audit; runs anywhere Node 18+ or
|
|
55
|
+
* Edge fetch is available. No filesystem access. Stateless.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import { fetchAudit } from "@ar-agents/incorporate";
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
+
// Types
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/** Shape of a single audit entry per RFC-004 § 2. */
|
|
65
|
+
interface AuditEntry {
|
|
66
|
+
id: string;
|
|
67
|
+
sessionId: string;
|
|
68
|
+
ts: string;
|
|
69
|
+
tool: string;
|
|
70
|
+
governance:
|
|
71
|
+
| "algorithm-only"
|
|
72
|
+
| "audit-logged"
|
|
73
|
+
| "mocked-upstream"
|
|
74
|
+
| "requires-confirmation";
|
|
75
|
+
input: unknown;
|
|
76
|
+
output?: unknown;
|
|
77
|
+
errored?: boolean;
|
|
78
|
+
durationMs?: number;
|
|
79
|
+
hmac: string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface AuditPayload {
|
|
83
|
+
sessionId: string;
|
|
84
|
+
entries: AuditEntry[];
|
|
85
|
+
total?: number;
|
|
86
|
+
verified?: number;
|
|
87
|
+
tampered?: number;
|
|
88
|
+
hmacWired?: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Sociedad-IA self-disclosure metadata. */
|
|
92
|
+
interface SociedadMetadata {
|
|
93
|
+
denominacion: string;
|
|
94
|
+
operatorCuit: string;
|
|
95
|
+
jurisdiction: "AR";
|
|
96
|
+
rfcConformance: string[]; // e.g. ["rfc-001-v1", "rfc-004-draft"]
|
|
97
|
+
auditBaseUrl: string; // for re-fetch by the regulator
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface ReportInput {
|
|
101
|
+
sociedad: SociedadMetadata;
|
|
102
|
+
periodStart: string; // ISO-8601 UTC
|
|
103
|
+
periodEnd: string; // ISO-8601 UTC
|
|
104
|
+
sessionIds: string[]; // sessions active in the period
|
|
105
|
+
baseUrl?: string; // /arg deployment, default ar-agents.ar
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface SessionSummary {
|
|
109
|
+
sessionId: string;
|
|
110
|
+
entriesCount: number;
|
|
111
|
+
verified: number;
|
|
112
|
+
tampered: number;
|
|
113
|
+
errored: number;
|
|
114
|
+
durationMs: { p50: number; p95: number; p99: number } | null;
|
|
115
|
+
governanceBreakdown: Record<AuditEntry["governance"], number>;
|
|
116
|
+
firstTs: string | null;
|
|
117
|
+
lastTs: string | null;
|
|
118
|
+
toolUsage: Record<string, number>;
|
|
119
|
+
anomalies: Anomaly[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
type Anomaly =
|
|
123
|
+
| {
|
|
124
|
+
kind: "clock-skew";
|
|
125
|
+
entryId: string;
|
|
126
|
+
previousTs: string;
|
|
127
|
+
currentTs: string;
|
|
128
|
+
skewMs: number;
|
|
129
|
+
}
|
|
130
|
+
| {
|
|
131
|
+
kind: "governance-shift";
|
|
132
|
+
entryIdA: string;
|
|
133
|
+
governanceA: AuditEntry["governance"];
|
|
134
|
+
entryIdB: string;
|
|
135
|
+
governanceB: AuditEntry["governance"];
|
|
136
|
+
}
|
|
137
|
+
| {
|
|
138
|
+
kind: "llm-error-without-fallback";
|
|
139
|
+
entryId: string;
|
|
140
|
+
tool: string;
|
|
141
|
+
}
|
|
142
|
+
| {
|
|
143
|
+
kind: "tampered-entry";
|
|
144
|
+
entryId: string;
|
|
145
|
+
}
|
|
146
|
+
| {
|
|
147
|
+
kind: "missing-hmac-in-production";
|
|
148
|
+
entryId: string;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
interface QuarterlyReport {
|
|
152
|
+
$schema: string;
|
|
153
|
+
generatedAt: string;
|
|
154
|
+
schemaVersion: "1.0";
|
|
155
|
+
sociedad: SociedadMetadata;
|
|
156
|
+
period: { start: string; end: string };
|
|
157
|
+
aggregates: {
|
|
158
|
+
sessionsCount: number;
|
|
159
|
+
entriesCount: number;
|
|
160
|
+
verified: number;
|
|
161
|
+
tampered: number;
|
|
162
|
+
errored: number;
|
|
163
|
+
governanceBreakdown: Record<AuditEntry["governance"], number>;
|
|
164
|
+
toolUsage: Record<string, number>;
|
|
165
|
+
durationMsByTool: Record<string, { p50: number; p95: number; p99: number }>;
|
|
166
|
+
};
|
|
167
|
+
sessions: SessionSummary[];
|
|
168
|
+
anomalies: Anomaly[];
|
|
169
|
+
conclusion: ReportConclusion;
|
|
170
|
+
reportHmac: string | null; // optional: HMAC over canonical(report-minus-this-field)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface ReportConclusion {
|
|
174
|
+
/** "clean" | "anomalies-noted" | "tampering-detected" */
|
|
175
|
+
status: "clean" | "anomalies-noted" | "tampering-detected";
|
|
176
|
+
/** Human-readable summary. Short. Regulator opens this first. */
|
|
177
|
+
summary: string;
|
|
178
|
+
/** Concrete remediation items if status != "clean". */
|
|
179
|
+
remediation: string[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
183
|
+
// Statistics helpers
|
|
184
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
function percentile(sorted: number[], p: number): number {
|
|
187
|
+
if (sorted.length === 0) return 0;
|
|
188
|
+
const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));
|
|
189
|
+
return sorted[idx];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function quantiles(values: number[]): { p50: number; p95: number; p99: number } {
|
|
193
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
194
|
+
return {
|
|
195
|
+
p50: percentile(sorted, 50),
|
|
196
|
+
p95: percentile(sorted, 95),
|
|
197
|
+
p99: percentile(sorted, 99),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function blankGovernance(): Record<AuditEntry["governance"], number> {
|
|
202
|
+
return {
|
|
203
|
+
"algorithm-only": 0,
|
|
204
|
+
"audit-logged": 0,
|
|
205
|
+
"mocked-upstream": 0,
|
|
206
|
+
"requires-confirmation": 0,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
211
|
+
// Per-session summarization
|
|
212
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
const CLOCK_SKEW_THRESHOLD_MS = 5 * 60 * 1000; // 5 min
|
|
215
|
+
|
|
216
|
+
function summarizeSession(payload: AuditPayload): SessionSummary {
|
|
217
|
+
const entries = payload.entries;
|
|
218
|
+
const governance = blankGovernance();
|
|
219
|
+
const toolUsage: Record<string, number> = {};
|
|
220
|
+
const durations: number[] = [];
|
|
221
|
+
const anomalies: Anomaly[] = [];
|
|
222
|
+
let errored = 0;
|
|
223
|
+
|
|
224
|
+
let prev: AuditEntry | null = null;
|
|
225
|
+
for (const e of entries) {
|
|
226
|
+
governance[e.governance]++;
|
|
227
|
+
toolUsage[e.tool] = (toolUsage[e.tool] ?? 0) + 1;
|
|
228
|
+
if (e.durationMs !== undefined) durations.push(e.durationMs);
|
|
229
|
+
if (e.errored) errored++;
|
|
230
|
+
|
|
231
|
+
// Anomaly: clock skew
|
|
232
|
+
if (prev) {
|
|
233
|
+
const prevMs = Date.parse(prev.ts);
|
|
234
|
+
const curMs = Date.parse(e.ts);
|
|
235
|
+
if (Number.isFinite(prevMs) && Number.isFinite(curMs)) {
|
|
236
|
+
const skew = prevMs - curMs;
|
|
237
|
+
if (skew > CLOCK_SKEW_THRESHOLD_MS) {
|
|
238
|
+
anomalies.push({
|
|
239
|
+
kind: "clock-skew",
|
|
240
|
+
entryId: e.id,
|
|
241
|
+
previousTs: prev.ts,
|
|
242
|
+
currentTs: e.ts,
|
|
243
|
+
skewMs: skew,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Anomaly: governance shift within session (allowed but worth flagging)
|
|
248
|
+
if (prev.governance !== e.governance) {
|
|
249
|
+
anomalies.push({
|
|
250
|
+
kind: "governance-shift",
|
|
251
|
+
entryIdA: prev.id,
|
|
252
|
+
governanceA: prev.governance,
|
|
253
|
+
entryIdB: e.id,
|
|
254
|
+
governanceB: e.governance,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Anomaly: errored LLM call (audit-logged + errored = LLM failed,
|
|
260
|
+
// operator should have a fallback documented).
|
|
261
|
+
if (e.errored && e.governance === "audit-logged") {
|
|
262
|
+
anomalies.push({ kind: "llm-error-without-fallback", entryId: e.id, tool: e.tool });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Anomaly: missing HMAC in production. (null hmac in dev OK; in prod
|
|
266
|
+
// it's a fatal misconfig per RFC-004 § 2.)
|
|
267
|
+
if (!e.hmac) {
|
|
268
|
+
anomalies.push({ kind: "missing-hmac-in-production", entryId: e.id });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
prev = e;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Tampering: derived from the verify endpoint output, not from anomalies.
|
|
275
|
+
// Each tampered entry is also enumerated as an anomaly so the regulator
|
|
276
|
+
// sees it in one list.
|
|
277
|
+
const tampered = payload.tampered ?? 0;
|
|
278
|
+
if (tampered > 0) {
|
|
279
|
+
// We don't know which entry IDs without re-verifying client-side. Flag
|
|
280
|
+
// the session collectively; downstream caller can drill in.
|
|
281
|
+
anomalies.push({
|
|
282
|
+
kind: "tampered-entry",
|
|
283
|
+
entryId: `(${tampered} entries in session ${payload.sessionId})`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
sessionId: payload.sessionId,
|
|
289
|
+
entriesCount: entries.length,
|
|
290
|
+
verified: payload.verified ?? 0,
|
|
291
|
+
tampered,
|
|
292
|
+
errored,
|
|
293
|
+
durationMs: durations.length ? quantiles(durations) : null,
|
|
294
|
+
governanceBreakdown: governance,
|
|
295
|
+
firstTs: entries[0]?.ts ?? null,
|
|
296
|
+
lastTs: entries[entries.length - 1]?.ts ?? null,
|
|
297
|
+
toolUsage,
|
|
298
|
+
anomalies,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
303
|
+
// Cross-session aggregation
|
|
304
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
function aggregate(
|
|
307
|
+
sessions: SessionSummary[],
|
|
308
|
+
payloads: AuditPayload[],
|
|
309
|
+
): QuarterlyReport["aggregates"] {
|
|
310
|
+
const governance = blankGovernance();
|
|
311
|
+
const toolUsage: Record<string, number> = {};
|
|
312
|
+
const durationsByTool: Record<string, number[]> = {};
|
|
313
|
+
let entries = 0;
|
|
314
|
+
let verified = 0;
|
|
315
|
+
let tampered = 0;
|
|
316
|
+
let errored = 0;
|
|
317
|
+
|
|
318
|
+
for (const s of sessions) {
|
|
319
|
+
entries += s.entriesCount;
|
|
320
|
+
verified += s.verified;
|
|
321
|
+
tampered += s.tampered;
|
|
322
|
+
errored += s.errored;
|
|
323
|
+
for (const g of Object.keys(governance) as AuditEntry["governance"][]) {
|
|
324
|
+
governance[g] += s.governanceBreakdown[g];
|
|
325
|
+
}
|
|
326
|
+
for (const [tool, n] of Object.entries(s.toolUsage)) {
|
|
327
|
+
toolUsage[tool] = (toolUsage[tool] ?? 0) + n;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Per-tool latency aggregation needs the raw entries.
|
|
332
|
+
for (const p of payloads) {
|
|
333
|
+
for (const e of p.entries) {
|
|
334
|
+
if (e.durationMs === undefined) continue;
|
|
335
|
+
const bucket = (durationsByTool[e.tool] ??= []);
|
|
336
|
+
bucket.push(e.durationMs);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const durationMsByTool: Record<string, { p50: number; p95: number; p99: number }> = {};
|
|
341
|
+
for (const [tool, ds] of Object.entries(durationsByTool)) {
|
|
342
|
+
durationMsByTool[tool] = quantiles(ds);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
sessionsCount: sessions.length,
|
|
347
|
+
entriesCount: entries,
|
|
348
|
+
verified,
|
|
349
|
+
tampered,
|
|
350
|
+
errored,
|
|
351
|
+
governanceBreakdown: governance,
|
|
352
|
+
toolUsage,
|
|
353
|
+
durationMsByTool,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function conclude(
|
|
358
|
+
aggregates: QuarterlyReport["aggregates"],
|
|
359
|
+
anomalies: Anomaly[],
|
|
360
|
+
): ReportConclusion {
|
|
361
|
+
if (aggregates.tampered > 0) {
|
|
362
|
+
return {
|
|
363
|
+
status: "tampering-detected",
|
|
364
|
+
summary: `${aggregates.tampered} of ${aggregates.entriesCount} entries failed HMAC verification. This is a chain-of-custody breach; the sociedad-IA cannot represent its operating history as complete for the reported period without remediation.`,
|
|
365
|
+
remediation: [
|
|
366
|
+
"Identify which entries failed verification (drill in per session via /api/play/audit/{sessionId}?verify=1).",
|
|
367
|
+
"If verification failure is due to key rotation without re-signing, document the rotation event and re-sign the affected entries under the new key with an explicit re-signing audit entry.",
|
|
368
|
+
"If verification failure is due to actual tampering, treat as a security incident: identify the access path, rotate keys, notify counterparties per RFC-001 § 9.4.",
|
|
369
|
+
"File a written explanation with the requesting regulator.",
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (anomalies.length > 0) {
|
|
374
|
+
const llmErrors = anomalies.filter(a => a.kind === "llm-error-without-fallback").length;
|
|
375
|
+
const skews = anomalies.filter(a => a.kind === "clock-skew").length;
|
|
376
|
+
const missingHmac = anomalies.filter(a => a.kind === "missing-hmac-in-production").length;
|
|
377
|
+
return {
|
|
378
|
+
status: "anomalies-noted",
|
|
379
|
+
summary: `No tampering detected (${aggregates.verified}/${aggregates.entriesCount} entries verified). ${anomalies.length} operational anomalies flagged for review (${llmErrors} LLM-call failures, ${skews} clock-skew events, ${missingHmac} missing-HMAC entries).`,
|
|
380
|
+
remediation: [
|
|
381
|
+
...(llmErrors > 0
|
|
382
|
+
? ["Review LLM-call failure paths; document fallback behavior for the errored tools."]
|
|
383
|
+
: []),
|
|
384
|
+
...(skews > 0
|
|
385
|
+
? ["Investigate clock-skew events (>5 min between consecutive entries within a session); typical cause is NTP drift or fork-then-merge."]
|
|
386
|
+
: []),
|
|
387
|
+
...(missingHmac > 0
|
|
388
|
+
? ["CRITICAL: missing-HMAC entries must not occur in production. Verify AUDIT_HMAC_SECRET is set in the production env and rotate."]
|
|
389
|
+
: []),
|
|
390
|
+
],
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
status: "clean",
|
|
395
|
+
summary: `${aggregates.verified}/${aggregates.entriesCount} entries verified across ${aggregates.sessionsCount} sessions. No anomalies. No tampering. ${aggregates.errored} errored tool-calls (within expected operational range; no remediation required).`,
|
|
396
|
+
remediation: [],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
401
|
+
// Optional: HMAC-sign the report itself (self-disclosure tamper-evidence)
|
|
402
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
function canonical(value: unknown): string {
|
|
405
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
406
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
407
|
+
const obj = value as Record<string, unknown>;
|
|
408
|
+
const keys = Object.keys(obj).sort();
|
|
409
|
+
return `{${keys.map(k => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function signReport(
|
|
413
|
+
report: Omit<QuarterlyReport, "reportHmac">,
|
|
414
|
+
secret: string,
|
|
415
|
+
): Promise<string> {
|
|
416
|
+
const enc = new TextEncoder();
|
|
417
|
+
const key = await crypto.subtle.importKey(
|
|
418
|
+
"raw",
|
|
419
|
+
enc.encode(secret),
|
|
420
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
421
|
+
false,
|
|
422
|
+
["sign"],
|
|
423
|
+
);
|
|
424
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(canonical(report)));
|
|
425
|
+
const hex = Array.from(new Uint8Array(sig))
|
|
426
|
+
.map(b => b.toString(16).padStart(2, "0"))
|
|
427
|
+
.join("");
|
|
428
|
+
return `sha256:${hex}`;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
432
|
+
// Public API
|
|
433
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
export async function generateQuarterlyComplianceReport(
|
|
436
|
+
input: ReportInput,
|
|
437
|
+
options: {
|
|
438
|
+
/** If set, the report is signed with this secret for tamper-evidence. */
|
|
439
|
+
reportSigningSecret?: string;
|
|
440
|
+
/** Override fetch impl (for testing). */
|
|
441
|
+
fetchImpl?: typeof fetch;
|
|
442
|
+
} = {},
|
|
443
|
+
): Promise<QuarterlyReport> {
|
|
444
|
+
const baseUrl = input.baseUrl ?? "https://ar-agents.ar";
|
|
445
|
+
|
|
446
|
+
// 1. Pull every session's audit log + verification result, in parallel.
|
|
447
|
+
const payloads: AuditPayload[] = await Promise.all(
|
|
448
|
+
input.sessionIds.map(async (sessionId) => {
|
|
449
|
+
const data = (await fetchAudit(sessionId, {
|
|
450
|
+
baseUrl,
|
|
451
|
+
verify: true,
|
|
452
|
+
fetchImpl: options.fetchImpl,
|
|
453
|
+
})) as AuditPayload;
|
|
454
|
+
// Defensive: API may not return verify counts in dev (hmacWired=false).
|
|
455
|
+
return {
|
|
456
|
+
sessionId,
|
|
457
|
+
entries: Array.isArray(data.entries) ? data.entries : [],
|
|
458
|
+
total: data.total,
|
|
459
|
+
verified: data.verified,
|
|
460
|
+
tampered: data.tampered,
|
|
461
|
+
hmacWired: data.hmacWired,
|
|
462
|
+
};
|
|
463
|
+
}),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// 2. Summarize each session.
|
|
467
|
+
const sessions = payloads.map(summarizeSession);
|
|
468
|
+
|
|
469
|
+
// 3. Aggregate across sessions.
|
|
470
|
+
const aggregates = aggregate(sessions, payloads);
|
|
471
|
+
|
|
472
|
+
// 4. Roll up anomalies.
|
|
473
|
+
const allAnomalies = sessions.flatMap(s => s.anomalies);
|
|
474
|
+
|
|
475
|
+
// 5. Conclude.
|
|
476
|
+
const conclusion = conclude(aggregates, allAnomalies);
|
|
477
|
+
|
|
478
|
+
// 6. Assemble.
|
|
479
|
+
const base: Omit<QuarterlyReport, "reportHmac"> = {
|
|
480
|
+
$schema: "https://ar-agents.ar/schemas/quarterly-compliance.v1.json",
|
|
481
|
+
generatedAt: new Date().toISOString(),
|
|
482
|
+
schemaVersion: "1.0",
|
|
483
|
+
sociedad: input.sociedad,
|
|
484
|
+
period: { start: input.periodStart, end: input.periodEnd },
|
|
485
|
+
aggregates,
|
|
486
|
+
sessions,
|
|
487
|
+
anomalies: allAnomalies,
|
|
488
|
+
conclusion,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const reportHmac = options.reportSigningSecret
|
|
492
|
+
? await signReport(base, options.reportSigningSecret)
|
|
493
|
+
: null;
|
|
494
|
+
|
|
495
|
+
return { ...base, reportHmac };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
499
|
+
// CLI entry — node 25-sociedad-ia-quarterly-compliance.ts <config.json>
|
|
500
|
+
//
|
|
501
|
+
// The config file is a JSON document with the ReportInput shape.
|
|
502
|
+
// Example:
|
|
503
|
+
// {
|
|
504
|
+
// "sociedad": {
|
|
505
|
+
// "denominacion": "Sociedad-IA Demo SAS",
|
|
506
|
+
// "operatorCuit": "20-41758101-5",
|
|
507
|
+
// "jurisdiction": "AR",
|
|
508
|
+
// "rfcConformance": ["rfc-001-v1", "rfc-004-draft"],
|
|
509
|
+
// "auditBaseUrl": "https://ar-agents.ar"
|
|
510
|
+
// },
|
|
511
|
+
// "periodStart": "2026-04-01T00:00:00.000Z",
|
|
512
|
+
// "periodEnd": "2026-06-30T23:59:59.999Z",
|
|
513
|
+
// "sessionIds": ["session-abc", "session-def", "session-ghi"],
|
|
514
|
+
// "baseUrl": "https://ar-agents.ar"
|
|
515
|
+
// }
|
|
516
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
declare const process: { argv: string[] } | undefined;
|
|
519
|
+
|
|
520
|
+
async function main() {
|
|
521
|
+
if (typeof process === "undefined") return;
|
|
522
|
+
const configPath = process.argv[2];
|
|
523
|
+
if (!configPath) {
|
|
524
|
+
console.error("usage: tsx 25-sociedad-ia-quarterly-compliance.ts <config.json>");
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const fs = await import("node:fs/promises");
|
|
528
|
+
const cfg = JSON.parse(await fs.readFile(configPath, "utf8")) as ReportInput;
|
|
529
|
+
const secret = (globalThis as { process?: { env?: Record<string, string> } }).process?.env
|
|
530
|
+
?.AUDIT_HMAC_SECRET;
|
|
531
|
+
const report = await generateQuarterlyComplianceReport(cfg, {
|
|
532
|
+
reportSigningSecret: secret,
|
|
533
|
+
});
|
|
534
|
+
console.log(JSON.stringify(report, null, 2));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const isMain = typeof require !== "undefined" && require.main === module;
|
|
538
|
+
if (isMain) {
|
|
539
|
+
main().catch((e) => {
|
|
540
|
+
console.error(e);
|
|
541
|
+
if (typeof process !== "undefined" && "exit" in process) {
|
|
542
|
+
(process as unknown as { exit: (code: number) => void }).exit(1);
|
|
543
|
+
}
|
|
544
|
+
});
|
|
545
|
+
}
|