abap-local-client 1.4.13 → 1.4.16
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/adt-manage-systems-handoff.md +278 -0
- package/config-cli.mjs +3 -63
- package/config-manager.mjs +127 -0
- package/package.json +1 -1
- package/sso-sap-client.mjs +270 -2
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# Handoff — implementar `add_system` / `list_systems` / `remove_system` / `update_system` no `abap-local-client`
|
|
2
|
+
|
|
3
|
+
> Este documento é para o agente/dev que vai alterar o repositório **`abap-local-client`**
|
|
4
|
+
> (pacote npm separado, não faz parte do repo `abap-mcp`). O lado do servidor MCP (`abap-mcp`)
|
|
5
|
+
> já está implementado e pronto — falta só o cliente local saber responder a estes comandos.
|
|
6
|
+
>
|
|
7
|
+
> **`add_system`**: spec confirmado diretamente pelo agente do `abap-local-client` — os 5 modos
|
|
8
|
+
> de autenticação e seus parâmetros abaixo refletem exatamente o CLI `add-system` atual.
|
|
9
|
+
>
|
|
10
|
+
> **`list_systems` / `remove_system` / `update_system`**: comandos **novos, propostos pelo lado
|
|
11
|
+
> do servidor** — ainda não confirmados/implementados no `abap-local-client`. Precisam de spec
|
|
12
|
+
> equivalente (ex.: `list-systems`, `remove-system`, `update-system` no CLI) antes de ativar essas
|
|
13
|
+
> operações em produção. Até lá, chamadas com essas operações vão falhar como qualquer comando
|
|
14
|
+
> desconhecido (ver seção "Comando desconhecido" abaixo).
|
|
15
|
+
|
|
16
|
+
## Contexto
|
|
17
|
+
|
|
18
|
+
Hoje, para adicionar um ambiente SAP, o usuário roda manualmente algo como:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx abap-local-client@1.4.14 add-system PIRA_S4D snc --host vhlbvs4dci.sap.piracanjuba.com.br --sysnr 00 --client 100 --snc-partner-name "p:CN=svc.ssoS4D" --language PT
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
O objetivo é permitir que o **agente de IA no Cursor/chat** faça isso por ele — adicionar, listar,
|
|
25
|
+
remover e atualizar ambientes — sem o usuário precisar abrir terminal. Do lado do servidor, todas
|
|
26
|
+
essas operações estão unificadas em uma única tool MCP: `adt_manage_systems({ operation: "add" | "list" | "remove" | "update", ... })`.
|
|
27
|
+
A tool já manda o comando certo pelo WebSocket — só falta o cliente local **escutar e tratar** cada
|
|
28
|
+
`action`.
|
|
29
|
+
|
|
30
|
+
O padrão de protocolo é **exatamente o mesmo** já usado por `switch_system` e `refresh_systems`
|
|
31
|
+
(que o cliente já implementa hoje) — são só novos `action`/`command` a mais. Cada operação da tool
|
|
32
|
+
MCP vira um `action` diferente no envelope WebSocket (`add_system`, `list_systems`,
|
|
33
|
+
`remove_system`, `update_system`) — não é um único action compartilhado.
|
|
34
|
+
|
|
35
|
+
## Operação `add` — os 5 modos de autenticação (spec confirmado)
|
|
36
|
+
|
|
37
|
+
| Modo | Obrigatórios | Opcionais |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `basic` | `--host`, `--port`, `--client`, `--username`, `--password` | `--saprouter`, `--language` |
|
|
40
|
+
| `spnego` | `--host`, `--port`, `--client` | `--service-principal`, `--language` |
|
|
41
|
+
| `x509` | `--host`, `--port`, `--client`, `--ca-cert`, `--ca-key`, `--login-id` | `--language` |
|
|
42
|
+
| `saml` | `--base-url`, `--client` | `--headless`, `--timeout-ms`, `--language` |
|
|
43
|
+
| `snc` (usado no Piracanjuba/Vale) | `--host`, `--sysnr`, `--client`, `--snc-partner-name` | `--snc-my-name`, `--snc-qop`, `--snc-lib`, `--username`, `--saprouter`, `--language` |
|
|
44
|
+
|
|
45
|
+
**Pontos importantes:**
|
|
46
|
+
- Só `basic` e `snc` aceitam `--saprouter`. Nos outros 3 modos (`spnego`, `x509`, `saml`) esse flag
|
|
47
|
+
não existe.
|
|
48
|
+
- `basic`/`spnego`/`x509` usam `--port` (porta HTTP/HTTPS); só `snc` usa `--sysnr` (número do
|
|
49
|
+
sistema SAP). Não são a mesma coisa — a tool MCP manda os dois como campos separados
|
|
50
|
+
(`port` vs `sysnr`) e nunca os dois juntos no mesmo comando.
|
|
51
|
+
- `saml` é o único modo que não usa `--host`/`--port` — usa `--base-url` direto.
|
|
52
|
+
|
|
53
|
+
### O que a tool MCP envia (`operation: "add"`)
|
|
54
|
+
|
|
55
|
+
Mensagem recebida pelo cliente local via WebSocket (mesmo canal/autenticação já existentes).
|
|
56
|
+
Exemplo real (modo `snc`, igual ao uso no Piracanjuba):
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"action": "add_system",
|
|
61
|
+
"callspecs": [
|
|
62
|
+
{
|
|
63
|
+
"command": "add_system",
|
|
64
|
+
"systemId": "PIRA_S4D",
|
|
65
|
+
"connectionType": "snc",
|
|
66
|
+
"host": "vhlbvs4dci.sap.piracanjuba.com.br",
|
|
67
|
+
"sysnr": "00",
|
|
68
|
+
"client": "100",
|
|
69
|
+
"sncPartnerName": "p:CN=svc.ssoS4D",
|
|
70
|
+
"language": "PT"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"requestId": "req_1730900000000_abc123xyz",
|
|
74
|
+
"timestamp": "2026-07-06T19:57:00.000Z"
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Ou seja: o payload relevante está em `callspecs[0]` (sempre um array de 1 elemento neste comando —
|
|
79
|
+
o servidor nunca manda mais de um por vez para este action). O cliente deve:
|
|
80
|
+
1. Ler `message.action === "add_system"`.
|
|
81
|
+
2. Pegar `message.callspecs[0]` como o comando real.
|
|
82
|
+
3. Executar o equivalente ao CLI `add-system` com esses parâmetros.
|
|
83
|
+
4. Responder usando o `requestId` recebido (ver formato de resposta abaixo).
|
|
84
|
+
|
|
85
|
+
### Mapeamento completo de campos → flags do CLI
|
|
86
|
+
|
|
87
|
+
| Campo no JSON | Flag do CLI | Modo(s) |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `systemId` | 1º posicional (`SYSTEM_ID`) | todos — já vem em UPPERCASE do servidor |
|
|
90
|
+
| `connectionType` | 2º posicional (`basic`\|`spnego`\|`x509`\|`saml`\|`snc`) | todos |
|
|
91
|
+
| `host` | `--host` | basic, spnego, x509, snc |
|
|
92
|
+
| `port` | `--port` | basic, spnego, x509 |
|
|
93
|
+
| `sysnr` | `--sysnr` | snc |
|
|
94
|
+
| `client` | `--client` | todos |
|
|
95
|
+
| `language` | `--language` | todos (opcional) |
|
|
96
|
+
| `username` | `--username` | basic (obrigatório), snc (opcional) |
|
|
97
|
+
| `password` | `--password` | basic |
|
|
98
|
+
| `saprouter` | `--saprouter` | basic, snc (opcional) |
|
|
99
|
+
| `servicePrincipal` | `--service-principal` | spnego (opcional) |
|
|
100
|
+
| `caCert` | `--ca-cert` | x509 |
|
|
101
|
+
| `caKey` | `--ca-key` | x509 |
|
|
102
|
+
| `loginId` | `--login-id` | x509 |
|
|
103
|
+
| `baseUrl` | `--base-url` | saml |
|
|
104
|
+
| `headless` | `--headless` | saml (opcional, boolean) |
|
|
105
|
+
| `timeoutMs` | `--timeout-ms` | saml (opcional, number) |
|
|
106
|
+
| `sncPartnerName` | `--snc-partner-name` | snc |
|
|
107
|
+
| `sncMyName` | `--snc-my-name` | snc (opcional) |
|
|
108
|
+
| `sncQop` | `--snc-qop` | snc (opcional) |
|
|
109
|
+
| `sncLib` | `--snc-lib` | snc (opcional) |
|
|
110
|
+
| `extraParams` | objeto `{ "flag-name": "valor" }` | qualquer flag futuro não mapeado — repassar como está |
|
|
111
|
+
|
|
112
|
+
## Operação `list` — proposta
|
|
113
|
+
|
|
114
|
+
### O que a tool MCP envia (`operation: "list"`)
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"action": "list_systems",
|
|
119
|
+
"callspecs": [ { "command": "list_systems" } ],
|
|
120
|
+
"requestId": "req_...",
|
|
121
|
+
"timestamp": "..."
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Nenhum parâmetro além do `command`. O cliente deve devolver todos os sistemas atualmente
|
|
126
|
+
disponíveis para `switch_system` (tanto os vindos do admin portal via `refresh_systems`, quanto os
|
|
127
|
+
adicionados localmente via `add_system`).
|
|
128
|
+
|
|
129
|
+
### Resposta esperada
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"type": "response",
|
|
134
|
+
"requestId": "req_...",
|
|
135
|
+
"success": true,
|
|
136
|
+
"data": {
|
|
137
|
+
"systems": [
|
|
138
|
+
{ "systemId": "PIRA_S4D", "connectionType": "snc", "host": "vhlbvs4dci.sap.piracanjuba.com.br", "client": "100" },
|
|
139
|
+
{ "systemId": "S4H_100", "connectionType": "basic", "host": "...", "client": "100" }
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Nunca incluir `password`/`caKey`/segredos no retorno** — só metadados de conexão (host,
|
|
146
|
+
connectionType, client, etc.).
|
|
147
|
+
|
|
148
|
+
## Operação `remove` — proposta
|
|
149
|
+
|
|
150
|
+
### O que a tool MCP envia (`operation: "remove"`)
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"action": "remove_system",
|
|
155
|
+
"callspecs": [ { "command": "remove_system", "systemId": "PIRA_S4D" } ],
|
|
156
|
+
"requestId": "req_...",
|
|
157
|
+
"timestamp": "..."
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Se `systemId` não existir, responder `success: false` com erro claro (não travar). Se for o
|
|
162
|
+
sistema ativo no momento, o cliente decide a política (ex.: bloquear remoção do sistema ativo, ou
|
|
163
|
+
remover e cair para "nenhum sistema selecionado") — documentar a escolha feita.
|
|
164
|
+
|
|
165
|
+
## Operação `update` — proposta
|
|
166
|
+
|
|
167
|
+
### O que a tool MCP envia (`operation: "update"`)
|
|
168
|
+
|
|
169
|
+
Mesmo formato de `add_system`, mas com `action: "update_system"` e apenas os campos que mudaram
|
|
170
|
+
(mais `systemId` e, se estiver trocando o modo de autenticação, `connectionType` + todos os campos
|
|
171
|
+
obrigatórios do novo modo):
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"action": "update_system",
|
|
176
|
+
"callspecs": [
|
|
177
|
+
{
|
|
178
|
+
"command": "update_system",
|
|
179
|
+
"systemId": "PIRA_S4D",
|
|
180
|
+
"host": "novo-host.sap.piracanjuba.com.br"
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
"requestId": "req_...",
|
|
184
|
+
"timestamp": "..."
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Semântica esperada: **patch parcial** — só os campos presentes são alterados; os demais mantêm o
|
|
189
|
+
valor já salvo. Se `systemId` não existir, responder `success: false` com erro claro.
|
|
190
|
+
|
|
191
|
+
## O que o cliente deve responder (todas as operações)
|
|
192
|
+
|
|
193
|
+
Mesmo formato de resposta que `switch_system`/`refresh_systems` já usam hoje — uma mensagem com
|
|
194
|
+
`type: "response"` e o **mesmo `requestId`** recebido na requisição:
|
|
195
|
+
|
|
196
|
+
**Sucesso (`add`/`remove`/`update`):**
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"type": "response",
|
|
200
|
+
"requestId": "req_1730900000000_abc123xyz",
|
|
201
|
+
"success": true,
|
|
202
|
+
"data": { "systemId": "PIRA_S4D", "message": "Sistema adicionado com sucesso" }
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Falha (ex.: sistema já existe, host inválido, flag faltando, sistema não encontrado):**
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"type": "response",
|
|
210
|
+
"requestId": "req_1730900000000_abc123xyz",
|
|
211
|
+
"success": false,
|
|
212
|
+
"error": "Sistema PIRA_S4D já existe. Use update_system ou remova-o primeiro."
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Importante:** para `add`/`remove`/`update`, `data` deve ser um **objeto simples, não um array** —
|
|
217
|
+
o lado do servidor (`tool-wrapper.ts`) só faz parsing item-a-item quando `data` é array (usado
|
|
218
|
+
pelas ações de ADT REST callspecs); para estes comandos o servidor apenas repassa a mensagem de
|
|
219
|
+
sucesso fixa (`successMessage`) e ignora o conteúdo de `data` quando ele não é array — mas ainda
|
|
220
|
+
assim mantenha `data` como objeto (nunca `null`/`undefined` em caso de sucesso) para consistência.
|
|
221
|
+
Para `list`, `data.systems` deve ser um array (ver seção da operação `list` acima).
|
|
222
|
+
|
|
223
|
+
## Regras de negócio a implementar
|
|
224
|
+
|
|
225
|
+
1. **Idempotência clara (`add`):** se `systemId` já existir na config local, retornar
|
|
226
|
+
`success: false` com `error` explicando isso (não sobrescrever silenciosamente, e não
|
|
227
|
+
travar/crashar).
|
|
228
|
+
2. **Validação por `connectionType` (`add`/`update`):** usar a tabela de obrigatórios/opcionais
|
|
229
|
+
acima. Se faltar campo obrigatório, responder `success: false` com `error` claro dizendo qual
|
|
230
|
+
campo falta — não travar o processo do cliente.
|
|
231
|
+
3. **Rejeitar `saprouter` fora de `basic`/`snc`** — se vier em `spnego`/`x509`/`saml`, é um uso
|
|
232
|
+
indevido (a tool MCP já bloqueia isso do lado do servidor, mas validar de novo no cliente é
|
|
233
|
+
defesa em profundidade).
|
|
234
|
+
4. **Nunca logar `password`/`caKey` em texto puro** (nem em stdout/console, nem no arquivo de
|
|
235
|
+
config em texto simples se possível).
|
|
236
|
+
5. **Nunca ecoar `password`/`caKey` de volta** — nem no campo `data` da resposta de sucesso de
|
|
237
|
+
`add`/`update`, nem na listagem de `list`.
|
|
238
|
+
6. **Comando desconhecido:** se o cliente ainda não tiver essa versão implementada e receber
|
|
239
|
+
`action` desconhecido (`add_system`, `list_systems`, `remove_system` ou `update_system`),
|
|
240
|
+
responder `success: false, error: "Comando '<action>' não suportado nesta versão do cliente. Atualize com: npx abap-local-client@latest"`
|
|
241
|
+
em vez de travar/ignorar silenciosamente — dá um erro legível no chat em vez de timeout
|
|
242
|
+
genérico.
|
|
243
|
+
7. **`remove`/`update` de sistema inexistente:** responder `success: false` com erro claro
|
|
244
|
+
("Sistema X não encontrado"), nunca criar um novo registro como efeito colateral.
|
|
245
|
+
|
|
246
|
+
## Como testar de ponta a ponta
|
|
247
|
+
|
|
248
|
+
1. Atualizar o `abap-local-client` local para a versão com os handlers novos, já conectado ao MCP
|
|
249
|
+
server (`abap-mcp`) com uma API key válida.
|
|
250
|
+
2. No chat/Cursor, chamar a tool MCP (exemplo `add`, modo `snc`):
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"operation": "add",
|
|
254
|
+
"system_id": "PIRA_S4D",
|
|
255
|
+
"connection_type": "snc",
|
|
256
|
+
"host": "vhlbvs4dci.sap.piracanjuba.com.br",
|
|
257
|
+
"sysnr": "00",
|
|
258
|
+
"client": "100",
|
|
259
|
+
"snc_partner_name": "p:CN=svc.ssoS4D",
|
|
260
|
+
"language": "PT"
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
3. Confirmar que o sistema aparece disponível para `adt_switch_system({ system: "PIRA_S4D" })`.
|
|
264
|
+
4. Chamar `{ "operation": "list" }` e confirmar que `PIRA_S4D` aparece na lista.
|
|
265
|
+
5. Repetir a chamada de `add` — deve retornar erro claro de "já existe", não duplicar nem quebrar.
|
|
266
|
+
6. Chamar `{ "operation": "update", "system_id": "PIRA_S4D", "language": "EN" }` e confirmar via
|
|
267
|
+
`list` que só `language` mudou.
|
|
268
|
+
7. Chamar `{ "operation": "remove", "system_id": "PIRA_S4D" }` e confirmar via `list` que sumiu.
|
|
269
|
+
8. Repetir o teste de `add` para os outros 4 modos (`basic`, `spnego`, `x509`, `saml`) com dados de
|
|
270
|
+
exemplo.
|
|
271
|
+
|
|
272
|
+
## Referência — arquivos do lado do servidor (abap-mcp, já implementado)
|
|
273
|
+
|
|
274
|
+
- `src/tools/system/manage-systems.ts` — definição e handler da tool MCP `adt_manage_systems`
|
|
275
|
+
(operations: add, list, remove, update)
|
|
276
|
+
- `src/websocket/tool-wrapper.ts` — `executeWithWebSocketFallback()`, monta o envelope `{ action, callspecs }`
|
|
277
|
+
- `src/websocket/connection-manager.ts` — `sendRequest()` adiciona `requestId`/`timestamp` e espera resposta com `type: "response"`
|
|
278
|
+
- `src/tools/system/switch-system.ts` / `refresh-systems.ts` — exemplos já funcionando do mesmo padrão de comando (ficam como tools separadas, não fazem parte de `adt_manage_systems`)
|
package/config-cli.mjs
CHANGED
|
@@ -151,73 +151,13 @@ switch (command) {
|
|
|
151
151
|
process.exit(1);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
console.error(`Invalid auth mode: ${authMode}. Use: ${validModes.join(', ')}`);
|
|
154
|
+
if (!config.VALID_AUTH_MODES.includes(authMode)) {
|
|
155
|
+
console.error(`Invalid auth mode: ${authMode}. Use: ${config.VALID_AUTH_MODES.join(', ')}`);
|
|
157
156
|
process.exit(1);
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
const opts = parseKeyValue(args.slice(3));
|
|
161
|
-
|
|
162
|
-
// Build connection object based on auth mode
|
|
163
|
-
let connection = {};
|
|
164
|
-
switch (authMode) {
|
|
165
|
-
case 'basic':
|
|
166
|
-
connection = {
|
|
167
|
-
host: opts.host || 'localhost',
|
|
168
|
-
port: opts.port || '44301',
|
|
169
|
-
client: opts.client || '100',
|
|
170
|
-
username: opts.username || '',
|
|
171
|
-
password: opts.password || '',
|
|
172
|
-
saprouter: opts.saprouter || '',
|
|
173
|
-
language: opts.language || 'EN',
|
|
174
|
-
};
|
|
175
|
-
break;
|
|
176
|
-
case 'spnego':
|
|
177
|
-
connection = {
|
|
178
|
-
host: opts.host || '',
|
|
179
|
-
port: opts.port || '44301',
|
|
180
|
-
client: opts.client || '100',
|
|
181
|
-
servicePrincipal: opts.service_principal || '',
|
|
182
|
-
language: opts.language || 'EN',
|
|
183
|
-
};
|
|
184
|
-
break;
|
|
185
|
-
case 'x509':
|
|
186
|
-
connection = {
|
|
187
|
-
host: opts.host || '',
|
|
188
|
-
port: opts.port || '44301',
|
|
189
|
-
client: opts.client || '100',
|
|
190
|
-
caCertPem: opts.ca_cert || '',
|
|
191
|
-
caKeyPem: opts.ca_key || '',
|
|
192
|
-
loginIdentifier: opts.login_id || '',
|
|
193
|
-
validityMinutes: parseInt(opts.validity_minutes || '5', 10),
|
|
194
|
-
language: opts.language || 'EN',
|
|
195
|
-
};
|
|
196
|
-
break;
|
|
197
|
-
case 'saml':
|
|
198
|
-
connection = {
|
|
199
|
-
baseUrl: opts.base_url || '',
|
|
200
|
-
client: opts.client || '100',
|
|
201
|
-
headless: opts.headless === 'true',
|
|
202
|
-
loginTimeoutMs: parseInt(opts.timeout_ms || '600000', 10),
|
|
203
|
-
language: opts.language || 'EN',
|
|
204
|
-
};
|
|
205
|
-
break;
|
|
206
|
-
case 'snc':
|
|
207
|
-
connection = {
|
|
208
|
-
host: opts.host || '',
|
|
209
|
-
sysnr: opts.sysnr || '00',
|
|
210
|
-
client: opts.client || '100',
|
|
211
|
-
username: opts.username || '',
|
|
212
|
-
saprouter: opts.saprouter || '',
|
|
213
|
-
sncPartnerName: opts.snc_partner_name || '',
|
|
214
|
-
sncMyName: opts.snc_my_name || '',
|
|
215
|
-
sncQop: opts.snc_qop || '3',
|
|
216
|
-
sncLib: opts.snc_lib || '',
|
|
217
|
-
language: opts.language || 'EN',
|
|
218
|
-
};
|
|
219
|
-
break;
|
|
220
|
-
}
|
|
160
|
+
const connection = config.buildConnection(authMode, opts);
|
|
221
161
|
|
|
222
162
|
config.addOrUpdateSystem(systemId, { authMode, connection, enabled: true });
|
|
223
163
|
console.log(`✅ System "${systemId}" (${authMode}) saved.`);
|
package/config-manager.mjs
CHANGED
|
@@ -207,6 +207,133 @@ export function addOrUpdateSystem(systemId, systemConfig) {
|
|
|
207
207
|
saveConfig(config);
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Auth-mode metadata: required/optional connection fields.
|
|
212
|
+
* Keys match the internal `opts` naming used by buildConnection() (snake_case,
|
|
213
|
+
* mirroring the CLI's --flag-name → flag_name convention).
|
|
214
|
+
*/
|
|
215
|
+
export const AUTH_MODE_FIELDS = {
|
|
216
|
+
basic: { required: ['host', 'port', 'client', 'username', 'password'], optional: ['saprouter', 'language'] },
|
|
217
|
+
spnego: { required: ['host', 'port', 'client'], optional: ['service_principal', 'language'] },
|
|
218
|
+
x509: { required: ['host', 'port', 'client', 'ca_cert', 'ca_key', 'login_id'], optional: ['language'] },
|
|
219
|
+
saml: { required: ['base_url', 'client'], optional: ['headless', 'timeout_ms', 'language'] },
|
|
220
|
+
snc: { required: ['host', 'sysnr', 'client', 'snc_partner_name'], optional: ['snc_my_name', 'snc_qop', 'snc_lib', 'username', 'saprouter', 'language'] },
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const VALID_AUTH_MODES = Object.keys(AUTH_MODE_FIELDS);
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Build a `connection` object for a system from raw options, honoring the
|
|
227
|
+
* same defaults for every auth mode. Shared by config-cli.mjs (CLI) and
|
|
228
|
+
* sso-sap-client.mjs (WebSocket `add_system` command) so both entry points
|
|
229
|
+
* behave identically.
|
|
230
|
+
* @param {string} authMode - basic|spnego|x509|saml|snc
|
|
231
|
+
* @param {object} opts - snake_case keys (host, port, sysnr, client, username,
|
|
232
|
+
* password, saprouter, language, service_principal, ca_cert, ca_key,
|
|
233
|
+
* login_id, validity_minutes, base_url, headless, timeout_ms,
|
|
234
|
+
* snc_partner_name, snc_my_name, snc_qop, snc_lib)
|
|
235
|
+
* @returns {object} connection
|
|
236
|
+
*/
|
|
237
|
+
export function buildConnection(authMode, opts = {}) {
|
|
238
|
+
const isTrue = (v) => v === true || v === 'true';
|
|
239
|
+
switch (authMode) {
|
|
240
|
+
case 'basic':
|
|
241
|
+
return {
|
|
242
|
+
host: opts.host || 'localhost',
|
|
243
|
+
port: opts.port || '44301',
|
|
244
|
+
client: opts.client || '100',
|
|
245
|
+
username: opts.username || '',
|
|
246
|
+
password: opts.password || '',
|
|
247
|
+
saprouter: opts.saprouter || '',
|
|
248
|
+
language: opts.language || 'EN',
|
|
249
|
+
};
|
|
250
|
+
case 'spnego':
|
|
251
|
+
return {
|
|
252
|
+
host: opts.host || '',
|
|
253
|
+
port: opts.port || '44301',
|
|
254
|
+
client: opts.client || '100',
|
|
255
|
+
servicePrincipal: opts.service_principal || '',
|
|
256
|
+
language: opts.language || 'EN',
|
|
257
|
+
};
|
|
258
|
+
case 'x509':
|
|
259
|
+
return {
|
|
260
|
+
host: opts.host || '',
|
|
261
|
+
port: opts.port || '44301',
|
|
262
|
+
client: opts.client || '100',
|
|
263
|
+
caCertPem: opts.ca_cert || '',
|
|
264
|
+
caKeyPem: opts.ca_key || '',
|
|
265
|
+
loginIdentifier: opts.login_id || '',
|
|
266
|
+
validityMinutes: parseInt(opts.validity_minutes || '5', 10),
|
|
267
|
+
language: opts.language || 'EN',
|
|
268
|
+
};
|
|
269
|
+
case 'saml':
|
|
270
|
+
return {
|
|
271
|
+
baseUrl: opts.base_url || '',
|
|
272
|
+
client: opts.client || '100',
|
|
273
|
+
headless: isTrue(opts.headless),
|
|
274
|
+
loginTimeoutMs: parseInt(opts.timeout_ms || '600000', 10),
|
|
275
|
+
language: opts.language || 'EN',
|
|
276
|
+
};
|
|
277
|
+
case 'snc':
|
|
278
|
+
return {
|
|
279
|
+
host: opts.host || '',
|
|
280
|
+
sysnr: opts.sysnr || '00',
|
|
281
|
+
client: opts.client || '100',
|
|
282
|
+
username: opts.username || '',
|
|
283
|
+
saprouter: opts.saprouter || '',
|
|
284
|
+
sncPartnerName: opts.snc_partner_name || '',
|
|
285
|
+
sncMyName: opts.snc_my_name || '',
|
|
286
|
+
sncQop: opts.snc_qop || '3',
|
|
287
|
+
sncLib: opts.snc_lib || '',
|
|
288
|
+
language: opts.language || 'EN',
|
|
289
|
+
};
|
|
290
|
+
default:
|
|
291
|
+
return {};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Validate that all required fields for an auth mode are present (non-empty)
|
|
297
|
+
* in opts. Returns an array of missing field names (snake_case), empty if OK.
|
|
298
|
+
*/
|
|
299
|
+
export function getMissingRequiredFields(authMode, opts = {}) {
|
|
300
|
+
const spec = AUTH_MODE_FIELDS[authMode];
|
|
301
|
+
if (!spec) return [`unknown authMode: ${authMode}`];
|
|
302
|
+
return spec.required.filter((f) => {
|
|
303
|
+
const v = opts[f];
|
|
304
|
+
return v === undefined || v === null || v === '';
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Fields that must never be exposed outside the encrypted local store
|
|
309
|
+
// (e.g. in list_systems responses sent back to the MCP server).
|
|
310
|
+
const SENSITIVE_CONNECTION_FIELDS = ['password', 'caKeyPem'];
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Return a copy of a connection object with secrets stripped out.
|
|
314
|
+
* @param {object} connection
|
|
315
|
+
*/
|
|
316
|
+
export function sanitizeConnection(connection = {}) {
|
|
317
|
+
const clean = { ...connection };
|
|
318
|
+
for (const f of SENSITIVE_CONNECTION_FIELDS) delete clean[f];
|
|
319
|
+
return clean;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* List all systems with sanitized connection metadata (no secrets).
|
|
324
|
+
* Used by the WebSocket `list_systems` command.
|
|
325
|
+
* @returns {object[]} [{ systemId, connectionType, enabled, host, client, ... }]
|
|
326
|
+
*/
|
|
327
|
+
export function listSystemsDetailed() {
|
|
328
|
+
const cfg = loadConfig();
|
|
329
|
+
return Object.entries(cfg.systems).map(([id, sys]) => ({
|
|
330
|
+
systemId: id,
|
|
331
|
+
connectionType: sys.authMode || 'basic',
|
|
332
|
+
enabled: sys.enabled !== false,
|
|
333
|
+
...sanitizeConnection(sys.connection || {}),
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
|
|
210
337
|
/**
|
|
211
338
|
* Remove a system by ID.
|
|
212
339
|
* @param {string} systemId
|
package/package.json
CHANGED
package/sso-sap-client.mjs
CHANGED
|
@@ -21,6 +21,196 @@ function resolveUsername(conn) {
|
|
|
21
21
|
return (conn.username || '').toUpperCase();
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
25
|
+
// add_system — handles the MCP `add_system` WebSocket command
|
|
26
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
|
|
28
|
+
// Maps the camelCase JSON fields sent by the MCP server to the snake_case
|
|
29
|
+
// `opts` keys expected by config.buildConnection()/getMissingRequiredFields().
|
|
30
|
+
const ADD_SYSTEM_FIELD_MAP = {
|
|
31
|
+
host: 'host',
|
|
32
|
+
port: 'port',
|
|
33
|
+
sysnr: 'sysnr',
|
|
34
|
+
client: 'client',
|
|
35
|
+
language: 'language',
|
|
36
|
+
username: 'username',
|
|
37
|
+
password: 'password',
|
|
38
|
+
saprouter: 'saprouter',
|
|
39
|
+
servicePrincipal: 'service_principal',
|
|
40
|
+
caCert: 'ca_cert',
|
|
41
|
+
caKey: 'ca_key',
|
|
42
|
+
loginId: 'login_id',
|
|
43
|
+
baseUrl: 'base_url',
|
|
44
|
+
headless: 'headless',
|
|
45
|
+
timeoutMs: 'timeout_ms',
|
|
46
|
+
sncPartnerName: 'snc_partner_name',
|
|
47
|
+
sncMyName: 'snc_my_name',
|
|
48
|
+
sncQop: 'snc_qop',
|
|
49
|
+
sncLib: 'snc_lib',
|
|
50
|
+
};
|
|
51
|
+
const OPTS_TO_FIELD_MAP = Object.fromEntries(Object.entries(ADD_SYSTEM_FIELD_MAP).map(([k, v]) => [v, k]));
|
|
52
|
+
|
|
53
|
+
function kebabToCamel(str) {
|
|
54
|
+
return str.replace(/[-_]([a-z0-9])/gi, (_, c) => c.toUpperCase());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handle the `add_system` command: validate the payload and persist a new
|
|
59
|
+
* SAP system into the encrypted local config, using the same connection
|
|
60
|
+
* builder as the `add-system` CLI command.
|
|
61
|
+
* @param {object} spec - callspecs[0] payload from the MCP server
|
|
62
|
+
* @returns {{ success: boolean, data?: object, error?: string }}
|
|
63
|
+
*/
|
|
64
|
+
function handleAddSystem(spec = {}) {
|
|
65
|
+
const systemId = String(spec.systemId || '').trim().toUpperCase();
|
|
66
|
+
const authMode = String(spec.connectionType || spec.authMode || '').trim().toLowerCase();
|
|
67
|
+
|
|
68
|
+
if (!systemId) return { success: false, error: 'systemId é obrigatório.' };
|
|
69
|
+
if (!config.VALID_AUTH_MODES.includes(authMode)) {
|
|
70
|
+
return { success: false, error: `connectionType inválido: "${spec.connectionType || ''}". Use um de: ${config.VALID_AUTH_MODES.join(', ')}.` };
|
|
71
|
+
}
|
|
72
|
+
if (config.getSystem(systemId)) {
|
|
73
|
+
return { success: false, error: `Sistema "${systemId}" já existe. Remova-o primeiro (remove-system) ou use outro systemId.` };
|
|
74
|
+
}
|
|
75
|
+
if (spec.saprouter && !['basic', 'snc'].includes(authMode)) {
|
|
76
|
+
return { success: false, error: `"saprouter" não é suportado no modo "${authMode}" (apenas basic/snc).` };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const opts = {};
|
|
80
|
+
for (const [jsonKey, optsKey] of Object.entries(ADD_SYSTEM_FIELD_MAP)) {
|
|
81
|
+
if (spec[jsonKey] !== undefined && spec[jsonKey] !== null) opts[optsKey] = spec[jsonKey];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const missing = config.getMissingRequiredFields(authMode, opts);
|
|
85
|
+
if (missing.length > 0) {
|
|
86
|
+
const friendly = missing.map(f => OPTS_TO_FIELD_MAP[f] || f);
|
|
87
|
+
return { success: false, error: `Campo(s) obrigatório(s) faltando para o modo "${authMode}": ${friendly.join(', ')}.` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const connection = config.buildConnection(authMode, opts);
|
|
91
|
+
|
|
92
|
+
// Pass through any future/unmapped flags verbatim (kebab-case → camelCase)
|
|
93
|
+
if (spec.extraParams && typeof spec.extraParams === 'object') {
|
|
94
|
+
for (const [k, v] of Object.entries(spec.extraParams)) connection[kebabToCamel(k)] = v;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
config.addOrUpdateSystem(systemId, { authMode, connection, enabled: true });
|
|
98
|
+
console.log(` ✅ System "${systemId}" (${authMode}) added via add_system command`);
|
|
99
|
+
return { success: true, data: { systemId, message: 'Sistema adicionado com sucesso' } };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Handle the `list_systems` command: return all configured systems with
|
|
104
|
+
* sanitized connection metadata (no password/caKey).
|
|
105
|
+
* @returns {{ success: boolean, data: { systems: object[] } }}
|
|
106
|
+
*/
|
|
107
|
+
function handleListSystems() {
|
|
108
|
+
return { success: true, data: { systems: config.listSystemsDetailed() } };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Handle the `remove_system` command.
|
|
113
|
+
* @param {object} spec - callspecs[0] payload from the MCP server
|
|
114
|
+
* @returns {{ success: boolean, data?: object, error?: string }}
|
|
115
|
+
*/
|
|
116
|
+
function handleRemoveSystem(spec = {}) {
|
|
117
|
+
const systemId = String(spec.systemId || '').trim().toUpperCase();
|
|
118
|
+
if (!systemId) return { success: false, error: 'systemId é obrigatório.' };
|
|
119
|
+
if (!config.getSystem(systemId)) {
|
|
120
|
+
return { success: false, error: `Sistema "${systemId}" não encontrado.` };
|
|
121
|
+
}
|
|
122
|
+
config.removeSystem(systemId);
|
|
123
|
+
console.log(` 🗑️ System "${systemId}" removed via remove_system command`);
|
|
124
|
+
return { success: true, data: { systemId, message: 'Sistema removido com sucesso' } };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Maps the camelCase JSON fields sent by the MCP server directly to the
|
|
128
|
+
// `connection` object's own key names (used for partial-patch merges in
|
|
129
|
+
// update_system — different from ADD_SYSTEM_FIELD_MAP, which maps to the
|
|
130
|
+
// snake_case `opts` keys consumed by buildConnection()).
|
|
131
|
+
const CONNECTION_FIELD_MAP = {
|
|
132
|
+
host: 'host',
|
|
133
|
+
port: 'port',
|
|
134
|
+
sysnr: 'sysnr',
|
|
135
|
+
client: 'client',
|
|
136
|
+
language: 'language',
|
|
137
|
+
username: 'username',
|
|
138
|
+
password: 'password',
|
|
139
|
+
saprouter: 'saprouter',
|
|
140
|
+
servicePrincipal: 'servicePrincipal',
|
|
141
|
+
caCert: 'caCertPem',
|
|
142
|
+
caKey: 'caKeyPem',
|
|
143
|
+
loginId: 'loginIdentifier',
|
|
144
|
+
baseUrl: 'baseUrl',
|
|
145
|
+
headless: 'headless',
|
|
146
|
+
sncPartnerName: 'sncPartnerName',
|
|
147
|
+
sncMyName: 'sncMyName',
|
|
148
|
+
sncQop: 'sncQop',
|
|
149
|
+
sncLib: 'sncLib',
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Handle the `update_system` command.
|
|
154
|
+
* - If `connectionType` is absent (or unchanged), applies a partial patch:
|
|
155
|
+
* only the provided fields are overwritten, everything else is preserved.
|
|
156
|
+
* - If `connectionType` differs from the stored mode, treats the payload like
|
|
157
|
+
* a fresh `add_system` for the new mode (full required-field validation,
|
|
158
|
+
* connection rebuilt from scratch — old mode's fields don't carry over).
|
|
159
|
+
* @param {object} spec - callspecs[0] payload from the MCP server
|
|
160
|
+
* @returns {{ success: boolean, data?: object, error?: string }}
|
|
161
|
+
*/
|
|
162
|
+
function handleUpdateSystem(spec = {}) {
|
|
163
|
+
const systemId = String(spec.systemId || '').trim().toUpperCase();
|
|
164
|
+
if (!systemId) return { success: false, error: 'systemId é obrigatório.' };
|
|
165
|
+
const existing = config.getSystem(systemId);
|
|
166
|
+
if (!existing) return { success: false, error: `Sistema "${systemId}" não encontrado.` };
|
|
167
|
+
|
|
168
|
+
let newMode = null;
|
|
169
|
+
if (spec.connectionType !== undefined && spec.connectionType !== null && String(spec.connectionType).trim() !== '') {
|
|
170
|
+
newMode = String(spec.connectionType).trim().toLowerCase();
|
|
171
|
+
if (!config.VALID_AUTH_MODES.includes(newMode)) {
|
|
172
|
+
return { success: false, error: `connectionType inválido: "${spec.connectionType}". Use um de: ${config.VALID_AUTH_MODES.join(', ')}.` };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const switchingMode = newMode !== null && newMode !== existing.authMode;
|
|
176
|
+
const effectiveMode = newMode || existing.authMode;
|
|
177
|
+
|
|
178
|
+
if (spec.saprouter && !['basic', 'snc'].includes(effectiveMode)) {
|
|
179
|
+
return { success: false, error: `"saprouter" não é suportado no modo "${effectiveMode}" (apenas basic/snc).` };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let connection;
|
|
183
|
+
if (switchingMode) {
|
|
184
|
+
const opts = {};
|
|
185
|
+
for (const [jsonKey, optsKey] of Object.entries(ADD_SYSTEM_FIELD_MAP)) {
|
|
186
|
+
if (spec[jsonKey] !== undefined && spec[jsonKey] !== null) opts[optsKey] = spec[jsonKey];
|
|
187
|
+
}
|
|
188
|
+
const missing = config.getMissingRequiredFields(effectiveMode, opts);
|
|
189
|
+
if (missing.length > 0) {
|
|
190
|
+
const friendly = missing.map(f => OPTS_TO_FIELD_MAP[f] || f);
|
|
191
|
+
return { success: false, error: `Campo(s) obrigatório(s) faltando para o modo "${effectiveMode}": ${friendly.join(', ')}.` };
|
|
192
|
+
}
|
|
193
|
+
connection = config.buildConnection(effectiveMode, opts);
|
|
194
|
+
} else {
|
|
195
|
+
connection = { ...(existing.connection || {}) };
|
|
196
|
+
for (const [jsonKey, connKey] of Object.entries(CONNECTION_FIELD_MAP)) {
|
|
197
|
+
if (spec[jsonKey] === undefined || spec[jsonKey] === null) continue;
|
|
198
|
+
connection[connKey] = jsonKey === 'headless' ? (spec[jsonKey] === true || spec[jsonKey] === 'true') : spec[jsonKey];
|
|
199
|
+
}
|
|
200
|
+
if (spec.timeoutMs !== undefined && spec.timeoutMs !== null && effectiveMode === 'saml') {
|
|
201
|
+
connection.loginTimeoutMs = parseInt(spec.timeoutMs, 10);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (spec.extraParams && typeof spec.extraParams === 'object') {
|
|
206
|
+
for (const [k, v] of Object.entries(spec.extraParams)) connection[kebabToCamel(k)] = v;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
config.addOrUpdateSystem(systemId, { authMode: effectiveMode, connection, enabled: existing.enabled !== false });
|
|
210
|
+
console.log(` ✅ System "${systemId}" updated via update_system command${switchingMode ? ` (mode: ${existing.authMode} → ${effectiveMode})` : ''}`);
|
|
211
|
+
return { success: true, data: { systemId, message: 'Sistema atualizado com sucesso' } };
|
|
212
|
+
}
|
|
213
|
+
|
|
24
214
|
// ════════════════════════════════════════════════════════════════════════════
|
|
25
215
|
// Multi-system auth session cache
|
|
26
216
|
// ════════════════════════════════════════════════════════════════════════════
|
|
@@ -712,6 +902,84 @@ class WebSocketClient {
|
|
|
712
902
|
return;
|
|
713
903
|
}
|
|
714
904
|
|
|
905
|
+
// ── add_system ─────────────────────────────────────────────────────
|
|
906
|
+
if (action === 'add_system') {
|
|
907
|
+
const spec = Array.isArray(callspecs) ? (callspecs[0] || {}) : (callspecs || {});
|
|
908
|
+
try {
|
|
909
|
+
const result = handleAddSystem(spec);
|
|
910
|
+
if (result.success) this.sendResponse(requestId, true, result.data);
|
|
911
|
+
else this.sendResponse(requestId, false, { error: result.error });
|
|
912
|
+
} catch (err) {
|
|
913
|
+
console.error(`❌ add_system error: ${err.message}`);
|
|
914
|
+
this.sendResponse(requestId, false, { error: err.message });
|
|
915
|
+
}
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// ── list_systems ───────────────────────────────────────────────────
|
|
920
|
+
if (action === 'list_systems') {
|
|
921
|
+
try {
|
|
922
|
+
const result = handleListSystems();
|
|
923
|
+
this.sendResponse(requestId, true, result.data);
|
|
924
|
+
} catch (err) {
|
|
925
|
+
console.error(`❌ list_systems error: ${err.message}`);
|
|
926
|
+
this.sendResponse(requestId, false, { error: err.message });
|
|
927
|
+
}
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// ── remove_system ──────────────────────────────────────────────────
|
|
932
|
+
if (action === 'remove_system') {
|
|
933
|
+
const spec = Array.isArray(callspecs) ? (callspecs[0] || {}) : (callspecs || {});
|
|
934
|
+
try {
|
|
935
|
+
const result = handleRemoveSystem(spec);
|
|
936
|
+
if (result.success) {
|
|
937
|
+
const removedId = result.data.systemId;
|
|
938
|
+
sessions.delete(removedId);
|
|
939
|
+
// Policy: removing the currently active system clears the active
|
|
940
|
+
// session (falls back to "no system selected") instead of leaving
|
|
941
|
+
// a dangling stale sapClient pointed at a config that no longer exists.
|
|
942
|
+
if (this.currentSystemId === removedId) {
|
|
943
|
+
this.sapClient = null;
|
|
944
|
+
this.currentSystemId = null;
|
|
945
|
+
this.lockManager.clearAll();
|
|
946
|
+
console.log(` ⚠️ Active system "${removedId}" was removed — session cleared, no system selected.`);
|
|
947
|
+
}
|
|
948
|
+
this.sendResponse(requestId, true, result.data);
|
|
949
|
+
} else {
|
|
950
|
+
this.sendResponse(requestId, false, { error: result.error });
|
|
951
|
+
}
|
|
952
|
+
} catch (err) {
|
|
953
|
+
console.error(`❌ remove_system error: ${err.message}`);
|
|
954
|
+
this.sendResponse(requestId, false, { error: err.message });
|
|
955
|
+
}
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// ── update_system ──────────────────────────────────────────────────
|
|
960
|
+
if (action === 'update_system') {
|
|
961
|
+
const spec = Array.isArray(callspecs) ? (callspecs[0] || {}) : (callspecs || {});
|
|
962
|
+
try {
|
|
963
|
+
const result = handleUpdateSystem(spec);
|
|
964
|
+
if (result.success) {
|
|
965
|
+
const updatedId = result.data.systemId;
|
|
966
|
+
// Invalidate any cached session so the next call re-reads the
|
|
967
|
+
// fresh config instead of reusing stale connection details.
|
|
968
|
+
if (this.currentSystemId === updatedId) {
|
|
969
|
+
sessions.delete(updatedId);
|
|
970
|
+
this.sapClient = null;
|
|
971
|
+
}
|
|
972
|
+
this.sendResponse(requestId, true, result.data);
|
|
973
|
+
} else {
|
|
974
|
+
this.sendResponse(requestId, false, { error: result.error });
|
|
975
|
+
}
|
|
976
|
+
} catch (err) {
|
|
977
|
+
console.error(`❌ update_system error: ${err.message}`);
|
|
978
|
+
this.sendResponse(requestId, false, { error: err.message });
|
|
979
|
+
}
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
|
|
715
983
|
// ── invoke_rfc ─────────────────────────────────────────────────────
|
|
716
984
|
if (action === 'invoke_rfc') {
|
|
717
985
|
if (!this.sapClient?.rfcClient) { this.sendResponse(requestId, false, { error: 'No RFC session' }); return; }
|
|
@@ -785,7 +1053,7 @@ class WebSocketClient {
|
|
|
785
1053
|
for (const lock of allLocks) {
|
|
786
1054
|
const unlockUrl = buildAdtUnlockUrl(lock.name, lock.type, lock.lockHandle);
|
|
787
1055
|
if (!unlockUrl) { console.log(` ⚠️ No ADT path for type ${lock.type} — skipping`); failed++; continue; }
|
|
788
|
-
const r = await this.sapClient.executeCallspec({ method: '
|
|
1056
|
+
const r = await this.sapClient.executeCallspec({ method: 'POST', url: unlockUrl, headers: {}, body: '' });
|
|
789
1057
|
if (r.success) { this.lockManager.clearLock(lock.name, lock.type); unlocked++; console.log(` ✅ Unlocked ${lock.type}:${lock.name}`); }
|
|
790
1058
|
else { failed++; console.log(` ⚠️ Failed to unlock ${lock.type}:${lock.name}: ${r.error || r.status}`); }
|
|
791
1059
|
}
|
|
@@ -798,7 +1066,7 @@ class WebSocketClient {
|
|
|
798
1066
|
const handle = lockHandle || this.lockManager.getLock(objectName, objectType)?.lockHandle || '';
|
|
799
1067
|
const unlockUrl = buildAdtUnlockUrl(objectName, objectType, handle);
|
|
800
1068
|
if (!unlockUrl) { results.push({ success: false, status: 0, error: `Unknown object type: ${objectType}` }); allSuccessful = false; break; }
|
|
801
|
-
const r = await this.sapClient.executeCallspec({ method: '
|
|
1069
|
+
const r = await this.sapClient.executeCallspec({ method: 'POST', url: unlockUrl, headers: {}, body: '' });
|
|
802
1070
|
if (r.success) this.lockManager.clearLock(objectName, objectType);
|
|
803
1071
|
results.push(r);
|
|
804
1072
|
if (!r.success) { allSuccessful = false; break; }
|