@qodalis/cli-server-node 1.0.0-beta.4 → 1.0.0-beta.6

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 (127) hide show
  1. package/README.md +315 -37
  2. package/dist/src/abstractions/index.d.ts +1 -1
  3. package/dist/src/abstractions/index.d.ts.map +1 -1
  4. package/dist/src/abstractions/index.js +4 -1
  5. package/dist/src/abstractions/index.js.map +1 -1
  6. package/dist/src/auth/cli-server-auth-gate.d.ts +31 -0
  7. package/dist/src/auth/cli-server-auth-gate.d.ts.map +1 -0
  8. package/dist/src/auth/cli-server-auth-gate.js +118 -0
  9. package/dist/src/auth/cli-server-auth-gate.js.map +1 -0
  10. package/dist/src/auth/cli-server-auth.d.ts +147 -0
  11. package/dist/src/auth/cli-server-auth.d.ts.map +1 -0
  12. package/dist/src/auth/cli-server-auth.js +143 -0
  13. package/dist/src/auth/cli-server-auth.js.map +1 -0
  14. package/dist/src/auth/index.d.ts +4 -0
  15. package/dist/src/auth/index.d.ts.map +1 -0
  16. package/dist/src/auth/index.js +19 -0
  17. package/dist/src/auth/index.js.map +1 -0
  18. package/dist/src/auth/ws-upgrade-auth.d.ts +30 -0
  19. package/dist/src/auth/ws-upgrade-auth.d.ts.map +1 -0
  20. package/dist/src/auth/ws-upgrade-auth.js +49 -0
  21. package/dist/src/auth/ws-upgrade-auth.js.map +1 -0
  22. package/dist/src/controllers/cli-controller.d.ts +8 -1
  23. package/dist/src/controllers/cli-controller.d.ts.map +1 -1
  24. package/dist/src/controllers/cli-controller.js +78 -3
  25. package/dist/src/controllers/cli-controller.js.map +1 -1
  26. package/dist/src/controllers/cli-version-controller.d.ts +16 -1
  27. package/dist/src/controllers/cli-version-controller.d.ts.map +1 -1
  28. package/dist/src/controllers/cli-version-controller.js +18 -5
  29. package/dist/src/controllers/cli-version-controller.js.map +1 -1
  30. package/dist/src/controllers/filesystem-controller.d.ts.map +1 -1
  31. package/dist/src/controllers/filesystem-controller.js +51 -7
  32. package/dist/src/controllers/filesystem-controller.js.map +1 -1
  33. package/dist/src/create-cli-server.d.ts +76 -5
  34. package/dist/src/create-cli-server.d.ts.map +1 -1
  35. package/dist/src/create-cli-server.js +88 -20
  36. package/dist/src/create-cli-server.js.map +1 -1
  37. package/dist/src/extensions/cli-builder.d.ts +19 -1
  38. package/dist/src/extensions/cli-builder.d.ts.map +1 -1
  39. package/dist/src/extensions/cli-builder.js +26 -0
  40. package/dist/src/extensions/cli-builder.js.map +1 -1
  41. package/dist/src/filesystem/filesystem-options.d.ts +1 -0
  42. package/dist/src/filesystem/filesystem-options.d.ts.map +1 -1
  43. package/dist/src/index.d.ts +5 -4
  44. package/dist/src/index.d.ts.map +1 -1
  45. package/dist/src/index.js +18 -3
  46. package/dist/src/index.js.map +1 -1
  47. package/dist/src/models/cli-server-command-descriptor.d.ts +2 -1
  48. package/dist/src/models/cli-server-command-descriptor.d.ts.map +1 -1
  49. package/dist/src/models/cli-server-output.d.ts +1 -22
  50. package/dist/src/models/cli-server-output.d.ts.map +1 -1
  51. package/dist/src/models/cli-server-response.d.ts +1 -5
  52. package/dist/src/models/cli-server-response.d.ts.map +1 -1
  53. package/dist/src/processors/cli-base64-command-processor.d.ts +1 -0
  54. package/dist/src/processors/cli-base64-command-processor.d.ts.map +1 -1
  55. package/dist/src/processors/cli-base64-command-processor.js +3 -0
  56. package/dist/src/processors/cli-base64-command-processor.js.map +1 -1
  57. package/dist/src/processors/cli-echo-command-processor.d.ts +1 -0
  58. package/dist/src/processors/cli-echo-command-processor.d.ts.map +1 -1
  59. package/dist/src/processors/cli-echo-command-processor.js +1 -0
  60. package/dist/src/processors/cli-echo-command-processor.js.map +1 -1
  61. package/dist/src/processors/cli-hash-command-processor.d.ts +1 -0
  62. package/dist/src/processors/cli-hash-command-processor.d.ts.map +1 -1
  63. package/dist/src/processors/cli-hash-command-processor.js +1 -0
  64. package/dist/src/processors/cli-hash-command-processor.js.map +1 -1
  65. package/dist/src/processors/cli-http-command-processor.d.ts +1 -0
  66. package/dist/src/processors/cli-http-command-processor.d.ts.map +1 -1
  67. package/dist/src/processors/cli-http-command-processor.js +85 -6
  68. package/dist/src/processors/cli-http-command-processor.js.map +1 -1
  69. package/dist/src/processors/cli-status-command-processor.d.ts +1 -0
  70. package/dist/src/processors/cli-status-command-processor.d.ts.map +1 -1
  71. package/dist/src/processors/cli-status-command-processor.js +1 -0
  72. package/dist/src/processors/cli-status-command-processor.js.map +1 -1
  73. package/dist/src/processors/cli-system-command-processor.d.ts +1 -0
  74. package/dist/src/processors/cli-system-command-processor.d.ts.map +1 -1
  75. package/dist/src/processors/cli-system-command-processor.js +1 -0
  76. package/dist/src/processors/cli-system-command-processor.js.map +1 -1
  77. package/dist/src/server.js +20 -6
  78. package/dist/src/server.js.map +1 -1
  79. package/dist/src/services/cli-command-executor-service.d.ts +24 -3
  80. package/dist/src/services/cli-command-executor-service.d.ts.map +1 -1
  81. package/dist/src/services/cli-command-executor-service.js +35 -3
  82. package/dist/src/services/cli-command-executor-service.js.map +1 -1
  83. package/dist/src/services/cli-command-registry.d.ts +11 -0
  84. package/dist/src/services/cli-command-registry.d.ts.map +1 -1
  85. package/dist/src/services/cli-command-registry.js +5 -0
  86. package/dist/src/services/cli-command-registry.js.map +1 -1
  87. package/dist/src/services/cli-event-socket-manager.d.ts +42 -0
  88. package/dist/src/services/cli-event-socket-manager.d.ts.map +1 -1
  89. package/dist/src/services/cli-event-socket-manager.js +139 -27
  90. package/dist/src/services/cli-event-socket-manager.js.map +1 -1
  91. package/dist/src/services/cli-log-socket-manager.d.ts +17 -1
  92. package/dist/src/services/cli-log-socket-manager.d.ts.map +1 -1
  93. package/dist/src/services/cli-log-socket-manager.js +59 -12
  94. package/dist/src/services/cli-log-socket-manager.js.map +1 -1
  95. package/dist/src/services/cli-response-builder.d.ts +9 -0
  96. package/dist/src/services/cli-response-builder.d.ts.map +1 -1
  97. package/dist/src/services/cli-response-builder.js +1 -0
  98. package/dist/src/services/cli-response-builder.js.map +1 -1
  99. package/dist/src/services/cli-shell-options.d.ts +107 -0
  100. package/dist/src/services/cli-shell-options.d.ts.map +1 -0
  101. package/dist/src/services/cli-shell-options.js +139 -0
  102. package/dist/src/services/cli-shell-options.js.map +1 -0
  103. package/dist/src/services/cli-shell-session-manager.d.ts +86 -2
  104. package/dist/src/services/cli-shell-session-manager.d.ts.map +1 -1
  105. package/dist/src/services/cli-shell-session-manager.js +493 -73
  106. package/dist/src/services/cli-shell-session-manager.js.map +1 -1
  107. package/dist/src/services/index.d.ts +2 -1
  108. package/dist/src/services/index.d.ts.map +1 -1
  109. package/dist/src/services/index.js +5 -1
  110. package/dist/src/services/index.js.map +1 -1
  111. package/dist/src/services/process-tree.d.ts +38 -0
  112. package/dist/src/services/process-tree.d.ts.map +1 -0
  113. package/dist/src/services/process-tree.js +249 -0
  114. package/dist/src/services/process-tree.js.map +1 -0
  115. package/dist/src/services/session-bound-sockets.d.ts +45 -0
  116. package/dist/src/services/session-bound-sockets.d.ts.map +1 -0
  117. package/dist/src/services/session-bound-sockets.js +129 -0
  118. package/dist/src/services/session-bound-sockets.js.map +1 -0
  119. package/dist/src/utils/logger.d.ts +11 -0
  120. package/dist/src/utils/logger.d.ts.map +1 -0
  121. package/dist/src/utils/logger.js +13 -0
  122. package/dist/src/utils/logger.js.map +1 -0
  123. package/package.json +12 -3
  124. package/dist/src/controllers/cli-controller-v2.d.ts +0 -4
  125. package/dist/src/controllers/cli-controller-v2.d.ts.map +0 -1
  126. package/dist/src/controllers/cli-controller-v2.js +0 -46
  127. package/dist/src/controllers/cli-controller-v2.js.map +0 -1
