@linkup-ai/abap-ai 0.1.0 → 0.1.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.
@@ -12,6 +12,7 @@ exports.adtPut = adtPut;
12
12
  exports.adtLock = adtLock;
13
13
  exports.adtUnlock = adtUnlock;
14
14
  exports.adtDelete = adtDelete;
15
+ exports.adtLockAndDelete = adtLockAndDelete;
15
16
  exports.adtPostXml = adtPostXml;
16
17
  exports.adtGetXmlWithParams = adtGetXmlWithParams;
17
18
  exports.adtPostText = adtPostText;
@@ -328,6 +329,76 @@ async function adtDelete(path, transportRequest) {
328
329
  });
329
330
  });
330
331
  }
332
+ /**
333
+ * Lock + Delete atômico — resolve o bug do axios-cookiejar-support@4
334
+ * que não mantém cookies entre POST (LOCK) e DELETE.
335
+ *
336
+ * Captura os cookies e o CSRF token da resposta do LOCK e os injeta
337
+ * manualmente no DELETE request, garantindo que o SAP ICM reconheça
338
+ * a mesma sessão.
339
+ */
340
+ async function adtLockAndDelete(objectPath, transportRequest) {
341
+ await withRetry("LOCK+DELETE", objectPath, async () => {
342
+ const csrf = await ensureSession();
343
+ // 1. LOCK — capturar response headers (cookies + lockHandle)
344
+ const lockResp = await http.post(objectPath, "", {
345
+ headers: { "X-CSRF-Token": csrf },
346
+ params: { _action: "LOCK", accessMode: "MODIFY" },
347
+ validateStatus: (status) => status < 500,
348
+ responseType: "text",
349
+ });
350
+ if (lockResp.status >= 300) {
351
+ throw new AdtError(`Falha ao bloquear ${objectPath} (status ${lockResp.status}). O objeto pode estar bloqueado por outro usuário.`, lockResp.status, objectPath);
352
+ }
353
+ // Extrair lockHandle
354
+ const lockData = typeof lockResp.data === "string" ? lockResp.data : "";
355
+ const handleMatch = lockData.match(/<LOCK_HANDLE>([^<]+)<\/LOCK_HANDLE>/);
356
+ const lockHandle = handleMatch?.[1] ?? "";
357
+ if (!lockHandle) {
358
+ throw new AdtError("Lock handle não retornado pelo SAP.", 0, objectPath);
359
+ }
360
+ // Capturar cookies da resposta do LOCK para reusar no DELETE
361
+ const setCookies = lockResp.headers["set-cookie"];
362
+ const cookieHeader = Array.isArray(setCookies)
363
+ ? setCookies.map((c) => c.split(";")[0]).join("; ")
364
+ : typeof setCookies === "string"
365
+ ? setCookies.split(";")[0]
366
+ : "";
367
+ // Capturar CSRF token atualizado (o LOCK pode retornar um novo)
368
+ const updatedCsrf = lockResp.headers["x-csrf-token"] || csrf;
369
+ // 2. DELETE — injetar cookies e lockHandle manualmente
370
+ const deleteParams = { lockHandle };
371
+ if (transportRequest)
372
+ deleteParams.corrNr = transportRequest;
373
+ const deleteHeaders = {
374
+ "X-CSRF-Token": updatedCsrf,
375
+ };
376
+ // Injetar cookies manualmente se capturados
377
+ if (cookieHeader) {
378
+ deleteHeaders["Cookie"] = cookieHeader;
379
+ }
380
+ const delResp = await http.delete(objectPath, {
381
+ headers: deleteHeaders,
382
+ params: deleteParams,
383
+ validateStatus: (status) => status < 500,
384
+ });
385
+ if (delResp.status >= 400) {
386
+ // Tentar unlock em caso de erro
387
+ await http.post(objectPath, "", {
388
+ headers: {
389
+ "X-CSRF-Token": updatedCsrf,
390
+ ...(cookieHeader ? { Cookie: cookieHeader } : {}),
391
+ },
392
+ params: { _action: "UNLOCK", lockHandle },
393
+ validateStatus: () => true,
394
+ }).catch(() => { });
395
+ const errorBody = typeof delResp.data === "string" ? delResp.data : "";
396
+ const sapMsg = errorBody.match(/<message[^>]*>([^<]+)<\/message>/i)?.[1]
397
+ ?? `HTTP ${delResp.status}`;
398
+ throw new AdtError(`Erro ao excluir ${objectPath}: ${sapMsg}`, delResp.status, objectPath);
399
+ }
400
+ });
401
+ }
331
402
  async function adtPostXml(path, body, params, accept = "application/xml") {
332
403
  return withRetry("POST_XML", path, async () => {
333
404
  const csrf = await ensureSession();
package/dist/index.js CHANGED
@@ -62,14 +62,20 @@ const security_audit_js_1 = require("./security-audit.js");
62
62
  const license_guard_js_1 = require("./license-guard.js");
63
63
  const server = new mcp_js_1.McpServer({
64
64
  name: "abap-adt",
65
- version: "2.0.0",
65
+ version: VERSION,
66
66
  });
67
67
  // ─── License Guard (wraps all tool handlers) ─────────────────────
68
68
  // Intercepta server.tool para injetar licenseGuard() automaticamente.
69
69
  // Toda tool retorna erro amigável se a licença é ausente ou expirada.
70
+ // Também coleta os nomes de todas as tools registradas para validação de segurança.
71
+ const _registeredToolNames = [];
70
72
  {
71
73
  const _origTool = server.tool.bind(server);
72
74
  server.tool = (...args) => {
75
+ // Coletar o nome da tool (1º argumento string)
76
+ if (typeof args[0] === "string") {
77
+ _registeredToolNames.push(args[0]);
78
+ }
73
79
  const handler = args[args.length - 1];
74
80
  args[args.length - 1] = async (...handlerArgs) => {
75
81
  const licBlock = (0, license_guard_js_1.licenseGuard)();
@@ -1429,6 +1435,9 @@ server.resource("security-policy", "abap://security-policy", { description: "Pol
1429
1435
  });
1430
1436
  // Inicializa o servidor via stdio (modo Claude Code MCP)
1431
1437
  async function main() {
1438
+ // Validação de segurança: toda tool registrada DEVE ter classificação de risco.
1439
+ // Falha rápido no startup se alguma tool foi adicionada sem classificar.
1440
+ (0, security_policy_js_1.validateToolRiskMap)(_registeredToolNames);
1432
1441
  const transport = new stdio_js_1.StdioServerTransport();
1433
1442
  await server.connect(transport);
1434
1443
  const profile = (0, system_profile_js_1.getProfile)();
@@ -11,6 +11,7 @@
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.getToolRisk = getToolRisk;
14
+ exports.validateToolRiskMap = validateToolRiskMap;
14
15
  exports.loadPolicy = loadPolicy;
15
16
  exports.checkToolAccess = checkToolAccess;
16
17
  exports.getEffectiveMaxRows = getEffectiveMaxRows;
@@ -23,7 +24,7 @@ const fs_1 = require("fs");
23
24
  const path_1 = require("path");
24
25
  /**
25
26
  * Mapa de cada tool para seu nível de risco.
26
- * Tools não listadas aqui são consideradas READ (safe by default).
27
+ * TODA tool registrada DEVE estar neste mapa tools não classificadas causam erro no startup.
27
28
  */
28
29
  const TOOL_RISK = {
29
30
  // READ — sem guard extra (default para tools não listadas)
@@ -64,7 +65,7 @@ const TOOL_RISK = {
64
65
  abap_knowledge: "READ",
65
66
  abap_traces: "READ",
66
67
  abap_breakpoints: "READ",
67
- abapgit_repos: "READ",
68
+ abap_git_repos: "READ",
68
69
  // WRITE — modifica objetos existentes
69
70
  abap_write: "WRITE",
70
71
  abap_activate: "WRITE",
@@ -74,8 +75,8 @@ const TOOL_RISK = {
74
75
  abap_publish_binding: "WRITE",
75
76
  abap_deploy_bsp: "WRITE",
76
77
  abap_assign_transport: "WRITE",
77
- abapgit_pull: "WRITE",
78
- abapgit_stage: "WRITE",
78
+ abap_git_pull: "WRITE",
79
+ abap_git_stage: "WRITE",
79
80
  // CREATE — cria objetos novos no SAP
80
81
  abap_create: "CREATE",
81
82
  abap_create_transport: "CREATE",
@@ -89,9 +90,28 @@ const TOOL_RISK = {
89
90
  };
90
91
  /**
91
92
  * Retorna o nível de risco de uma tool.
93
+ * Lança erro se a tool não estiver classificada — toda tool DEVE estar no TOOL_RISK map.
92
94
  */
93
95
  function getToolRisk(toolName) {
94
- return TOOL_RISK[toolName] ?? "READ";
96
+ const risk = TOOL_RISK[toolName];
97
+ if (!risk) {
98
+ throw new Error(`[SECURITY] Tool "${toolName}" não está classificada no TOOL_RISK map. `
99
+ + `Toda tool DEVE ter um nível de risco definido em security-policy.ts antes de ser registrada.`);
100
+ }
101
+ return risk;
102
+ }
103
+ /**
104
+ * Valida que todas as tools fornecidas estão classificadas no TOOL_RISK map.
105
+ * Deve ser chamada no startup do MCP server, após registrar todas as tools.
106
+ * Lança erro listando TODAS as tools não classificadas (para corrigir de uma vez).
107
+ */
108
+ function validateToolRiskMap(registeredToolNames) {
109
+ const missing = registeredToolNames.filter((name) => !(name in TOOL_RISK));
110
+ if (missing.length > 0) {
111
+ throw new Error(`[SECURITY] ${missing.length} tool(s) registrada(s) sem classificação de risco no TOOL_RISK map:\n`
112
+ + missing.map((t) => ` - ${t}`).join("\n") + "\n"
113
+ + `Adicione essas tools em src/security-policy.ts antes de continuar.`);
114
+ }
95
115
  }
96
116
  // ─── Policies padrão por ambiente ─────────────────────────────────
97
117
  const POLICY_DEVELOPMENT = {
@@ -118,7 +138,7 @@ const POLICY_QUALITY = {
118
138
  "abap_quick_fix",
119
139
  "abap_publish_binding",
120
140
  "abap_deploy_bsp",
121
- "abapgit_pull",
141
+ "abap_git_pull",
122
142
  ],
123
143
  require_transport: true,
124
144
  max_preview_rows: 100,
@@ -141,8 +161,8 @@ const POLICY_PRODUCTION = {
141
161
  "abap_quick_fix",
142
162
  "abap_publish_binding",
143
163
  "abap_deploy_bsp",
144
- "abapgit_pull",
145
- "abapgit_stage",
164
+ "abap_git_pull",
165
+ "abap_git_stage",
146
166
  "abap_assign_transport",
147
167
  ],
148
168
  require_transport: true,
@@ -7,39 +7,6 @@ async function abapDeleteObject(input) {
7
7
  const name = object_name.toUpperCase();
8
8
  const adtPath = (0, adt_client_js_1.resolveAdtPath)(object_type);
9
9
  const objectPath = `/${adtPath}/${name}`;
10
- const csrf = await (0, adt_client_js_1.ensureSession)();
11
- // Lock — obter lockHandle
12
- const lockResp = await adt_client_js_1.http.post(objectPath, "", {
13
- headers: { "X-CSRF-Token": csrf },
14
- params: { _action: "LOCK", accessMode: "MODIFY" },
15
- validateStatus: (status) => status < 500,
16
- responseType: "text",
17
- });
18
- const lockData = typeof lockResp.data === "string" ? lockResp.data : "";
19
- const handleMatch = lockData.match(/<LOCK_HANDLE>([^<]+)<\/LOCK_HANDLE>/);
20
- const lockHandle = handleMatch?.[1] ?? "";
21
- if (!lockHandle) {
22
- throw new Error(`Falha ao obter lock para ${name}. O objeto pode estar bloqueado por outro usuário.`);
23
- }
24
- // Delete com lockHandle
25
- const params = { lockHandle };
26
- if (transport_request)
27
- params.corrNr = transport_request;
28
- const delResp = await adt_client_js_1.http.delete(objectPath, {
29
- headers: { "X-CSRF-Token": csrf },
30
- params,
31
- validateStatus: (status) => status < 500,
32
- });
33
- if (delResp.status >= 400) {
34
- // Unlock em caso de erro
35
- await adt_client_js_1.http.post(objectPath, "", {
36
- headers: { "X-CSRF-Token": csrf },
37
- params: { _action: "UNLOCK" },
38
- validateStatus: () => true,
39
- });
40
- const data = typeof delResp.data === "string" ? delResp.data : "";
41
- const msg = data.match(/<message[^>]*>([^<]+)<\/message>/i)?.[1] ?? `HTTP ${delResp.status}`;
42
- throw new Error(`Erro ao excluir ${name}: ${msg}`);
43
- }
10
+ await (0, adt_client_js_1.adtLockAndDelete)(objectPath, transport_request);
44
11
  return `Objeto ${name} (${object_type}) excluído com sucesso.`;
45
12
  }
@@ -61,8 +61,8 @@ async function abapSystemInfo() {
61
61
  // Detectar tipo do sistema
62
62
  const systemType = detectSystemType(discoveryXml, sapBasisVersion, abapPlatform);
63
63
  results.push(`System Type: ${systemType}`);
64
- // Detectar release S/4HANA
65
- const release = detectRelease(sapBasisVersion);
64
+ // Detectar release (depende do tipo: S/4HANA release vs ECC EHP)
65
+ const release = detectRelease(sapBasisVersion, systemType);
66
66
  results.push(`Release: ${release}`);
67
67
  // 3. Probe ETag behavior
68
68
  const etagQuirk = await probeEtagBehavior();
@@ -237,35 +237,74 @@ function detectSystemType(discoveryXml, basisVersion, platform) {
237
237
  if (bv < 750)
238
238
  return "ON_PREMISE_ECC";
239
239
  // BASIS 750+: distinguir S/4HANA de ECC EHP8 (ambos têm BASIS 750)
240
- // S/4HANA sempre inclui collections RAP/Service no discovery XML.
241
- // ECC 750 NÃO tem esses endpoints — essa é a diferença chave.
240
+ //
241
+ // Heurísticas (em ordem de confiança):
242
+ //
243
+ // 1. productName no discovery XML — S/4HANA systems incluem "S/4" ou "SAP S/4HANA"
244
+ // ECC nunca inclui isso.
245
+ const productName = extractAttr(discoveryXml, "productName") ||
246
+ extractBetween(discoveryXml, "<productName>", "</productName>");
247
+ if (productName) {
248
+ if (/s.?4.?hana/i.test(productName))
249
+ return "ON_PREMISE_S4";
250
+ if (/ecc|erp/i.test(productName))
251
+ return "ON_PREMISE_ECC";
252
+ }
253
+ // 2. BASIS >= 751 é SEMPRE S/4HANA (ECC parou no 750 com EHP8)
254
+ if (bv >= 751)
255
+ return "ON_PREMISE_S4";
256
+ // 3. BASIS 750: pode ser S/4HANA 1511 ou ECC EHP8.
257
+ // S/4HANA 1511+ inclui collections de RAP/Service Bindings que ECC não tem.
242
258
  const hasS4Collections = discoveryXml.includes("behaviordefinitions") ||
243
259
  discoveryXml.includes("ServiceBindings") ||
244
260
  discoveryXml.includes("ddic/srvd/sources");
245
261
  if (hasS4Collections)
246
262
  return "ON_PREMISE_S4";
247
- // BASIS 750 sem collections S/4 = ECC EHP8
263
+ // BASIS 750 sem indicadores S/4 = ECC EHP8
248
264
  return "ON_PREMISE_ECC";
249
265
  }
250
- function detectRelease(basisVersion) {
266
+ function detectRelease(basisVersion, systemType) {
251
267
  const bv = parseInt(basisVersion, 10) || 0;
252
- if (bv >= 758)
253
- return "2023";
254
- if (bv >= 757)
255
- return "2022";
256
- if (bv >= 756)
257
- return "2021";
258
- if (bv >= 755)
259
- return "2020";
260
- if (bv >= 754)
261
- return "1909";
262
- if (bv >= 753)
263
- return "1809";
264
- if (bv >= 752)
265
- return "1709";
266
- if (bv >= 751)
267
- return "1610";
268
- if (bv >= 750)
269
- return "1511";
270
- return basisVersion;
268
+ // S/4HANA releases (BASIS S/4HANA version)
269
+ if (systemType === "ON_PREMISE_S4") {
270
+ if (bv >= 758)
271
+ return "S/4HANA 2023";
272
+ if (bv >= 757)
273
+ return "S/4HANA 2022";
274
+ if (bv >= 756)
275
+ return "S/4HANA 2021";
276
+ if (bv >= 755)
277
+ return "S/4HANA 2020";
278
+ if (bv >= 754)
279
+ return "S/4HANA 1909";
280
+ if (bv >= 753)
281
+ return "S/4HANA 1809";
282
+ if (bv >= 752)
283
+ return "S/4HANA 1709";
284
+ if (bv >= 751)
285
+ return "S/4HANA 1610";
286
+ if (bv >= 750)
287
+ return "S/4HANA 1511";
288
+ return `S/4HANA (BASIS ${basisVersion})`;
289
+ }
290
+ // ECC releases (BASIS → EHP)
291
+ if (systemType === "ON_PREMISE_ECC") {
292
+ if (bv >= 750)
293
+ return "ECC EHP8 (BASIS 750)";
294
+ if (bv >= 749)
295
+ return "ECC EHP7 (BASIS 749)";
296
+ if (bv >= 748)
297
+ return "ECC EHP6 (BASIS 748)";
298
+ if (bv >= 740)
299
+ return "ECC EHP5 (BASIS 740)";
300
+ if (bv >= 731)
301
+ return "ECC EHP4 (BASIS 731)";
302
+ if (bv >= 730)
303
+ return "ECC (BASIS 730)";
304
+ return `ECC (BASIS ${basisVersion})`;
305
+ }
306
+ // BTP / BW / outros
307
+ if (systemType === "BTP_ABAP_ENV")
308
+ return `BTP ABAP ${basisVersion}`;
309
+ return `BASIS ${basisVersion}`;
271
310
  }
@@ -45,24 +45,53 @@ async function abapAssignTransport(input) {
45
45
  const adtPath = (0, adt_client_js_1.resolveAdtPath)(object_type);
46
46
  const objectUri = `/sap/bc/adt/${adtPath}/${object_name.toUpperCase()}`;
47
47
  const csrf = await (0, adt_client_js_1.ensureSession)();
48
+ // Endpoint correto: POST /cts/transportchecks com o URI do objeto
49
+ // Retorna os transportes candidatos. Se transport_request é fornecido,
50
+ // passamos como corrNr para atribuição direta.
51
+ //
52
+ // Ref: ADT CTS API — transportchecks aceita POST com objectReference
53
+ // e corrNr como query param para atribuição.
48
54
  const payload = `<?xml version="1.0" encoding="UTF-8"?>
49
55
  <tm:root xmlns:tm="http://www.sap.com/cts/adt/tm" xmlns:adtcore="http://www.sap.com/adt/core">
50
- <tm:workbenchObjects>
51
- <tm:abapObject>
52
- <adtcore:objectReference adtcore:uri="${objectUri}" adtcore:type="${object_type}" adtcore:name="${object_name.toUpperCase()}"/>
53
- </tm:abapObject>
54
- </tm:workbenchObjects>
56
+ <tm:workbenchRequest>
57
+ <tm:abapObjects>
58
+ <tm:abapObject tm:pgmid="R3TR" tm:type="${object_type.split("/")[0]}" tm:name="${object_name.toUpperCase()}"
59
+ adtcore:uri="${objectUri}" adtcore:type="${object_type}" adtcore:name="${object_name.toUpperCase()}"/>
60
+ </tm:abapObjects>
61
+ </tm:workbenchRequest>
55
62
  </tm:root>`;
56
- const response = await adt_client_js_1.http.put(`/cts/transportrequests/${transport_request}/tasks/objects`, payload, {
63
+ const response = await adt_client_js_1.http.post("/cts/transportchecks", payload, {
57
64
  headers: {
58
- "Content-Type": "application/xml",
65
+ "Content-Type": "application/vnd.sap.as+xml; charset=UTF-8; dataname=com.sap.adt.transport.service.checkData",
59
66
  "X-CSRF-Token": csrf,
67
+ Accept: "application/vnd.sap.as+xml; charset=UTF-8; dataname=com.sap.adt.transport.service.checkData",
60
68
  },
69
+ params: { corrNr: transport_request },
61
70
  validateStatus: (s) => s >= 200 && s < 500,
62
71
  });
63
- if (response.status === 200 || response.status === 201 || response.status === 204) {
72
+ if (response.status >= 200 && response.status < 300) {
64
73
  return `Objeto ${object_name.toUpperCase()} (${object_type}) vinculado à ordem ${transport_request} com sucesso.`;
65
74
  }
75
+ // Fallback: se transportchecks não aceita corrNr, tentar via lock com corrNr
76
+ // (padrão ADT — lock do objeto atribui ao transporte automaticamente)
77
+ if (response.status === 404 || response.status === 400) {
78
+ const lockResp = await adt_client_js_1.http.post(objectUri, null, {
79
+ headers: { "X-CSRF-Token": csrf },
80
+ params: { _action: "LOCK", accessMode: "MODIFY", corrNr: transport_request },
81
+ validateStatus: (s) => s >= 200 && s < 500,
82
+ });
83
+ if (lockResp.status >= 200 && lockResp.status < 300) {
84
+ // Unlock imediatamente — o lock já atribuiu o objeto ao transporte
85
+ await adt_client_js_1.http.post(objectUri, null, {
86
+ headers: { "X-CSRF-Token": csrf },
87
+ params: { _action: "UNLOCK" },
88
+ validateStatus: () => true,
89
+ });
90
+ return `Objeto ${object_name.toUpperCase()} (${object_type}) vinculado à ordem ${transport_request} via lock/unlock.`;
91
+ }
92
+ const lockError = typeof lockResp.data === "string" ? lockResp.data : JSON.stringify(lockResp.data);
93
+ throw new Error(`Erro ao vincular objeto (lock fallback, HTTP ${lockResp.status}): ${lockError.slice(0, 500)}`);
94
+ }
66
95
  const errorBody = typeof response.data === "string" ? response.data : JSON.stringify(response.data);
67
96
  throw new Error(`Erro ao vincular objeto à ordem (HTTP ${response.status}): ${errorBody.slice(0, 500)}`);
68
97
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linkup-ai/abap-ai",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "LKPABAP.ai — AI-powered ABAP development tools for SAP S/4HANA via ADT REST API",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -32,7 +32,11 @@
32
32
  "start": "node dist/index.js",
33
33
  "dev": "tsc --watch",
34
34
  "prepublishOnly": "npm run build",
35
- "postinstall": "node dist/postinstall.js || true"
35
+ "postinstall": "node dist/postinstall.js || true",
36
+ "release:patch": "npm version patch && npm publish",
37
+ "release:minor": "npm version minor && npm publish",
38
+ "release:next": "npm version prerelease --preid=next && npm publish --tag next",
39
+ "promote:next": "npm dist-tag add @linkup-ai/abap-ai@$(node -p \"require('./package.json').version\") latest"
36
40
  },
37
41
  "dependencies": {
38
42
  "@modelcontextprotocol/sdk": "^1.0.0",