latinfo 0.2.0 → 0.4.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/dist/index.js CHANGED
@@ -9,10 +9,19 @@ const http_1 = __importDefault(require("http"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const os_1 = __importDefault(require("os"));
11
11
  const child_process_1 = require("child_process");
12
+ const VERSION = '0.4.0';
12
13
  const API_URL = process.env.LATINFO_API_URL || 'https://api.latinfo.dev';
13
- const GITHUB_CLIENT_ID = 'Ov23li5fcQaiCsVtaMKK';
14
+ const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID || 'Ov23li5fcQaiCsVtaMKK';
14
15
  const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.latinfo');
15
16
  const CONFIG_FILE = path_1.default.join(CONFIG_DIR, 'config.json');
17
+ // --- JSON mode ---
18
+ const jsonFlag = process.argv.includes('--json');
19
+ const liveFlag = process.argv.includes('--live');
20
+ const rawArgs = process.argv.slice(2).filter(a => a !== '--json' && a !== '--live');
21
+ function jsonError(error, message) {
22
+ process.stderr.write(JSON.stringify({ error, message }) + '\n');
23
+ process.exit(1);
24
+ }
16
25
  function loadConfig() {
17
26
  try {
18
27
  return JSON.parse(fs_1.default.readFileSync(CONFIG_FILE, 'utf-8'));
@@ -43,7 +52,7 @@ async function waitForCallback(port) {
43
52
  const code = url.searchParams.get('code');
44
53
  if (code) {
45
54
  res.writeHead(200, { 'Content-Type': 'text/html' });
46
- res.end('<h2>Listo. Puedes cerrar esta ventana.</h2>');
55
+ res.end('<h2>Done. You can close this window.</h2>');
47
56
  server.close();
48
57
  resolve(code);
49
58
  }
@@ -51,132 +60,330 @@ async function waitForCallback(port) {
51
60
  res.writeHead(400);
52
61
  res.end('Missing code');
53
62
  server.close();
54
- reject(new Error('No se recibió código de GitHub'));
63
+ reject(new Error('No code received from GitHub'));
55
64
  }
56
65
  });
57
66
  server.listen(port, () => { });
58
- setTimeout(() => { server.close(); reject(new Error('Timeout esperando autorización')); }, 120_000);
67
+ setTimeout(() => { server.close(); reject(new Error('Timeout waiting for authorization')); }, 120_000);
68
+ });
69
+ }
70
+ function requireAuth() {
71
+ const config = loadConfig();
72
+ if (!config) {
73
+ if (jsonFlag)
74
+ jsonError('auth_required', 'Not logged in. Run: latinfo login');
75
+ console.error('Not logged in. Run: latinfo login');
76
+ process.exit(1);
77
+ }
78
+ return config;
79
+ }
80
+ async function apiRequest(config, path) {
81
+ const res = await fetch(`${API_URL}${path}`, {
82
+ headers: { Authorization: `Bearer ${config.api_key}` },
59
83
  });
84
+ if (!res.ok) {
85
+ const err = await res.json();
86
+ if (jsonFlag)
87
+ jsonError(err.error, err.message || err.error);
88
+ console.error(err.message || err.error);
89
+ process.exit(1);
90
+ }
91
+ return res;
60
92
  }
