@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.
Files changed (200) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/LICENSE +21 -0
  3. package/README.md +108 -0
  4. package/dist/adapters/fake/index.d.ts +98 -0
  5. package/dist/adapters/fake/index.d.ts.map +1 -0
  6. package/dist/adapters/fake/index.js +131 -0
  7. package/dist/adapters/fake/index.js.map +1 -0
  8. package/dist/adapters/node/index.d.ts +22 -0
  9. package/dist/adapters/node/index.d.ts.map +1 -0
  10. package/dist/adapters/node/index.js +65 -0
  11. package/dist/adapters/node/index.js.map +1 -0
  12. package/dist/adapters/node/portal.d.ts +8 -0
  13. package/dist/adapters/node/portal.d.ts.map +1 -0
  14. package/dist/adapters/node/portal.js +222 -0
  15. package/dist/adapters/node/portal.js.map +1 -0
  16. package/dist/adapters/node/response.d.ts +21 -0
  17. package/dist/adapters/node/response.d.ts.map +1 -0
  18. package/dist/adapters/node/response.js +48 -0
  19. package/dist/adapters/node/response.js.map +1 -0
  20. package/dist/audit/audit.d.ts +8 -0
  21. package/dist/audit/audit.d.ts.map +1 -0
  22. package/dist/audit/audit.js +15 -0
  23. package/dist/audit/audit.js.map +1 -0
  24. package/dist/audit/index.d.ts +2 -0
  25. package/dist/audit/index.d.ts.map +1 -0
  26. package/dist/audit/index.js +2 -0
  27. package/dist/audit/index.js.map +1 -0
  28. package/dist/auth/auth.d.ts +43 -0
  29. package/dist/auth/auth.d.ts.map +1 -0
  30. package/dist/auth/auth.js +219 -0
  31. package/dist/auth/auth.js.map +1 -0
  32. package/dist/auth/index.d.ts +3 -0
  33. package/dist/auth/index.d.ts.map +1 -0
  34. package/dist/auth/index.js +3 -0
  35. package/dist/auth/index.js.map +1 -0
  36. package/dist/auth/login-error.d.ts +8 -0
  37. package/dist/auth/login-error.d.ts.map +1 -0
  38. package/dist/auth/login-error.js +18 -0
  39. package/dist/auth/login-error.js.map +1 -0
  40. package/dist/auth/session.d.ts +31 -0
  41. package/dist/auth/session.d.ts.map +1 -0
  42. package/dist/auth/session.js +52 -0
  43. package/dist/auth/session.js.map +1 -0
  44. package/dist/cli.d.ts +3 -0
  45. package/dist/cli.d.ts.map +1 -0
  46. package/dist/cli.js +6 -0
  47. package/dist/cli.js.map +1 -0
  48. package/dist/config/config.d.ts +38 -0
  49. package/dist/config/config.d.ts.map +1 -0
  50. package/dist/config/config.js +40 -0
  51. package/dist/config/config.js.map +1 -0
  52. package/dist/config/index.d.ts +2 -0
  53. package/dist/config/index.d.ts.map +1 -0
  54. package/dist/config/index.js +2 -0
  55. package/dist/config/index.js.map +1 -0
  56. package/dist/errors/errors.d.ts +56 -0
  57. package/dist/errors/errors.d.ts.map +1 -0
  58. package/dist/errors/errors.js +62 -0
  59. package/dist/errors/errors.js.map +1 -0
  60. package/dist/errors/index.d.ts +2 -0
  61. package/dist/errors/index.d.ts.map +1 -0
  62. package/dist/errors/index.js +2 -0
  63. package/dist/errors/index.js.map +1 -0
  64. package/dist/format/format.d.ts +5 -0
  65. package/dist/format/format.d.ts.map +1 -0
  66. package/dist/format/format.js +14 -0
  67. package/dist/format/format.js.map +1 -0
  68. package/dist/format/index.d.ts +2 -0
  69. package/dist/format/index.d.ts.map +1 -0
  70. package/dist/format/index.js +2 -0
  71. package/dist/format/index.js.map +1 -0
  72. package/dist/identity/identity.d.ts +56 -0
  73. package/dist/identity/identity.d.ts.map +1 -0
  74. package/dist/identity/identity.js +93 -0
  75. package/dist/identity/identity.js.map +1 -0
  76. package/dist/identity/index.d.ts +2 -0
  77. package/dist/identity/index.d.ts.map +1 -0
  78. package/dist/identity/index.js +2 -0
  79. package/dist/identity/index.js.map +1 -0
  80. package/dist/index.d.ts +24 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +24 -0
  83. package/dist/index.js.map +1 -0
  84. package/dist/node.d.ts +9 -0
  85. package/dist/node.d.ts.map +1 -0
  86. package/dist/node.js +22 -0
  87. package/dist/node.js.map +1 -0
  88. package/dist/periodo/anio.d.ts +15 -0
  89. package/dist/periodo/anio.d.ts.map +1 -0
  90. package/dist/periodo/anio.js +42 -0
  91. package/dist/periodo/anio.js.map +1 -0
  92. package/dist/periodo/index.d.ts +3 -0
  93. package/dist/periodo/index.d.ts.map +1 -0
  94. package/dist/periodo/index.js +3 -0
  95. package/dist/periodo/index.js.map +1 -0
  96. package/dist/periodo/periodo.d.ts +19 -0
  97. package/dist/periodo/periodo.d.ts.map +1 -0
  98. package/dist/periodo/periodo.js +55 -0
  99. package/dist/periodo/periodo.js.map +1 -0
  100. package/dist/portal/bte-comunas.d.ts +9 -0
  101. package/dist/portal/bte-comunas.d.ts.map +1 -0
  102. package/dist/portal/bte-comunas.js +400 -0
  103. package/dist/portal/bte-comunas.js.map +1 -0
  104. package/dist/portal/bte-emit.d.ts +70 -0
  105. package/dist/portal/bte-emit.d.ts.map +1 -0
  106. package/dist/portal/bte-emit.js +266 -0
  107. package/dist/portal/bte-emit.js.map +1 -0
  108. package/dist/portal/bte.d.ts +55 -0
  109. package/dist/portal/bte.d.ts.map +1 -0
  110. package/dist/portal/bte.js +215 -0
  111. package/dist/portal/bte.js.map +1 -0
  112. package/dist/portal/dte-public.d.ts +36 -0
  113. package/dist/portal/dte-public.d.ts.map +1 -0
  114. package/dist/portal/dte-public.js +192 -0
  115. package/dist/portal/dte-public.js.map +1 -0
  116. package/dist/portal/f22/declaraciones.d.ts +37 -0
  117. package/dist/portal/f22/declaraciones.d.ts.map +1 -0
  118. package/dist/portal/f22/declaraciones.js +86 -0
  119. package/dist/portal/f22/declaraciones.js.map +1 -0
  120. package/dist/portal/f22/grid.d.ts +14 -0
  121. package/dist/portal/f22/grid.d.ts.map +1 -0
  122. package/dist/portal/f22/grid.js +51 -0
  123. package/dist/portal/f22/grid.js.map +1 -0
  124. package/dist/portal/f22/historial.d.ts +34 -0
  125. package/dist/portal/f22/historial.d.ts.map +1 -0
  126. package/dist/portal/f22/historial.js +66 -0
  127. package/dist/portal/f22/historial.js.map +1 -0
  128. package/dist/portal/f22/index.d.ts +7 -0
  129. package/dist/portal/f22/index.d.ts.map +1 -0
  130. package/dist/portal/f22/index.js +11 -0
  131. package/dist/portal/f22/index.js.map +1 -0
  132. package/dist/portal/f22/observaciones.d.ts +20 -0
  133. package/dist/portal/f22/observaciones.d.ts.map +1 -0
  134. package/dist/portal/f22/observaciones.js +38 -0
  135. package/dist/portal/f22/observaciones.js.map +1 -0
  136. package/dist/portal/f22/shared.d.ts +32 -0
  137. package/dist/portal/f22/shared.d.ts.map +1 -0
  138. package/dist/portal/f22/shared.js +130 -0
  139. package/dist/portal/f22/shared.js.map +1 -0
  140. package/dist/portal/f22-codigos.d.ts +28 -0
  141. package/dist/portal/f22-codigos.d.ts.map +1 -0
  142. package/dist/portal/f22-codigos.js +141 -0
  143. package/dist/portal/f22-codigos.js.map +1 -0
  144. package/dist/portal/f29-codigos.d.ts +15 -0
  145. package/dist/portal/f29-codigos.d.ts.map +1 -0
  146. package/dist/portal/f29-codigos.js +552 -0
  147. package/dist/portal/f29-codigos.js.map +1 -0
  148. package/dist/portal/f29.d.ts +62 -0
  149. package/dist/portal/f29.d.ts.map +1 -0
  150. package/dist/portal/f29.js +244 -0
  151. package/dist/portal/f29.js.map +1 -0
  152. package/dist/portal/rcv.d.ts +62 -0
  153. package/dist/portal/rcv.d.ts.map +1 -0
  154. package/dist/portal/rcv.js +252 -0
  155. package/dist/portal/rcv.js.map +1 -0
  156. package/dist/portal/representacion.d.ts +24 -0
  157. package/dist/portal/representacion.d.ts.map +1 -0
  158. package/dist/portal/representacion.js +153 -0
  159. package/dist/portal/representacion.js.map +1 -0
  160. package/dist/rut/index.d.ts +2 -0
  161. package/dist/rut/index.d.ts.map +1 -0
  162. package/dist/rut/index.js +2 -0
  163. package/dist/rut/index.js.map +1 -0
  164. package/dist/rut/rut.d.ts +16 -0
  165. package/dist/rut/rut.d.ts.map +1 -0
  166. package/dist/rut/rut.js +70 -0
  167. package/dist/rut/rut.js.map +1 -0
  168. package/dist/seams/index.d.ts +126 -0
  169. package/dist/seams/index.d.ts.map +1 -0
  170. package/dist/seams/index.js +6 -0
  171. package/dist/seams/index.js.map +1 -0
  172. package/dist/tasks/auth.d.ts +16 -0
  173. package/dist/tasks/auth.d.ts.map +1 -0
  174. package/dist/tasks/auth.js +22 -0
  175. package/dist/tasks/auth.js.map +1 -0
  176. package/dist/tasks/bte.d.ts +52 -0
  177. package/dist/tasks/bte.d.ts.map +1 -0
  178. package/dist/tasks/bte.js +169 -0
  179. package/dist/tasks/bte.js.map +1 -0
  180. package/dist/tasks/dte.d.ts +9 -0
  181. package/dist/tasks/dte.d.ts.map +1 -0
  182. package/dist/tasks/dte.js +30 -0
  183. package/dist/tasks/dte.js.map +1 -0
  184. package/dist/tasks/f22.d.ts +69 -0
  185. package/dist/tasks/f22.d.ts.map +1 -0
  186. package/dist/tasks/f22.js +189 -0
  187. package/dist/tasks/f22.js.map +1 -0
  188. package/dist/tasks/f29.d.ts +67 -0
  189. package/dist/tasks/f29.d.ts.map +1 -0
  190. package/dist/tasks/f29.js +213 -0
  191. package/dist/tasks/f29.js.map +1 -0
  192. package/dist/tasks/operate.d.ts +22 -0
  193. package/dist/tasks/operate.d.ts.map +1 -0
  194. package/dist/tasks/operate.js +34 -0
  195. package/dist/tasks/operate.js.map +1 -0
  196. package/dist/tasks/rcv.d.ts +17 -0
  197. package/dist/tasks/rcv.d.ts.map +1 -0
  198. package/dist/tasks/rcv.js +76 -0
  199. package/dist/tasks/rcv.js.map +1 -0
  200. 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,2 @@
1
+ export * from './audit.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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,2 @@
1
+ export * from './audit.js';
2
+ //# sourceMappingURL=index.js.map
@@ -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