@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.
@@ -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 };
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Recipe 21 — Cross-jurisdictional commerce w/ AP2 mandate verification.
3
+ *
4
+ * # Pattern
5
+ *
6
+ * A USA-LLC agent (e.g., a Wyoming DAO LLC operating an Etsy-style
7
+ * marketplace) wants to sell to AR consumers via MP, but doesn't have AR
8
+ * fiscal residency. The AR-side is a sociedad-IA (or pre-launch SAS) that
9
+ * (a) issues factura A/B/C to the AR consumer, (b) cobra MP, (c) settles
10
+ * inter-entity to the USA-LLC weekly.
11
+ *
12
+ * Each cross-entity transaction is gated by an AP2 (Google Agent Payments
13
+ * Protocol) mandate the USA-LLC signs and the AR sociedad verifies before
14
+ * acting. This is what RFC-001 § 7 sketches as the contract surface for
15
+ * "agent commerce that crosses jurisdictions".
16
+ *
17
+ * The mandate has 3 critical fields:
18
+ * - issuer: USA-LLC's stable identifier (DID, DAO LLC EIN, etc.)
19
+ * - subject: the AR sociedad's CUIT
20
+ * - claims: { action: "factura.emit", amount, currency: "ARS",
21
+ * consumer: { dni, email }, allowance: { capPerMonth, capPerOp } }
22
+ *
23
+ * Verification:
24
+ * 1. AP2 verify chain — JWS ES256 signature against the issuer's pinned key.
25
+ * 2. Mandate hasn't expired (`exp` claim).
26
+ * 3. Operation respects the cap (cumulative against the audit log).
27
+ * 4. Consumer-side checks (CUIT validity, BCRA situation if amount large).
28
+ *
29
+ * If any check fails, the AR sociedad refuses + logs the refusal in the
30
+ * audit log. RFC-001 § 9.2 makes that audit entry probative — the USA-LLC
31
+ * can later challenge "you said no, prove the rule".
32
+ *
33
+ * # Edge Runtime
34
+ *
35
+ * Yes — @ar-agents/ap2 + @ar-agents/incorporate are both fetch-only.
36
+ * Verification uses Web Crypto's verify() against the issuer's pinned JWK.
37
+ *
38
+ * # Production caveats
39
+ *
40
+ * - The AR sociedad MUST emit the factura under its own CUIT, not the
41
+ * USA-LLC's. Cross-CUIT factura is not legal in AR.
42
+ * - Settlement between entities is an SLA between the operator and the
43
+ * USA-LLC. The toolkit doesn't dictate the cadence; recipe 22 (planned)
44
+ * covers nightly reconciliation.
45
+ * - For amounts > certain BCRA thresholds, AR sociedad needs to hold
46
+ * funds for ~2 days while AFIP IBPP processes. The agent should surface
47
+ * this to the consumer at checkout.
48
+ */
49
+
50
+ import { fetchAudit } from "@ar-agents/incorporate";
51
+ import { verifyMandate, type Mandate } from "@ar-agents/ap2";
52
+ import {
53
+ identityTools,
54
+ UnconfiguredAfipPadronAdapter,
55
+ } from "@ar-agents/identity";
56
+ import { facturacionTools } from "@ar-agents/facturacion";
57
+ import { bankingTools } from "@ar-agents/banking";
58
+
59
+ // ─────────────────────────────────────────────────────────────────────────────
60
+ // Mandate the USA-LLC sends to the AR sociedad
61
+ // ─────────────────────────────────────────────────────────────────────────────
62
+
63
+ interface CrossJurisdictionalClaims {
64
+ action: "factura.emit" | "mp.charge" | "shipment.create";
65
+ amountArs: number;
66
+ currency: "ARS";
67
+ consumer: { cuit: string; email: string };
68
+ allowance: {
69
+ capPerOpArs: number;
70
+ capPerMonthArs: number;
71
+ };
72
+ /** ISO 8601. Mandate expires here. */
73
+ exp: string;
74
+ /** USA-LLC's transaction reference for reconciliation. */
75
+ externalId: string;
76
+ }
77
+
78
+ // ─────────────────────────────────────────────────────────────────────────────
79
+ // AR sociedad's verify-then-act handler
80
+ // ─────────────────────────────────────────────────────────────────────────────
81
+
82
+ interface VerifyAndActOptions {
83
+ /**
84
+ * The AR sociedad's audit-log session id. All operations under this
85
+ * mandate land in the same forensic timeline.
86
+ */
87
+ sessionId: string;
88
+ /**
89
+ * The USA-LLC's JWK that issued the mandate. In production: pinned
90
+ * via the platform's vendor table.
91
+ */
92
+ issuerJwk: JsonWebKey;
93
+ /** The mandate the USA-LLC delivered. */
94
+ mandate: Mandate<CrossJurisdictionalClaims>;
95
+ }
96
+
97
+ interface ActResult {
98
+ ok: boolean;
99
+ reason?: string;
100
+ facturaCae?: string;
101
+ auditDashboardUrl: string;
102
+ }
103
+
104
+ export async function verifyAndAct(
105
+ options: VerifyAndActOptions,
106
+ ): Promise<ActResult> {
107
+ const { sessionId, issuerJwk, mandate } = options;
108
+
109
+ // ─── 1. Verify the AP2 mandate ──────────────────────────────────────
110
+ const verification = await verifyMandate(mandate, { issuerJwk });
111
+ if (!verification.valid) {
112
+ return {
113
+ ok: false,
114
+ reason: `Mandate verification failed: ${verification.reason}`,
115
+ auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
116
+ };
117
+ }
118
+
119
+ // ─── 2. Expiry check ────────────────────────────────────────────────
120
+ if (Date.parse(mandate.claims.exp) < Date.now()) {
121
+ return {
122
+ ok: false,
123
+ reason: "Mandate expired (exp claim in the past)",
124
+ auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
125
+ };
126
+ }
127
+
128
+ // ─── 3. Per-op cap ──────────────────────────────────────────────────
129
+ if (mandate.claims.amountArs > mandate.claims.allowance.capPerOpArs) {
130
+ return {
131
+ ok: false,
132
+ reason: `Amount $${mandate.claims.amountArs} exceeds per-op cap $${mandate.claims.allowance.capPerOpArs}.`,
133
+ auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
134
+ };
135
+ }
136
+
137
+ // ─── 4. Cumulative cap (querying the audit log) ─────────────────────
138
+ // Pull this month's prior emissions on the same sessionId + sum.
139
+ const audit = (await fetchAudit(sessionId, { verify: false })) as {
140
+ entries: Array<{
141
+ tool: string;
142
+ ts: string;
143
+ input: { amountArs?: number; externalId?: string };
144
+ }>;
145
+ };
146
+ const monthStart = new Date();
147
+ monthStart.setDate(1);
148
+ monthStart.setHours(0, 0, 0, 0);
149
+ const cumulative = audit.entries
150
+ .filter(
151
+ (e) =>
152
+ e.tool === "cross_jurisdictional_factura_emit" &&
153
+ Date.parse(e.ts) >= monthStart.getTime(),
154
+ )
155
+ .reduce((sum, e) => sum + (e.input.amountArs ?? 0), 0);
156
+
157
+ if (cumulative + mandate.claims.amountArs > mandate.claims.allowance.capPerMonthArs) {
158
+ return {
159
+ ok: false,
160
+ reason: `Cumulative monthly emissions ($${cumulative} + $${mandate.claims.amountArs}) exceed cap $${mandate.claims.allowance.capPerMonthArs}.`,
161
+ auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
162
+ };
163
+ }
164
+
165
+ // ─── 5. Idempotency: did we already process this externalId? ────────
166
+ if (
167
+ audit.entries.some(
168
+ (e) =>
169
+ e.tool === "cross_jurisdictional_factura_emit" &&
170
+ e.input.externalId === mandate.claims.externalId,
171
+ )
172
+ ) {
173
+ return {
174
+ ok: false,
175
+ reason: `externalId ${mandate.claims.externalId} already processed (idempotency).`,
176
+ auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
177
+ };
178
+ }
179
+
180
+ // ─── 6. Validate the consumer's CUIT ────────────────────────────────
181
+ // (Real prod would have an AfipPadronAdapter wired — using the
182
+ // unconfigured shim here just to demonstrate the flow.)
183
+ const idTools = identityTools({ afip: new UnconfiguredAfipPadronAdapter() });
184
+ const cuitCheck = await idTools.validate_cuit.execute({
185
+ cuit: mandate.claims.consumer.cuit,
186
+ });
187
+ if (!cuitCheck.valid) {
188
+ return {
189
+ ok: false,
190
+ reason: `Consumer CUIT ${mandate.claims.consumer.cuit} is malformed.`,
191
+ auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
192
+ };
193
+ }
194
+
195
+ // ─── 7. For amounts above $500k ARS, BCRA credit-situation check ────
196
+ if (mandate.claims.amountArs > 500_000) {
197
+ const bk = bankingTools();
198
+ // In production this hits BCRA Central de Deudores for real; mock here.
199
+ const credit = await bk.lookup_credit_situation.execute({
200
+ cuit: mandate.claims.consumer.cuit,
201
+ });
202
+ if (
203
+ credit.available === false ||
204
+ (credit.worstSituation && credit.worstSituation > 2)
205
+ ) {
206
+ return {
207
+ ok: false,
208
+ reason: `Consumer has BCRA situation ${credit.worstSituation} (>2 = high risk).`,
209
+ auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
210
+ };
211
+ }
212
+ }
213
+
214
+ // ─── 8. Emit the factura ────────────────────────────────────────────
215
+ // (Real prod requires AFIP cert; using mock returns CAE here.)
216
+ const fact = facturacionTools();
217
+ // Note: in real code, you'd select cbteTipo ("FACTURA_B" for
218
+ // CONSUMIDOR_FINAL etc.) based on AFIP padron lookup of the consumer.
219
+ // Demo just hardcodes Factura B + 21% IVA.
220
+ const result = await fact.crear_factura.execute({
221
+ cbteTipo: "FACTURA_B",
222
+ docTipo: "CUIT",
223
+ docNro: mandate.claims.consumer.cuit,
224
+ impTotal: mandate.claims.amountArs,
225
+ impNeto: Math.round(mandate.claims.amountArs / 1.21),
226
+ impIVA: mandate.claims.amountArs - Math.round(mandate.claims.amountArs / 1.21),
227
+ });
228
+
229
+ // ─── 9. Audit log entry happens automatically via the tool wrapper.
230
+ // Surface the result + dashboard URL for the USA-LLC's records.
231
+
232
+ return {
233
+ ok: true,
234
+ facturaCae: result.cae ?? undefined,
235
+ auditDashboardUrl: `https://ar-agents.ar/dashboard/${sessionId}`,
236
+ };
237
+ }
238
+
239
+ // ─────────────────────────────────────────────────────────────────────────────
240
+ // Example: handle a cross-jurisdictional factura request
241
+ // ─────────────────────────────────────────────────────────────────────────────
242
+
243
+ async function main() {
244
+ const sessionId = "tenant-clawbank-llc-ar"; // pinned per USA-LLC tenant
245
+
246
+ // Mandate the USA-LLC produced (signed JWS; pretend it's already verified
247
+ // structurally — in real code, JWS parsing happens inside verifyMandate).
248
+ const mandate = {
249
+ issuer: "wyoming-dao-llc:claw-bank",
250
+ subject: "ar-sociedad:30123456789",
251
+ claims: {
252
+ action: "factura.emit" as const,
253
+ amountArs: 75_000,
254
+ currency: "ARS" as const,
255
+ consumer: { cuit: "20-12345678-9", email: "consumidor@example.com" },
256
+ allowance: {
257
+ capPerOpArs: 100_000,
258
+ capPerMonthArs: 2_000_000,
259
+ },
260
+ exp: new Date(Date.now() + 3600 * 1000).toISOString(),
261
+ externalId: "claw-bank:tx_42",
262
+ },
263
+ signature: "<JWS-ES256-signature>", // produced by USA-LLC
264
+ } as unknown as Mandate<CrossJurisdictionalClaims>;
265
+
266
+ // The USA-LLC's pinned issuer key. In production: pulled from the
267
+ // platform's vendor table at signup time.
268
+ const issuerJwk: JsonWebKey = {
269
+ kty: "EC",
270
+ crv: "P-256",
271
+ x: "<base64url-x>",
272
+ y: "<base64url-y>",
273
+ };
274
+
275
+ const result = await verifyAndAct({ sessionId, issuerJwk, mandate });
276
+
277
+ if (!result.ok) {
278
+ console.error("Refused:", result.reason);
279
+ console.error("Audit dashboard:", result.auditDashboardUrl);
280
+ process.exit(0);
281
+ }
282
+
283
+ console.log("Factura emitted:", result.facturaCae);
284
+ console.log("Audit dashboard:", result.auditDashboardUrl);
285
+ console.log(
286
+ "Settlement reference:",
287
+ `clawbank-llc:${mandate.claims.externalId}`,
288
+ );
289
+ }
290
+
291
+ if (typeof require !== "undefined" && require.main === module) {
292
+ main().catch((err) => {
293
+ console.error("Recipe 21 failed:", err);
294
+ process.exit(1);
295
+ });
296
+ }
297
+
298
+ export { main };