93
+ // --- Commands ---
61
94
  async function login() {
62
95
  const port = 8400;
63
96
  const redirectUri = `http://localhost:${port}/callback`;
64
97
  const scope = 'read:user,user:email';
65
98
  const authUrl = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${scope}`;
66
- console.log('Abriendo GitHub...');
99
+ console.log('Opening GitHub...');
67
100
  openBrowser(authUrl);
68
- // 1. Wait for GitHub to redirect back with code
69
101
  const code = await waitForCallback(port);
70
- // 2. Exchange code for access token (server-side, needs client_secret)
71
102
  const authRes = await fetch(`${API_URL}/auth/github`, {
72
103
  method: 'POST',
73
104
  headers: { 'Content-Type': 'application/json' },
74
105
  body: JSON.stringify({ code, redirect_uri: redirectUri }),
75
106
  });
76
107
  if (!authRes.ok) {
77
- console.error('Error obteniendo API key:', await authRes.text());
108
+ console.error('Error getting API key:', await authRes.text());
78
109
  process.exit(1);
79
110
  }
80
111
  const authData = await authRes.json();
81
112
  saveConfig({ api_key: authData.api_key, github_username: authData.github_username });
82
- console.log(`Logueado como ${authData.github_username}`);
113
+ console.log(`Logged in as ${authData.github_username}`);
83
114
  }
84
- // --- Commands ---
85
115
  async function ruc(rucNumber) {
86
- const config = loadConfig();
87
- if (!config) {
88
- console.error('No logueado. Ejecuta: latinfo login');
116
+ const config = requireAuth();
117
+ if (!rucNumber || !/^\d{11}$/.test(rucNumber)) {
118
+ if (jsonFlag)
119
+ jsonError('invalid_input', 'Invalid RUC. Must be 11 digits.');
120
+ console.error('Invalid RUC. Must be 11 digits.');
89
121
  process.exit(1);
90
122
  }
91
- if (!/^\d{11}$/.test(rucNumber)) {
92
- console.error('RUC inválido. Debe tener 11 dígitos.');
93
- process.exit(1);
123
+ const res = await apiRequest(config, `/pe/ruc/${rucNumber}`);
124
+ const data = await res.json();
125
+ if (jsonFlag) {
126
+ console.log(JSON.stringify(data));
127
+ return;
94
128
  }
95
- const res = await fetch(`${API_URL}/ruc/${rucNumber}`, {
96
- headers: { Authorization: `Bearer ${config.api_key}` },
97
- });
98
- if (!res.ok) {
99
- const err = await res.json();
100
- console.error(err.message || err.error);
129
+ console.log(`
130
+ RUC: ${data.ruc}
131
+ Razón Social: ${data.razon_social}
132
+ Estado: ${data.estado}
133
+ Condición: ${data.condicion}
134
+ Ubigeo: ${data.ubigeo}
135
+ Dirección: ${[data.tipo_via, data.nombre_via, data.numero].filter(v => v && v !== '-').join(' ')}
136
+ Zona: ${[data.codigo_zona, data.tipo_zona].filter(v => v && v !== '-').join(' ')}
137
+ `.trim());
138
+ }
139
+ async function dni(dniNumber) {
140
+ const config = requireAuth();
141
+ if (!dniNumber || !/^\d{8}$/.test(dniNumber)) {
142
+ if (jsonFlag)
143
+ jsonError('invalid_input', 'Invalid DNI. Must be 8 digits.');
144
+ console.error('Invalid DNI. Must be 8 digits.');
101
145
  process.exit(1);
102
146
  }
147
+ const res = await apiRequest(config, `/pe/dni/${dniNumber}`);
103
148
  const data = await res.json();
149
+ if (jsonFlag) {
150
+ console.log(JSON.stringify(data));
151
+ return;
152
+ }
104
153
  console.log(`
105
154
  RUC: ${data.ruc}
106
- Razón Social: ${data.razonSocial}
155
+ Razón Social: ${data.razon_social}
107
156
  Estado: ${data.estado}
108
157
  Condición: ${data.condicion}
109
158
  Ubigeo: ${data.ubigeo}
110
- Dirección: ${[data.tipoVia, data.nombreVia, data.numero].filter(v => v && v !== '-').join(' ')}
111
- Zona: ${[data.codigoZona, data.tipoZona].filter(v => v && v !== '-').join(' ')}
159
+ Dirección: ${[data.tipo_via, data.nombre_via, data.numero].filter(v => v && v !== '-').join(' ')}
160
+ Zona: ${[data.codigo_zona, data.tipo_zona].filter(v => v && v !== '-').join(' ')}
112
161
  `.trim());
113
162
  }
114
163
  async function search(query) {
115
- const config = loadConfig();
116
- if (!config) {
117
- console.error('No logueado. Ejecuta: latinfo login');
164
+ const config = requireAuth();
165
+ if (!query) {
166
+ if (jsonFlag)
167
+ jsonError('invalid_input', 'Search query is required.');
168
+ console.error('Search query is required.');
118
169
  process.exit(1);
119
170
  }
120
- const res = await fetch(`${API_URL}/search?q=${encodeURIComponent(query)}`, {
121
- headers: { Authorization: `Bearer ${config.api_key}` },
171
+ const res = await apiRequest(config, `/pe/search?q=${encodeURIComponent(query)}`);
172
+ const results = await res.json();
173
+ if (jsonFlag) {
174
+ console.log(JSON.stringify(results));
175
+ return;
176
+ }
177
+ if (results.length === 0) {
178
+ console.log('No results found.');
179
+ return;
180
+ }
181
+ for (const r of results) {
182
+ console.log(` ${r.ruc} ${r.razon_social} [${r.estado}]`);
183
+ }
184
+ console.log(`\n${results.length} result(s)`);
185
+ }
186
+ function whoami() {
187
+ const config = requireAuth();
188
+ if (jsonFlag) {
189
+ console.log(JSON.stringify({ username: config.github_username, api_key: config.api_key }));
190
+ return;
191
+ }
192
+ console.log(config.github_username);
193
+ }
194
+ function plan() {
195
+ const config = requireAuth();
196
+ if (jsonFlag) {
197
+ console.log(JSON.stringify({ plan: 'free', limit: '1M requests/month' }));
198
+ return;
199
+ }
200
+ console.log(`
201
+ User: ${config.github_username}
202
+ Plan: free
203
+ Limit: 1M requests/month
204
+ `.trim());
205
+ }
206
+ function calcCf(requests) {
207
+ if (requests <= 3_000_000)
208
+ return { cost: 0, tier: 'free' };
209
+ if (requests <= 10_000_000)
210
+ return { cost: 5, tier: 'paid' };
211
+ return { cost: 5 + Math.ceil((requests - 10_000_000) / 1_000_000) * 0.50, tier: 'paid' };
212
+ }
213
+ function printCosts(data) {
214
+ if (jsonFlag) {
215
+ console.log(JSON.stringify(data));
216
+ return;
217
+ }
218
+ const fmt = (n) => n.toLocaleString();
219
+ const fmtMoney = (n) => `$${n.toFixed(2)}`;
220
+ console.log(`
221
+ ${data.month ? `Month: ${data.month}\n ` : ''}Users: ${fmt(data.users)}
222
+ Pro users: ${fmt(data.pro_users)}
223
+ Requests: ${fmt(data.requests)}/month
224
+
225
+ CF tier: ${data.cf_tier}
226
+ CF cost: ${fmtMoney(data.cf_cost)}/month
227
+ Revenue: ${fmtMoney(data.revenue)}/month
228
+ Margin: ${data.margin >= 0 ? '+' : ''}${fmtMoney(data.margin)}/month
229
+
230
+ Status: ${data.safe ? 'SAFE' : 'DEFICIT'}
231
+ `.trim());
232
+ }
233
+ async function costsLive() {
234
+ const adminSecret = process.env.LATINFO_ADMIN_SECRET;
235
+ if (!adminSecret) {
236
+ if (jsonFlag)
237
+ jsonError('missing_secret', 'Set LATINFO_ADMIN_SECRET env var.');
238
+ console.error('Set LATINFO_ADMIN_SECRET env var.');
239
+ process.exit(1);
240
+ }
241
+ const res = await fetch(`${API_URL}/admin/stats`, {
242
+ headers: { Authorization: `Bearer ${adminSecret}` },
122
243
  });
123
244
  if (!res.ok) {
124
245
  const err = await res.json();
246
+ if (jsonFlag)
247
+ jsonError(err.error, err.message || err.error);
125
248
  console.error(err.message || err.error);
126
249
  process.exit(1);
127
250
  }
128
251
  const data = await res.json();
129
- if (data.count === 0) {
130
- console.log('Sin resultados.');
131
- return;
132
- }
133
- for (const r of data.results) {
134
- console.log(` ${r.ruc} ${r.razonSocial} [${r.estado}]`);
135
- }
136
- console.log(`\n${data.count} resultado(s)`);
252
+ printCosts(data);
137
253
  }
138
- function whoami() {
139
- const config = loadConfig();
140
- if (!config) {
141
- console.error('No logueado. Ejecuta: latinfo login');
254
+ function costsSimulate(usersStr, rpmStr, proPctStr) {
255
+ const users = parseInt(usersStr);
256
+ const avgRpm = parseInt(rpmStr || '1000');
257
+ const proPct = parseFloat(proPctStr || '1');
258
+ if (!users || users < 0) {
259
+ if (jsonFlag)
260
+ jsonError('invalid_input', 'Usage: latinfo costs <users> [avg_req/user/month] [pro_%]');
261
+ console.error('Usage: latinfo costs <users> [avg_req/user/month] [pro_%]');
142
262
  process.exit(1);
143
263
  }
144
- console.log(config.github_username);
264
+ const requests = users * avgRpm;
265
+ const proUsers = Math.floor(users * proPct / 100);
266
+ const revenue = proUsers * 1;
267
+ const { cost: cfCost, tier: cfTier } = calcCf(requests);
268
+ const margin = revenue - cfCost;
269
+ printCosts({ users, pro_users: proUsers, requests, cf_tier: cfTier, cf_cost: cfCost, revenue, margin, safe: margin >= 0 });
145
270
  }
146
271
  function logout() {
147
272
  deleteConfig();
148
- console.log('Sesión cerrada.');
273
+ console.log('Logged out.');
149
274
  }
150
275
  function help() {
151
- console.log(`
152
- latinfo - Public data API for Latin America
153
-
154
- Comandos:
155
- login Autenticarse con GitHub
156
- logout Cerrar sesión
157
- whoami Ver usuario actual
158
- ruc <ruc> Consultar un RUC (11 dígitos)
159
- search <texto> Buscar por razón social
160
- help Mostrar esta ayuda
161
- `.trim());
276
+ console.log(`latinfo v${VERSION} — Public data API for Latin America
277
+
278
+ API base: ${API_URL}
279
+
280
+ USAGE
281
+ latinfo <command> [args] [--json]
282
+
283
+ COMMANDS
284
+ login
285
+ Authenticate via GitHub OAuth. Opens a browser, waits for callback
286
+ on localhost:8400, and stores the API key in ~/.latinfo/config.json.
287
+
288
+ logout
289
+ Remove stored credentials from ~/.latinfo/config.json.
290
+
291
+ whoami
292
+ Show the authenticated GitHub username.
293
+ Output fields (--json): username, api_key
294
+
295
+ plan
296
+ Show current plan and limits.
297
+ Output fields (--json): plan, limit
298
+
299
+ ruc <ruc>
300
+ Lookup a Peruvian taxpayer by RUC (11 digits).
301
+ Output fields (--json): ruc, razon_social, estado, condicion,
302
+ ubigeo, tipo_via, nombre_via, numero, codigo_zona, tipo_zona,
303
+ departamento, provincia, distrito, direccion_completa, tipo_contribuyente
304
+
305
+ dni <dni>
306
+ Lookup a Peruvian taxpayer by DNI (8 digits). Finds the RUC
307
+ associated with the person and returns the same fields as 'ruc'.
308
+ Output fields (--json): same as ruc
309
+
310
+ search <query>
311
+ Search taxpayers by business name (razón social). Returns an array.
312
+ Output fields (--json): array of objects with same fields as ruc
313
+
314
+ costs <users> [avg_req] [pro_%]
315
+ Simulate Cloudflare cost vs revenue. Defaults: 1000 req/user, 1% Pro.
316
+ Output fields (--json): users, pro_users, requests, cf_tier,
317
+ cf_cost, revenue, margin, safe
318
+
319
+ costs --live
320
+ Real-time cost report from production data (admin only).
321
+ Requires LATINFO_ADMIN_SECRET env var.
322
+ Output fields (--json): month, users, pro_users, requests,
323
+ cf_tier, cf_cost, revenue, margin, safe
324
+
325
+ help
326
+ Show this help text.
327
+
328
+ FLAGS
329
+ --json Output raw JSON instead of human-formatted text.
330
+ Errors are written to stderr as {"error":"...","message":"..."}.
331
+ --live Use real-time production data (for 'costs' command).
332
+ --version, -v
333
+ Print version and exit.
334
+
335
+ COUNTRIES
336
+ pe Peru (SUNAT padrón). Active and available.
337
+
338
+ AUTH
339
+ Run 'latinfo login' to authenticate with GitHub. This opens your
340
+ browser for OAuth, then stores your API key locally. All data
341
+ commands (ruc, dni, search) require authentication.
342
+
343
+ Config file: ~/.latinfo/config.json
344
+ Environment: LATINFO_API_URL to override API base URL.
345
+
346
+ EXIT CODES
347
+ 0 Success
348
+ 1 Error (invalid input, auth failure, API error)`);
349
+ }
350
+ function version() {
351
+ console.log(`latinfo/${VERSION}`);
162
352
  }
163
353
  // --- Main ---
164
- const [command, ...args] = process.argv.slice(2);
165
- switch (command) {
166
- case 'login':
167
- login().catch(e => { console.error(e); process.exit(1); });
168
- break;
169
- case 'logout':
170
- logout();
171
- break;
172
- case 'whoami':
173
- whoami();
174
- break;
175
- case 'ruc':
176
- ruc(args[0]).catch(e => { console.error(e); process.exit(1); });
177
- break;
178
- case 'search':
179
- search(args.join(' ')).catch(e => { console.error(e); process.exit(1); });
180
- break;
181
- default: help();
354
+ const [command, ...args] = rawArgs;
355
+ if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
356
+ version();
357
+ }
358
+ else {
359
+ switch (command) {
360
+ case 'login':
361
+ login().catch(e => { console.error(e); process.exit(1); });
362
+ break;
363
+ case 'logout':
364
+ logout();
365
+ break;
366
+ case 'whoami':
367
+ whoami();
368
+ break;
369
+ case 'plan':
370
+ plan();
371
+ break;
372
+ case 'costs':
373
+ (liveFlag ? costsLive() : Promise.resolve(costsSimulate(args[0], args[1], args[2]))).catch(e => { console.error(e); process.exit(1); });
374
+ break;
375
+ case 'ruc':
376
+ ruc(args[0]).catch(e => { console.error(e); process.exit(1); });
377
+ break;
378
+ case 'dni':
379
+ dni(args[0]).catch(e => { console.error(e); process.exit(1); });
380
+ break;
381
+ case 'search':
382
+ search(args.join(' ')).catch(e => { console.error(e); process.exit(1); });
383
+ break;
384
+ case 'help':
385
+ help();
386
+ break;
387
+ default: help();
388
+ }
182
389
  }
package/dist/sdk.d.ts CHANGED
@@ -1,13 +1,13 @@
1
- interface RucResult {
1
+ export interface PeRecord {
2
2
  ruc: string;
3
- razonSocial: string;
3
+ razon_social: string;
4
4
  estado: string;
5
5
  condicion: string;
6
6
  ubigeo: string;
7
- tipoVia: string;
8
- nombreVia: string;
9
- codigoZona: string;
10
- tipoZona: string;
7
+ tipo_via: string;
8
+ nombre_via: string;
9
+ codigo_zona: string;
10
+ tipo_zona: string;
11
11
  numero: string;
12
12
  interior: string;
13
13
  lote: string;
@@ -15,28 +15,25 @@ interface RucResult {
15
15
  manzana: string;
16
16
  kilometro: string;
17
17
  }
18
- interface DniResult extends RucResult {
19
- dni: string;
20
- }
21
- interface SearchResult {
22
- ruc: string;
23
- razonSocial: string;
24
- estado: string;
25
- condicion: string;
18
+ declare class Country {
19
+ private request;
20
+ private prefix;
21
+ constructor(request: <T>(path: string) => Promise<T>, prefix: string);
22
+ protected countryRequest<T>(path: string): Promise<T>;
26
23
  }
27
- interface SearchResponse {
28
- count: number;
29
- results: SearchResult[];
24
+ declare class Peru extends Country {
25
+ constructor(request: <T>(path: string) => Promise<T>);
26
+ ruc(ruc: string): Promise<PeRecord>;
27
+ dni(dni: string): Promise<PeRecord>;
28
+ search(query: string): Promise<PeRecord[]>;
30
29
  }
31
30
  export declare class Latinfo {
32
31
  private apiKey;
33
32
  private baseUrl;
33
+ pe: Peru;
34
34
  constructor(apiKey: string, options?: {
35
35
  baseUrl?: string;
36
36
  });
37
37
  private request;
38
- ruc(ruc: string): Promise<RucResult>;
39
- dni(dni: string): Promise<DniResult>;
40
- search(query: string): Promise<SearchResponse>;
41
38
  }
42
39
  export {};
package/dist/sdk.js CHANGED
@@ -2,12 +2,39 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Latinfo = void 0;
4
4
  const DEFAULT_API_URL = 'https://api.latinfo.dev';
5
+ class Country {
6
+ request;
7
+ prefix;
8
+ constructor(request, prefix) {
9
+ this.request = request;
10
+ this.prefix = prefix;
11
+ }
12
+ countryRequest(path) {
13
+ return this.request(`/${this.prefix}${path}`);
14
+ }
15
+ }
16
+ class Peru extends Country {
17
+ constructor(request) {
18
+ super(request, 'pe');
19
+ }
20
+ async ruc(ruc) {
21
+ return this.countryRequest(`/ruc/${ruc}`);
22
+ }
23
+ async dni(dni) {
24
+ return this.countryRequest(`/dni/${dni}`);
25
+ }
26
+ async search(query) {
27
+ return this.countryRequest(`/search?q=${encodeURIComponent(query)}`);
28
+ }
29
+ }
5
30
  class Latinfo {
6
31
  apiKey;
7
32
  baseUrl;
33
+ pe;
8
34
  constructor(apiKey, options) {
9
35
  this.apiKey = apiKey;
10
36
  this.baseUrl = options?.baseUrl || DEFAULT_API_URL;
37
+ this.pe = new Peru(this.request.bind(this));
11
38
  }
12
39
  async request(path) {
13
40
  const res = await fetch(`${this.baseUrl}${path}`, {
@@ -19,14 +46,5 @@ class Latinfo {
19
46
  }
20
47
  return res.json();
21
48
  }
22
- async ruc(ruc) {
23
- return this.request(`/ruc/${ruc}`);
24
- }
25
- async dni(dni) {
26
- return this.request(`/dni/${dni}`);
27
- }
28
- async search(query) {
29
- return this.request(`/search?q=${encodeURIComponent(query)}`);
30
- }
31
49
  }
32
50
  exports.Latinfo = Latinfo;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "latinfo",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Public data API for Latin America - SDK & CLI",
5
5
  "main": "dist/sdk.js",
6
6
  "types": "dist/sdk.d.ts",