@dozimple/abap-adt 1.0.0-rc.1

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 (75) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +19 -0
  4. package/README.es.md +356 -0
  5. package/README.md +356 -0
  6. package/SECURITY.md +68 -0
  7. package/THIRD_PARTY_NOTICES.md +134 -0
  8. package/config/systems.example.json +39 -0
  9. package/dist/core/activation.js +18 -0
  10. package/dist/core/atc.js +94 -0
  11. package/dist/core/audit.js +93 -0
  12. package/dist/core/catalog.en.js +92 -0
  13. package/dist/core/catalog.js +139 -0
  14. package/dist/core/checks.js +19 -0
  15. package/dist/core/config.js +136 -0
  16. package/dist/core/confirm.js +43 -0
  17. package/dist/core/connection.js +159 -0
  18. package/dist/core/credentials.js +44 -0
  19. package/dist/core/datapolicy.js +175 -0
  20. package/dist/core/diff.js +115 -0
  21. package/dist/core/edits.js +31 -0
  22. package/dist/core/errors.js +68 -0
  23. package/dist/core/feeds.js +60 -0
  24. package/dist/core/notes.js +18 -0
  25. package/dist/core/objects.js +143 -0
  26. package/dist/core/output.js +27 -0
  27. package/dist/core/policy.js +126 -0
  28. package/dist/core/prompts.js +89 -0
  29. package/dist/core/registry.js +316 -0
  30. package/dist/core/revisions.js +97 -0
  31. package/dist/core/sidecar.js +85 -0
  32. package/dist/core/sql.js +54 -0
  33. package/dist/core/telemetry.js +42 -0
  34. package/dist/core/tool.js +5 -0
  35. package/dist/core/transport.js +109 -0
  36. package/dist/index.js +55 -0
  37. package/dist/scripts/audit-verify.js +19 -0
  38. package/dist/scripts/smoke.js +87 -0
  39. package/dist/tools/core/activate.js +27 -0
  40. package/dist/tools/core/api_release_state.js +58 -0
  41. package/dist/tools/core/atc_quickfix.js +159 -0
  42. package/dist/tools/core/co_change.js +73 -0
  43. package/dist/tools/core/create_transport.js +44 -0
  44. package/dist/tools/core/ddic_type_info.js +81 -0
  45. package/dist/tools/core/dumps.js +55 -0
  46. package/dist/tools/core/edit_preflight.js +112 -0
  47. package/dist/tools/core/function_modules.js +75 -0
  48. package/dist/tools/core/gateway_errors.js +45 -0
  49. package/dist/tools/core/get_source.js +70 -0
  50. package/dist/tools/core/inactive_objects.js +29 -0
  51. package/dist/tools/core/object_versions.js +35 -0
  52. package/dist/tools/core/package_contents.js +59 -0
  53. package/dist/tools/core/run_atc.js +117 -0
  54. package/dist/tools/core/run_unit_tests.js +60 -0
  55. package/dist/tools/core/search_objects.js +26 -0
  56. package/dist/tools/core/sql_query.js +25 -0
  57. package/dist/tools/core/support.js +95 -0
  58. package/dist/tools/core/syntax_check.js +35 -0
  59. package/dist/tools/core/table_contents.js +57 -0
  60. package/dist/tools/core/text_elements.js +108 -0
  61. package/dist/tools/core/transaction_info.js +43 -0
  62. package/dist/tools/core/transport_contents.js +49 -0
  63. package/dist/tools/core/transport_diff.js +111 -0
  64. package/dist/tools/core/where_used.js +37 -0
  65. package/dist/tools/core/write_source.js +95 -0
  66. package/dist/tools/docs/docs.js +159 -0
  67. package/dist/tools/local/growth.js +104 -0
  68. package/dist/tools/local/sap_systems.js +65 -0
  69. package/dist/tools/transport-risk/_service.js +63 -0
  70. package/dist/tools/transport-risk/risk_extras.js +90 -0
  71. package/dist/tools/transport-risk/transport_risk.js +42 -0
  72. package/docs/THREAT_MODEL.md +70 -0
  73. package/docs/TOOLS.md +980 -0
  74. package/package.json +68 -0
  75. package/scripts/set-password.sh +31 -0
