@albertomarturelo/sii-core 0.3.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 +114 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/adapters/fake/index.d.ts +98 -0
- package/dist/adapters/fake/index.d.ts.map +1 -0
- package/dist/adapters/fake/index.js +131 -0
- package/dist/adapters/fake/index.js.map +1 -0
- package/dist/adapters/node/index.d.ts +22 -0
- package/dist/adapters/node/index.d.ts.map +1 -0
- package/dist/adapters/node/index.js +65 -0
- package/dist/adapters/node/index.js.map +1 -0
- package/dist/adapters/node/portal.d.ts +8 -0
- package/dist/adapters/node/portal.d.ts.map +1 -0
- package/dist/adapters/node/portal.js +222 -0
- package/dist/adapters/node/portal.js.map +1 -0
- package/dist/adapters/node/response.d.ts +21 -0
- package/dist/adapters/node/response.d.ts.map +1 -0
- package/dist/adapters/node/response.js +48 -0
- package/dist/adapters/node/response.js.map +1 -0
- package/dist/audit/audit.d.ts +8 -0
- package/dist/audit/audit.d.ts.map +1 -0
- package/dist/audit/audit.js +15 -0
- package/dist/audit/audit.js.map +1 -0
- package/dist/audit/index.d.ts +2 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +2 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/auth/auth.d.ts +43 -0
- package/dist/auth/auth.d.ts.map +1 -0
- package/dist/auth/auth.js +219 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +3 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/login-error.d.ts +8 -0
- package/dist/auth/login-error.d.ts.map +1 -0
- package/dist/auth/login-error.js +18 -0
- package/dist/auth/login-error.js.map +1 -0
- package/dist/auth/session.d.ts +31 -0
- package/dist/auth/session.d.ts.map +1 -0
- package/dist/auth/session.js +52 -0
- package/dist/auth/session.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/config.d.ts +38 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +40 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/index.d.ts +2 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +2 -0
- package/dist/config/index.js.map +1 -0
- package/dist/errors/errors.d.ts +56 -0
- package/dist/errors/errors.d.ts.map +1 -0
- package/dist/errors/errors.js +62 -0
- package/dist/errors/errors.js.map +1 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +2 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/format/format.d.ts +5 -0
- package/dist/format/format.d.ts.map +1 -0
- package/dist/format/format.js +14 -0
- package/dist/format/format.js.map +1 -0
- package/dist/format/index.d.ts +2 -0
- package/dist/format/index.d.ts.map +1 -0
- package/dist/format/index.js +2 -0
- package/dist/format/index.js.map +1 -0
- package/dist/identity/identity.d.ts +56 -0
- package/dist/identity/identity.d.ts.map +1 -0
- package/dist/identity/identity.js +93 -0
- package/dist/identity/identity.js.map +1 -0
- package/dist/identity/index.d.ts +2 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +2 -0
- package/dist/identity/index.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/node.d.ts +9 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +22 -0
- package/dist/node.js.map +1 -0
- package/dist/periodo/anio.d.ts +15 -0
- package/dist/periodo/anio.d.ts.map +1 -0
- package/dist/periodo/anio.js +42 -0
- package/dist/periodo/anio.js.map +1 -0
- package/dist/periodo/index.d.ts +3 -0
- package/dist/periodo/index.d.ts.map +1 -0
- package/dist/periodo/index.js +3 -0
- package/dist/periodo/index.js.map +1 -0
- package/dist/periodo/periodo.d.ts +19 -0
- package/dist/periodo/periodo.d.ts.map +1 -0
- package/dist/periodo/periodo.js +55 -0
- package/dist/periodo/periodo.js.map +1 -0
- package/dist/portal/bte-comunas.d.ts +9 -0
- package/dist/portal/bte-comunas.d.ts.map +1 -0
- package/dist/portal/bte-comunas.js +400 -0
- package/dist/portal/bte-comunas.js.map +1 -0
- package/dist/portal/bte-emit.d.ts +70 -0
- package/dist/portal/bte-emit.d.ts.map +1 -0
- package/dist/portal/bte-emit.js +266 -0
- package/dist/portal/bte-emit.js.map +1 -0
- package/dist/portal/bte.d.ts +55 -0
- package/dist/portal/bte.d.ts.map +1 -0
- package/dist/portal/bte.js +215 -0
- package/dist/portal/bte.js.map +1 -0
- package/dist/portal/dte-public.d.ts +36 -0
- package/dist/portal/dte-public.d.ts.map +1 -0
- package/dist/portal/dte-public.js +192 -0
- package/dist/portal/dte-public.js.map +1 -0
- package/dist/portal/f22/declaraciones.d.ts +37 -0
- package/dist/portal/f22/declaraciones.d.ts.map +1 -0
- package/dist/portal/f22/declaraciones.js +86 -0
- package/dist/portal/f22/declaraciones.js.map +1 -0
- package/dist/portal/f22/grid.d.ts +14 -0
- package/dist/portal/f22/grid.d.ts.map +1 -0
- package/dist/portal/f22/grid.js +51 -0
- package/dist/portal/f22/grid.js.map +1 -0
- package/dist/portal/f22/historial.d.ts +34 -0
- package/dist/portal/f22/historial.d.ts.map +1 -0
- package/dist/portal/f22/historial.js +66 -0
- package/dist/portal/f22/historial.js.map +1 -0
- package/dist/portal/f22/index.d.ts +7 -0
- package/dist/portal/f22/index.d.ts.map +1 -0
- package/dist/portal/f22/index.js +11 -0
- package/dist/portal/f22/index.js.map +1 -0
- package/dist/portal/f22/observaciones.d.ts +20 -0
- package/dist/portal/f22/observaciones.d.ts.map +1 -0
- package/dist/portal/f22/observaciones.js +38 -0
- package/dist/portal/f22/observaciones.js.map +1 -0
- package/dist/portal/f22/shared.d.ts +32 -0
- package/dist/portal/f22/shared.d.ts.map +1 -0
- package/dist/portal/f22/shared.js +130 -0
- package/dist/portal/f22/shared.js.map +1 -0
- package/dist/portal/f22-codigos.d.ts +28 -0
- package/dist/portal/f22-codigos.d.ts.map +1 -0
- package/dist/portal/f22-codigos.js +141 -0
- package/dist/portal/f22-codigos.js.map +1 -0
- package/dist/portal/f29-codigos.d.ts +15 -0
- package/dist/portal/f29-codigos.d.ts.map +1 -0
- package/dist/portal/f29-codigos.js +552 -0
- package/dist/portal/f29-codigos.js.map +1 -0
- package/dist/portal/f29.d.ts +62 -0
- package/dist/portal/f29.d.ts.map +1 -0
- package/dist/portal/f29.js +244 -0
- package/dist/portal/f29.js.map +1 -0
- package/dist/portal/rcv.d.ts +62 -0
- package/dist/portal/rcv.d.ts.map +1 -0
- package/dist/portal/rcv.js +252 -0
- package/dist/portal/rcv.js.map +1 -0
- package/dist/portal/representacion.d.ts +24 -0
- package/dist/portal/representacion.d.ts.map +1 -0
- package/dist/portal/representacion.js +153 -0
- package/dist/portal/representacion.js.map +1 -0
- package/dist/rut/index.d.ts +2 -0
- package/dist/rut/index.d.ts.map +1 -0
- package/dist/rut/index.js +2 -0
- package/dist/rut/index.js.map +1 -0
- package/dist/rut/rut.d.ts +16 -0
- package/dist/rut/rut.d.ts.map +1 -0
- package/dist/rut/rut.js +70 -0
- package/dist/rut/rut.js.map +1 -0
- package/dist/seams/index.d.ts +126 -0
- package/dist/seams/index.d.ts.map +1 -0
- package/dist/seams/index.js +6 -0
- package/dist/seams/index.js.map +1 -0
- package/dist/tasks/auth.d.ts +16 -0
- package/dist/tasks/auth.d.ts.map +1 -0
- package/dist/tasks/auth.js +22 -0
- package/dist/tasks/auth.js.map +1 -0
- package/dist/tasks/bte.d.ts +52 -0
- package/dist/tasks/bte.d.ts.map +1 -0
- package/dist/tasks/bte.js +169 -0
- package/dist/tasks/bte.js.map +1 -0
- package/dist/tasks/dte.d.ts +9 -0
- package/dist/tasks/dte.d.ts.map +1 -0
- package/dist/tasks/dte.js +30 -0
- package/dist/tasks/dte.js.map +1 -0
- package/dist/tasks/f22.d.ts +69 -0
- package/dist/tasks/f22.d.ts.map +1 -0
- package/dist/tasks/f22.js +189 -0
- package/dist/tasks/f22.js.map +1 -0
- package/dist/tasks/f29.d.ts +67 -0
- package/dist/tasks/f29.d.ts.map +1 -0
- package/dist/tasks/f29.js +213 -0
- package/dist/tasks/f29.js.map +1 -0
- package/dist/tasks/operate.d.ts +22 -0
- package/dist/tasks/operate.d.ts.map +1 -0
- package/dist/tasks/operate.js +34 -0
- package/dist/tasks/operate.js.map +1 -0
- package/dist/tasks/rcv.d.ts +17 -0
- package/dist/tasks/rcv.d.ts.map +1 -0
- package/dist/tasks/rcv.js +76 -0
- package/dist/tasks/rcv.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { LOGIN_HOST, loginUrl } from '../../config/index.js';
|
|
2
|
+
import { LoginFailedError } from '../../errors/index.js';
|
|
3
|
+
import { parseSiiLoginError } from '../../auth/login-error.js';
|
|
4
|
+
import { charsetOf, formLoginWallError, nonJsonResponseError } from './response.js';
|
|
5
|
+
/** Lazy-load playwright — an OPTIONAL peer since ADR-016. Only the launch paths of
|
|
6
|
+
* this default driver need it, so a consumer that injects its own PortalDriver (or
|
|
7
|
+
* only uses the pure barrel) never pays the module. A missing install fails HERE,
|
|
8
|
+
* at first actual use, with an actionable message — never at library import time.
|
|
9
|
+
* Only the not-found case for 'playwright' itself is translated; any other failure
|
|
10
|
+
* (a broken transitive import, etc.) propagates untouched. */
|
|
11
|
+
async function loadChromium() {
|
|
12
|
+
try {
|
|
13
|
+
return (await import('playwright')).chromium;
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
const code = err.code;
|
|
17
|
+
const notFound = code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND';
|
|
18
|
+
if (notFound && err instanceof Error && err.message.includes('playwright')) {
|
|
19
|
+
throw new Error('El PortalDriver por defecto necesita `playwright` (peer opcional, ADR-016). ' +
|
|
20
|
+
'Instálalo en el proyecto consumidor: `npm i playwright` y luego ' +
|
|
21
|
+
'`npx playwright install chromium`.');
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Extract SII's verbatim login-error message from the failed-login page (rendered
|
|
27
|
+
* on zeusr.sii.cl at /cgi_AUT2000/CAutInicio.cgi). Observed 2026-06-28: the page
|
|
28
|
+
* shows "<causa>" then "El código de este mensaje es <código>" — the line BEFORE
|
|
29
|
+
* the código line is the human cause (e.g. "La Clave Tributaria ingresada no es
|
|
30
|
+
* correcta…"). Pass it through unchanged (CONVENTIONS); fall back to a clear,
|
|
31
|
+
* no-retry message if the page shape changed. */
|
|
32
|
+
async function readLoginError(page) {
|
|
33
|
+
const fallback = 'El SII rechazó el login (Clave incorrecta o cuenta bloqueada). NO reintentes a ciegas: ' +
|
|
34
|
+
'varios intentos fallidos bloquean la cuenta. Verifica tu Clave o usa `sii auth login`.';
|
|
35
|
+
try {
|
|
36
|
+
// Pull the rendered body text and parse in Node (testable; keeps the DOM out
|
|
37
|
+
// of core). The string expr returns a string, so the boundary cast is safe.
|
|
38
|
+
const body = (await page.evaluate('(document.body && document.body.innerText) || ""'));
|
|
39
|
+
return parseSiiLoginError(body) ?? fallback;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** A session owns its browser; close() tears the whole instance down. */
|
|
46
|
+
class PlaywrightPortalSession {
|
|
47
|
+
browser;
|
|
48
|
+
context;
|
|
49
|
+
page;
|
|
50
|
+
constructor(browser, context, page) {
|
|
51
|
+
this.browser = browser;
|
|
52
|
+
this.context = context;
|
|
53
|
+
this.page = page;
|
|
54
|
+
}
|
|
55
|
+
async goto(url) {
|
|
56
|
+
// domcontentloaded is enough: SII serves its state as inline scripts
|
|
57
|
+
// (e.g. DatosCntrNow), which have executed by this point.
|
|
58
|
+
await this.page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
59
|
+
return this.page.url();
|
|
60
|
+
}
|
|
61
|
+
async evaluate(expression) {
|
|
62
|
+
return (await this.page.evaluate(expression));
|
|
63
|
+
}
|
|
64
|
+
async requestJson(url, options = {}) {
|
|
65
|
+
// Issue from the browser's APIRequestContext so the session cookies ride along
|
|
66
|
+
// (the SDI facades on www4.sii.cl authorize by the SPA session). domcontentloaded
|
|
67
|
+
// is irrelevant here — this is a raw XHR-equivalent, not a navigation.
|
|
68
|
+
const response = await this.context.request.fetch(url, {
|
|
69
|
+
method: options.method ?? 'POST',
|
|
70
|
+
...(options.headers ? { headers: options.headers } : {}),
|
|
71
|
+
...(options.body !== undefined ? { data: JSON.stringify(options.body) } : {}),
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
return await response.json();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// A non-JSON body from an authenticated SDI POST means the dead session was
|
|
78
|
+
// bounced to SII's login wall — surface an actionable SessionExpiredError, not a
|
|
79
|
+
// parse error. No extra round-trip: this first SDI POST IS the liveness test.
|
|
80
|
+
// Classification is a pure helper so it is unit-tested (nonJsonResponseError).
|
|
81
|
+
throw nonJsonResponseError(response.url(), response.headers()['content-type'] ?? '', response.status());
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async requestForm(url, options = {}) {
|
|
85
|
+
// Authenticated x-www-form-urlencoded POST from the browser's APIRequestContext,
|
|
86
|
+
// so the session cookies ride along (the legacy TMBECN_* emit CGIs on loa.sii.cl
|
|
87
|
+
// authorize by the SSO cookie). The response is HTML by design, so — unlike
|
|
88
|
+
// requestJson — a non-JSON body is NOT a login wall; a dead session is detected
|
|
89
|
+
// URL-based (the request bounced to LOGIN_HOST). `form` sets the urlencoded body
|
|
90
|
+
// + content-type automatically (Playwright).
|
|
91
|
+
const response = await this.context.request.fetch(url, {
|
|
92
|
+
method: options.method ?? 'POST',
|
|
93
|
+
...(options.headers ? { headers: options.headers } : {}),
|
|
94
|
+
...(options.form ? { form: options.form } : {}),
|
|
95
|
+
});
|
|
96
|
+
const wall = formLoginWallError(response.url());
|
|
97
|
+
if (wall)
|
|
98
|
+
throw wall;
|
|
99
|
+
const buffer = await response.body();
|
|
100
|
+
const text = new TextDecoder(charsetOf(response.headers()['content-type'])).decode(buffer);
|
|
101
|
+
return { status: response.status(), body: text };
|
|
102
|
+
}
|
|
103
|
+
async cookie(url, name) {
|
|
104
|
+
const cookies = await this.context.cookies(url);
|
|
105
|
+
return cookies.find((c) => c.name === name)?.value ?? null;
|
|
106
|
+
}
|
|
107
|
+
async storageState() {
|
|
108
|
+
// Cookies-only (ADR-006): drop localStorage/origins before persisting so no
|
|
109
|
+
// page-scoped data ever lands on disk.
|
|
110
|
+
const state = await this.context.storageState();
|
|
111
|
+
return { cookies: state.cookies };
|
|
112
|
+
}
|
|
113
|
+
async close() {
|
|
114
|
+
await this.browser.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export class PlaywrightPortalDriver {
|
|
118
|
+
async interactiveLogin(options) {
|
|
119
|
+
// HEADED: the user must see and type into SII's real Clave Tributaria page.
|
|
120
|
+
const chromium = await loadChromium();
|
|
121
|
+
const browser = await chromium.launch({ headless: false });
|
|
122
|
+
try {
|
|
123
|
+
const context = await browser.newContext();
|
|
124
|
+
const page = await context.newPage();
|
|
125
|
+
await page.goto(loginUrl(options.destination), { waitUntil: 'domcontentloaded' });
|
|
126
|
+
// Login completes when the browser redirects OFF the login host. Window
|
|
127
|
+
// close or timeout rejects (no partial session is ever written).
|
|
128
|
+
await page.waitForURL((url) => url.hostname !== LOGIN_HOST, {
|
|
129
|
+
timeout: options.timeoutMs,
|
|
130
|
+
});
|
|
131
|
+
return new PlaywrightPortalSession(browser, context, page);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Close the whole browser on ANY failure so a launched instance never leaks.
|
|
135
|
+
await browser.close();
|
|
136
|
+
throw new LoginFailedError('Login no completado (tiempo agotado o ventana cerrada). Reintenta `sii auth login`.');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async credentialLogin(options) {
|
|
140
|
+
// HEADLESS (ADR-010): the user typed the Clave into the TERMINAL; we fill SII's
|
|
141
|
+
// real login form and let its own JS derive the hidden rut/dv + referencia and
|
|
142
|
+
// POST. We never hand-build the POST. The Clave is used only here — only cookies
|
|
143
|
+
// are persisted by the caller. ONE attempt, never retried (account-lock safety,
|
|
144
|
+
// ADR-004). Selectors observed 2026-06-28 (docs/sii-contract/auth-login.md):
|
|
145
|
+
// #rutcntr (full RUT, text) · #clave (password) · #bt_ingresar (submit).
|
|
146
|
+
const chromium = await loadChromium();
|
|
147
|
+
const browser = await chromium.launch({ headless: true });
|
|
148
|
+
try {
|
|
149
|
+
const context = await browser.newContext();
|
|
150
|
+
const page = await context.newPage();
|
|
151
|
+
await page.goto(loginUrl(options.destination), { waitUntil: 'domcontentloaded' });
|
|
152
|
+
await page.fill('#rutcntr', options.rut);
|
|
153
|
+
await page.fill('#clave', options.clave);
|
|
154
|
+
// The submit POSTs to /cgi_AUT2000/CAutInicio.cgi (observed 2026-06-28). BOTH
|
|
155
|
+
// outcomes are a navigation that settles into a document: success redirects OFF
|
|
156
|
+
// the login host (→ Mi-SII); a rejected Clave / locked account stays ON
|
|
157
|
+
// zeusr.sii.cl and RENDERS the error page there. Wait for the settled document
|
|
158
|
+
// and decide by host — so a failure fails in seconds, never hanging to timeout.
|
|
159
|
+
await Promise.all([
|
|
160
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: options.timeoutMs }),
|
|
161
|
+
page.click('#bt_ingresar'),
|
|
162
|
+
]);
|
|
163
|
+
if (new URL(page.url()).hostname === LOGIN_HOST) {
|
|
164
|
+
// Rejected — surface SII's verbatim message (CONVENTIONS); do NOT retry.
|
|
165
|
+
throw new LoginFailedError(await readLoginError(page));
|
|
166
|
+
}
|
|
167
|
+
return new PlaywrightPortalSession(browser, context, page);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
await browser.close();
|
|
171
|
+
if (err instanceof LoginFailedError)
|
|
172
|
+
throw err; // already carries SII's message
|
|
173
|
+
throw new LoginFailedError('Login con Clave por consola no completado (el formulario del SII no respondió ' +
|
|
174
|
+
'o cambió). NO reintentes; usa `sii auth login` (navegador).');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async restore(storageState) {
|
|
178
|
+
// HEADLESS: cookies-only readback for liveness / portal reads. No UI.
|
|
179
|
+
const chromium = await loadChromium();
|
|
180
|
+
const browser = await chromium.launch({ headless: true });
|
|
181
|
+
try {
|
|
182
|
+
const context = await browser.newContext({
|
|
183
|
+
storageState: storageState,
|
|
184
|
+
});
|
|
185
|
+
const page = await context.newPage();
|
|
186
|
+
return new PlaywrightPortalSession(browser, context, page);
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
// Don't leak the launched browser; surface the original error to the caller
|
|
190
|
+
// (probeLive / statusRefresh treat a failed restore as "not live").
|
|
191
|
+
await browser.close();
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async requestPublic(url, options = {}) {
|
|
196
|
+
// UNAUTHENTICATED public consulta (ADR-014): a cold HTTP request — no browser, no
|
|
197
|
+
// cookies, no session. Node's global fetch (undici) is the right tool; a public SII
|
|
198
|
+
// CGI needs nothing Chromium provides (the Python original used a plain httpx POST,
|
|
199
|
+
// and the endpoint requires no cookie / Referer / User-Agent — observed). Decode the
|
|
200
|
+
// body per the response's DECLARED charset so Latin-1 accents survive (the palena
|
|
201
|
+
// DTE report is ISO-8859-1, which a default UTF-8 decode would corrupt).
|
|
202
|
+
const headers = { ...options.headers };
|
|
203
|
+
let body;
|
|
204
|
+
if (options.form) {
|
|
205
|
+
body = new URLSearchParams(options.form).toString();
|
|
206
|
+
headers['Content-Type'] ??= 'application/x-www-form-urlencoded';
|
|
207
|
+
}
|
|
208
|
+
// Bound the request so a hung CGI fails loud instead of blocking the surface
|
|
209
|
+
// indefinitely (ADR-004 "never hang"); 30s mirrors the ported Python timeout. On
|
|
210
|
+
// timeout fetch rejects → the facade wraps it as DteError (no retry, ADR-004).
|
|
211
|
+
const response = await fetch(url, {
|
|
212
|
+
method: options.method ?? 'POST',
|
|
213
|
+
headers,
|
|
214
|
+
signal: AbortSignal.timeout(30_000),
|
|
215
|
+
...(body !== undefined ? { body } : {}),
|
|
216
|
+
});
|
|
217
|
+
const buffer = await response.arrayBuffer();
|
|
218
|
+
const text = new TextDecoder(charsetOf(response.headers.get('content-type'))).decode(buffer);
|
|
219
|
+
return { status: response.status, body: text };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=portal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"portal.js","sourceRoot":"","sources":["../../../src/adapters/node/portal.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Classify a non-JSON SDI response. A dead/expired session makes an authenticated
|
|
2
|
+
* SDI POST get bounced to SII's login wall (HTML) instead of JSON; detect it the same
|
|
3
|
+
* URL-based way the rest of the auth flow does — landing on `LOGIN_HOST`, with an
|
|
4
|
+
* HTML content-type fallback for a same-host wall (ADR-009) — and return an ACTIONABLE
|
|
5
|
+
* `SessionExpiredError`. Anything else is a genuinely unexpected response → a generic
|
|
6
|
+
* Error (the facade maps it to its own typed error). Pure, so it is unit-tested
|
|
7
|
+
* without launching Playwright. */
|
|
8
|
+
export declare function nonJsonResponseError(finalUrl: string, contentType: string, status: number): Error;
|
|
9
|
+
/** Login-wall detection for an authenticated FORM POST (ADR-017). Unlike `requestJson`,
|
|
10
|
+
* an HTML body is EXPECTED (the `TMBECN_*` emit CGIs render HTML), so the content-type
|
|
11
|
+
* heuristic can't apply — a dead session is detected purely by the response landing on
|
|
12
|
+
* `LOGIN_HOST` (URL-based, ADR-009). Returns an actionable `SessionExpiredError`, else
|
|
13
|
+
* null. Pure → unit-tested without a browser. */
|
|
14
|
+
export declare function formLoginWallError(finalUrl: string): Error | null;
|
|
15
|
+
/** Extract the charset label from a `Content-Type` header for decoding a public
|
|
16
|
+
* (unauthenticated) response body (ADR-014). SII's palena reports declare
|
|
17
|
+
* `text/html; charset=ISO-8859-1`, so a UTF-8 decode would mangle accents (ó, é,
|
|
18
|
+
* °) — we honour the DECLARED charset and fall back to UTF-8 when it is absent or
|
|
19
|
+
* not a label `TextDecoder` accepts. Pure → unit-tested without launching fetch. */
|
|
20
|
+
export declare function charsetOf(contentType: string | null | undefined): string;
|
|
21
|
+
//# sourceMappingURL=response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.d.ts","sourceRoot":"","sources":["../../../src/adapters/node/response.ts"],"names":[],"mappings":"AAGA;;;;;;oCAMoC;AACpC,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAOjG;AAED;;;;kDAIkD;AAClD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,IAAI,CAIjE;AAED;;;;qFAIqF;AACrF,wBAAgB,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAYxE"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { LOGIN_HOST } from '../../config/index.js';
|
|
2
|
+
import { SessionExpiredError } from '../../errors/index.js';
|
|
3
|
+
/** Classify a non-JSON SDI response. A dead/expired session makes an authenticated
|
|
4
|
+
* SDI POST get bounced to SII's login wall (HTML) instead of JSON; detect it the same
|
|
5
|
+
* URL-based way the rest of the auth flow does — landing on `LOGIN_HOST`, with an
|
|
6
|
+
* HTML content-type fallback for a same-host wall (ADR-009) — and return an ACTIONABLE
|
|
7
|
+
* `SessionExpiredError`. Anything else is a genuinely unexpected response → a generic
|
|
8
|
+
* Error (the facade maps it to its own typed error). Pure, so it is unit-tested
|
|
9
|
+
* without launching Playwright. */
|
|
10
|
+
export function nonJsonResponseError(finalUrl, contentType, status) {
|
|
11
|
+
const ct = contentType.toLowerCase();
|
|
12
|
+
const bouncedToLogin = new URL(finalUrl).hostname === LOGIN_HOST;
|
|
13
|
+
if (bouncedToLogin || ct.includes('text/html')) {
|
|
14
|
+
return new SessionExpiredError('La sesión expiró. Ejecuta `sii auth login`.');
|
|
15
|
+
}
|
|
16
|
+
return new Error(`Respuesta no-JSON de SII (HTTP ${status}, ${ct || 'sin content-type'}).`);
|
|
17
|
+
}
|
|
18
|
+
/** Login-wall detection for an authenticated FORM POST (ADR-017). Unlike `requestJson`,
|
|
19
|
+
* an HTML body is EXPECTED (the `TMBECN_*` emit CGIs render HTML), so the content-type
|
|
20
|
+
* heuristic can't apply — a dead session is detected purely by the response landing on
|
|
21
|
+
* `LOGIN_HOST` (URL-based, ADR-009). Returns an actionable `SessionExpiredError`, else
|
|
22
|
+
* null. Pure → unit-tested without a browser. */
|
|
23
|
+
export function formLoginWallError(finalUrl) {
|
|
24
|
+
return new URL(finalUrl).hostname === LOGIN_HOST
|
|
25
|
+
? new SessionExpiredError('La sesión expiró. Ejecuta `sii auth login`.')
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
28
|
+
/** Extract the charset label from a `Content-Type` header for decoding a public
|
|
29
|
+
* (unauthenticated) response body (ADR-014). SII's palena reports declare
|
|
30
|
+
* `text/html; charset=ISO-8859-1`, so a UTF-8 decode would mangle accents (ó, é,
|
|
31
|
+
* °) — we honour the DECLARED charset and fall back to UTF-8 when it is absent or
|
|
32
|
+
* not a label `TextDecoder` accepts. Pure → unit-tested without launching fetch. */
|
|
33
|
+
export function charsetOf(contentType) {
|
|
34
|
+
const label = /charset=([^;]+)/i
|
|
35
|
+
.exec(contentType ?? '')?.[1]
|
|
36
|
+
?.trim()
|
|
37
|
+
.replace(/^["']|["']$/g, '');
|
|
38
|
+
if (!label)
|
|
39
|
+
return 'utf-8';
|
|
40
|
+
try {
|
|
41
|
+
new TextDecoder(label); // validate the label; an unknown one throws RangeError
|
|
42
|
+
return label;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return 'utf-8';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=response.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response.js","sourceRoot":"","sources":["../../../src/adapters/node/response.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACnD,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAE5D;;;;;;oCAMoC;AACpC,MAAM,UAAU,oBAAoB,CAAC,QAAgB,EAAE,WAAmB,EAAE,MAAc;IACxF,MAAM,EAAE,GAAG,WAAW,CAAC,WAAW,EAAE,CAAC;IACrC,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC;IACjE,IAAI,cAAc,IAAI,EAAE,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;QAC/C,OAAO,IAAI,mBAAmB,CAAC,6CAA6C,CAAC,CAAC;IAChF,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,kCAAkC,MAAM,KAAK,EAAE,IAAI,kBAAkB,IAAI,CAAC,CAAC;AAC9F,CAAC;AAED;;;;kDAIkD;AAClD,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,OAAO,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,QAAQ,KAAK,UAAU;QAC9C,CAAC,CAAC,IAAI,mBAAmB,CAAC,6CAA6C,CAAC;QACxE,CAAC,CAAC,IAAI,CAAC;AACX,CAAC;AAED;;;;qFAIqF;AACrF,MAAM,UAAU,SAAS,CAAC,WAAsC;IAC9D,MAAM,KAAK,GAAG,kBAAkB;SAC7B,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC7B,EAAE,IAAI,EAAE;SACP,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;IAC/B,IAAI,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC;IAC3B,IAAI,CAAC;QACH,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,uDAAuD;QAC/E,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AuditEntry, AuditSink, Clock } from '../seams/index.js';
|
|
2
|
+
/** Compose the final receipt: stamp `ts` from the clock, drop secret-substring
|
|
3
|
+
* keys, hand it to the sink. Never throws (the sink is best-effort). (ADR-004) */
|
|
4
|
+
export declare function recordAudit(deps: {
|
|
5
|
+
clock: Clock;
|
|
6
|
+
audit: AuditSink;
|
|
7
|
+
}, entry: AuditEntry): void;
|
|
8
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/audit/audit.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAMtE;mFACmF;AACnF,wBAAgB,WAAW,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAO7F"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Keys whose presence (substring, case-insensitive) means "secret" — dropped
|
|
2
|
+
* before the line hits the sink. RUTs are NOT secrets and pass through. */
|
|
3
|
+
const SECRET_KEY = /password|clave|cookie|secret|token/i;
|
|
4
|
+
/** Compose the final receipt: stamp `ts` from the clock, drop secret-substring
|
|
5
|
+
* keys, hand it to the sink. Never throws (the sink is best-effort). (ADR-004) */
|
|
6
|
+
export function recordAudit(deps, entry) {
|
|
7
|
+
const safe = { ts: deps.clock.now().toISOString() };
|
|
8
|
+
for (const [k, v] of Object.entries(entry)) {
|
|
9
|
+
if (SECRET_KEY.test(k))
|
|
10
|
+
continue;
|
|
11
|
+
safe[k] = v;
|
|
12
|
+
}
|
|
13
|
+
deps.audit.record(safe);
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/audit/audit.ts"],"names":[],"mappings":"AAEA;4EAC4E;AAC5E,MAAM,UAAU,GAAG,qCAAqC,CAAC;AAEzD;mFACmF;AACnF,MAAM,UAAU,WAAW,CAAC,IAAwC,EAAE,KAAiB;IACrF,MAAM,IAAI,GAA4B,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;IAC7E,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;YAAE,SAAS;QACjC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACd,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAkB,CAAC,CAAC;AACxC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AccountType } from '../identity/index.js';
|
|
2
|
+
import type { Runtime } from '../seams/index.js';
|
|
3
|
+
export interface AuthIdentity {
|
|
4
|
+
readonly rut: string;
|
|
5
|
+
readonly nombre: string | null;
|
|
6
|
+
readonly accountType: AccountType;
|
|
7
|
+
}
|
|
8
|
+
export interface AuthStatusLocal {
|
|
9
|
+
/** LOCAL-only: a cookie jar exists on disk. NOT a server-side liveness claim. */
|
|
10
|
+
readonly authenticated: boolean;
|
|
11
|
+
readonly rut: string | null;
|
|
12
|
+
readonly sessionSource: 'cached' | 'none';
|
|
13
|
+
}
|
|
14
|
+
export interface AuthLoginResult {
|
|
15
|
+
readonly authenticated: true;
|
|
16
|
+
readonly rut: string;
|
|
17
|
+
readonly reason: 'browser_login' | 'console_login' | 'already_authenticated';
|
|
18
|
+
}
|
|
19
|
+
export interface AuthLogoutResult {
|
|
20
|
+
readonly loggedOut: boolean;
|
|
21
|
+
readonly serverClosed: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** Pure local read — NO portal call (sii-py "local-only" labelling). */
|
|
24
|
+
export declare function localStatus(store: Runtime['store']): Promise<AuthStatusLocal>;
|
|
25
|
+
/** Browser cookies-only login (ADR-006). Only this + `consoleLogin` mint a session
|
|
26
|
+
* (ADR-019 lineage). Idempotent: a live cached session returns
|
|
27
|
+
* `already_authenticated` without opening a window. */
|
|
28
|
+
export declare function login(runtime: Runtime): Promise<AuthLoginResult>;
|
|
29
|
+
/** CLI-only console login (ADR-010): the Clave is typed into the TERMINAL, used
|
|
30
|
+
* once to fill SII's real form headless, and never persisted — only cookies are
|
|
31
|
+
* stored, exactly like the browser path. ONE attempt, never retried (ADR-004).
|
|
32
|
+
* The Clave never reaches MCP (this task is CLI-only) nor the audit log. */
|
|
33
|
+
export declare function consoleLogin(runtime: Runtime, credentials: {
|
|
34
|
+
rut: string;
|
|
35
|
+
clave: string;
|
|
36
|
+
}): Promise<AuthLoginResult>;
|
|
37
|
+
/** Server-side close (best-effort) + wipe local session + operate context. */
|
|
38
|
+
export declare function logout(runtime: Runtime): Promise<AuthLogoutResult>;
|
|
39
|
+
/** Curated identity readback from the portal. Requires a live session (no implicit
|
|
40
|
+
* login) — acquired via `withSession`; here an expired jar is an explicit
|
|
41
|
+
* NotAuthenticated (URL-based detection), since the whole job is the readback. */
|
|
42
|
+
export declare function statusRefresh(runtime: Runtime): Promise<AuthIdentity>;
|
|
43
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/auth/auth.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAiB,MAAM,sBAAsB,CAAC;AAGvE,OAAO,KAAK,EAAiB,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAsBhE,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;CACnC;AAED,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,QAAQ,GAAG,MAAM,CAAC;CAC3C;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,GAAG,uBAAuB,CAAC;CAC9E;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAChC;AAED,wEAAwE;AACxE,wBAAsB,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,eAAe,CAAC,CAKnF;AA+HD;;wDAEwD;AACxD,wBAAsB,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,eAAe,CAAC,CAkBtE;AAED;;;6EAG6E;AAC7E,wBAAsB,YAAY,CAChC,OAAO,EAAE,OAAO,EAChB,WAAW,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,eAAe,CAAC,CAoB1B;AAED,8EAA8E;AAC9E,wBAAsB,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAuBxE;AAED;;mFAEmF;AACnF,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,YAAY,CAAC,CAS3E"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { HOSTS, LOGIN_HOST, LOGOUT_URL } from '../config/index.js';
|
|
2
|
+
import { LoginFailedError, NotAuthenticatedError } from '../errors/index.js';
|
|
3
|
+
import { Rut } from '../rut/index.js';
|
|
4
|
+
import { recordAudit } from '../audit/index.js';
|
|
5
|
+
import { clearOperateState, initOperateState } from '../identity/index.js';
|
|
6
|
+
import { fetchEmpresasAutorizadas } from '../portal/representacion.js';
|
|
7
|
+
import { deleteSession, readSession, withSession, writeSession } from './session.js';
|
|
8
|
+
const DEFAULT_LOGIN_TIMEOUT_MS = 180_000;
|
|
9
|
+
// Console login submits machine-fast (no human typing in the browser) and fails
|
|
10
|
+
// fast on a rejected Clave, so it needs a far smaller budget than the headed flow.
|
|
11
|
+
const CONSOLE_LOGIN_TIMEOUT_MS = 60_000;
|
|
12
|
+
// The Mi-SII landing serves this inline JS object with the contribuyente snapshot.
|
|
13
|
+
const DATOS_EXPR = "typeof DatosCntrNow !== 'undefined' ? DatosCntrNow : null";
|
|
14
|
+
/** Pure local read — NO portal call (sii-py "local-only" labelling). */
|
|
15
|
+
export async function localStatus(store) {
|
|
16
|
+
const session = await readSession(store);
|
|
17
|
+
return session
|
|
18
|
+
? { authenticated: true, rut: session.rut, sessionSource: 'cached' }
|
|
19
|
+
: { authenticated: false, rut: null, sessionSource: 'none' };
|
|
20
|
+
}
|
|
21
|
+
function identityFromDatos(datos) {
|
|
22
|
+
const c = datos?.contribuyente;
|
|
23
|
+
if (!c || c.rut === undefined || c.dv === undefined) {
|
|
24
|
+
throw new LoginFailedError('No se pudo leer la identidad del portal (DatosCntrNow ausente).');
|
|
25
|
+
}
|
|
26
|
+
const rut = Rut.parse(`${c.rut}-${c.dv}`).canonical;
|
|
27
|
+
const accountType = c.razonSocial ? 'empresa' : 'persona';
|
|
28
|
+
const joined = [c.nombres, c.apellidoPaterno, c.apellidoMaterno].filter(Boolean).join(' ').trim();
|
|
29
|
+
const nombre = c.razonSocial ?? (joined || null);
|
|
30
|
+
return { rut, nombre, accountType };
|
|
31
|
+
}
|
|
32
|
+
function landedOnLoginHost(landed) {
|
|
33
|
+
return new URL(landed).hostname === LOGIN_HOST;
|
|
34
|
+
}
|
|
35
|
+
/** The session-principal RUT if the cached session is still live on the portal, else
|
|
36
|
+
* null — never throws (the login path needs "is it warm?", not an error). A single
|
|
37
|
+
* `withSession` acquisition (no separate pre-read); a missing/expired session → null. */
|
|
38
|
+
async function liveSessionRut(runtime) {
|
|
39
|
+
try {
|
|
40
|
+
return await withSession(runtime, async (s, ctx) => landedOnLoginHost(await s.goto(HOSTS.miSii)) ? null : ctx.sessionRut);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** If a cached session is still live, return `already_authenticated` (no mint).
|
|
47
|
+
* Shared by both login paths so neither re-mints over a warm session. */
|
|
48
|
+
async function reuseLiveSession(runtime) {
|
|
49
|
+
const rut = await liveSessionRut(runtime);
|
|
50
|
+
if (rut) {
|
|
51
|
+
recordAudit(runtime, {
|
|
52
|
+
action: 'auth_login',
|
|
53
|
+
result: 'ok',
|
|
54
|
+
rut,
|
|
55
|
+
reason: 'already_authenticated',
|
|
56
|
+
});
|
|
57
|
+
return { authenticated: true, rut, reason: 'already_authenticated' };
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/** Best-effort operable-set fetch on login (ADR-005). Persona accounts ask SII for
|
|
62
|
+
* the empresas they can operate (getDcvEmpresasAutorizadas); empresa accounts have
|
|
63
|
+
* no representación, so operable = [self]. ANY failure degrades to [self] — a login
|
|
64
|
+
* must never fail because the operable lookup did. Razón social is PII → never
|
|
65
|
+
* audited (only the count). */
|
|
66
|
+
async function resolveOperable(runtime, session, identity) {
|
|
67
|
+
const self = {
|
|
68
|
+
rut: identity.rut,
|
|
69
|
+
razonSocial: identity.nombre ?? identity.rut,
|
|
70
|
+
isSelf: true,
|
|
71
|
+
};
|
|
72
|
+
if (identity.accountType === 'empresa')
|
|
73
|
+
return [self];
|
|
74
|
+
try {
|
|
75
|
+
const { empresas } = await fetchEmpresasAutorizadas(session, identity.rut);
|
|
76
|
+
const entries = empresas
|
|
77
|
+
.filter((e) => e.rut !== null)
|
|
78
|
+
.map((e) => ({
|
|
79
|
+
rut: e.rut,
|
|
80
|
+
razonSocial: e.razonSocial ?? e.rut,
|
|
81
|
+
isSelf: e.rut === identity.rut,
|
|
82
|
+
}));
|
|
83
|
+
// The endpoint includes self, but be defensive: guarantee exactly one self row.
|
|
84
|
+
const operable = entries.some((e) => e.isSelf) ? entries : [self, ...entries];
|
|
85
|
+
recordAudit(runtime, {
|
|
86
|
+
action: 'operable_fetch',
|
|
87
|
+
result: 'ok',
|
|
88
|
+
rut: identity.rut,
|
|
89
|
+
count: operable.length,
|
|
90
|
+
});
|
|
91
|
+
return operable;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
recordAudit(runtime, { action: 'operable_fetch', result: 'failed', rut: identity.rut });
|
|
95
|
+
return [self];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Turn a freshly-minted PortalSession into a persisted cookies-only session:
|
|
99
|
+
* confirm we landed off the login host, read identity, persist cookies (NO
|
|
100
|
+
* secret), default operate to self. Shared by the browser + console paths. */
|
|
101
|
+
async function finalizeFreshSession(runtime, session, reason, start) {
|
|
102
|
+
const landed = await session.goto(HOSTS.miSii);
|
|
103
|
+
if (landedOnLoginHost(landed)) {
|
|
104
|
+
throw new LoginFailedError('Login no completado (seguimos en la página de autenticación).');
|
|
105
|
+
}
|
|
106
|
+
const datos = await session.evaluate(DATOS_EXPR);
|
|
107
|
+
const identity = identityFromDatos(datos);
|
|
108
|
+
const cookies = await session.storageState();
|
|
109
|
+
await writeSession(runtime.store, {
|
|
110
|
+
rut: identity.rut,
|
|
111
|
+
cookies,
|
|
112
|
+
savedAt: runtime.clock.now().toISOString(),
|
|
113
|
+
});
|
|
114
|
+
// Operate defaults to self; the operable set is fetched best-effort (ADR-005).
|
|
115
|
+
const operable = await resolveOperable(runtime, session, identity);
|
|
116
|
+
await initOperateState(runtime.store, {
|
|
117
|
+
selfRut: identity.rut,
|
|
118
|
+
accountType: identity.accountType,
|
|
119
|
+
operable,
|
|
120
|
+
});
|
|
121
|
+
recordAudit(runtime, {
|
|
122
|
+
action: 'auth_login',
|
|
123
|
+
result: 'ok',
|
|
124
|
+
rut: identity.rut,
|
|
125
|
+
reason,
|
|
126
|
+
durationMs: runtime.clock.now().getTime() - start,
|
|
127
|
+
});
|
|
128
|
+
return { authenticated: true, rut: identity.rut, reason };
|
|
129
|
+
}
|
|
130
|
+
/** Browser cookies-only login (ADR-006). Only this + `consoleLogin` mint a session
|
|
131
|
+
* (ADR-019 lineage). Idempotent: a live cached session returns
|
|
132
|
+
* `already_authenticated` without opening a window. */
|
|
133
|
+
export async function login(runtime) {
|
|
134
|
+
const start = runtime.clock.now().getTime();
|
|
135
|
+
const warm = await reuseLiveSession(runtime);
|
|
136
|
+
if (warm)
|
|
137
|
+
return warm;
|
|
138
|
+
let session = null;
|
|
139
|
+
try {
|
|
140
|
+
session = await runtime.portal.interactiveLogin({
|
|
141
|
+
destination: HOSTS.miSii,
|
|
142
|
+
timeoutMs: DEFAULT_LOGIN_TIMEOUT_MS,
|
|
143
|
+
});
|
|
144
|
+
return await finalizeFreshSession(runtime, session, 'browser_login', start);
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
recordAudit(runtime, { action: 'auth_login', result: 'failed', reason: 'browser_login' });
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await session?.close();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** CLI-only console login (ADR-010): the Clave is typed into the TERMINAL, used
|
|
155
|
+
* once to fill SII's real form headless, and never persisted — only cookies are
|
|
156
|
+
* stored, exactly like the browser path. ONE attempt, never retried (ADR-004).
|
|
157
|
+
* The Clave never reaches MCP (this task is CLI-only) nor the audit log. */
|
|
158
|
+
export async function consoleLogin(runtime, credentials) {
|
|
159
|
+
const start = runtime.clock.now().getTime();
|
|
160
|
+
const warm = await reuseLiveSession(runtime);
|
|
161
|
+
if (warm)
|
|
162
|
+
return warm;
|
|
163
|
+
let session = null;
|
|
164
|
+
try {
|
|
165
|
+
session = await runtime.portal.credentialLogin({
|
|
166
|
+
rut: credentials.rut,
|
|
167
|
+
clave: credentials.clave,
|
|
168
|
+
destination: HOSTS.miSii,
|
|
169
|
+
timeoutMs: CONSOLE_LOGIN_TIMEOUT_MS,
|
|
170
|
+
});
|
|
171
|
+
return await finalizeFreshSession(runtime, session, 'console_login', start);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
recordAudit(runtime, { action: 'auth_login', result: 'failed', reason: 'console_login' });
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
await session?.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Server-side close (best-effort) + wipe local session + operate context. */
|
|
182
|
+
export async function logout(runtime) {
|
|
183
|
+
const session = await readSession(runtime.store);
|
|
184
|
+
if (!session) {
|
|
185
|
+
recordAudit(runtime, { action: 'logout', result: 'ok', serverClosed: false });
|
|
186
|
+
return { loggedOut: false, serverClosed: false };
|
|
187
|
+
}
|
|
188
|
+
let serverClosed = false;
|
|
189
|
+
let s = null;
|
|
190
|
+
try {
|
|
191
|
+
s = await runtime.portal.restore(session.cookies);
|
|
192
|
+
const landed = await s.goto(LOGOUT_URL);
|
|
193
|
+
serverClosed = new URL(landed).pathname !== new URL(LOGOUT_URL).pathname;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// best-effort server close; the local wipe still runs
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await s?.close();
|
|
200
|
+
}
|
|
201
|
+
await deleteSession(runtime.store);
|
|
202
|
+
await clearOperateState(runtime.store);
|
|
203
|
+
recordAudit(runtime, { action: 'logout', result: 'ok', rut: session.rut, serverClosed });
|
|
204
|
+
return { loggedOut: true, serverClosed };
|
|
205
|
+
}
|
|
206
|
+
/** Curated identity readback from the portal. Requires a live session (no implicit
|
|
207
|
+
* login) — acquired via `withSession`; here an expired jar is an explicit
|
|
208
|
+
* NotAuthenticated (URL-based detection), since the whole job is the readback. */
|
|
209
|
+
export async function statusRefresh(runtime) {
|
|
210
|
+
return withSession(runtime, async (s) => {
|
|
211
|
+
if (landedOnLoginHost(await s.goto(HOSTS.miSii))) {
|
|
212
|
+
throw new NotAuthenticatedError('La sesión expiró. Ejecuta `sii auth login`.');
|
|
213
|
+
}
|
|
214
|
+
const identity = identityFromDatos(await s.evaluate(DATOS_EXPR));
|
|
215
|
+
recordAudit(runtime, { action: 'auth_status_refresh', result: 'ok', rut: identity.rut });
|
|
216
|
+
return identity;
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=auth.js.map
|