package/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Qodalis CLI Server (Node.js)
2
2
 
3
+ [![npm @qodalis/cli-server-node](https://img.shields.io/npm/v/@qodalis/cli-server-node?label=%40qodalis%2Fcli-server-node)](https://www.npmjs.com/package/@qodalis/cli-server-node)
4
+ [![npm @qodalis/cli-server-abstractions](https://img.shields.io/npm/v/@qodalis/cli-server-abstractions?label=%40qodalis%2Fcli-server-abstractions)](https://www.npmjs.com/package/@qodalis/cli-server-abstractions)
5
+
3
6
  A Node.js CLI server framework for the [Qodalis CLI](https://github.com/qodalis-solutions/web-cli) ecosystem. Build custom server-side commands that integrate with the Qodalis web terminal.
4
7
 
5
8
  ## Installation
@@ -297,36 +300,109 @@ Every processor receives a `CliProcessCommand` with the parsed command input:
297
300
  | `rawCommand` | `string` | Original unprocessed input |
298
301
  | `data` | `any` | Arbitrary data payload from the client |
299
302
 
300
- ## API Versioning
303
+ ## API Endpoints
304
+
305
+ The server exposes versioned endpoints:
306
+
307
+ | Method | Path | Description |
308
+ |--------|------|-------------|
309
+ | GET | `/api/qcli/versions` | Version discovery (supported versions, preferred version) |
310
+ | GET | `/api/v1/qcli/version` | V1 server version |
311
+ | GET | `/api/v1/qcli/commands` | V1 commands (all processors) |
312
+ | POST | `/api/v1/qcli/execute` | V1 execute |
313
+ | POST | `/api/v1/qcli/execute/stream` | V1 SSE streaming execute |
314
+ | WS | `/ws/qcli/events` | WebSocket events (also `/ws/v1/qcli/events`) |
315
+ | WS | `/ws/v1/qcli/shell` | Interactive PTY shell session |
316
+
317
+ `GET /capabilities` reports `shell: true|false` and, when the shell is enabled, `shellAuth: 'users' | 'token' | 'custom' | 'anonymous' | 'locked'`.
301
318
 
302
- Processors declare which API version they target. The default is version 1.
319
+ ## Remote Shell Security
320
+
321
+ The PTY shell (`/ws/v1/qcli/shell`, `/ws/qcli/shell`) gives OS-level access to the server. It is **off** unless `enableShell: true`. When enabled, exactly one access mode applies:
322
+
323
+ | Mode (`shellAuth`) | When | Client must |
324
+ |---|---|---|
325
+ | `users` | the users plugin is mounted | connect with a WS ticket (`?ticket=`) of a session holding `shell:execute` |
326
+ | `token` | no users plugin, `shell.token` set | send `Authorization: Bearer <token>` (header, or the `_header_Authorization` query parameter) |
327
+ | `custom` | no users plugin, `shell.authorize` set | pass the host's `authorize(request, { url, remoteAddress, mode })` check |
328
+ | `anonymous` | no users plugin, `shell.allowAnonymous: true` | nothing (a startup WARNING is logged) |
329
+ | `locked` | none of the above | — every connection is closed with 4401 and a startup ERROR explains the options |
330
+
331
+ Precedence: users plugin mounted (even with `protectServer: false`) ⇒ `users`; otherwise `token`, then `custom`, then `anonymous`. Whatever the mode, **every configured check among `token` and `authorize` must pass** (so with both set, both are required and `shellAuth` reports `token`); `allowAnonymous` is ignored once either is set. Tokens are compared in constant time (SHA-256 of both, then `timingSafeEqual`) and never logged; an `authorize` that throws or rejects denies.
332
+
333
+ ### With the users plugin
303
334
 
304
335
  ```typescript
305
- class DashboardProcessor extends CliCommandProcessor {
306
- command = 'dashboard';
307
- description = 'Server dashboard (v2 only)';
308
- apiVersion = 2;
336
+ const { mountPlugin, eventSocketManager } = createCliServer({ enableShell: true });
337
+ mountPlugin(new CliUsersBuilder().build((m) => eventSocketManager.broadcastMessage(m)));
338
+ // Grant shell:execute to a group (root/admin has '*'):
339
+ // PUT /api/v1/qcli/groups/ops { "permissions": ["commands:execute", "shell:execute"] }
340
+ ```
309
341
 
310
- async handleAsync(command: CliProcessCommand): Promise<string> {
311
- return 'Dashboard data...';
312
- }
342
+ An open shell is bound to the session that opened it: logout, idle/absolute session expiry, admin revoke (kick), user disabled or deleted, or losing `shell:execute` (checked once more right after the shell is registered and before it is spawned, so a logout racing the open is not missed; then immediately on session/user/group changes and every `sessionCheckIntervalMs`, default 30 s, max 60 s), or a password change becoming required closes it with **4401** `session ended` and kills its process tree. Users authenticated by an external auth adapter have no stored session: their shells are re-checked by calling the adapter again with the shell's original upgrade request (see the users plugin README). Re-checks never extend the session by themselves; stdin received since the previous check counts as session activity (at most one touch per 30 s), so an active shell user is not idle-logged-out. Opens and closes are audited as `shell_opened` / `shell_closed` (`userId`, `actorId`, `metadata: { mode: 'interactive' | 'command', sessionId, durationMs, reason, exitCode? }`), never with the command text or stdin. `reason` is one of `exited` (then `exitCode` is set), `closed`, `session ended`, `permission removed`, `shell timed out`, `server shutdown`, `error`.
343
+
344
+ ### Without the users plugin (token mode)
345
+
346
+ ```typescript
347
+ createCliServer({
348
+ enableShell: true,
349
+ shell: { token: process.env.QCLI_SHELL_TOKEN }, // long random secret
350
+ });
351
+ ```
352
+
353
+ Browser clients cannot set WebSocket headers: configure the server in web-cli with `headers: { Authorization: 'Bearer <token>' }` and the client forwards it as `?_header_Authorization=Bearer%20<token>`. The standalone server (`src/server.ts`) enables token mode when `QCLI_SHELL_TOKEN` is set. Use HTTPS/WSS so the token is not sent in clear text.
354
+
355
+ For your own scheme (reverse-proxy identity, IP allow-list, …) use `authorize`:
356
+
357
+ ```typescript
358
+ createCliServer({
359
+ enableShell: true,
360
+ shell: { authorize: async (req, { remoteAddress }) => remoteAddress === '127.0.0.1' && req.headers['x-ops'] === 'yes' },
361
+ });
362
+ ```
363
+
364
+ Non-users modes log one info line per open/close (remote address, mode, reason) without the command text.
365
+
366
+ ### Options
367
+
368
+ ```typescript
369
+ shell: {
370
+ token?: string; // token mode (or extra requirement with the users plugin)
371
+ authorize?: (req, ctx) => boolean | Promise<boolean>; // custom mode (or extra requirement)
372
+ allowAnonymous?: boolean; // open to anyone who can reach the server (not recommended)
373
+ maxSessionsPerUser?: number; // per user (users mode) or per remote address (default 3; X-Forwarded-For only with trustProxy)
374
+ maxSessions?: number; // total concurrent shells (default 20)
375
+ idleTimeoutMs?: number; // close after no stdin (resize does not count; default 15 min, 0 = off)
376
+ maxDurationMs?: number; // absolute limit (default none)
377
+ sessionCheckIntervalMs?: number; // users-mode re-check (default 30 000, max 60 000)
378
+ cwd?: string; // working directory (default: home directory)
379
+ shell?: string; // shell binary (default: detected)
380
+ env?: { allow?: string[]; set?: Record<string, string> };
313
381
  }
314
382
  ```
315
383
 
316
- The server exposes versioned endpoints:
384
+ The shell does **not** inherit the server environment: it gets only `PATH`, `HOME`, `USER`, `LANG`, `TERM`, `COLUMNS`, `LINES`, `SHELL` (plus the variables Windows needs to start PowerShell), the names in `env.allow`, and `env.set`. Secrets such as database passwords stay out of the shell.
317
385
 
318
- | Method | Path | Description |
319
- |--------|------|-------------|
320
- | GET | `/api/cli/versions` | Version discovery (supported versions, preferred version) |
321
- | GET | `/api/v1/cli/version` | V1 server version |
322
- | GET | `/api/v1/cli/commands` | V1 commands (all processors) |
323
- | POST | `/api/v1/cli/execute` | V1 execute |
324
- | GET | `/api/v2/cli/version` | V2 server version |
325
- | GET | `/api/v2/cli/commands` | V2 commands (only `apiVersion >= 2`) |
326
- | POST | `/api/v2/cli/execute` | V2 execute |
327
- | WS | `/ws/cli/events` | WebSocket events (also `/ws/v1/cli/events`, `/ws/v2/cli/events`) |
386
+ ### Process cleanup
328
387
 
329
- The Qodalis CLI client auto-negotiates the highest mutually supported version via the `/api/cli/versions` discovery endpoint.
388
+ Whenever a shell ends (exit, client close, session ended or permission removed, timeout, server shutdown), the server first collects everything the shell started — its process group, its descendants by parent PID (including background jobs that interactive shells move to their own process groups, and `setsid`/`nohup` children), on Linux every process of its session, and every process tagged with the shell's marker (below) — and then sends SIGHUP, SIGTERM and, after a 1 s grace period, SIGKILL to all of them (re-collecting before the SIGKILL). The server's own process and process group are never targeted. Linux reads `/proc` for everything (no `ps` needed); macOS and other Unixes run `ps`. On Windows only the shell process is killed.
389
+
390
+ Every shell gets a unique marker in its environment, `QCLI_SHELL_ID=<random 128-bit hex>`, added after the `env.allow`/`env.set` filtering so configuration can neither override nor remove it. Children inherit it, so processes that were re-parented to init *and* left the shell's process group and session — e.g. `setsid sleep 100 &` from an interactive shell, double-forking daemons — are still found: on Linux by reading `/proc/<pid>/environ`, on macOS through `ps -A -E -ww` (which shows the environment of the server user's processes).
391
+
392
+ Limitations: a process escapes only if it deliberately unsets or changes `QCLI_SHELL_ID` before detaching (`env -u QCLI_SHELL_ID setsid …`), or runs as a different user whose environment the server cannot read (e.g. via `sudo`). On macOS the environment of Apple platform binaries (`/bin/sleep`, `/usr/bin/perl`, …) is hidden from `ps -E`, so such a binary that left the shell's tree and session is not found there either. Run the server in a container or under a service manager (cgroup) if every process must be contained.
393
+
394
+ ### Close codes
395
+
396
+ Refusals are sent as *accept, then close* so browsers can read them (a foreign `Origin` is still rejected with HTTP 403):
397
+
398
+ | Code | Reason | When |
399
+ |---|---|---|
400
+ | 4401 | `login required` / `session ended` / `shell access is not configured` | missing ticket or (Bearer) token; session ended (logout, expiry, kick, user disabled/deleted, `shell:execute` removed, password change now required) — shell, events and logs sockets; shell locked |
401
+ | 4403 | `forbidden` | wrong token, `authorize` false or threw, no `shell:execute`, password change required |
402
+ | 4404 | `shell disabled` | `enableShell` is false |
403
+ | 4408 | `shell timed out` | idle timeout or `maxDurationMs` |
404
+ | 4429 | `too many shell sessions` | `maxSessionsPerUser` or `maxSessions` reached |
405
+ | 1001 | `server shutdown` | the server is shutting down |
330
406
 
331
407
  ## Processor Base Class Reference
332
408
 
@@ -342,19 +418,31 @@ The Qodalis CLI client auto-negotiates the highest mutually supported version vi
342
418
  | `allowUnlistedCommands` | `boolean` | `undefined` | Accept sub-commands not in `processors` |
343
419
  | `valueRequired` | `boolean` | `undefined` | Require a positional value |
344
420
  | `version` | `string` | `'1.0.0'` | Processor version string |
345
- | `apiVersion` | `number` | `1` | Target API version |
346
421
  | `author` | `ICliCommandAuthor` | default author | Author metadata (name, email) |
347
422
 
348
423
  ## Server Options
349
424
 
350
425
  ```typescript
351
426
  interface CliServerOptions {
352
- basePath?: string; // API base path (default: '/api/cli')
353
- cors?: boolean | cors.CorsOptions; // CORS config (default: false)
427
+ basePath?: string; // API base path (default: '/api/qcli')
428
+ cors?: boolean | cors.CorsOptions; // CORS config (default: only allowedOrigins; off when none)
429
+ allowedOrigins?: string[]; // CORS + WebSocket Origin allow-list ('http://host:*' = any port)
430
+ enableShell?: boolean; // Interactive shell WebSocket (default: false)
431
+ shell?: CliShellOptions; // Shell access mode, limits, timeouts, env (see Remote Shell Security)
432
+ trustProxy?: boolean | number; // Trusted proxy hops for X-Forwarded-* (true = 1; default false = none)
433
+ auth?: ICliServerAuth; // Protect the whole server (e.g. usersPlugin.serverAuth)
354
434
  configure?: (builder: CliBuilder) => void; // Processor registration
355
435
  }
356
436
  ```
357
437
 
438
+ CORS is disabled unless `allowedOrigins` (or an explicit `cors` option) is given. The shell socket is off unless `enableShell: true`, and even then it is locked until an access mode applies (see [Remote Shell Security](#remote-shell-security)).
439
+
440
+ Same-origin requests (e.g. the bundled admin dashboard at `/qcli/admin`) are always allowed by the Origin, CORS and CSRF checks, even when `allowedOrigins` does not list the server's own origin (`X-Forwarded-Proto`/`X-Forwarded-Host` are honoured only with `trustProxy`, and read like `X-Forwarded-For`: the entry at index `len - hops`, the leftmost when the list is shorter, an empty entry ignored).
441
+
442
+ `trustProxy` is the number of trusted reverse-proxy hops in front of the server (`true` = 1, `false`/`0`/absent = none). With n hops the client IP (Express `req.ip`, the shell's per-address limits) is the `X-Forwarded-For` entry at index `len - n` of the comma-separated list, i.e. with one hop the rightmost entry, appended by your proxy; the leftmost entries are client-controlled and never used unless the list is shorter than n. Without the header (or when the selected entry is empty) the socket address is used. Set it to the exact number of proxies you run, otherwise clients can spoof their IP.
443
+
444
+ When the users plugin is mounted (`mountPlugin(usersPlugin)`) or passed as `auth`, every `/api/...` route and every WebSocket requires a users session (see [Users Plugin](#users-plugin)); without it the server behaves as before.
445
+
358
446
  `createCliServer()` returns:
359
447
 
360
448
  ```typescript
@@ -362,7 +450,11 @@ interface CliServerOptions {
362
450
  app: Express; // Configured Express app
363
451
  registry: CliCommandRegistry; // Processor registry
364
452
  builder: CliBuilder; // Registration builder
365
- eventSocketManager: CliEventSocketManager; // WebSocket manager
453
+ executor: CliCommandExecutorService; // Command executor
454
+ eventSocketManager: CliEventSocketManager; // WebSocket event manager
455
+ logSocketManager: CliLogSocketManager; // WebSocket log manager
456
+ mountPlugin(plugin: MountablePlugin): void; // Mount a plugin with its routes (installs plugin.serverAuth)
457
+ setAuth(auth?: ICliServerAuth): void; // Install/remove the server auth provider
366
458
  }
367
459
  ```
368
460
 
@@ -411,19 +503,19 @@ import {
411
503
 
412
504
  ## File Storage
413
505
 
414
- The server includes a pluggable file storage system exposed at `/api/cli/fs/*`. Enable it with `setFileStorageProvider()` and choose a storage backend.
506
+ The server includes a pluggable file storage system exposed at `/api/qcli/fs/*`. Enable it with `setFileStorageProvider()` and choose a storage backend.
415
507
 
416
508
  ### Filesystem API Endpoints
417
509
 
418
510
  | Method | Path | Description |
419
511
  |--------|------|-------------|
420
- | GET | `/api/cli/fs/ls?path=/` | List directory contents |
421
- | GET | `/api/cli/fs/cat?path=/file.txt` | Read file content |
422
- | GET | `/api/cli/fs/stat?path=/file.txt` | File/directory metadata |
423
- | GET | `/api/cli/fs/download?path=/file.txt` | Download file |
424
- | POST | `/api/cli/fs/upload` | Upload file (multipart) |
425
- | POST | `/api/cli/fs/mkdir` | Create directory |
426
- | DELETE | `/api/cli/fs/rm?path=/file.txt` | Delete file or directory |
512
+ | GET | `/api/qcli/fs/ls?path=/` | List directory contents |
513
+ | GET | `/api/qcli/fs/cat?path=/file.txt` | Read file content |
514
+ | GET | `/api/qcli/fs/stat?path=/file.txt` | File/directory metadata |
515
+ | GET | `/api/qcli/fs/download?path=/file.txt` | Download file |
516
+ | POST | `/api/qcli/fs/upload` | Upload file (multipart) |
517
+ | POST | `/api/qcli/fs/mkdir` | Create directory |
518
+ | DELETE | `/api/qcli/fs/rm?path=/file.txt` | Delete file or directory |
427
519
 
428
520
  ### Storage Providers
429
521
 
@@ -492,6 +584,180 @@ class MyProvider implements IFileStorageProvider {
492
584
  builder.setFileStorageProvider(new MyProvider());
493
585
  ```
494
586
 
587
+ ## Data Explorer
588
+
589
+ The Data Explorer plugin provides interactive, full-screen REPL access to data sources. It exposes a provider-based API where each data source type (SQL, MongoDB, etc.) is a separate plugin implementing `IDataExplorerProvider`.
590
+
591
+ ### API Endpoints
592
+
593
+ | Method | Path | Description |
594
+ |--------|------|-------------|
595
+ | GET | `/api/qcli/data-explorer/sources` | List registered data sources with metadata |
596
+ | POST | `/api/qcli/data-explorer/execute` | Execute a query against a named source |
597
+
598
+ ### SQL Provider
599
+
600
+ ```typescript
601
+ import { SqlDataExplorerProvider } from '@qodalis/cli-server-plugin-data-explorer-sql';
602
+ import { DataExplorerLanguage, DataExplorerOutputFormat } from '@qodalis/cli-server-abstractions';
603
+
604
+ const { app } = createCliServer({
605
+ configure: (builder) => {
606
+ builder.addDataExplorerProvider(
607
+ new SqlDataExplorerProvider({ type: 'sqlite', filename: './data.db' }),
608
+ {
609
+ name: 'my-database',
610
+ description: 'Application database',
611
+ language: DataExplorerLanguage.Sql,
612
+ defaultOutputFormat: DataExplorerOutputFormat.Table,
613
+ timeout: 30000,
614
+ maxRows: 1000,
615
+ templates: [
616
+ {
617
+ name: 'list_tables',
618
+ query: "SELECT name FROM sqlite_master WHERE type='table'",
619
+ description: 'List all tables',
620
+ },
621
+ ],
622
+ },
623
+ );
624
+ },
625
+ });
626
+ ```
627
+
628
+ ### MongoDB Provider
629
+
630
+ ```typescript
631
+ import { MongoDataExplorerProvider } from '@qodalis/cli-server-plugin-data-explorer-mongo';
632
+
633
+ builder.addDataExplorerProvider(
634
+ new MongoDataExplorerProvider({
635
+ connectionString: 'mongodb://localhost:27017',
636
+ database: 'myapp',
637
+ }),
638
+ {
639
+ name: 'mongo-primary',
640
+ description: 'Primary MongoDB database',
641
+ language: DataExplorerLanguage.Json,
642
+ defaultOutputFormat: DataExplorerOutputFormat.Json,
643
+ templates: [
644
+ { name: 'show_collections', query: 'show collections', description: 'List all collections' },
645
+ { name: 'find_users', query: 'db.users.find({})', description: 'Find all users' },
646
+ ],
647
+ },
648
+ );
649
+ ```
650
+
651
+ **Supported MongoDB operations:** `db.collection.find({...})`, `findOne`, `aggregate([...])`, `insertOne`, `insertMany`, `updateOne`, `updateMany`, `deleteOne`, `deleteMany`, `countDocuments`, `distinct`. Convenience commands: `show collections`, `show dbs`.
652
+
653
+ ### Custom Provider
654
+
655
+ Implement `IDataExplorerProvider` to add your own data source:
656
+
657
+ ```typescript
658
+ import { IDataExplorerProvider, DataExplorerExecutionContext, DataExplorerResult } from '@qodalis/cli-server-abstractions';
659
+
660
+ class MyProvider implements IDataExplorerProvider {
661
+ async executeAsync(context: DataExplorerExecutionContext): Promise<DataExplorerResult> {
662
+ // context.query — the user's query string
663
+ // context.parameters — key-value parameters
664
+ // context.options — provider options (name, language, etc.)
665
+ return {
666
+ success: true,
667
+ source: context.options.name,
668
+ language: context.options.language,
669
+ defaultOutputFormat: context.options.defaultOutputFormat,
670
+ executionTime: 0,
671
+ columns: ['id', 'name'], // null for document-oriented results
672
+ rows: [[1, 'Alice'], [2, 'Bob']], // objects when columns is null
673
+ rowCount: 2,
674
+ truncated: false,
675
+ error: null,
676
+ };
677
+ }
678
+ }
679
+
680
+ builder.addDataExplorerProvider(new MyProvider(), { name: 'custom', ... });
681
+ ```
682
+
683
+ The same provider class can be registered multiple times with different configurations (e.g., two databases with different names).
684
+
685
+ ## AWS Cloud Services
686
+
687
+ The AWS plugin adds commands for managing AWS resources (S3, EC2, Lambda, CloudWatch, SNS, SQS, IAM, DynamoDB, ECS) directly from the CLI. It uses AWS SDK v3 and supports the full credential chain.
688
+
689
+ ```typescript
690
+ import { AwsModule } from '@qodalis/cli-server-plugin-aws';
691
+
692
+ const { app } = createCliServer({
693
+ configure: (builder) => {
694
+ builder.addModule(new AwsModule());
695
+ },
696
+ });
697
+ ```
698
+
699
+ ### Authentication
700
+
701
+ The plugin resolves credentials in this order:
702
+
703
+ 1. **CLI configure**: `aws configure set --key <KEY> --secret <SECRET> --region <REGION>`
704
+ 2. **Environment variables**: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`
705
+ 3. **AWS profiles**: `aws configure set --profile <name>`
706
+ 4. **IAM roles**: Automatic on EC2/ECS/Lambda
707
+
708
+ Verify connectivity with `aws status`.
709
+
710
+ ### Available Commands
711
+
712
+ | Service | Commands |
713
+ |---------|----------|
714
+ | **configure** | `aws configure set`, `aws configure get`, `aws configure profiles` |
715
+ | **status** | `aws status` — STS GetCallerIdentity connectivity check |
716
+ | **S3** | `aws s3 ls`, `aws s3 cp`, `aws s3 rm`, `aws s3 mb`, `aws s3 rb`, `aws s3 presign` |
717
+ | **EC2** | `aws ec2 list`, `aws ec2 describe`, `aws ec2 start`, `aws ec2 stop`, `aws ec2 reboot`, `aws ec2 sg list` |
718
+ | **Lambda** | `aws lambda list`, `aws lambda invoke`, `aws lambda logs` |
719
+ | **CloudWatch** | `aws cloudwatch alarms`, `aws cloudwatch logs`, `aws cloudwatch metrics` |
720
+ | **SNS** | `aws sns topics`, `aws sns publish`, `aws sns subscriptions` |
721
+ | **SQS** | `aws sqs list`, `aws sqs send`, `aws sqs receive`, `aws sqs purge` |
722
+ | **IAM** | `aws iam users`, `aws iam roles`, `aws iam policies` |
723
+ | **DynamoDB** | `aws dynamodb tables`, `aws dynamodb describe`, `aws dynamodb scan`, `aws dynamodb query` |
724
+ | **ECS** | `aws ecs clusters`, `aws ecs services`, `aws ecs tasks` |
725
+
726
+ All commands support `--region` (`-r`) for region override and `--output` (`-o`) for format selection (`table`, `json`, `text`). Destructive commands support `--dry-run`.
727
+
728
+ See [`plugins/aws/README.md`](plugins/aws/README.md) for the full command reference.
729
+
730
+ ### Users Plugin
731
+
732
+ The users plugin (`@qodalis/cli-server-plugin-users`) adds REST endpoints under `/api/v1/qcli` for authentication, users, groups with permissions, sessions and an audit log, with swappable storage (in-memory by default). It is the server side of the `@qodalis/cli-users` browser plugin's *server mode*.
733
+
734
+ ```typescript
735
+ import { CliUsersBuilder } from '@qodalis/cli-server-plugin-users';
736
+
737
+ const usersPlugin = new CliUsersBuilder()
738
+ .build((msg) => eventSocketManager.broadcastMessage(msg));
739
+ mountPlugin(usersPlugin); // protects the whole server (protectServer: true)
740
+ ```
741
+
742
+ Once mounted, every server route and socket requires a users session (login, version discovery and capabilities stay public):
743
+
744
+ | Surface | Permission |
745
+ |---|---|
746
+ | `GET /commands` | authenticated |
747
+ | `POST /execute`, `/execute/stream` | `commands:execute` + the processor's `requiredPermissions` |
748
+ | fs `ls`/`cat`/`stat`/`download` | `files:read` |
749
+ | fs `upload`/`mkdir`/`rm` | `files:write` |
750
+ | data explorer | `data:query` |
751
+ | jobs read / manage | `jobs:read` / `jobs:manage` |
752
+ | events / logs / shell WebSocket | authenticated / `logs:read` / `shell:execute` (see [Remote Shell Security](#remote-shell-security)) |
753
+ | admin dashboard | `admin:access` |
754
+
755
+ WebSockets authenticate with a single-use ticket: `POST /api/v1/qcli/sessions/ws-ticket` → `{ ticket, expiresIn: 30 }`, then connect with `?ticket=<ticket>`. Host routes can use `usersPlugin.requireAuth()` / `usersPlugin.requirePermission('perm')`. Processors declare `requiredPermissions: ['deploy:run']` to be enforced server-side.
756
+
757
+ Events and logs sockets authenticated with a stored session are bound to it: when the session ends (logout, revoke/kick, user disabled or deleted, a password change becoming required, idle/absolute expiry) the socket is closed with **4401** `session ended`; for a revoke the events socket first receives `session-kicked`. Bound sockets are re-checked on every session/user/group change and after each expired-session sweep (`setCleanupInterval`, default 5 minutes). Sockets authenticated by an external auth adapter have no stored session and are not re-checked.
758
+
759
+ See [`plugins/users/README.md`](plugins/users/README.md) for options, endpoints, custom storage and external auth adapters.
760
+
495
761
  ## Built-in Processors
496
762
 
497
763
  These processors ship with the library and are included in the standalone server:
@@ -504,7 +770,6 @@ These processors ship with the library and are included in the standalone server
504
770
  | `http` | HTTP request operations |
505
771
  | `hash` | Hash computation (MD5, SHA1, SHA256, SHA512) |
506
772
  | `base64` | Base64 encode/decode (sub-commands) |
507
- | `uuid` | UUID generation |
508
773
 
509
774
  ## Docker
510
775
 
@@ -540,10 +805,22 @@ packages/
540
805
  cli-command-parameter-descriptor.ts # Parameter declaration
541
806
  cli-command-author.ts # Author metadata
542
807
  plugins/
808
+ admin/ # Admin dashboard plugin (auth, status, config, logs)
809
+ aws/ # AWS cloud services (S3, EC2, Lambda, CloudWatch, etc.)
810
+ data-explorer/ # Core data explorer abstraction
811
+ data-explorer-sql/ # SQL data explorer provider (SQLite)
812
+ data-explorer-mongo/ # MongoDB data explorer provider
813
+ data-explorer-postgres/ # PostgreSQL data explorer provider
814
+ data-explorer-mysql/ # MySQL data explorer provider
815
+ data-explorer-mssql/ # MS SQL data explorer provider
816
+ data-explorer-redis/ # Redis data explorer provider
817
+ data-explorer-elasticsearch/ # Elasticsearch data explorer provider
543
818
  filesystem/ # Core file storage abstraction (IFileStorageProvider, InMemory, OS)
544
819
  filesystem-json/ # JSON file persistence provider
545
820
  filesystem-sqlite/ # SQLite persistence provider (better-sqlite3)
546
821
  filesystem-s3/ # Amazon S3 storage provider (@aws-sdk/client-s3)
822
+ jobs/ # Job scheduling plugin
823
+ users/ # Users plugin (auth, sessions, groups, audit)
547
824
  weather/ # Weather module (example plugin)
548
825
  src/
549
826
  abstractions/ # Re-exports from @qodalis/cli-server-abstractions
@@ -556,10 +833,11 @@ src/
556
833
  cli-command-executor-service.ts # Command execution pipeline
557
834
  cli-response-builder.ts # Structured output builder
558
835
  cli-event-socket-manager.ts # WebSocket event broadcasting
836
+ cli-log-socket-manager.ts # WebSocket log streaming
837
+ cli-shell-session-manager.ts # PTY shell session management
559
838
  controllers/
560
- cli-controller.ts # V1 REST API (/api/v1/cli)
561
- cli-controller-v2.ts # V2 REST API (/api/v2/cli)
562
- cli-version-controller.ts # Version discovery (/api/cli/versions)
839
+ cli-controller.ts # V1 REST API (/api/v1/qcli)
840
+ cli-version-controller.ts # Version discovery (/api/qcli/versions)
563
841
  extensions/
564
842
  cli-builder.ts # Fluent registration API (addProcessor, addModule)
565
843
  processors/ # Built-in processors
@@ -1,2 +1,2 @@
1
- export { ICliCommandAuthor, CliCommandAuthor, DefaultLibraryAuthor, CliProcessCommand, ICliCommandParameterDescriptor, CliCommandParameterDescriptor, ICliCommandProcessor, CliCommandProcessor, ICliModule, CliModule, ICliJob, ICliJobExecutionContext, ICliJobLogger, CliJobOptions, JobOverlapPolicy, ICliJobStorageProvider, JobExecution, JobExecutionStatus, JobState, JobStatus, JobLogEntry, JobLogLevel, } from '@qodalis/cli-server-abstractions';
1
+ export { ICliCommandAuthor, CliCommandAuthor, DefaultLibraryAuthor, CliProcessCommand, ICliCommandParameterDescriptor, CliCommandParameterDescriptor, ICliCommandProcessor, CliCommandProcessor, ICliModule, CliModule, ICliJob, ICliJobExecutionContext, ICliJobLogger, CliJobOptions, JobOverlapPolicy, ICliJobStorageProvider, JobExecution, JobExecutionStatus, JobState, JobStatus, JobLogEntry, JobLogLevel, CliStructuredOutput, CliStructuredResponse, IDataExplorerProvider, DataExplorerProviderOptions, DataExplorerLanguage, DataExplorerOutputFormat, DataExplorerTemplate, DataExplorerParameterDescriptor, DataExplorerExecutionContext, DataExplorerResult, DataExplorerExecuteRequest, DataExplorerSourceInfo, ICliProcessorFilter, ICliStreamCommandProcessor, isStreamCapable, } from '@qodalis/cli-server-abstractions';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/abstractions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,iBAAiB,EACjB,8BAA8B,EAC9B,6BAA6B,EAC7B,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,SAAS,EACT,OAAO,EACP,uBAAuB,EACvB,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,SAAS,EACT,WAAW,EACX,WAAW,GACd,MAAM,kCAAkC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/abstractions/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,iBAAiB,EACjB,gBAAgB,EAChB,oBAAoB,EACpB,iBAAiB,EACjB,8BAA8B,EAC9B,6BAA6B,EAC7B,oBAAoB,EACpB,mBAAmB,EACnB,UAAU,EACV,SAAS,EACT,OAAO,EACP,uBAAuB,EACvB,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,sBAAsB,EACtB,YAAY,EACZ,kBAAkB,EAClB,QAAQ,EACR,SAAS,EACT,WAAW,EACX,WAAW,EACX,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,2BAA2B,EAC3B,oBAAoB,EACpB,wBAAwB,EACxB,oBAAoB,EACpB,+BAA+B,EAC/B,4BAA4B,EAC5B,kBAAkB,EAClB,0BAA0B,EAC1B,sBAAsB,EACtB,mBAAmB,EACnB,0BAA0B,EAC1B,eAAe,GAClB,MAAM,kCAAkC,CAAC"}
@@ -1,10 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.CliModule = exports.CliCommandProcessor = exports.CliCommandParameterDescriptor = exports.DefaultLibraryAuthor = exports.CliCommandAuthor = void 0;
3
+ exports.isStreamCapable = exports.DataExplorerOutputFormat = exports.DataExplorerLanguage = exports.CliModule = exports.CliCommandProcessor = exports.CliCommandParameterDescriptor = exports.DefaultLibraryAuthor = exports.CliCommandAuthor = void 0;
4
4
  var cli_server_abstractions_1 = require("@qodalis/cli-server-abstractions");
5
5
  Object.defineProperty(exports, "CliCommandAuthor", { enumerable: true, get: function () { return cli_server_abstractions_1.CliCommandAuthor; } });
6
6
  Object.defineProperty(exports, "DefaultLibraryAuthor", { enumerable: true, get: function () { return cli_server_abstractions_1.DefaultLibraryAuthor; } });
7
7
  Object.defineProperty(exports, "CliCommandParameterDescriptor", { enumerable: true, get: function () { return cli_server_abstractions_1.CliCommandParameterDescriptor; } });
8
8
  Object.defineProperty(exports, "CliCommandProcessor", { enumerable: true, get: function () { return cli_server_abstractions_1.CliCommandProcessor; } });
9
9
  Object.defineProperty(exports, "CliModule", { enumerable: true, get: function () { return cli_server_abstractions_1.CliModule; } });
10
+ Object.defineProperty(exports, "DataExplorerLanguage", { enumerable: true, get: function () { return cli_server_abstractions_1.DataExplorerLanguage; } });
11
+ Object.defineProperty(exports, "DataExplorerOutputFormat", { enumerable: true, get: function () { return cli_server_abstractions_1.DataExplorerOutputFormat; } });
12
+ Object.defineProperty(exports, "isStreamCapable", { enumerable: true, get: function () { return cli_server_abstractions_1.isStreamCapable; } });
10
13
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/abstractions/index.ts"],"names":[],"mappings":";;;AAAA,4EAuB0C;AArBtC,2HAAA,gBAAgB,OAAA;AAChB,+HAAA,oBAAoB,OAAA;AAGpB,wIAAA,6BAA6B,OAAA;AAE7B,8HAAA,mBAAmB,OAAA;AAEnB,oHAAA,SAAS,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/abstractions/index.ts"],"names":[],"mappings":";;;AAAA,4EAsC0C;AApCtC,2HAAA,gBAAgB,OAAA;AAChB,+HAAA,oBAAoB,OAAA;AAGpB,wIAAA,6BAA6B,OAAA;AAE7B,8HAAA,mBAAmB,OAAA;AAEnB,oHAAA,SAAS,OAAA;AAiBT,+HAAA,oBAAoB,OAAA;AACpB,mIAAA,wBAAwB,OAAA;AASxB,0HAAA,eAAe,OAAA"}
@@ -0,0 +1,31 @@
1
+ import { RequestHandler } from 'express';
2
+ import { ICliCommandRegistry } from '../services/cli-command-registry';
3
+ import { CliServerAuthContext, ICliServerAuth } from './cli-server-auth';
4
+ declare global {
5
+ namespace Express {
6
+ interface Request {
7
+ /** Set by the server auth gate when the users plugin protects the server. */
8
+ qcliServerAuth?: CliServerAuthContext;
9
+ }
10
+ }
11
+ }
12
+ /** Normalizes a request path for classification: lower-case, single slashes, no trailing slash. */
13
+ export declare function normalizePath(path: string): string;
14
+ export interface ServerAuthGateOptions {
15
+ registry: ICliCommandRegistry;
16
+ /** Prefixes serving the CLI controller (`/api/v1/qcli`, custom basePath). */
17
+ cliPrefixes: string[];
18
+ /** Extra public routes as `"METHOD /path"` (e.g. a plugin's own login). */
19
+ publicRoutes: Set<string>;
20
+ }
21
+ /** Collects `requiredPermissions` along the resolved processor chain. */
22
+ export declare function processorPermissions(registry: ICliCommandRegistry, command: unknown, chain: unknown): string[];
23
+ /** Permissions required for a (protected) server route. */
24
+ export declare function requiredServerPermissions(method: string, path: string, body: any, options: ServerAuthGateOptions): string[];
25
+ /**
26
+ * Express middleware protecting every `/api/...` route of the CLI server when
27
+ * an auth provider with `protectServer` is installed. Without a provider it is
28
+ * a no-op, so servers without the users plugin behave as before.
29
+ */
30
+ export declare function createServerAuthGate(getAuth: () => ICliServerAuth | undefined, options: ServerAuthGateOptions): RequestHandler;
31
+ //# sourceMappingURL=cli-server-auth-gate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-server-auth-gate.d.ts","sourceRoot":"","sources":["../../../src/auth/cli-server-auth-gate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmC,cAAc,EAAE,MAAM,SAAS,CAAC;AAE1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAA6B,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAKpG,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,OAAO,CAAC;QACd,UAAU,OAAO;YACb,6EAA6E;YAC7E,cAAc,CAAC,EAAE,oBAAoB,CAAC;SACzC;KACJ;CACJ;AAED,mGAAmG;AACnG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAGlD;AAgBD,MAAM,WAAW,qBAAqB;IAClC,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,2EAA2E;IAC3E,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,CAChC,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,OAAO,GACf,MAAM,EAAE,CAkBV;AAED,2DAA2D;AAC3D,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,qBAAqB,GAAG,MAAM,EAAE,CAiB3H;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAChC,OAAO,EAAE,MAAM,cAAc,GAAG,SAAS,EACzC,OAAO,EAAE,qBAAqB,GAC/B,cAAc,CAkDhB"}
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizePath = normalizePath;
4
+ exports.processorPermissions = processorPermissions;
5
+ exports.requiredServerPermissions = requiredServerPermissions;
6
+ exports.createServerAuthGate = createServerAuthGate;
7
+ const cli_server_auth_1 = require("./cli-server-auth");
8
+ const logger_1 = require("../utils/logger");
9
+ const logger = (0, logger_1.createLogger)('ServerAuth');
10
+ /** Normalizes a request path for classification: lower-case, single slashes, no trailing slash. */
11
+ function normalizePath(path) {
12
+ const p = path.toLowerCase().replace(/\/{2,}/g, '/').replace(/\/+$/, '');
13
+ return p || '/';
14
+ }
15
+ const PUBLIC_GET = new Set([
16
+ '/api/qcli/versions',
17
+ '/api/qcli/version',
18
+ '/api/qcli/capabilities',
19
+ '/api/cli/versions',
20
+ '/api/v1/qcli/version',
21
+ '/api/v1/qcli/capabilities',
22
+ '/health',
23
+ '/healthz',
24
+ '/api/health',
25
+ ]);
26
+ const FS_READ = new Set(['ls', 'cat', 'stat', 'download']);
27
+ /** Collects `requiredPermissions` along the resolved processor chain. */
28
+ function processorPermissions(registry, command, chain) {
29
+ if (typeof command !== 'string')
30
+ return [];
31
+ const root = registry.processors.find((p) => p.command.toLowerCase() === command.toLowerCase());
32
+ if (!root)
33
+ return [];
34
+ const out = [...(root.requiredPermissions ?? [])];
35
+ let current = root;
36
+ if (Array.isArray(chain)) {
37
+ for (const sub of chain) {
38
+ if (typeof sub !== 'string')
39
+ break;
40
+ const child = current.processors?.find((p) => p.command.toLowerCase() === sub.toLowerCase());
41
+ if (!child)
42
+ break;
43
+ out.push(...(child.requiredPermissions ?? []));
44
+ current = child;
45
+ }
46
+ }
47
+ return out;
48
+ }
49
+ /** Permissions required for a (protected) server route. */
50
+ function requiredServerPermissions(method, path, body, options) {
51
+ for (const prefix of options.cliPrefixes) {
52
+ if (path === `${prefix}/execute` || path === `${prefix}/execute/stream`) {
53
+ return [cli_server_auth_1.CliServerPermissions.CommandsExecute, ...processorPermissions(options.registry, body?.command, body?.chainCommands)];
54
+ }
55
+ }
56
+ if (path === '/api/qcli/fs' || path.startsWith('/api/qcli/fs/')) {
57
+ const op = path.slice('/api/qcli/fs/'.length);
58
+ return [method === 'GET' && FS_READ.has(op) ? cli_server_auth_1.CliServerPermissions.FilesRead : cli_server_auth_1.CliServerPermissions.FilesWrite];
59
+ }
60
+ if (path === '/api/qcli/data-explorer' || path.startsWith('/api/qcli/data-explorer/')) {
61
+ return [cli_server_auth_1.CliServerPermissions.DataQuery];
62
+ }
63
+ if (path === '/api/v1/qcli/jobs' || path.startsWith('/api/v1/qcli/jobs/')) {
64
+ return [method === 'GET' ? cli_server_auth_1.CliServerPermissions.JobsRead : cli_server_auth_1.CliServerPermissions.JobsManage];
65
+ }
66
+ return [];
67
+ }
68
+ /**
69
+ * Express middleware protecting every `/api/...` route of the CLI server when
70
+ * an auth provider with `protectServer` is installed. Without a provider it is
71
+ * a no-op, so servers without the users plugin behave as before.
72
+ */
73
+ function createServerAuthGate(getAuth, options) {
74
+ return (req, res, next) => {
75
+ const auth = getAuth();
76
+ if (!auth || !auth.protectServer) {
77
+ next();
78
+ return;
79
+ }
80
+ const method = req.method.toUpperCase();
81
+ if (method === 'OPTIONS') {
82
+ next();
83
+ return;
84
+ }
85
+ const path = normalizePath(req.path);
86
+ if (!path.startsWith('/api/') && path !== '/api') {
87
+ next();
88
+ return;
89
+ }
90
+ if ((method === 'GET' || method === 'HEAD') && PUBLIC_GET.has(path)) {
91
+ next();
92
+ return;
93
+ }
94
+ if (options.publicRoutes.has(`${method} ${path}`) || auth.handlesPath(path, method)) {
95
+ next();
96
+ return;
97
+ }
98
+ auth.authenticateRequest(req).then((ctx) => {
99
+ const missing = requiredServerPermissions(method, path, req.body, options).filter((p) => !auth.hasPermission(ctx, p));
100
+ if (missing.length > 0) {
101
+ res.status(403).json({ error: `forbidden: missing permission ${missing.join(', ')}` });
102
+ return;
103
+ }
104
+ req.qcliServerAuth = ctx;
105
+ next();
106
+ }, (err) => {
107
+ if (typeof err?.status !== 'number') {
108
+ // not an auth error (e.g. a store failure): never expose its message
109
+ logger.error('authentication failed unexpectedly:', err);
110
+ res.status(500).json({ error: 'internal server error' });
111
+ return;
112
+ }
113
+ const status = err.status;
114
+ res.status(status).json({ error: status === 401 ? err.message || 'unauthorized' : err.message || 'forbidden' });
115
+ });
116
+ };
117
+ }
118
+ //# sourceMappingURL=cli-server-auth-gate.js.map