@@ -0,0 +1,159 @@
1
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { ADTClient, createSSLConfig, session_types } from "abap-adt-api";
5
+ import { forgetPassword, getPassword } from "./credentials.js";
6
+ import { normalizeError, ToolError } from "./errors.js";
7
+ import { normalizeValue, wrapSql } from "./sql.js";
8
+ const CACHE_DIR = join(homedir(), ".cache", "abap-adt-dozimple");
9
+ const DISCOVERY_TTL_MS = 7 * 24 * 3600 * 1000;
10
+ const REQUEST_TIMEOUT_MS = 120_000;
11
+ /**
12
+ * TLS: con caFile se verifica contra ese certificado (lo correcto para un
13
+ * sistema con certificado propio); allowSelfSigned desactiva la verificación
14
+ * y queda como último recurso, avisado en sap_systems.
15
+ */
16
+ export function tlsOptions(s) {
17
+ if (s.caFile)
18
+ return createSSLConfig(false, readFileSync(s.caFile, "utf8"));
19
+ if (s.allowSelfSigned)
20
+ return createSSLConfig(true);
21
+ return {};
22
+ }
23
+ /**
24
+ * Conexión perezosa a un sistema. Un cliente sin estado para lecturas y uno
25
+ * nuevo con sesión stateful para cada operación de escritura (bloqueo,
26
+ * guardado, desbloqueo), que se cierra al terminar para soltar el enqueue.
27
+ */
28
+ export class SapConnection {
29
+ system;
30
+ reader;
31
+ caps;
32
+ basisRelease;
33
+ constructor(system) {
34
+ this.system = system;
35
+ }
36
+ async newClient() {
37
+ const s = this.system;
38
+ const password = await getPassword(s);
39
+ const options = { ...tlsOptions(s), timeout: REQUEST_TIMEOUT_MS };
40
+ return new ADTClient(s.url.replace(/\/$/, ""), s.user, password, s.client, s.language, options);
41
+ }
42
+ async login(c) {
43
+ try {
44
+ await c.login();
45
+ return c;
46
+ }
47
+ catch (e) {
48
+ const te = normalizeError(e, this.system.id);
49
+ if (te.kind === "AUTH")
50
+ forgetPassword(this.system.id);
51
+ throw te;
52
+ }
53
+ }
54
+ /** Cliente de lectura, reutilizado entre llamadas. */
55
+ async adt() {
56
+ if (!this.reader) {
57
+ this.reader = this.newClient().then((c) => this.login(c));
58
+ this.reader.catch(() => (this.reader = undefined));
59
+ }
60
+ return this.reader;
61
+ }
62
+ /** Ejecuta fn con una sesión stateful propia y la cierra siempre. */
63
+ async stateful(fn) {
64
+ const c = await this.login(await this.newClient());
65
+ c.stateful = session_types.stateful;
66
+ try {
67
+ return await fn(c);
68
+ }
69
+ finally {
70
+ try {
71
+ await c.logout();
72
+ }
73
+ catch {
74
+ /* la sesión ya estaba cerrada */
75
+ }
76
+ }
77
+ }
78
+ async query(sql, rows = 100) {
79
+ const c = await this.adt();
80
+ const r = await c.runQuery(wrapSql(sql), rows, true);
81
+ for (const row of r.values)
82
+ for (const k of Object.keys(row))
83
+ row[k] = normalizeValue(row[k]);
84
+ return r;
85
+ }
86
+ /** Release de SAP_BASIS (750, 758…), o undefined si no se pudo leer. */
87
+ release() {
88
+ if (!this.basisRelease) {
89
+ this.basisRelease = this.query("SELECT release FROM cvers WHERE component = 'SAP_BASIS'", 1)
90
+ .then((r) => r.values[0]?.RELEASE?.trim())
91
+ .catch(() => undefined);
92
+ }
93
+ return this.basisRelease;
94
+ }
95
+ capabilities(refresh = false) {
96
+ if (!this.caps || refresh)
97
+ this.caps = this.loadCapabilities(refresh);
98
+ return this.caps;
99
+ }
100
+ async loadCapabilities(refresh) {
101
+ const file = join(CACHE_DIR, `discovery-${this.system.id.toUpperCase()}.json`);
102
+ if (!refresh && existsSync(file)) {
103
+ try {
104
+ const cached = JSON.parse(readFileSync(file, "utf8"));
105
+ if (Date.now() - Date.parse(cached.fetchedAt) < DISCOVERY_TTL_MS)
106
+ return cached;
107
+ }
108
+ catch {
109
+ /* caché corrupta: se vuelve a leer */
110
+ }
111
+ }
112
+ try {
113
+ const c = await this.adt();
114
+ const disc = await c.adtDiscovery();
115
+ const collections = [...new Set(disc.flatMap((w) => w.collection.map((col) => col.href)))].sort();
116
+ const caps = { collections, fetchedAt: new Date().toISOString(), known: true };
117
+ mkdirSync(CACHE_DIR, { recursive: true, mode: 0o700 });
118
+ writeFileSync(file, JSON.stringify(caps, null, 1), { mode: 0o600 });
119
+ return caps;
120
+ }
121
+ catch (e) {
122
+ const te = normalizeError(e, this.system.id);
123
+ // Sin red o sin credenciales no hay nada que decidir: se propaga.
124
+ if (te.kind === "NETWORK" || te.kind === "AUTH")
125
+ throw te;
126
+ return { collections: [], fetchedAt: new Date().toISOString(), known: false };
127
+ }
128
+ }
129
+ /**
130
+ * Colecciones requeridas que el discovery NO lista. Es una pista, no un
131
+ * veto: el discovery no enumera todo (en un S/4 2023 /runtime/dumps funciona y no
132
+ * aparece). Solo un 404 real confirma que falta.
133
+ */
134
+ async missingCapabilities(required) {
135
+ const caps = await this.capabilities();
136
+ if (!caps.known)
137
+ return [];
138
+ return required.filter((r) => !caps.collections.some((h) => h === r || h.startsWith(r + "/")));
139
+ }
140
+ /** Error CAPABILITY con evidencia: SAP respondió 404 y el discovery tampoco lo lista. */
141
+ async capabilityError(missing) {
142
+ const rel = await this.release();
143
+ return new ToolError("CAPABILITY", `${this.system.id}${rel ? ` (SAP_BASIS ${rel})` : ""} respondió 404 en ${missing.join(", ")}, ` +
144
+ `que tampoco figura en su discovery ADT: este sistema no tiene esa función.`);
145
+ }
146
+ }
147
+ export class ConnectionPool {
148
+ pool = new Map();
149
+ get(system) {
150
+ const key = system.id.toUpperCase();
151
+ let c = this.pool.get(key);
152
+ if (!c) {
153
+ c = new SapConnection(system);
154
+ this.pool.set(key, c);
155
+ }
156
+ return c;
157
+ }
158
+ }
159
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1,44 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { ToolError } from "./errors.js";
4
+ const run = promisify(execFile);
5
+ /** Servicio del llavero de macOS bajo el que se guardan las contraseñas. */
6
+ export const KEYCHAIN_SERVICE = "abap-adt-dozimple";
7
+ const cache = new Map();
8
+ /**
9
+ * Devuelve la contraseña del sistema. Nunca se escribe en logs ni en la
10
+ * respuesta de una tool: solo viaja a la cabecera Basic de ADT.
11
+ */
12
+ export async function getPassword(s) {
13
+ const hit = cache.get(s.id);
14
+ if (hit)
15
+ return hit;
16
+ let pwd;
17
+ if (s.password.startsWith("env:")) {
18
+ pwd = process.env[s.password.slice(4)];
19
+ if (!pwd) {
20
+ throw new ToolError("AUTH", `La variable ${s.password.slice(4)} no está definida para el sistema ${s.id}.`);
21
+ }
22
+ }
23
+ else {
24
+ try {
25
+ // macOS: llavero. Linux: Secret Service (GNOME Keyring / KWallet) vía secret-tool.
26
+ const { stdout } = process.platform === "linux"
27
+ ? await run("secret-tool", ["lookup", "service", KEYCHAIN_SERVICE, "account", s.id])
28
+ : await run("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", s.id, "-w"]);
29
+ pwd = stdout.replace(/\n$/, "");
30
+ if (!pwd)
31
+ throw new Error("vacía");
32
+ }
33
+ catch {
34
+ throw new ToolError("AUTH", `No hay contraseña para ${s.id} en el llavero. Guárdala con: scripts/set-password.sh ${s.id}`);
35
+ }
36
+ }
37
+ cache.set(s.id, pwd);
38
+ return pwd;
39
+ }
40
+ /** Tras un 401 conviene olvidar la contraseña cacheada por si se cambió. */
41
+ export function forgetPassword(id) {
42
+ cache.delete(id);
43
+ }
44
+ //# sourceMappingURL=credentials.js.map
@@ -0,0 +1,175 @@
1
+ import { normalizeError, ToolError } from "./errors.js";
2
+ import { sqlLiteral } from "./objects.js";
3
+ import { assertNotSensitive, assertSelectOnly, rejectSensitive, sensitiveHits, sqlWords } from "./policy.js";
4
+ export const dataClassOf = (s) => s.dataClass ?? (s.role === "DEV" ? "test" : "prod");
5
+ const DEFAULT_CAP = { test: 5000, masked: 1000, prod: 200 };
6
+ export const rowCap = (s) => s.maxRows ?? DEFAULT_CAP[dataClassOf(s)];
7
+ /**
8
+ * Columnas con datos personales en las tablas estándar (deudores, acreedores,
9
+ * direcciones, bancos, interlocutores). Se reconocen por nombre: una Z con
10
+ * otro nombre no se detecta, por eso el tope de filas sigue aplicando.
11
+ */
12
+ export const PII_COLUMNS = new Set([
13
+ "NAME1", "NAME2", "NAME3", "NAME4", "NAME_FIRST", "NAME_LAST", "NAME_LST2", "NAMEMIDDLE", "NAME_CO", "NICKNAME",
14
+ "NAME_ORG1", "NAME_ORG2", "MC_NAME1", "MC_NAME2", "SORTL", "MCOD1", "MCOD2",
15
+ "STRAS", "STREET", "STR_SUPPL1", "STR_SUPPL2", "STR_SUPPL3", "HOUSE_NUM1", "PSTLZ", "POST_CODE1", "PFACH", "PO_BOX",
16
+ "TELF1", "TELF2", "TELFX", "TELBX", "TEL_NUMBER", "TEL_EXTENS", "FAX_NUMBER", "MOB_NUMBER", "SMTP_ADDR", "E_MAIL",
17
+ "STCD1", "STCD2", "STCD3", "STCD4", "STCD5", "STCEG", "TAXNUM",
18
+ "BANKN", "BKONT", "IBAN", "KOINH", "ACCNAME", "CCNUM",
19
+ "GBDAT", "BIRTHDT", "GBORT", "PERID", "IDNUMBER",
20
+ ]);
21
+ export const MASK = "‹oculto›";
22
+ /**
23
+ * En sistemas masked/prod una columna personal solo puede salir como columna
24
+ * simple, para enmascararla. Usarla en un WHERE, un alias o una expresión
25
+ * dejaría deducir su valor sin que aparezca en el resultado.
26
+ */
27
+ export function maskPii(cls, sql, columns, rows) {
28
+ if (cls === "test")
29
+ return [];
30
+ const cols = new Set(columns.map((c) => c.toUpperCase()));
31
+ const mentioned = new Set(sqlWords(sql).flatMap((w) => w.split("~")).filter((w) => PII_COLUMNS.has(w)));
32
+ const hidden = [...mentioned].filter((w) => !cols.has(w));
33
+ if (hidden.length) {
34
+ throw new ToolError("POLICY", `En un sistema con datos ${cls === "prod" ? "productivos" : "enmascarados"}, las columnas personales (${hidden.join(", ")}) ` +
35
+ `solo pueden pedirse como columna simple del SELECT: no en WHERE, alias ni expresiones.`, "Filtra por la clave (KUNNR, LIFNR, PARTNER…) y deja que la columna salga enmascarada, o consulta un sistema de test.");
36
+ }
37
+ const masked = columns.filter((c) => PII_COLUMNS.has(c.toUpperCase()));
38
+ for (const row of rows)
39
+ for (const c of masked)
40
+ if (row[c] !== "" && row[c] !== null && row[c] !== undefined)
41
+ row[c] = MASK;
42
+ return masked;
43
+ }
44
+ const KEYWORDS = new Set(("SELECT FROM WHERE AND OR NOT IN AS JOIN INNER LEFT RIGHT OUTER CROSS ON GROUP BY ORDER HAVING DISTINCT COUNT SUM " +
45
+ "MIN MAX AVG CASE WHEN THEN ELSE END LIKE BETWEEN IS NULL ASC DESC UNION ALL EXISTS WITH UP TO ROWS SINGLE ESCAPE " +
46
+ "COALESCE CAST CONCAT SUBSTRING LENGTH UPPER LOWER ABS ROUND FLOOR CEIL DIV MOD CLIENT SPECIFIED FIELDS DEFINE VIEW " +
47
+ "ENTITY ASSOCIATION PROJECTION KEY ROOT CHILD PARENT COMPOSITION TRUE FALSE ABAP ENDUSER").split(" "));
48
+ /** Palabras que pueden ser nombres de tabla, vista o entidad CDS. */
49
+ export function candidateNames(text, limit = 80) {
50
+ const out = new Set();
51
+ const code = text
52
+ .replace(/\/\*[\s\S]*?\*\//g, " ")
53
+ .replace(/\/\/.*$/gm, " ")
54
+ .replace(/@[^\n]*/g, " ") // anotaciones CDS: sus valores (#CDS_MODELING_…) no son tablas
55
+ .replace(/#[A-Za-z0-9_]+/g, " ");
56
+ for (const w of sqlWords(code).flatMap((x) => x.split("~"))) {
57
+ // 40 = longitud máxima de una entidad CDS; una tabla o vista clásica no pasa de 30.
58
+ if (w.length < 3 || w.length > 40 || KEYWORDS.has(w) || /^\d/.test(w) || !/^([A-Z]|\/[A-Z0-9]+\/)[A-Z0-9_]*$/.test(w))
59
+ continue;
60
+ out.add(w);
61
+ if (out.size >= limit)
62
+ break;
63
+ }
64
+ return [...out];
65
+ }
66
+ const MAX_EXAMINED = 1500;
67
+ const chunks = (xs, n) => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, i * n + n));
68
+ /** Resultado por nombre: nombres base vetados que esconde (vacío = limpio). Por conexión y proceso. */
69
+ const deepCache = new WeakMap();
70
+ /**
71
+ * Una vista (DD26S) o una entidad CDS puede leer USR02 sin nombrarla. Se
72
+ * resuelve hasta 3 niveles: vistas de base de datos y de mantenimiento por
73
+ * DD26S; CDS por DDLDEPENDENCY + palabras de su fuente DDL.
74
+ */
75
+ export async function assertNoSensitiveViews(sap, sql, depth = 3) {
76
+ let cache = deepCache.get(sap);
77
+ if (!cache)
78
+ deepCache.set(sap, (cache = new Map()));
79
+ let frontier = candidateNames(sql).filter((n) => !cache.has(n));
80
+ const origin = new Map(); // nombre descubierto → nombre de la consulta que lo arrastra
81
+ for (const n of frontier)
82
+ origin.set(n, n);
83
+ let examined = frontier.length;
84
+ for (let level = 0; level < depth && frontier.length; level++) {
85
+ const found = new Map(); // nombre → nombres base
86
+ const add = (parent, child) => {
87
+ if (!found.has(parent))
88
+ found.set(parent, new Set());
89
+ found.get(parent).add(child.toUpperCase());
90
+ };
91
+ for (const chunk of chunks(frontier, 60)) {
92
+ const classic = chunk.filter((n) => n.length <= 30); // DD26S-VIEWNAME es C(30): un literal más largo lo rechaza SAP
93
+ if (classic.length) {
94
+ const views = await sap.query(`SELECT viewname, tabname FROM dd26s WHERE viewname IN ( ${classic.map(sqlLiteral).join(", ")} )`, 5000);
95
+ for (const v of views.values)
96
+ add(String(v.VIEWNAME).trim(), String(v.TABNAME).trim());
97
+ }
98
+ let ddls = { values: [] };
99
+ try {
100
+ ddls = await sap.query(`SELECT ddlname, objectname FROM ddldependency WHERE objectname IN ( ${chunk.map(sqlLiteral).join(", ")} )`, 2000);
101
+ }
102
+ catch (e) {
103
+ const te = normalizeError(e);
104
+ // Solo un release sin CDS (la tabla no existe) permite seguir; cualquier otro fallo cierra.
105
+ if (!(te.kind === "SAP" && /DDLDEPENDENCY/i.test(te.message))) {
106
+ throw new ToolError("POLICY", `No se pudo comprobar qué CDS intervienen en la consulta (${te.message}): no se ejecuta sin esa comprobación.`);
107
+ }
108
+ }
109
+ const byDdl = new Map();
110
+ for (const d of ddls.values) {
111
+ const ddl = String(d.DDLNAME).trim();
112
+ byDdl.set(ddl, [...(byDdl.get(ddl) ?? []), String(d.OBJECTNAME).trim()]);
113
+ }
114
+ if (byDdl.size) {
115
+ const c = await sap.adt();
116
+ for (const [ddl, objs] of byDdl) {
117
+ let src;
118
+ try {
119
+ src = await c.getObjectSource(`/sap/bc/adt/ddic/ddl/sources/${encodeURIComponent(ddl.toLowerCase())}/source/main`);
120
+ }
121
+ catch (e) {
122
+ throw new ToolError("POLICY", `No se pudo comprobar qué tablas lee la CDS ${ddl} (${normalizeError(e).message}): la consulta no se ejecuta sin esa comprobación.`);
123
+ }
124
+ for (const o of objs)
125
+ for (const w of candidateNames(src, 400))
126
+ if (w !== o && w !== ddl)
127
+ add(o, w);
128
+ }
129
+ }
130
+ }
131
+ const next = [];
132
+ for (const [parent, children] of found) {
133
+ const root = origin.get(parent) ?? parent;
134
+ const hits = sensitiveHits(children);
135
+ if (hits.secrets.length || hits.hr.length)
136
+ rejectSensitive(hits, `${root}${root !== parent ? ` → ${parent}` : ""}`);
137
+ for (const ch of children) {
138
+ if (!origin.has(ch) && !cache.has(ch)) {
139
+ origin.set(ch, root);
140
+ next.push(ch);
141
+ }
142
+ }
143
+ }
144
+ examined += next.length;
145
+ if (examined > MAX_EXAMINED) {
146
+ throw new ToolError("POLICY", `La consulta arrastra más de ${MAX_EXAMINED} tablas y vistas: demasiado para comprobar que no lee datos vetados.`, "Consulta las tablas base directamente.");
147
+ }
148
+ frontier = next;
149
+ }
150
+ // Solo se recuerdan como limpios los nombres de la consulta: si algo hubiera saltado, ya habríamos lanzado.
151
+ for (const n of candidateNames(sql))
152
+ cache.set(n, []);
153
+ }
154
+ /** Toda consulta de datos del usuario pasa por aquí: veto, vistas, tope y enmascarado. */
155
+ export async function guardedQuery(sap, system, sql, requestedRows) {
156
+ const q = assertSelectOnly(sql);
157
+ assertNotSensitive(q);
158
+ await assertNoSensitiveViews(sap, q);
159
+ const cls = dataClassOf(system);
160
+ const cap = Math.min(requestedRows, rowCap(system));
161
+ const r = await sap.query(q, cap);
162
+ const columns = r.columns.map((c) => c.name);
163
+ const notes = [];
164
+ const masked = maskPii(cls, q, columns, r.values);
165
+ if (masked.length)
166
+ notes.push(`Columnas personales enmascaradas (sistema con datos ${cls === "prod" ? "productivos" : "enmascarados"}): ${masked.join(", ")}.`);
167
+ const capped = r.values.length >= cap;
168
+ if (capped) {
169
+ notes.push(cap < requestedRows
170
+ ? `Tope de ${cap} filas de ${system.id} (datos ${cls}) alcanzado: puede haber más. Agrega o filtra en vez de pedir más filas.`
171
+ : `Tope de ${cap} filas alcanzado: puede haber más.`);
172
+ }
173
+ return { columns, values: r.values, cap, capped, notes };
174
+ }
175
+ //# sourceMappingURL=datapolicy.js.map
@@ -0,0 +1,115 @@
1
+ function myers(a, b, maxD) {
2
+ const n = a.length;
3
+ const m = b.length;
4
+ if (n === 0)
5
+ return b.map((_, j) => ["+", -1, j]);
6
+ if (m === 0)
7
+ return a.map((_, i) => ["-", i, -1]);
8
+ const max = n + m;
9
+ const off = max + 1;
10
+ const v = new Int32Array(2 * max + 3);
11
+ // trace[d] = v en [-(d+1), d+1] al empezar la iteración d (memoria O(D²)).
12
+ const trace = [];
13
+ let done = false;
14
+ for (let d = 0; d <= Math.min(max, maxD) && !done; d++) {
15
+ trace.push(v.slice(off - d - 1, off + d + 2));
16
+ for (let k = -d; k <= d; k += 2) {
17
+ let x = k === -d || (k !== d && v[off + k - 1] < v[off + k + 1]) ? v[off + k + 1] : v[off + k - 1] + 1;
18
+ let y = x - k;
19
+ while (x < n && y < m && a[x] === b[y]) {
20
+ x++;
21
+ y++;
22
+ }
23
+ v[off + k] = x;
24
+ if (x >= n && y >= m) {
25
+ done = true;
26
+ break;
27
+ }
28
+ }
29
+ }
30
+ if (!done)
31
+ return undefined;
32
+ const out = [];
33
+ let x = n;
34
+ let y = m;
35
+ for (let d = trace.length - 1; d >= 0; d--) {
36
+ const snap = trace[d];
37
+ const at = (k) => snap[k + d + 1];
38
+ const k = x - y;
39
+ const prevK = k === -d || (k !== d && at(k - 1) < at(k + 1)) ? k + 1 : k - 1;
40
+ const prevX = at(prevK);
41
+ const prevY = prevX - prevK;
42
+ while (x > prevX && y > prevY) {
43
+ out.push([" ", x - 1, y - 1]);
44
+ x--;
45
+ y--;
46
+ }
47
+ if (d > 0) {
48
+ if (x === prevX)
49
+ out.push(["+", -1, prevY]);
50
+ else
51
+ out.push(["-", prevX, -1]);
52
+ }
53
+ x = prevX;
54
+ y = prevY;
55
+ }
56
+ return out.reverse();
57
+ }
58
+ export function diffLines(oldText, newText, maxD = 2500) {
59
+ const a = oldText.replace(/\r\n/g, "\n").split("\n");
60
+ const b = newText.replace(/\r\n/g, "\n").split("\n");
61
+ let pre = 0;
62
+ while (pre < a.length && pre < b.length && a[pre] === b[pre])
63
+ pre++;
64
+ let suf = 0;
65
+ while (suf < a.length - pre && suf < b.length - pre && a[a.length - 1 - suf] === b[b.length - 1 - suf])
66
+ suf++;
67
+ const mid = myers(a.slice(pre, a.length - suf), b.slice(pre, b.length - suf), maxD);
68
+ if (!mid)
69
+ return undefined;
70
+ const ops = [];
71
+ for (let i = 0; i < pre; i++)
72
+ ops.push({ op: " ", text: a[i], a: i + 1, b: i + 1 });
73
+ for (const [op, i, j] of mid) {
74
+ if (op === " ")
75
+ ops.push({ op, text: a[pre + i], a: pre + i + 1, b: pre + j + 1 });
76
+ else if (op === "-")
77
+ ops.push({ op, text: a[pre + i], a: pre + i + 1 });
78
+ else
79
+ ops.push({ op, text: b[pre + j], b: pre + j + 1 });
80
+ }
81
+ for (let i = suf; i > 0; i--)
82
+ ops.push({ op: " ", text: a[a.length - i], a: a.length - i + 1, b: b.length - i + 1 });
83
+ return ops;
84
+ }
85
+ /** Formato unificado con `context` líneas alrededor de cada cambio. */
86
+ export function unified(ops, context = 3) {
87
+ const changed = ops.map((o, i) => (o.op !== " " ? i : -1)).filter((i) => i >= 0);
88
+ const added = ops.filter((o) => o.op === "+").length;
89
+ const removed = ops.filter((o) => o.op === "-").length;
90
+ if (!changed.length)
91
+ return { text: "", added: 0, removed: 0, hunks: 0 };
92
+ const ranges = [];
93
+ for (const i of changed) {
94
+ const from = Math.max(0, i - context);
95
+ const to = Math.min(ops.length - 1, i + context);
96
+ const last = ranges[ranges.length - 1];
97
+ if (last && from <= last[1] + 1)
98
+ last[1] = Math.max(last[1], to);
99
+ else
100
+ ranges.push([from, to]);
101
+ }
102
+ const out = [];
103
+ for (const [from, to] of ranges) {
104
+ const slice = ops.slice(from, to + 1);
105
+ const aStart = slice.find((o) => o.a !== undefined)?.a ?? 0;
106
+ const bStart = slice.find((o) => o.b !== undefined)?.b ?? 0;
107
+ const aLen = slice.filter((o) => o.op !== "+").length;
108
+ const bLen = slice.filter((o) => o.op !== "-").length;
109
+ out.push(`@@ -${aStart},${aLen} +${bStart},${bLen} @@`);
110
+ for (const o of slice)
111
+ out.push(o.op + o.text);
112
+ }
113
+ return { text: out.join("\n"), added, removed, hunks: ranges.length };
114
+ }
115
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1,31 @@
1
+ /** Offset de (línea 1-based, columna 0-based) en el texto. */
2
+ function offsetOf(lines, line, column) {
3
+ let off = 0;
4
+ for (let i = 0; i < line - 1 && i < lines.length; i++)
5
+ off += lines[i].length + 1;
6
+ return off + Math.min(column, lines[line - 1]?.length ?? 0);
7
+ }
8
+ /**
9
+ * Aplica ediciones ADT (quickfixes, refactorings) a un fuente, de atrás hacia
10
+ * delante para que los offsets no se muevan. Lanza si dos ediciones se pisan.
11
+ */
12
+ export function applyEdits(source, edits) {
13
+ const text = source.replace(/\r\n/g, "\n");
14
+ const lines = text.split("\n");
15
+ const spans = edits
16
+ .map((e) => ({
17
+ from: offsetOf(lines, e.range.start.line, e.range.start.column),
18
+ to: offsetOf(lines, e.range.end.line, e.range.end.column),
19
+ content: e.content ?? "",
20
+ }))
21
+ .sort((a, b) => b.from - a.from);
22
+ for (let i = 1; i < spans.length; i++) {
23
+ if (spans[i].to > spans[i - 1].from)
24
+ throw new Error("Ediciones solapadas: SAP devolvió cambios incompatibles.");
25
+ }
26
+ let out = text;
27
+ for (const s of spans)
28
+ out = out.slice(0, s.from) + s.content + out.slice(s.to);
29
+ return out;
30
+ }
31
+ //# sourceMappingURL=edits.js.map
@@ -0,0 +1,68 @@
1
+ import { isAdtError, isCsrfError, isHttpError } from "abap-adt-api";
2
+ export class ToolError extends Error {
3
+ kind;
4
+ hint;
5
+ constructor(kind, message, hint) {
6
+ super(message);
7
+ this.kind = kind;
8
+ this.hint = hint;
9
+ }
10
+ }
11
+ const NETWORK_CODES = new Set([
12
+ "ENETUNREACH", "EHOSTUNREACH", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT",
13
+ "ECONNRESET", "EAI_AGAIN", "ECONNABORTED",
14
+ ]);
15
+ function errCode(e) {
16
+ return e?.code ?? e?.parent?.code ?? e?.cause?.code ?? e?.parent?.cause?.code;
17
+ }
18
+ export function normalizeError(e, systemId) {
19
+ if (e instanceof ToolError)
20
+ return e;
21
+ const any = e;
22
+ const code = errCode(any);
23
+ if (code && NETWORK_CODES.has(code)) {
24
+ return new ToolError("NETWORK", `No se llega a ${systemId ?? "SAP"} (${code}).`, "¿Está levantada la VPN de ese cliente?");
25
+ }
26
+ if (isCsrfError(e)) {
27
+ return new ToolError("AUTH", `Sesión o token CSRF rechazado por ${systemId ?? "SAP"}: ${any.message}`);
28
+ }
29
+ if (isHttpError(e)) {
30
+ const st = any.status;
31
+ if (st === 401)
32
+ return new ToolError("AUTH", `Usuario o contraseña rechazados por ${systemId ?? "SAP"} (401).`);
33
+ if (st === 403)
34
+ return new ToolError("AUTH", `Sin autorización (403): ${any.message}`);
35
+ if (st === 404)
36
+ return new ToolError("NOT_FOUND", `No encontrado (404): ${any.message}`);
37
+ return new ToolError("SAP", `HTTP ${st ?? "?"}: ${any.message}`);
38
+ }
39
+ if (isAdtError(e)) {
40
+ const st = any.err;
41
+ const msg = any.localizedMessage || any.message || "sin mensaje";
42
+ if (st === 401)
43
+ return new ToolError("AUTH", `Usuario o contraseña rechazados (401): ${msg}`);
44
+ if (st === 403)
45
+ return new ToolError("AUTH", `Sin autorización (403): ${msg}`);
46
+ if (st === 404)
47
+ return new ToolError("NOT_FOUND", msg);
48
+ return new ToolError("SAP", `${msg}${st ? ` (HTTP ${st})` : ""}`);
49
+ }
50
+ const message = any?.message ? String(any.message) : String(e);
51
+ return new ToolError("INTERNAL", message || "Error sin mensaje");
52
+ }
53
+ export function renderError(te) {
54
+ const labels = {
55
+ NETWORK: "Sin conexión",
56
+ AUTH: "Autenticación/autorización",
57
+ NOT_FOUND: "No encontrado",
58
+ SAP: "Error de SAP",
59
+ POLICY: "Bloqueado por política",
60
+ CAPABILITY: "No disponible en este sistema",
61
+ MODULE: "Módulo no habilitado",
62
+ INPUT: "Parámetros inválidos",
63
+ INTERNAL: "Error interno",
64
+ };
65
+ return `${labels[te.kind]}: ${te.message}${te.hint ? `\n${te.hint}` : ""}\n` +
66
+ `(No se completó la operación: no interpretes esto como un resultado vacío.)`;
67
+ }
68
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1,60 @@
1
+ const attr = (tag, name) => new RegExp(`${name}="([^"]*)"`).exec(tag)?.[1] ?? "";
2
+ /** Entidades HTML/XML a texto. `&amp;` va la última: si no, «&amp;lt;» acabaría en «<» (doble decodificación). */
3
+ export function decodeEntities(s) {
4
+ return s
5
+ .replace(/&nbsp;/g, " ")
6
+ .replace(/&lt;/g, "<")
7
+ .replace(/&gt;/g, ">")
8
+ .replace(/&quot;/g, '"')
9
+ .replace(/&(#39|apos);/g, "'")
10
+ .replace(/&#(\d{1,5});/g, (_, n) => String.fromCharCode(Number(n)))
11
+ .replace(/&amp;/g, "&");
12
+ }
13
+ /**
14
+ * Quita etiquetas hasta que no quede ninguna: una sola pasada deja restos como
15
+ * «<scr<script>ipt>». Los «<» sueltos que queden se eliminan (en HTML válido el
16
+ * texto los trae como &lt;, que se decodifica después).
17
+ */
18
+ export function stripTags(html, sep = " ") {
19
+ let out = html;
20
+ for (let prev = ""; prev !== out;) {
21
+ prev = out;
22
+ out = out.replace(/<[^<>]*>/g, sep);
23
+ }
24
+ return out.replace(/</g, "");
25
+ }
26
+ const decode = decodeEntities;
27
+ const inner = (xml, tag) => decode(new RegExp(`<(?:atom:)?${tag}[^>]*>([\\s\\S]*?)</(?:atom:)?${tag}>`).exec(xml)?.[1] ?? "").trim();
28
+ export function parseAtom(xml) {
29
+ return [...xml.matchAll(/<(?:atom:)?entry[\s>][\s\S]*?<\/(?:atom:)?entry>/g)].map((m) => {
30
+ const e = m[0];
31
+ return {
32
+ id: inner(e, "id"),
33
+ title: inner(e, "title"),
34
+ updated: inner(e, "updated"),
35
+ author: inner(inner(e, "author") ? e.replace(/[\s\S]*?<(?:atom:)?author>/, "<x>") : "", "name") || inner(e, "name"),
36
+ summary: stripTags(inner(e, "summary")).replace(/\s+/g, " ").trim(),
37
+ categories: [...e.matchAll(/<(?:atom:)?category [^>]*>/g)].map((c) => ({ term: attr(c[0], "term"), label: attr(c[0], "label") })),
38
+ links: [...e.matchAll(/<(?:atom:)?link [^>]*>/g)].map((l) => ({ href: attr(l[0], "href"), rel: attr(l[0], "rel"), type: attr(l[0], "type") })),
39
+ };
40
+ });
41
+ }
42
+ export function htmlToText(html) {
43
+ let noScripts = html;
44
+ for (let prev = ""; prev !== noScripts;) {
45
+ prev = noScripts;
46
+ noScripts = noScripts.replace(/<(script|style)\b[\s\S]*?<\/\1\s*>/gi, "");
47
+ }
48
+ const withBreaks = noScripts
49
+ .replace(/<br\s*\/?>/gi, "\n")
50
+ .replace(/<\/(p|div|h\d|li|tr|table)>/gi, "\n")
51
+ .replace(/<\/t[dh]>/gi, "\t");
52
+ return decodeEntities(stripTags(withBreaks))
53
+ .replace(/\r\n?/g, "\n")
54
+ .replace(/[ \t]+\n/g, "\n")
55
+ .replace(/\n[ \t]+/g, "\n")
56
+ .replace(/[ ]{2,}/g, " ")
57
+ .replace(/\n{3,}/g, "\n\n")
58
+ .trim();
59
+ }
60
+ //# sourceMappingURL=feeds.js.map
@@ -0,0 +1,18 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ /**
3
+ * Notas que una tool deja durante su ejecución para que el registro las anteponga a la respuesta (p. ej. «se pidió
4
+ * PROG, se resolvió como include PROG/I»). Así cualquier tool que use resolveObject lo cuenta sin tocar su texto.
5
+ */
6
+ const store = new AsyncLocalStorage();
7
+ export async function withNotes(fn) {
8
+ const notes = [];
9
+ const result = await store.run(notes, fn);
10
+ return { result, notes };
11
+ }
12
+ export function addNote(note) {
13
+ const notes = store.getStore();
14
+ if (notes && !notes.includes(note))
15
+ notes.push(note);
16
+ }
17
+ export const renderNotes = (notes) => (notes.length ? notes.map((n) => `Nota: ${n}`).join("\n") + "\n\n" : "");
18
+ //# sourceMappingURL=notes.js.map