@qodalis/cli-server-node 1.0.0-beta.5 → 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.
- package/README.md +175 -19
- package/dist/src/auth/cli-server-auth-gate.d.ts +31 -0
- package/dist/src/auth/cli-server-auth-gate.d.ts.map +1 -0
- package/dist/src/auth/cli-server-auth-gate.js +118 -0
- package/dist/src/auth/cli-server-auth-gate.js.map +1 -0
- package/dist/src/auth/cli-server-auth.d.ts +147 -0
- package/dist/src/auth/cli-server-auth.d.ts.map +1 -0
- package/dist/src/auth/cli-server-auth.js +143 -0
- package/dist/src/auth/cli-server-auth.js.map +1 -0
- package/dist/src/auth/index.d.ts +4 -0
- package/dist/src/auth/index.d.ts.map +1 -0
- package/dist/src/auth/index.js +19 -0
- package/dist/src/auth/index.js.map +1 -0
- package/dist/src/auth/ws-upgrade-auth.d.ts +30 -0
- package/dist/src/auth/ws-upgrade-auth.d.ts.map +1 -0
- package/dist/src/auth/ws-upgrade-auth.js +49 -0
- package/dist/src/auth/ws-upgrade-auth.js.map +1 -0
- package/dist/src/controllers/cli-controller.d.ts +2 -1
- package/dist/src/controllers/cli-controller.d.ts.map +1 -1
- package/dist/src/controllers/cli-controller.js +7 -2
- package/dist/src/controllers/cli-controller.js.map +1 -1
- package/dist/src/controllers/cli-version-controller.d.ts +12 -1
- package/dist/src/controllers/cli-version-controller.d.ts.map +1 -1
- package/dist/src/controllers/cli-version-controller.js +7 -2
- package/dist/src/controllers/cli-version-controller.js.map +1 -1
- package/dist/src/create-cli-server.d.ts +45 -1
- package/dist/src/create-cli-server.d.ts.map +1 -1
- package/dist/src/create-cli-server.js +71 -10
- package/dist/src/create-cli-server.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +15 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/server.js +12 -0
- package/dist/src/server.js.map +1 -1
- package/dist/src/services/cli-event-socket-manager.d.ts +37 -1
- package/dist/src/services/cli-event-socket-manager.d.ts.map +1 -1
- package/dist/src/services/cli-event-socket-manager.js +126 -28
- package/dist/src/services/cli-event-socket-manager.js.map +1 -1
- package/dist/src/services/cli-log-socket-manager.d.ts +15 -1
- package/dist/src/services/cli-log-socket-manager.d.ts.map +1 -1
- package/dist/src/services/cli-log-socket-manager.js +53 -13
- package/dist/src/services/cli-log-socket-manager.js.map +1 -1
- package/dist/src/services/cli-shell-options.d.ts +107 -0
- package/dist/src/services/cli-shell-options.d.ts.map +1 -0
- package/dist/src/services/cli-shell-options.js +139 -0
- package/dist/src/services/cli-shell-options.js.map +1 -0
- package/dist/src/services/cli-shell-session-manager.d.ts +83 -10
- package/dist/src/services/cli-shell-session-manager.d.ts.map +1 -1
- package/dist/src/services/cli-shell-session-manager.js +474 -85
- package/dist/src/services/cli-shell-session-manager.js.map +1 -1
- package/dist/src/services/index.d.ts +2 -1
- package/dist/src/services/index.d.ts.map +1 -1
- package/dist/src/services/index.js +5 -1
- package/dist/src/services/index.js.map +1 -1
- package/dist/src/services/process-tree.d.ts +38 -0
- package/dist/src/services/process-tree.d.ts.map +1 -0
- package/dist/src/services/process-tree.js +249 -0
- package/dist/src/services/process-tree.js.map +1 -0
- package/dist/src/services/session-bound-sockets.d.ts +45 -0
- package/dist/src/services/session-bound-sockets.d.ts.map +1 -0
- package/dist/src/services/session-bound-sockets.js +129 -0
- package/dist/src/services/session-bound-sockets.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Qodalis CLI Server (Node.js)
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@qodalis/cli-server-node)
|
|
4
|
+
[](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
|
|
@@ -303,11 +306,103 @@ The server exposes versioned endpoints:
|
|
|
303
306
|
|
|
304
307
|
| Method | Path | Description |
|
|
305
308
|
|--------|------|-------------|
|
|
306
|
-
| GET | `/api/
|
|
307
|
-
| GET | `/api/v1/
|
|
308
|
-
| GET | `/api/v1/
|
|
309
|
-
| POST | `/api/v1/
|
|
310
|
-
|
|
|
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'`.
|
|
318
|
+
|
|
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
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
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
|
+
```
|
|
341
|
+
|
|
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> };
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
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.
|
|
385
|
+
|
|
386
|
+
### Process cleanup
|
|
387
|
+
|
|
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 |
|
|
311
406
|
|
|
312
407
|
## Processor Base Class Reference
|
|
313
408
|
|
|
@@ -329,12 +424,25 @@ The server exposes versioned endpoints:
|
|
|
329
424
|
|
|
330
425
|
```typescript
|
|
331
426
|
interface CliServerOptions {
|
|
332
|
-
basePath?: string; // API base path (default: '/api/
|
|
333
|
-
cors?: boolean | cors.CorsOptions; // CORS config (default:
|
|
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)
|
|
334
434
|
configure?: (builder: CliBuilder) => void; // Processor registration
|
|
335
435
|
}
|
|
336
436
|
```
|
|
337
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
|
+
|
|
338
446
|
`createCliServer()` returns:
|
|
339
447
|
|
|
340
448
|
```typescript
|
|
@@ -342,7 +450,11 @@ interface CliServerOptions {
|
|
|
342
450
|
app: Express; // Configured Express app
|
|
343
451
|
registry: CliCommandRegistry; // Processor registry
|
|
344
452
|
builder: CliBuilder; // Registration builder
|
|
345
|
-
|
|
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
|
|
346
458
|
}
|
|
347
459
|
```
|
|
348
460
|
|
|
@@ -391,19 +503,19 @@ import {
|
|
|
391
503
|
|
|
392
504
|
## File Storage
|
|
393
505
|
|
|
394
|
-
The server includes a pluggable file storage system exposed at `/api/
|
|
506
|
+
The server includes a pluggable file storage system exposed at `/api/qcli/fs/*`. Enable it with `setFileStorageProvider()` and choose a storage backend.
|
|
395
507
|
|
|
396
508
|
### Filesystem API Endpoints
|
|
397
509
|
|
|
398
510
|
| Method | Path | Description |
|
|
399
511
|
|--------|------|-------------|
|
|
400
|
-
| GET | `/api/
|
|
401
|
-
| GET | `/api/
|
|
402
|
-
| GET | `/api/
|
|
403
|
-
| GET | `/api/
|
|
404
|
-
| POST | `/api/
|
|
405
|
-
| POST | `/api/
|
|
406
|
-
| DELETE | `/api/
|
|
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 |
|
|
407
519
|
|
|
408
520
|
### Storage Providers
|
|
409
521
|
|
|
@@ -615,6 +727,37 @@ All commands support `--region` (`-r`) for region override and `--output` (`-o`)
|
|
|
615
727
|
|
|
616
728
|
See [`plugins/aws/README.md`](plugins/aws/README.md) for the full command reference.
|
|
617
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
|
+
|
|
618
761
|
## Built-in Processors
|
|
619
762
|
|
|
620
763
|
These processors ship with the library and are included in the standalone server:
|
|
@@ -627,7 +770,6 @@ These processors ship with the library and are included in the standalone server
|
|
|
627
770
|
| `http` | HTTP request operations |
|
|
628
771
|
| `hash` | Hash computation (MD5, SHA1, SHA256, SHA512) |
|
|
629
772
|
| `base64` | Base64 encode/decode (sub-commands) |
|
|
630
|
-
| `uuid` | UUID generation |
|
|
631
773
|
|
|
632
774
|
## Docker
|
|
633
775
|
|
|
@@ -663,10 +805,22 @@ packages/
|
|
|
663
805
|
cli-command-parameter-descriptor.ts # Parameter declaration
|
|
664
806
|
cli-command-author.ts # Author metadata
|
|
665
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
|
|
666
818
|
filesystem/ # Core file storage abstraction (IFileStorageProvider, InMemory, OS)
|
|
667
819
|
filesystem-json/ # JSON file persistence provider
|
|
668
820
|
filesystem-sqlite/ # SQLite persistence provider (better-sqlite3)
|
|
669
821
|
filesystem-s3/ # Amazon S3 storage provider (@aws-sdk/client-s3)
|
|
822
|
+
jobs/ # Job scheduling plugin
|
|
823
|
+
users/ # Users plugin (auth, sessions, groups, audit)
|
|
670
824
|
weather/ # Weather module (example plugin)
|
|
671
825
|
src/
|
|
672
826
|
abstractions/ # Re-exports from @qodalis/cli-server-abstractions
|
|
@@ -679,9 +833,11 @@ src/
|
|
|
679
833
|
cli-command-executor-service.ts # Command execution pipeline
|
|
680
834
|
cli-response-builder.ts # Structured output builder
|
|
681
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
|
|
682
838
|
controllers/
|
|
683
|
-
cli-controller.ts # V1 REST API (/api/v1/
|
|
684
|
-
cli-version-controller.ts # Version discovery (/api/
|
|
839
|
+
cli-controller.ts # V1 REST API (/api/v1/qcli)
|
|
840
|
+
cli-version-controller.ts # Version discovery (/api/qcli/versions)
|
|
685
841
|
extensions/
|
|
686
842
|
cli-builder.ts # Fluent registration API (addProcessor, addModule)
|
|
687
843
|
processors/ # Built-in processors
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-server-auth-gate.js","sourceRoot":"","sources":["../../../src/auth/cli-server-auth-gate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'http';
|
|
2
|
+
import type { Request } from 'express';
|
|
3
|
+
/** Identity of an authenticated caller (HTTP request or WebSocket). */
|
|
4
|
+
export interface CliServerAuthContext {
|
|
5
|
+
userId: string;
|
|
6
|
+
userName: string;
|
|
7
|
+
/** Absent for host-authenticated (adapter) callers. */
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
/** Effective permissions (`resource:action`, `*` wildcards). */
|
|
10
|
+
permissions: string[];
|
|
11
|
+
}
|
|
12
|
+
/** Error thrown by {@link ICliServerAuth.authenticateRequest}; rendered as `{ error }`. */
|
|
13
|
+
export interface CliServerAuthError extends Error {
|
|
14
|
+
status?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Authentication provider that protects the whole CLI server (routes and
|
|
18
|
+
* sockets). Implemented by the users plugin (`usersPlugin.serverAuth`).
|
|
19
|
+
*/
|
|
20
|
+
export interface ICliServerAuth {
|
|
21
|
+
/** When false the server stays open; sockets only use tickets for optional identity. */
|
|
22
|
+
readonly protectServer: boolean;
|
|
23
|
+
/** True for routes the provider authenticates itself (its own API, its login). */
|
|
24
|
+
handlesPath(path: string, method: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Authenticates an HTTP request. Throws an error with `status`
|
|
27
|
+
* (401 unauthenticated, 403 e.g. password change required / CSRF); any
|
|
28
|
+
* other error is logged and answered with a generic 500.
|
|
29
|
+
*/
|
|
30
|
+
authenticateRequest(req: Request): Promise<CliServerAuthContext>;
|
|
31
|
+
/**
|
|
32
|
+
* Consumes a single-use WebSocket ticket; undefined when invalid/expired/used.
|
|
33
|
+
* May throw an error with `status: 403` when the identity is valid but not
|
|
34
|
+
* allowed yet (e.g. password change required).
|
|
35
|
+
*/
|
|
36
|
+
consumeTicket(ticket: string): Promise<CliServerAuthContext | undefined>;
|
|
37
|
+
hasPermission(ctx: CliServerAuthContext, permission: string): boolean;
|
|
38
|
+
/** Called once when installed into a server. */
|
|
39
|
+
attachToServer?(server: {
|
|
40
|
+
allowedOrigins?: string[];
|
|
41
|
+
broadcast: (message: Record<string, unknown>) => void;
|
|
42
|
+
}): void;
|
|
43
|
+
/**
|
|
44
|
+
* Re-validates a socket identity without sliding the session: the session still
|
|
45
|
+
* exists and is not expired, the user exists and is enabled. Returns the identity
|
|
46
|
+
* with fresh permissions, or undefined when it ended. Used to bind long-lived
|
|
47
|
+
* sockets (the shell) to their session. `request` is the socket's original
|
|
48
|
+
* upgrade request, for identities without a stored session that the provider
|
|
49
|
+
* re-authenticates from the request (e.g. a host auth adapter reading cookies).
|
|
50
|
+
*/
|
|
51
|
+
revalidate?(ctx: CliServerAuthContext, request?: IncomingMessage): Promise<CliServerAuthContext | undefined>;
|
|
52
|
+
/** Records activity on the session behind `ctx` (slides the idle expiry). */
|
|
53
|
+
touch?(ctx: CliServerAuthContext): Promise<void>;
|
|
54
|
+
/** Writes an audit entry (never include secrets, command text or stdin). */
|
|
55
|
+
audit?(entry: CliServerAuditEntry): Promise<void>;
|
|
56
|
+
/** Subscribes to session lifecycle events; returns an unsubscribe function. */
|
|
57
|
+
onSessionEvent?(listener: (event: CliServerSessionEvent) => void): () => void;
|
|
58
|
+
}
|
|
59
|
+
/** Audit entry written by the server through {@link ICliServerAuth.audit}. */
|
|
60
|
+
export interface CliServerAuditEntry {
|
|
61
|
+
event: string;
|
|
62
|
+
userId: string;
|
|
63
|
+
userName: string;
|
|
64
|
+
/** Acting user (defaults to the user). */
|
|
65
|
+
actorId?: string;
|
|
66
|
+
actorName?: string;
|
|
67
|
+
details?: string;
|
|
68
|
+
metadata?: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Session lifecycle events emitted by the auth provider:
|
|
72
|
+
* - `session-ended`: logout, expiry, revoke (kick), user disabled/deleted;
|
|
73
|
+
* `revoked` is true for revocations, which also broadcast `session-kicked`;
|
|
74
|
+
* - `access-changed`: users/groups/permissions changed, or the expired-session
|
|
75
|
+
* sweep ran (re-check open sockets).
|
|
76
|
+
*/
|
|
77
|
+
export type CliServerSessionEvent = {
|
|
78
|
+
type: 'session-ended';
|
|
79
|
+
sessionId: string;
|
|
80
|
+
userId?: string;
|
|
81
|
+
reason?: string;
|
|
82
|
+
revoked?: boolean;
|
|
83
|
+
} | {
|
|
84
|
+
type: 'access-changed';
|
|
85
|
+
userId?: string;
|
|
86
|
+
};
|
|
87
|
+
/** Server permissions enforced when the users plugin protects the server. */
|
|
88
|
+
export declare const CliServerPermissions: {
|
|
89
|
+
readonly CommandsExecute: "commands:execute";
|
|
90
|
+
readonly FilesRead: "files:read";
|
|
91
|
+
readonly FilesWrite: "files:write";
|
|
92
|
+
readonly DataQuery: "data:query";
|
|
93
|
+
readonly JobsRead: "jobs:read";
|
|
94
|
+
readonly JobsManage: "jobs:manage";
|
|
95
|
+
readonly LogsRead: "logs:read";
|
|
96
|
+
readonly ShellExecute: "shell:execute";
|
|
97
|
+
readonly AdminAccess: "admin:access";
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Exact origin match. An entry ending in `:*` matches any port of that
|
|
101
|
+
* scheme+host (e.g. `http://127.0.0.1:*`).
|
|
102
|
+
*/
|
|
103
|
+
export declare function isOriginAllowed(origin: string | undefined, allowed: readonly string[] | undefined): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Number of trusted reverse-proxy hops for a `trustProxy` setting:
|
|
106
|
+
* `false`/`0`/absent → 0 (untrusted), `true` → 1, a number n → n (values
|
|
107
|
+
* below 1 other than 0 count as 1).
|
|
108
|
+
*/
|
|
109
|
+
export declare function trustedProxyHops(trustProxy: boolean | number | undefined): number;
|
|
110
|
+
/**
|
|
111
|
+
* The client address: the socket address, or with `trustProxy` (a hop count,
|
|
112
|
+
* `true` = 1) the `X-Forwarded-For` entry at index `len - hops`.
|
|
113
|
+
*/
|
|
114
|
+
export declare function clientAddress(req: {
|
|
115
|
+
headers: Record<string, string | string[] | undefined>;
|
|
116
|
+
socket?: {
|
|
117
|
+
remoteAddress?: string;
|
|
118
|
+
} | null;
|
|
119
|
+
}, trustProxy: boolean | number | undefined): string;
|
|
120
|
+
/** Minimal request shape for {@link isSameOrigin} (Node `IncomingMessage` or Express `Request`). */
|
|
121
|
+
export interface OriginCheckRequest {
|
|
122
|
+
headers: Record<string, string | string[] | undefined>;
|
|
123
|
+
socket?: {
|
|
124
|
+
encrypted?: boolean;
|
|
125
|
+
remoteAddress?: string;
|
|
126
|
+
} | null;
|
|
127
|
+
/** Express app (when present, its `trust proxy` setting is honoured). */
|
|
128
|
+
app?: {
|
|
129
|
+
get?: (name: string) => unknown;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The request's own origin (`scheme://host:port`, normalized): the scheme from
|
|
134
|
+
* the socket (TLS or not) and the `Host` header. Behind a trusted proxy
|
|
135
|
+
* (`trustProxy`, or the Express app's `trust proxy` setting when omitted)
|
|
136
|
+
* `X-Forwarded-Proto` / `X-Forwarded-Host` win, read like `X-Forwarded-For`:
|
|
137
|
+
* the entry at index `len - hops` (the rightmost with one hop; the leftmost
|
|
138
|
+
* when the list is shorter; an empty entry is ignored).
|
|
139
|
+
*/
|
|
140
|
+
export declare function requestOrigin(req: OriginCheckRequest, trustProxy?: boolean | number): string | undefined;
|
|
141
|
+
/**
|
|
142
|
+
* True when `origin` is the request's own origin (e.g. the bundled admin
|
|
143
|
+
* dashboard served by this server). Same-origin requests are not cross-site
|
|
144
|
+
* and always pass Origin/CSRF checks. Case-insensitive; default ports normalized.
|
|
145
|
+
*/
|
|
146
|
+
export declare function isSameOrigin(origin: string | undefined, req: OriginCheckRequest, trustProxy?: boolean | number): boolean;
|
|
147
|
+
//# sourceMappingURL=cli-server-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-server-auth.d.ts","sourceRoot":"","sources":["../../../src/auth/cli-server-auth.ts"],"names":[],"mappings":"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"}
|