@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,193 @@
1
+ /**
2
+ * Recipe 29 — Publish your sociedad-IA's Ed25519 public key (RFC-005 § 4).
3
+ *
4
+ * # Pattern
5
+ *
6
+ * Real operators rotate keys, custody privates in secrets managers, and
7
+ * publish public keys at /.well-known/sociedad-ia/keys. Recipe 29 is the
8
+ * one-time bootstrap + the recurring rotation flow:
9
+ *
10
+ * 1. Generate an Ed25519 keypair locally (Web Crypto OR Node crypto).
11
+ * 2. Convert public key → SPKI base64url (RFC-005 § 4 wire format).
12
+ * 3. Print the public key JSON the operator can copy into
13
+ * apps/.../public/.well-known/sociedad-ia/keys.json.
14
+ * 4. Print the private key as base64url PKCS8 — to paste into the
15
+ * operator's secrets manager (Vercel env `AUDIT_ED25519_PRIVATE_KEY`,
16
+ * 1Password, AWS Secrets Manager, etc.). NEVER commit to a repo.
17
+ *
18
+ * The same keyId can stay valid indefinitely. Rotation is additive:
19
+ * generate a new keypair, append to the published keys list with a
20
+ * later validFrom + null validUntil. Set validUntil on the previous
21
+ * key. Old entries signed with the rotated-out key remain verifiable
22
+ * because the public key stays in the published list.
23
+ *
24
+ * # When to use
25
+ *
26
+ * - First-time setup: right after deploying your sociedad-IA. Generate
27
+ * once, paste public key into your repo, paste private key into
28
+ * your secrets manager.
29
+ * - Scheduled rotation: at 6- or 12-month cadence.
30
+ * - Incident response: if you suspect the private key was compromised.
31
+ *
32
+ * # No Web app needed
33
+ *
34
+ * Recipe 29 is a pure CLI script. Outputs the JSON snippets the
35
+ * operator pastes into their own infrastructure.
36
+ *
37
+ * # Edge / Node compatibility
38
+ *
39
+ * Web Crypto Ed25519 stable in Node 22+ and Vercel Edge. The script
40
+ * uses Web Crypto throughout so it runs anywhere.
41
+ */
42
+
43
+ declare const process: { argv: string[]; exit: (n: number) => void; stdout: { write: (s: string) => void } } | undefined;
44
+
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+ // Generation
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+
49
+ function b64urlEncode(bytes: Uint8Array): string {
50
+ let s = "";
51
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
52
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
53
+ }
54
+
55
+ function hexEncode(bytes: Uint8Array): string {
56
+ return Array.from(bytes)
57
+ .map((b) => b.toString(16).padStart(2, "0"))
58
+ .join("");
59
+ }
60
+
61
+ export interface GeneratedKeypair {
62
+ keyId: string;
63
+ alg: "ed25519";
64
+ /** base64url SPKI (DER-encoded SubjectPublicKeyInfo). */
65
+ publicKey: string;
66
+ /** Raw 32-byte Ed25519 point as hex. */
67
+ publicKeyRaw: string;
68
+ /** base64url PKCS8 (DER-encoded PrivateKeyInfo). DO NOT publish. */
69
+ privateKey: string;
70
+ validFrom: string;
71
+ validUntil: null;
72
+ }
73
+
74
+ /**
75
+ * Generate a fresh Ed25519 keypair + return both the public and private
76
+ * parts in the wire formats RFC-005 § 4 expects.
77
+ */
78
+ export async function generateKeypair(keyId: string): Promise<GeneratedKeypair> {
79
+ const kp = await crypto.subtle.generateKey(
80
+ { name: "Ed25519" } as unknown as AlgorithmIdentifier,
81
+ true,
82
+ ["sign", "verify"],
83
+ );
84
+ const pubSpki = await crypto.subtle.exportKey("spki", (kp as CryptoKeyPair).publicKey);
85
+ const privPkcs8 = await crypto.subtle.exportKey("pkcs8", (kp as CryptoKeyPair).privateKey);
86
+ const pubSpkiBytes = new Uint8Array(pubSpki);
87
+ // SPKI for Ed25519 = 0x30 0x2a 0x30 0x05 0x06 0x03 0x2b 0x65 0x70 0x03 0x21 0x00 || 32-byte point
88
+ const pubRaw = pubSpkiBytes.slice(-32);
89
+ return {
90
+ keyId,
91
+ alg: "ed25519",
92
+ publicKey: b64urlEncode(pubSpkiBytes),
93
+ publicKeyRaw: hexEncode(pubRaw),
94
+ privateKey: b64urlEncode(new Uint8Array(privPkcs8)),
95
+ validFrom: new Date().toISOString(),
96
+ validUntil: null,
97
+ };
98
+ }
99
+
100
+ // ─────────────────────────────────────────────────────────────────────────────
101
+ // Output formatting
102
+ // ─────────────────────────────────────────────────────────────────────────────
103
+
104
+ interface PublishedKeysFile {
105
+ $schema: string;
106
+ spec: string;
107
+ issuer: {
108
+ jurisdiction: string;
109
+ entityId: string;
110
+ denominacion: string;
111
+ };
112
+ keys: Array<{
113
+ keyId: string;
114
+ alg: "ed25519";
115
+ publicKey: string;
116
+ publicKeyRaw: string;
117
+ validFrom: string;
118
+ validUntil: string | null;
119
+ }>;
120
+ note: string;
121
+ }
122
+
123
+ export function publishedKeysFromKeypair(
124
+ kp: GeneratedKeypair,
125
+ issuer: { jurisdiction: string; entityId: string; denominacion: string },
126
+ ): PublishedKeysFile {
127
+ return {
128
+ $schema: "https://ar-agents.ar/schemas/keys.v1.json",
129
+ spec: "https://ar-agents.ar/rfcs/005",
130
+ issuer,
131
+ keys: [
132
+ {
133
+ keyId: kp.keyId,
134
+ alg: kp.alg,
135
+ publicKey: kp.publicKey,
136
+ publicKeyRaw: kp.publicKeyRaw,
137
+ validFrom: kp.validFrom,
138
+ validUntil: kp.validUntil,
139
+ },
140
+ ],
141
+ note: "Ed25519 public key for this sociedad-IA's RFC-004/005 audit-log signatures. Private key custody lives in the operator's secrets manager.",
142
+ };
143
+ }
144
+
145
+ // ─────────────────────────────────────────────────────────────────────────────
146
+ // CLI: tsx 29-publish-your-keys.ts <keyId> [<cuit>] [<denominacion>]
147
+ // ─────────────────────────────────────────────────────────────────────────────
148
+
149
+ async function main() {
150
+ if (typeof process === "undefined") return;
151
+ const [, , keyIdArg, cuit, denominacionArg] = process.argv;
152
+ const keyId = keyIdArg ?? `${defaultKeyIdPrefix()}-${new Date().getUTCFullYear()}-${String(new Date().getUTCMonth() + 1).padStart(2, "0")}`;
153
+ const jurisdiction = "AR";
154
+ const entityId = cuit ? `ar-sociedad:${cuit}` : "ar-sociedad:replace-with-your-cuit";
155
+ const denominacion = denominacionArg ?? "(replace with your sociedad's denominación)";
156
+
157
+ const kp = await generateKeypair(keyId);
158
+ const published = publishedKeysFromKeypair(kp, { jurisdiction, entityId, denominacion });
159
+
160
+ const writeLn = (s: string) => process!.stdout.write(`${s}\n`);
161
+ writeLn("# ════════════════════════════════════════════════════════════════════════");
162
+ writeLn("# 1. Public key — drop this into:");
163
+ writeLn(`# apps/<your-sociedad-app>/public/.well-known/sociedad-ia/keys.json`);
164
+ writeLn("# ════════════════════════════════════════════════════════════════════════");
165
+ writeLn(JSON.stringify(published, null, 2));
166
+ writeLn("");
167
+ writeLn("# ════════════════════════════════════════════════════════════════════════");
168
+ writeLn("# 2. Private key — paste into your operator's secrets manager:");
169
+ writeLn("# Vercel env var: AUDIT_ED25519_PRIVATE_KEY (recommended)");
170
+ writeLn("# DO NOT commit this to a repo. DO NOT publish.");
171
+ writeLn("# ════════════════════════════════════════════════════════════════════════");
172
+ writeLn(kp.privateKey);
173
+ writeLn("");
174
+ writeLn("# ════════════════════════════════════════════════════════════════════════");
175
+ writeLn("# 3. Verify with curl:");
176
+ writeLn("# curl https://your-sociedad.example/.well-known/sociedad-ia/keys.json | jq .keys[0].publicKey");
177
+ writeLn("# Should match the publicKey field in section 1 above.");
178
+ writeLn("# ════════════════════════════════════════════════════════════════════════");
179
+ }
180
+
181
+ function defaultKeyIdPrefix(): string {
182
+ return "sociedad-ia-key";
183
+ }
184
+
185
+ const isMain = typeof require !== "undefined" && require.main === module;
186
+ if (isMain) {
187
+ main().catch((e) => {
188
+ console.error(e);
189
+ if (typeof process !== "undefined" && "exit" in process) {
190
+ (process as unknown as { exit: (code: number) => void }).exit(1);
191
+ }
192
+ });
193
+ }
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Recipe 30 — Submit your sociedad-IA to the public /registro.
3
+ *
4
+ * # Pattern
5
+ *
6
+ * Once your sociedad-IA is deployed + scoring >= 60 on the public
7
+ * certifier (rating C or better), you can list it in the public
8
+ * registry at /registro. Recipe 30 is the pre-flight check + the
9
+ * PR-body generator.
10
+ *
11
+ * The flow:
12
+ *
13
+ * 1. Run recipe 28 (operator readiness) against your URL.
14
+ * 2. Run recipe 26 (RFC certifier) against your URL.
15
+ * 3. If both pass, recipe 30 produces a single Markdown block you
16
+ * paste into a GitHub PR titled
17
+ * "[/registro] Add <your-sociedad-name>".
18
+ *
19
+ * 4. The PR review checks (manually):
20
+ * - The URL resolves
21
+ * - /.well-known/agents.json is valid
22
+ * - The disclosure is honest (e.g. claims "demo" not "productive"
23
+ * if the sociedad doesn't actually transact)
24
+ * - operatorCuit matches the entityId in the manifest
25
+ *
26
+ * 5. Merge → live in /registro within ~1 hour (next build).
27
+ *
28
+ * # When to use
29
+ *
30
+ * - First-time: after your sociedad-IA is deployed + you want
31
+ * public visibility.
32
+ * - Update: same flow, just amend the existing entry by name.
33
+ *
34
+ * # No silent failures
35
+ *
36
+ * Recipe 30 refuses to produce a PR body if the readiness or certifier
37
+ * checks fail. Returns a remediation report instead. This prevents
38
+ * over-eager submission of half-built sociedades.
39
+ */
40
+
41
+ import { certifySociedad } from "./26-certify-by-fetch";
42
+ import { checkOperatorReadiness } from "./28-operator-onboarding-checklist";
43
+
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+ // Types
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+
48
+ export interface SubmissionInput {
49
+ /** Display name for the registry. */
50
+ name: string;
51
+ /** Type: "demo", "productive-sociedad-ia", "library-only". */
52
+ type: "demo" | "productive-sociedad-ia" | "library-only";
53
+ /** Operator's CUIT (no formatting). */
54
+ operatorCuit: string;
55
+ /** Operator's full name (legal). */
56
+ operatorName: string;
57
+ /** Public URL of the deployed sociedad. */
58
+ publicUrl: string;
59
+ /** RFC versions claimed. */
60
+ rfcConformance: string[];
61
+ /** Plain-English honest disclosure (1-3 sentences). */
62
+ disclosure: string;
63
+ }
64
+
65
+ export interface SubmissionResult {
66
+ ok: boolean;
67
+ url: string;
68
+ certScore?: number;
69
+ certRating?: string;
70
+ readiness?: "ready" | "almost" | "blocked" | "not-deployed";
71
+ failures: string[];
72
+ prBody?: string;
73
+ }
74
+
75
+ // ─────────────────────────────────────────────────────────────────────────────
76
+ // Submission pipeline
77
+ // ─────────────────────────────────────────────────────────────────────────────
78
+
79
+ const MINIMUM_CERT_SCORE = 60;
80
+
81
+ export async function buildRegistrySubmission(
82
+ input: SubmissionInput,
83
+ options: { apiBaseUrl?: string; fetchImpl?: typeof fetch } = {},
84
+ ): Promise<SubmissionResult> {
85
+ const failures: string[] = [];
86
+
87
+ // 1. Operator readiness (recipe 28).
88
+ const readiness = await checkOperatorReadiness(input.publicUrl, {
89
+ fetchImpl: options.fetchImpl,
90
+ });
91
+ if (readiness.readiness === "blocked") {
92
+ failures.push(
93
+ `Operator readiness is "blocked" (${readiness.passedCount}/${readiness.totalCount} items passing). Fix the blocking items before submitting.`,
94
+ );
95
+ }
96
+
97
+ // 2. RFC certifier (recipe 26).
98
+ const cert = await certifySociedad(input.publicUrl, {
99
+ fetchImpl: options.fetchImpl,
100
+ });
101
+ if (cert.score < MINIMUM_CERT_SCORE) {
102
+ failures.push(
103
+ `Certifier score ${cert.score}/${cert.rating} is below the minimum ${MINIMUM_CERT_SCORE} required for /registro listing.`,
104
+ );
105
+ }
106
+
107
+ // 3. Honesty heuristics.
108
+ if (
109
+ input.type === "productive-sociedad-ia" &&
110
+ !input.disclosure.toLowerCase().includes("real")
111
+ ) {
112
+ failures.push(
113
+ `Type is "productive-sociedad-ia" but disclosure doesn't mention "real". Be specific about what real-world transactions the sociedad performs (factura emission, MP cobros, etc.).`,
114
+ );
115
+ }
116
+ if (
117
+ input.type === "demo" &&
118
+ !input.disclosure.toLowerCase().includes("not a productive") &&
119
+ !input.disclosure.toLowerCase().includes("demo")
120
+ ) {
121
+ failures.push(
122
+ `Type is "demo" but disclosure doesn't say "demo" or "not a productive sociedad-IA". Be explicit so a reader doesn't confuse it with a real one.`,
123
+ );
124
+ }
125
+ if (!input.operatorCuit.match(/^\d{2}-\d{8}-\d$/)) {
126
+ failures.push(
127
+ `operatorCuit "${input.operatorCuit}" doesn't match CUIT format XX-XXXXXXXX-X.`,
128
+ );
129
+ }
130
+
131
+ // 4. If everything passes, generate the PR body.
132
+ let prBody: string | undefined;
133
+ if (failures.length === 0) {
134
+ prBody = generatePrBody(input, cert.score, cert.rating, readiness.readiness);
135
+ }
136
+
137
+ return {
138
+ ok: failures.length === 0,
139
+ url: input.publicUrl,
140
+ certScore: cert.score,
141
+ certRating: cert.rating,
142
+ readiness: readiness.readiness,
143
+ failures,
144
+ prBody,
145
+ };
146
+ }
147
+
148
+ function generatePrBody(
149
+ input: SubmissionInput,
150
+ certScore: number,
151
+ certRating: string,
152
+ readiness: string,
153
+ ): string {
154
+ return `## [/registro] Add \`${input.name}\`
155
+
156
+ ### What this PR does
157
+
158
+ Adds the following entry to the public /registro of known sociedad-IA
159
+ implementations:
160
+
161
+ \`\`\`ts
162
+ {
163
+ name: "${input.name}",
164
+ type: "${input.type}",
165
+ jurisdiction: "AR",
166
+ operator: "${input.operatorName}",
167
+ operatorCuit: "${input.operatorCuit}",
168
+ publicUrl: "${input.publicUrl}",
169
+ rfcConformance: [${input.rfcConformance.map((r) => `"${r}"`).join(", ")}],
170
+ disclosure: "${input.disclosure.replace(/"/g, '\\"')}",
171
+ status: "live",
172
+ listedSince: "${new Date().toISOString().slice(0, 10)}",
173
+ }
174
+ \`\`\`
175
+
176
+ ### Pre-submission checks
177
+
178
+ - ✅ **Operator readiness** (recipe 28): \`${readiness}\`
179
+ - ✅ **RFC conformance** (recipe 26): score **${certScore}/100** rating **${certRating}**
180
+ - ✅ **CUIT format**: \`${input.operatorCuit}\` matches XX-XXXXXXXX-X
181
+ - ✅ **Disclosure honesty**: type=\`${input.type}\` aligns with disclosure text
182
+
183
+ ### What I'm claiming
184
+
185
+ I attest that I am the legitimate operator of this sociedad-IA. The
186
+ \`operatorCuit\` corresponds to a real CUIT under my control. The
187
+ \`/.well-known/agents.json\` at the public URL declares this entity.
188
+ I will keep the deployed endpoints live for at least 90 days from
189
+ merge; if I take the sociedad-IA down, I will open a follow-up PR
190
+ removing the entry.
191
+
192
+ ### Verifier instructions for the maintainer
193
+
194
+ 1. \`curl https://ar-agents.ar/api/certifier?url=${input.publicUrl}\`
195
+ — expect score >= 60.
196
+ 2. \`curl ${input.publicUrl}/.well-known/agents.json\` — expect issuer.operatorCuit to match \`${input.operatorCuit}\`.
197
+ 3. Verify disclosure honesty by eye.
198
+ 4. Merge.
199
+ `;
200
+ }
201
+
202
+ // ─────────────────────────────────────────────────────────────────────────────
203
+ // CLI: tsx 30-submit-to-registry.ts <config.json>
204
+ // ─────────────────────────────────────────────────────────────────────────────
205
+
206
+ declare const process: { argv: string[] } | undefined;
207
+
208
+ async function main() {
209
+ if (typeof process === "undefined") return;
210
+ const configPath = process.argv[2];
211
+ if (!configPath) {
212
+ console.error("usage: tsx 30-submit-to-registry.ts <config.json>");
213
+ console.error("");
214
+ console.error("config.json shape:");
215
+ console.error(JSON.stringify(
216
+ {
217
+ name: "My Sociedad-IA",
218
+ type: "demo",
219
+ operatorCuit: "20-12345678-9",
220
+ operatorName: "Jane Doe",
221
+ publicUrl: "https://my-sociedad.vercel.app",
222
+ rfcConformance: ["rfc-001-v1", "rfc-002-v1"],
223
+ disclosure: "Single-library demo. Not a productive sociedad-IA.",
224
+ },
225
+ null,
226
+ 2,
227
+ ));
228
+ return;
229
+ }
230
+ const fs = await import("node:fs/promises");
231
+ const cfg = JSON.parse(await fs.readFile(configPath, "utf8")) as SubmissionInput;
232
+
233
+ const result = await buildRegistrySubmission(cfg);
234
+
235
+ if (result.ok) {
236
+ console.log("--- PR body (copy-paste into your registry PR) ---\n");
237
+ console.log(result.prBody);
238
+ } else {
239
+ console.error("--- Submission blocked: failures need remediation ---\n");
240
+ for (const f of result.failures) console.error(` ✗ ${f}`);
241
+ console.error("\n Cert score:", result.certScore, result.certRating);
242
+ console.error(" Operator readiness:", result.readiness);
243
+ if (typeof process !== "undefined" && "exit" in process) {
244
+ (process as unknown as { exit: (code: number) => void }).exit(1);
245
+ }
246
+ }
247
+ }
248
+
249
+ const isMain = typeof require !== "undefined" && require.main === module;
250
+ if (isMain) {
251
+ main().catch((e) => {
252
+ console.error(e);
253
+ if (typeof process !== "undefined" && "exit" in process) {
254
+ (process as unknown as { exit: (code: number) => void }).exit(1);
255
+ }
256
+ });
257
+ }
package/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var core = require('@ar-agents/core');
3
4
  var ai = require('ai');
4
5
  var zod = require('zod');
5
6
 
@@ -71,19 +72,25 @@ var init_crypto = __esm({
71
72
  encoder = new TextEncoder();
72
73
  }
73
74
  });
