@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,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 };
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recipe 20 — Multi-tenant marketplace spawning vendor sociedades-IA.
|
|
3
|
+
*
|
|
4
|
+
* # Pattern
|
|
5
|
+
*
|
|
6
|
+
* A platform (think: Tienda Nube, Mercado Shops, an SaaS marketplace)
|
|
7
|
+
* onboards vendors who each need a thin AR-side legal entity to operate
|
|
8
|
+
* within Argentina's jurisdiction. Pre-sociedad-IA, this was a manual
|
|
9
|
+
* escribono job per vendor. With `@ar-agents/incorporate`, the platform
|
|
10
|
+
* can spawn a fresh sociedad-IA spec on-demand at vendor sign-up — one
|
|
11
|
+
* API call, the vendor's deploy URL is materialized, the platform
|
|
12
|
+
* tracks the audit log per tenant.
|
|
13
|
+
*
|
|
14
|
+
* # When to use
|
|
15
|
+
*
|
|
16
|
+
* - Vertical SaaS where each vendor needs their own AR fiscal identity
|
|
17
|
+
* (factura emission under their CUIT, not the platform's).
|
|
18
|
+
* - Marketplaces that scale to hundreds of vendors and can't manually
|
|
19
|
+
* coordinate escribano + contador per spawn.
|
|
20
|
+
* - Cross-jurisdictional plays: a USA platform onboarding AR sellers who
|
|
21
|
+
* need a properly-incorporated sociedad to receive Mercado Pago.
|
|
22
|
+
*
|
|
23
|
+
* # Architecture
|
|
24
|
+
*
|
|
25
|
+
* Platform /api/auto-incorporate
|
|
26
|
+
* ┌──────────┐ POST ┌────────────────┐
|
|
27
|
+
* │ vendor │ ─────────────▶ │ ar-agents │
|
|
28
|
+
* │ signup │ with vendor │ generates spec │
|
|
29
|
+
* │ flow │ details + a │ + signs audit │
|
|
30
|
+
* └──────────┘ per-vendor │ entry under │
|
|
31
|
+
* │ sessionId │ vendor's id │
|
|
32
|
+
* ▼ └────────────────┘
|
|
33
|
+
* ┌──────────┐ │
|
|
34
|
+
* │ Vercel │ one-click deploy URL ◀───┘
|
|
35
|
+
* │ deploy │
|
|
36
|
+
* └──────────┘
|
|
37
|
+
* │
|
|
38
|
+
* ▼
|
|
39
|
+
* ┌──────────┐ GET /api/play/audit/{tenantId}?verify=1
|
|
40
|
+
* │ ops │ ◀──────────────────────────────────────┐
|
|
41
|
+
* │ dash │ │
|
|
42
|
+
* └──────────┘ │
|
|
43
|
+
* │ │
|
|
44
|
+
* ▼ (recipe 19 cron) │
|
|
45
|
+
* compliance digest per tenant ──────────────────────┘
|
|
46
|
+
*
|
|
47
|
+
* # Idempotency
|
|
48
|
+
*
|
|
49
|
+
* Calling /api/auto-incorporate with the same input + sessionId twice
|
|
50
|
+
* is idempotent on the spec output but writes two audit entries (one per
|
|
51
|
+
* call). For exact-once semantics, dedupe on the platform side keyed by
|
|
52
|
+
* (vendor_id, sociedad_denominacion). The audit log will show both
|
|
53
|
+
* attempts which is the more honest signal anyway.
|
|
54
|
+
*
|
|
55
|
+
* # Edge Runtime
|
|
56
|
+
*
|
|
57
|
+
* Yes — the @ar-agents/incorporate client is fetch-only.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
import {
|
|
61
|
+
incorporate,
|
|
62
|
+
fetchAudit,
|
|
63
|
+
type IncorporateInput,
|
|
64
|
+
type IncorporateSuccess,
|
|
65
|
+
} from "@ar-agents/incorporate";
|
|
66
|
+
|
|
67
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
// Platform's vendor model
|
|
69
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
interface Vendor {
|
|
72
|
+
id: string; // platform's internal vendor id
|
|
73
|
+
legalName: string;
|
|
74
|
+
representanteName: string;
|
|
75
|
+
representanteCuit: string;
|
|
76
|
+
contactEmail: string;
|
|
77
|
+
/** What the vendor sells. Used as the seed for objeto social. */
|
|
78
|
+
category: "software" | "services" | "products" | "ecommerce";
|
|
79
|
+
expectedMonthlyRevenueArs: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
// Spawn a sociedad-IA for a vendor at sign-up
|
|
84
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const OBJETO_BY_CATEGORY: Record<Vendor["category"], string> = {
|
|
87
|
+
software:
|
|
88
|
+
"Desarrollo y comercialización de software propio para empresas y consumidores en Argentina, incluyendo pero no limitándose a aplicaciones web, móviles y servicios cloud.",
|
|
89
|
+
services:
|
|
90
|
+
"Prestación de servicios profesionales digitales (consultoría, desarrollo, marketing, atención al cliente) a empresas y consumidores en territorio argentino.",
|
|
91
|
+
products:
|
|
92
|
+
"Comercialización mayorista y minorista de productos físicos y digitales propios y de terceros, incluyendo importación, almacenamiento, marketing y logística en Argentina.",
|
|
93
|
+
ecommerce:
|
|
94
|
+
"Operación de tiendas online y marketplaces para la venta de productos y servicios al consumidor final argentino, incluyendo procesamiento de pagos y logística.",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The platform's tenantId for this vendor's sociedad-IA. Becomes the
|
|
99
|
+
* sessionId for the entire forensic timeline (incorporation + ongoing
|
|
100
|
+
* tool calls). Pick something stable + opaque — UUID v4 or a hashed
|
|
101
|
+
* concatenation of vendor_id + a per-platform secret.
|
|
102
|
+
*/
|
|
103
|
+
function tenantSessionIdFor(vendor: Vendor): string {
|
|
104
|
+
// For the cookbook, derive deterministically. In production, prefer
|
|
105
|
+
// crypto.randomUUID() per tenant to avoid leaking enumerable ids.
|
|
106
|
+
return `tenant-${vendor.id.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 56)}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function spawnVendorSociedad(vendor: Vendor): Promise<IncorporateSuccess> {
|
|
110
|
+
const sessionId = tenantSessionIdFor(vendor);
|
|
111
|
+
|
|
112
|
+
// Pieza selection driven by vendor profile.
|
|
113
|
+
const piezas: IncorporateInput["piezas"] = [
|
|
114
|
+
"identity",
|
|
115
|
+
"gde-tad",
|
|
116
|
+
"mercadopago",
|
|
117
|
+
"banking",
|
|
118
|
+
"facturacion",
|
|
119
|
+
"boletin-oficial",
|
|
120
|
+
"igj",
|
|
121
|
+
];
|
|
122
|
+
if (vendor.category === "ecommerce" || vendor.category === "products") {
|
|
123
|
+
piezas.push("shipping");
|
|
124
|
+
}
|
|
125
|
+
if (vendor.expectedMonthlyRevenueArs > 1_000_000) {
|
|
126
|
+
// Large-revenue tenants need WhatsApp customer-comms + ACP for LLM-buyer
|
|
127
|
+
// checkout. Smaller tenants can stay lean.
|
|
128
|
+
piezas.push("whatsapp", "agentic-commerce-bridge", "ap2");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Capital social: SAS minimum is 100k. Bump for large-revenue vendors so
|
|
132
|
+
// BCRA and downstream banks don't flag the disparity later.
|
|
133
|
+
const capitalSocial =
|
|
134
|
+
vendor.expectedMonthlyRevenueArs > 1_000_000 ? 500_000 : 200_000;
|
|
135
|
+
|
|
136
|
+
const input: IncorporateInput = {
|
|
137
|
+
denominacion: `${vendor.legalName} SAS`,
|
|
138
|
+
tipo: "SAS", // SOCIEDAD-IA when the regime ships
|
|
139
|
+
capitalSocial,
|
|
140
|
+
objeto: OBJETO_BY_CATEGORY[vendor.category],
|
|
141
|
+
representante: {
|
|
142
|
+
nombre: vendor.representanteName,
|
|
143
|
+
cuit: vendor.representanteCuit,
|
|
144
|
+
},
|
|
145
|
+
emailContacto: vendor.contactEmail,
|
|
146
|
+
piezas,
|
|
147
|
+
sessionId,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const result = await incorporate(input);
|
|
151
|
+
|
|
152
|
+
if (!result.ok) {
|
|
153
|
+
// Pre-flight failure — surface the findings + reject the signup.
|
|
154
|
+
const errors = result.validation.findings
|
|
155
|
+
.filter((f) => f.severity === "error")
|
|
156
|
+
.map((f) => `${f.field}: ${f.message}`);
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Vendor signup rejected at IGJ pre-flight: ${errors.join("; ")}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
// Platform's spawn flow: handle a fresh signup
|
|
166
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
interface SignupRecord {
|
|
169
|
+
vendorId: string;
|
|
170
|
+
tenantSessionId: string;
|
|
171
|
+
slug: string;
|
|
172
|
+
deployUrl: string;
|
|
173
|
+
auditDashboardUrl: string;
|
|
174
|
+
auditVerifyUrl: string;
|
|
175
|
+
badgeSvgUrl: string;
|
|
176
|
+
createdAt: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Persist this in the platform's vendor table (Postgres / KV / whatever).
|
|
181
|
+
* The badge SVG URL is what you embed in the vendor's profile page so any
|
|
182
|
+
* visitor sees their "verified · N/M" forensic-clean status.
|
|
183
|
+
*/
|
|
184
|
+
function recordFor(
|
|
185
|
+
vendor: Vendor,
|
|
186
|
+
result: IncorporateSuccess,
|
|
187
|
+
): SignupRecord {
|
|
188
|
+
return {
|
|
189
|
+
vendorId: vendor.id,
|
|
190
|
+
tenantSessionId: result.audit.sessionId,
|
|
191
|
+
slug: result.sociedad.slug,
|
|
192
|
+
deployUrl: result.deploy.oneClickUrl,
|
|
193
|
+
auditDashboardUrl: result.audit.dashboardUrl,
|
|
194
|
+
auditVerifyUrl: result.audit.verifyUrl,
|
|
195
|
+
badgeSvgUrl: `https://ar-agents.ar/api/badge/${result.audit.sessionId}`,
|
|
196
|
+
createdAt: new Date().toISOString(),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
201
|
+
// Periodic compliance digest per tenant (recipe 19 in a fan-out loop)
|
|
202
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
interface TenantHealth {
|
|
205
|
+
vendorId: string;
|
|
206
|
+
totalEvents: number;
|
|
207
|
+
tampered: number;
|
|
208
|
+
hmacWired: boolean;
|
|
209
|
+
alert: string | null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export async function platformComplianceSweep(
|
|
213
|
+
records: SignupRecord[],
|
|
214
|
+
): Promise<TenantHealth[]> {
|
|
215
|
+
const results: TenantHealth[] = [];
|
|
216
|
+
for (const r of records) {
|
|
217
|
+
const audit = (await fetchAudit(r.tenantSessionId, { verify: true })) as {
|
|
218
|
+
count: number;
|
|
219
|
+
verification?: { tampered: number; verified: number; total: number; hmacWired: boolean };
|
|
220
|
+
};
|
|
221
|
+
const v = audit.verification ?? { tampered: 0, verified: 0, total: 0, hmacWired: false };
|
|
222
|
+
let alert: string | null = null;
|
|
223
|
+
if (!v.hmacWired) alert = "HMAC not wired in deploy — log unverified.";
|
|
224
|
+
else if (v.tampered > 0)
|
|
225
|
+
alert = `Tampering detected on tenant ${r.vendorId}: ${v.tampered} entries.`;
|
|
226
|
+
results.push({
|
|
227
|
+
vendorId: r.vendorId,
|
|
228
|
+
totalEvents: audit.count,
|
|
229
|
+
tampered: v.tampered,
|
|
230
|
+
hmacWired: v.hmacWired,
|
|
231
|
+
alert,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return results;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
// Example: signup + initial audit verify
|
|
239
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
async function main() {
|
|
242
|
+
// 1. New vendor signs up on the platform.
|
|
243
|
+
const vendor: Vendor = {
|
|
244
|
+
id: "v_42",
|
|
245
|
+
legalName: "Berreta Software",
|
|
246
|
+
representanteName: "Pérez, Juan",
|
|
247
|
+
representanteCuit: "20-12345678-9",
|
|
248
|
+
contactEmail: "ops@berreta.example",
|
|
249
|
+
category: "software",
|
|
250
|
+
expectedMonthlyRevenueArs: 800_000,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// 2. Spawn the sociedad-IA spec.
|
|
254
|
+
const result = await spawnVendorSociedad(vendor);
|
|
255
|
+
const record = recordFor(vendor, result);
|
|
256
|
+
|
|
257
|
+
console.log("Vendor onboarded:", record);
|
|
258
|
+
|
|
259
|
+
// 3. Verify the signup audit entry was actually written + signed.
|
|
260
|
+
const sweep = await platformComplianceSweep([record]);
|
|
261
|
+
console.log("Initial compliance sweep:", sweep);
|
|
262
|
+
|
|
263
|
+
// 4. Embed the badge in the vendor's profile page in the platform UI.
|
|
264
|
+
console.log("Badge URL for vendor profile:", record.badgeSvgUrl);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (typeof require !== "undefined" && require.main === module) {
|
|
268
|
+
main().catch((err) => {
|
|
269
|
+
console.error("Recipe 20 failed:", err);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export { main };
|