@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.
- package/dist/adt-client.js +71 -0
- package/dist/index.js +10 -1
- package/dist/security-policy.js +28 -8
- package/dist/tools/delete.js +1 -34
- package/dist/tools/system-info.js +64 -25
- package/dist/tools/transports.js +37 -8
- package/package.json +6 -2
package/dist/adt-client.js
CHANGED
|
@@ -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:
|
|
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)();
|
package/dist/security-policy.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
145
|
-
"
|
|
164
|
+
"abap_git_pull",
|
|
165
|
+
"abap_git_stage",
|
|
146
166
|
"abap_assign_transport",
|
|
147
167
|
],
|
|
148
168
|
require_transport: true,
|
package/dist/tools/delete.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
241
|
-
//
|
|
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
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
}
|
package/dist/tools/transports.js
CHANGED
|
@@ -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:
|
|
51
|
-
<tm:
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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",
|