74
-
75
- // src/errors.ts
76
- var MercadoPagoError = class extends Error {
77
- constructor(message, status, endpoint, mpResponse) {
78
- super(message);
79
- this.status = status;
80
- this.endpoint = endpoint;
81
- this.mpResponse = mpResponse;
82
- this.name = "MercadoPagoError";
83
- }
75
+ var MercadoPagoError = class extends core.ArAgentsError {
84
76
  status;
85
77
  endpoint;
86
78
  mpResponse;
79
+ constructor(message, status, endpoint, mpResponse, init = {}) {
80
+ super(message, {
81
+ code: init.code ?? "mp_api_error",
82
+ retryable: init.retryable ?? (status >= 500 || status === 429 || status === 0),
83
+ context: {
84
+ status,
85
+ endpoint,
86
+ ...mpResponse !== void 0 ? { mpResponse } : {}
87
+ }
88
+ });
89
+ this.name = "MercadoPagoError";
90
+ this.status = status;
91
+ this.endpoint = endpoint;
92
+ if (mpResponse !== void 0) this.mpResponse = mpResponse;
93
+ }
87
94
  };
88
95
  var MercadoPagoAuthError = class extends MercadoPagoError {
89
96
  constructor(endpoint, body) {
@@ -91,7 +98,8 @@ var MercadoPagoAuthError = class extends MercadoPagoError {
91
98
  "Mercado Pago rejected the request as unauthorized. Check the access token (TEST- prefix for sandbox, APP_USR- for production).",
92
99
  401,
93
100
  endpoint,
94
- body
101
+ body,
102
+ { code: "mp_auth_failed", retryable: false }
95
103
  );
96
104
  this.name = "MercadoPagoAuthError";
97
105
  }
@@ -161,7 +169,8 @@ var MercadoPagoRateLimitError = class extends MercadoPagoError {
161
169
  `Mercado Pago rate limit hit on ${endpoint}. ${retryAfterSeconds ? `Retry after ${retryAfterSeconds}s.` : "Retry with exponential backoff."}`,
162
170
  429,
163
171
  endpoint,
164
- body
172
+ body,
173
+ { code: "mp_rate_limited", retryable: true }
165
174
  );
166
175
  this.retryAfterSeconds = retryAfterSeconds;
167
176
  this.name = "MercadoPagoRateLimitError";
@@ -173,7 +182,9 @@ var MercadoPagoOverloadedError = class extends MercadoPagoError {
173
182
  super(
174
183
  `Mercado Pago appears overloaded \u2014 returned a non-JSON ${status} response for ${endpoint}. Wait a few seconds and retry.`,
175
184
  status,
176
- endpoint
185
+ endpoint,
186
+ void 0,
187
+ { code: "mp_overloaded", retryable: true }
177
188
  );
178
189
  this.name = "MercadoPagoOverloadedError";
179
190
  }
@@ -183,7 +194,9 @@ var MercadoPagoTimeoutError = class extends MercadoPagoError {
183
194
  super(
184
195
  `Mercado Pago request timed out after ${timeoutMs}ms on ${endpoint}. Increase requestTimeoutMs or check connectivity.`,
185
196
  0,
186
- endpoint
197
+ endpoint,
198
+ void 0,
199
+ { code: "mp_timeout", retryable: true }
187
200
  );
188
201
  this.timeoutMs = timeoutMs;
189
202
  this.name = "MercadoPagoTimeoutError";