nestjs-web-repl 1.1.0 → 2.0.0
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 +83 -29
- package/dist/index.d.ts +2 -1
- package/dist/index.js +3 -1
- package/dist/interfaces/web-repl-options.interface.d.ts +22 -22
- package/dist/web-repl.controller.js +3 -5
- package/dist/web-repl.module-definition.d.ts +10 -0
- package/dist/web-repl.module-definition.js +82 -0
- package/dist/web-repl.module.d.ts +2 -7
- package/dist/web-repl.module.js +3 -68
- package/dist/web-repl.service.js +6 -6
- package/package.json +1 -1
- package/skill/SKILL.md +11 -6
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ import { WebReplModule } from 'nestjs-web-repl';
|
|
|
35
35
|
|
|
36
36
|
@Module({
|
|
37
37
|
imports: [
|
|
38
|
-
WebReplModule.
|
|
38
|
+
WebReplModule.register({
|
|
39
39
|
enabled: process.env.REPL_ENABLED === 'true',
|
|
40
40
|
}),
|
|
41
41
|
],
|
|
@@ -51,7 +51,7 @@ resolves from the whole DI container, not just the module that imports
|
|
|
51
51
|
|
|
52
52
|
A runnable example lives in [`example/`](./example): `example/cat.service.ts`
|
|
53
53
|
registers a trivial `CatService`, `example/app.module.ts` wires up
|
|
54
|
-
`WebReplModule.
|
|
54
|
+
`WebReplModule.register(...)`, and `example/main.ts` boots it. Run it with:
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
57
|
REPL_ENABLED=true PORT=3000 npx ts-node -T example/main.ts
|
|
@@ -118,7 +118,8 @@ curl -X POST http://localhost:3000/repl/dev \
|
|
|
118
118
|
## Securing it
|
|
119
119
|
|
|
120
120
|
Because the module ships no auth, the supported way to lock this down is to
|
|
121
|
-
|
|
121
|
+
subclass the built-in controller, add your own guard, and pass it in via the
|
|
122
|
+
`controller` extra:
|
|
122
123
|
|
|
123
124
|
```ts
|
|
124
125
|
import { Controller, UseGuards } from '@nestjs/common';
|
|
@@ -127,31 +128,35 @@ import { AdminGuard } from './admin.guard';
|
|
|
127
128
|
|
|
128
129
|
@Controller('internal/repl')
|
|
129
130
|
@UseGuards(AdminGuard)
|
|
130
|
-
|
|
131
|
+
class SecureReplController extends WebReplController {}
|
|
131
132
|
```
|
|
132
133
|
|
|
133
134
|
```ts
|
|
134
135
|
@Module({
|
|
135
136
|
imports: [
|
|
136
|
-
WebReplModule.
|
|
137
|
+
WebReplModule.register({
|
|
137
138
|
enabled: process.env.REPL_ENABLED === 'true',
|
|
138
|
-
|
|
139
|
+
controller: SecureReplController, // replaces the unguarded default controller
|
|
139
140
|
}),
|
|
140
141
|
],
|
|
141
|
-
controllers: [SecureReplController], // register yours instead
|
|
142
142
|
})
|
|
143
143
|
export class AppModule {}
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
146
|
+
`controller` is available to both `register` and `registerAsync` — it is a
|
|
147
|
+
static, module-definition-time choice, so it's passed alongside `useFactory`/
|
|
148
|
+
`inject` rather than resolved by it:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
WebReplModule.registerAsync({
|
|
152
|
+
imports: [ConfigModule],
|
|
153
|
+
inject: [ConfigService],
|
|
154
|
+
useFactory: (config: ConfigService) => ({
|
|
155
|
+
enabled: config.get('REPL_ENABLED') === 'true',
|
|
156
|
+
}),
|
|
157
|
+
controller: SecureReplController,
|
|
158
|
+
});
|
|
159
|
+
```
|
|
155
160
|
|
|
156
161
|
## Adapter / multi-instance
|
|
157
162
|
|
|
@@ -192,7 +197,9 @@ solves this with an **ownership + fan-out** protocol:
|
|
|
192
197
|
By default this coordination happens via `InMemoryWebReplAdapter`, which only
|
|
193
198
|
works within a single process (fine for local dev / single-instance
|
|
194
199
|
deployments). For real multi-instance deployments, provide your own adapter
|
|
195
|
-
|
|
200
|
+
via the `adapter` extra — a ready instance, `{ useClass, imports }`, or
|
|
201
|
+
`{ useFactory, inject, imports }` (all DI-capable, so the adapter can itself
|
|
202
|
+
depend on other providers) — that implements:
|
|
196
203
|
|
|
197
204
|
```ts
|
|
198
205
|
export interface WebReplAdapter {
|
|
@@ -242,37 +249,83 @@ export class RedisWebReplAdapter implements WebReplAdapter, OnModuleDestroy {
|
|
|
242
249
|
```
|
|
243
250
|
|
|
244
251
|
```ts
|
|
245
|
-
|
|
252
|
+
// as a ready-made instance
|
|
253
|
+
WebReplModule.register({
|
|
246
254
|
enabled: process.env.REPL_ENABLED === 'true',
|
|
247
255
|
adapter: new RedisWebReplAdapter(),
|
|
248
256
|
instanceId: process.env.HOSTNAME, // shows up in `command` SSE events and
|
|
249
257
|
// internal webrepl:sys claim messages
|
|
250
258
|
});
|
|
259
|
+
|
|
260
|
+
// or let Nest construct it (and its dependencies) for you
|
|
261
|
+
WebReplModule.register({
|
|
262
|
+
enabled: process.env.REPL_ENABLED === 'true',
|
|
263
|
+
adapter: { useClass: RedisWebReplAdapter, imports: [RedisModule] },
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// or build it with a factory
|
|
267
|
+
WebReplModule.register({
|
|
268
|
+
enabled: process.env.REPL_ENABLED === 'true',
|
|
269
|
+
adapter: {
|
|
270
|
+
useFactory: (redis: RedisService) => new RedisWebReplAdapter(redis),
|
|
271
|
+
inject: [RedisService],
|
|
272
|
+
imports: [RedisModule],
|
|
273
|
+
},
|
|
274
|
+
});
|
|
251
275
|
```
|
|
252
276
|
|
|
253
277
|
## Options (`WebReplModuleOptions`)
|
|
254
278
|
|
|
255
279
|
| Option | Type | Default | Notes |
|
|
256
280
|
| ------------------- | ---------------- | --------------------------- | --------------------------------------------------- |
|
|
257
|
-
| `enabled` | `boolean` | *(required)* | When `false`, the module
|
|
258
|
-
| `adapter` | `WebReplAdapter` | `InMemoryWebReplAdapter` | Provide for multi-instance coordination. |
|
|
281
|
+
| `enabled` | `boolean` | *(required)* | When `false`, routes 404 and the module does not subscribe to the adapter. |
|
|
259
282
|
| `instanceId` | `string` | random `inst_xxxxxxxx` | Shown in `command` SSE events and internal `webrepl:sys` claim/release messages. |
|
|
260
283
|
| `sessionTtl` | `number` (ms) | `1_800_000` (30 min) | Idle time before a channel's ownership is released. |
|
|
261
284
|
| `replayBufferSize` | `number` | `200` | Events kept per channel for SSE `Last-Event-ID` replay. |
|
|
262
285
|
| `heartbeatInterval` | `number` (ms) | `15_000` | SSE `system` `{ ping: true }` interval. |
|
|
263
286
|
| `ownerHeartbeatInterval` | `number` (ms) | `10_000` | How often an instance re-announces `claim` for each channel it owns, keeping its ownership lease alive. |
|
|
264
287
|
| `ownerLeaseTtl` | `number` (ms) | `30_000` | How long an ownership record is trusted since the last claim/heartbeat, before a stale owner's channel may be taken over. Enforced minimum `ownerHeartbeatInterval * 2` (a live owner always keeps a full heartbeat interval of slack against delivery jitter); if the configured value is below that, it's clamped up to `ownerHeartbeatInterval * 2` and a warning is logged (never throws). |
|
|
265
|
-
| `registerController`| `boolean` | `true` | Set `false` to omit the default controller (see [Securing it](#securing-it)). `forRoot` only. |
|
|
266
288
|
|
|
267
|
-
`
|
|
268
|
-
|
|
269
|
-
|
|
289
|
+
`register`/`registerAsync` also accept two "extras", passed alongside the
|
|
290
|
+
options above (or alongside `useFactory`/`inject`/`imports` for the async
|
|
291
|
+
form) rather than through them, since both are static, module-definition-time
|
|
292
|
+
choices:
|
|
293
|
+
|
|
294
|
+
| Extra | Type | Default | Notes |
|
|
295
|
+
| ------------ | --------------------- | --------------------- | --------------------------------------------------- |
|
|
296
|
+
| `controller` | `Type<WebReplController>` | built-in `WebReplController` | Bring your own controller (subclass + guards). See [Securing it](#securing-it). |
|
|
297
|
+
| `adapter` | `WebReplAdapter \| Type<WebReplAdapter> \| { useClass, imports? } \| { useFactory, inject?, imports? }` | `InMemoryWebReplAdapter` | Multi-instance coordination. See [Adapter / multi-instance](#adapter--multi-instance). |
|
|
298
|
+
|
|
299
|
+
`WebReplModule.registerAsync({ useFactory, inject, imports, controller?, adapter? })`
|
|
300
|
+
is also available for options that need DI (e.g. reading a `ConfigService`).
|
|
270
301
|
|
|
271
302
|
## Exports
|
|
272
303
|
|
|
273
304
|
`WebReplModule`, `WebReplController`, `WebReplService`, `InMemoryWebReplAdapter`,
|
|
274
|
-
and the
|
|
275
|
-
|
|
305
|
+
and `WEB_REPL_OPTIONS` (the DI token for the resolved options, useful when
|
|
306
|
+
injecting them into a sibling-registered controller subclass), plus the types
|
|
307
|
+
`WebReplAdapter`, `WebReplModuleOptions`, `WebReplModuleExtras`,
|
|
308
|
+
`WebReplAdapterConfig`, `WebReplEvent`, `SseEventType`.
|
|
309
|
+
|
|
310
|
+
## Migrating from 1.x → 2.0
|
|
311
|
+
|
|
312
|
+
- `WebReplModule.forRoot(...)` → `WebReplModule.register(...)`.
|
|
313
|
+
- `WebReplModule.forRootAsync(...)` → `WebReplModule.registerAsync(...)`.
|
|
314
|
+
- `adapter` is no longer an option resolved by `useFactory`; it's a static
|
|
315
|
+
"extra" passed alongside the options (or alongside `useFactory`/`inject` for
|
|
316
|
+
the async form), and now also accepts `{ useClass, imports? }` or
|
|
317
|
+
`{ useFactory, inject?, imports? }` in addition to a ready instance — see
|
|
318
|
+
[Adapter / multi-instance](#adapter--multi-instance).
|
|
319
|
+
- `registerController: false` is gone. To run your own guarded controller
|
|
320
|
+
instead of the default, subclass `WebReplController`, add `@UseGuards(...)`,
|
|
321
|
+
and pass it as the `controller` extra to `register`/`registerAsync` — see
|
|
322
|
+
[Securing it](#securing-it). Unlike the old `registerController` flag, this
|
|
323
|
+
works identically for both the sync and async form.
|
|
324
|
+
- A disabled module (`enabled: false`) now still registers the
|
|
325
|
+
controller/service, but every route 404s and the module never subscribes to
|
|
326
|
+
the adapter — it no longer silently registers nothing. If you were relying
|
|
327
|
+
on a disabled module contributing zero routes/providers to the Nest module
|
|
328
|
+
graph, that is no longer the case.
|
|
276
329
|
|
|
277
330
|
## AI skill
|
|
278
331
|
|
|
@@ -337,15 +390,16 @@ We tell you this because the honest thing to do is let you judge the code on
|
|
|
337
390
|
its merits rather than guess at its origins. If you are skeptical of AI-written
|
|
338
391
|
code, here is what to actually look at:
|
|
339
392
|
|
|
340
|
-
- **The tests.**
|
|
393
|
+
- **The tests.** 83 automated tests, including a two-instance end-to-end test
|
|
341
394
|
that proves cross-instance command routing and output fan-out, and an
|
|
342
395
|
execution-proof test that resolves a real provider through the live REPL
|
|
343
396
|
context. `npm test`, `npm run build`, and `npx tsc --noEmit` are all green.
|
|
344
397
|
- **The commit history.** The real TDD trail is preserved — failing test,
|
|
345
398
|
implementation, fixes — including several rounds where review caught genuine
|
|
346
399
|
defects (the trickiest: `node:repl` completion detection on modern Node, and
|
|
347
|
-
a runtime-vs-registration security bug where
|
|
348
|
-
shipped the arbitrary-code endpoint live with
|
|
400
|
+
a runtime-vs-registration security bug where an earlier async-registration
|
|
401
|
+
API could have shipped the arbitrary-code endpoint live with
|
|
402
|
+
`enabled: false`; `registerAsync` now enforces `enabled` at runtime instead).
|
|
349
403
|
- **The security invariants** are documented in [`AGENTS.md`](./AGENTS.md) and
|
|
350
404
|
enforced by tests, not left as prose.
|
|
351
405
|
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { WebReplModule } from './web-repl.module';
|
|
|
2
2
|
export { WebReplController } from './web-repl.controller';
|
|
3
3
|
export { WebReplService } from './web-repl.service';
|
|
4
4
|
export { InMemoryWebReplAdapter } from './adapters/in-memory-web-repl.adapter';
|
|
5
|
+
export { WEB_REPL_OPTIONS } from './constants';
|
|
5
6
|
export type { WebReplAdapter } from './interfaces/web-repl-adapter.interface';
|
|
6
|
-
export type { WebReplModuleOptions,
|
|
7
|
+
export type { WebReplModuleOptions, WebReplModuleExtras, WebReplAdapterConfig, } from './interfaces/web-repl-options.interface';
|
|
7
8
|
export type { WebReplEvent, SseEventType, } from './interfaces/web-repl-messages.interface';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.InMemoryWebReplAdapter = exports.WebReplService = exports.WebReplController = exports.WebReplModule = void 0;
|
|
3
|
+
exports.WEB_REPL_OPTIONS = exports.InMemoryWebReplAdapter = exports.WebReplService = exports.WebReplController = exports.WebReplModule = void 0;
|
|
4
4
|
var web_repl_module_1 = require("./web-repl.module");
|
|
5
5
|
Object.defineProperty(exports, "WebReplModule", { enumerable: true, get: function () { return web_repl_module_1.WebReplModule; } });
|
|
6
6
|
var web_repl_controller_1 = require("./web-repl.controller");
|
|
@@ -9,3 +9,5 @@ var web_repl_service_1 = require("./web-repl.service");
|
|
|
9
9
|
Object.defineProperty(exports, "WebReplService", { enumerable: true, get: function () { return web_repl_service_1.WebReplService; } });
|
|
10
10
|
var in_memory_web_repl_adapter_1 = require("./adapters/in-memory-web-repl.adapter");
|
|
11
11
|
Object.defineProperty(exports, "InMemoryWebReplAdapter", { enumerable: true, get: function () { return in_memory_web_repl_adapter_1.InMemoryWebReplAdapter; } });
|
|
12
|
+
var constants_1 = require("./constants");
|
|
13
|
+
Object.defineProperty(exports, "WEB_REPL_OPTIONS", { enumerable: true, get: function () { return constants_1.WEB_REPL_OPTIONS; } });
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { ModuleMetadata } from '@nestjs/common';
|
|
1
|
+
import type { ModuleMetadata, Type } from '@nestjs/common';
|
|
2
2
|
import type { WebReplAdapter } from './web-repl-adapter.interface';
|
|
3
|
+
import type { WebReplController } from '../web-repl.controller';
|
|
4
|
+
/** Runtime options — the only thing registerAsync's factory resolves. */
|
|
3
5
|
export interface WebReplModuleOptions {
|
|
4
|
-
/** Required. When false the module
|
|
6
|
+
/** Required. When false the module is inert: every route 404s, no subscribe. */
|
|
5
7
|
enabled: boolean;
|
|
6
|
-
/** Multi-instance coordination. Defaults to InMemoryWebReplAdapter. */
|
|
7
|
-
adapter?: WebReplAdapter;
|
|
8
8
|
/** Identifies this instance in ownership/system messages. Auto-generated if omitted. */
|
|
9
9
|
instanceId?: string;
|
|
10
10
|
/** Idle ms before a session is disposed. Default 30 min. */
|
|
@@ -13,28 +13,28 @@ export interface WebReplModuleOptions {
|
|
|
13
13
|
replayBufferSize?: number;
|
|
14
14
|
/** SSE ping-comment interval in ms. Default 15000. */
|
|
15
15
|
heartbeatInterval?: number;
|
|
16
|
-
/**
|
|
17
|
-
* How often an instance re-announces `claim` for each channel it currently
|
|
18
|
-
* owns, keeping its ownership lease alive. Default 10000.
|
|
19
|
-
*/
|
|
16
|
+
/** How often an instance re-announces `claim` for channels it owns. Default 10000. */
|
|
20
17
|
ownerHeartbeatInterval?: number;
|
|
21
18
|
/**
|
|
22
|
-
* How long an ownership record is trusted since the last claim/heartbeat
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* Default 30000. Enforced minimum: `ownerHeartbeatInterval * 2` -- a live
|
|
26
|
-
* owner must always have at least one full heartbeat interval of slack
|
|
27
|
-
* against publish/delivery jitter, or a single late-arriving heartbeat
|
|
28
|
-
* could make it look stale to a peer and get preempted. If the configured
|
|
29
|
-
* value is below that minimum, the effective lease is clamped up to
|
|
30
|
-
* `ownerHeartbeatInterval * 2` and a warning is logged (this never
|
|
31
|
-
* throws).
|
|
19
|
+
* How long an ownership record is trusted since the last claim/heartbeat.
|
|
20
|
+
* Default 30000. Clamped to at least `ownerHeartbeatInterval * 2` (see
|
|
21
|
+
* WebReplService's constructor).
|
|
32
22
|
*/
|
|
33
23
|
ownerLeaseTtl?: number;
|
|
34
|
-
/** When false, the default controller is not registered (user registers a subclass). Default true. */
|
|
35
|
-
registerController?: boolean;
|
|
36
24
|
}
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
/** Adapter configured as a DI-capable provider (or a ready instance). */
|
|
26
|
+
export type WebReplAdapterConfig = WebReplAdapter | Type<WebReplAdapter> | {
|
|
27
|
+
useClass: Type<WebReplAdapter>;
|
|
28
|
+
imports?: ModuleMetadata['imports'];
|
|
29
|
+
} | {
|
|
30
|
+
useFactory: (...args: any[]) => WebReplAdapter | Promise<WebReplAdapter>;
|
|
39
31
|
inject?: any[];
|
|
32
|
+
imports?: ModuleMetadata['imports'];
|
|
33
|
+
};
|
|
34
|
+
/** Static composition — passed synchronously to both register and registerAsync. */
|
|
35
|
+
export interface WebReplModuleExtras {
|
|
36
|
+
/** Bring your own controller (extend WebReplController + add guards). Default: built-in. */
|
|
37
|
+
controller?: Type<WebReplController>;
|
|
38
|
+
/** Multi-instance coordination. Default: InMemoryWebReplAdapter. */
|
|
39
|
+
adapter?: WebReplAdapterConfig;
|
|
40
40
|
}
|
|
@@ -51,11 +51,9 @@ let WebReplController = class WebReplController {
|
|
|
51
51
|
this.options = options;
|
|
52
52
|
}
|
|
53
53
|
async dispatch(channel, body) {
|
|
54
|
-
// CRITICAL:
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// request time so a disabled async module 404s on every route, just
|
|
58
|
-
// like the sync forRoot(enabled: false) path that registers nothing.
|
|
54
|
+
// CRITICAL: register/registerAsync always register this controller;
|
|
55
|
+
// `enabled` is enforced at runtime so a disabled module 404s on every
|
|
56
|
+
// route.
|
|
59
57
|
if (!this.options.enabled)
|
|
60
58
|
throw new common_1.NotFoundException();
|
|
61
59
|
if (typeof body?.command !== 'string' || body.command.trim().length === 0) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type DynamicModule, type Provider } from '@nestjs/common';
|
|
2
|
+
import type { WebReplModuleOptions, WebReplModuleExtras, WebReplAdapterConfig } from './interfaces/web-repl-options.interface';
|
|
3
|
+
type BuiltAdapter = {
|
|
4
|
+
provider: Provider;
|
|
5
|
+
imports: NonNullable<DynamicModule['imports']>;
|
|
6
|
+
};
|
|
7
|
+
/** Maps a WebReplAdapterConfig to the WEB_REPL_ADAPTER provider + any imports. */
|
|
8
|
+
export declare function buildAdapterProvider(config: WebReplAdapterConfig | undefined): BuiltAdapter;
|
|
9
|
+
export declare const ConfigurableModuleClass: import("@nestjs/common").ConfigurableModuleCls<WebReplModuleOptions, "register", "create", WebReplModuleExtras>, MODULE_OPTIONS_TOKEN: string | symbol;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MODULE_OPTIONS_TOKEN = exports.ConfigurableModuleClass = void 0;
|
|
4
|
+
exports.buildAdapterProvider = buildAdapterProvider;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const core_1 = require("@nestjs/core");
|
|
7
|
+
const constants_1 = require("./constants");
|
|
8
|
+
const in_memory_web_repl_adapter_1 = require("./adapters/in-memory-web-repl.adapter");
|
|
9
|
+
const web_repl_service_1 = require("./web-repl.service");
|
|
10
|
+
const web_repl_controller_1 = require("./web-repl.controller");
|
|
11
|
+
const build_repl_context_1 = require("./context/build-repl-context");
|
|
12
|
+
const CONTEXT_FACTORY = 'WEB_REPL_CONTEXT_FACTORY';
|
|
13
|
+
/** Maps a WebReplAdapterConfig to the WEB_REPL_ADAPTER provider + any imports. */
|
|
14
|
+
function buildAdapterProvider(config) {
|
|
15
|
+
if (config === undefined) {
|
|
16
|
+
return {
|
|
17
|
+
provider: { provide: constants_1.WEB_REPL_ADAPTER, useClass: in_memory_web_repl_adapter_1.InMemoryWebReplAdapter },
|
|
18
|
+
imports: [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (typeof config === 'function') {
|
|
22
|
+
return { provider: { provide: constants_1.WEB_REPL_ADAPTER, useClass: config }, imports: [] };
|
|
23
|
+
}
|
|
24
|
+
if ('useClass' in config) {
|
|
25
|
+
return {
|
|
26
|
+
provider: { provide: constants_1.WEB_REPL_ADAPTER, useClass: config.useClass },
|
|
27
|
+
imports: config.imports ?? [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if ('useFactory' in config) {
|
|
31
|
+
return {
|
|
32
|
+
provider: {
|
|
33
|
+
provide: constants_1.WEB_REPL_ADAPTER,
|
|
34
|
+
useFactory: config.useFactory,
|
|
35
|
+
inject: config.inject ?? [],
|
|
36
|
+
},
|
|
37
|
+
imports: config.imports ?? [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Otherwise it must be a ready-made WebReplAdapter instance — verify it
|
|
41
|
+
// actually looks like one so a malformed block (e.g. a typo'd `usClass`)
|
|
42
|
+
// fails fast at registration instead of silently becoming a useValue.
|
|
43
|
+
if (typeof config !== 'object' ||
|
|
44
|
+
config === null ||
|
|
45
|
+
typeof config.publish !== 'function' ||
|
|
46
|
+
typeof config.subscribe !== 'function') {
|
|
47
|
+
throw new Error('WebReplModule: invalid adapter config — expected a WebReplAdapter instance, a Type, or a { useClass } / { useFactory } block');
|
|
48
|
+
}
|
|
49
|
+
return { provider: { provide: constants_1.WEB_REPL_ADAPTER, useValue: config }, imports: [] };
|
|
50
|
+
}
|
|
51
|
+
// The builder generates its own options token (MODULE_OPTIONS_TOKEN). We keep
|
|
52
|
+
// the stable WEB_REPL_OPTIONS token for WebReplService/WebReplController to
|
|
53
|
+
// inject, aliasing it to the builder's token. The transform closure runs at
|
|
54
|
+
// register()-time (after this module is fully evaluated), so it reads
|
|
55
|
+
// `optionsToken`, assigned right after build() below.
|
|
56
|
+
let optionsToken;
|
|
57
|
+
const definition = new common_1.ConfigurableModuleBuilder()
|
|
58
|
+
.setClassMethodName('register')
|
|
59
|
+
.setExtras({ controller: undefined, adapter: undefined }, (def, extras) => {
|
|
60
|
+
const adapter = buildAdapterProvider(extras.adapter);
|
|
61
|
+
return {
|
|
62
|
+
...def,
|
|
63
|
+
imports: [...(def.imports ?? []), ...adapter.imports],
|
|
64
|
+
controllers: [extras.controller ?? web_repl_controller_1.WebReplController],
|
|
65
|
+
providers: [
|
|
66
|
+
...(def.providers ?? []),
|
|
67
|
+
{ provide: constants_1.WEB_REPL_OPTIONS, useExisting: optionsToken },
|
|
68
|
+
adapter.provider,
|
|
69
|
+
{
|
|
70
|
+
provide: CONTEXT_FACTORY,
|
|
71
|
+
useFactory: (moduleRef) => () => (0, build_repl_context_1.buildReplContext)(moduleRef),
|
|
72
|
+
inject: [core_1.ModuleRef],
|
|
73
|
+
},
|
|
74
|
+
{ provide: constants_1.WEB_REPL_CLOCK, useValue: () => Date.now() },
|
|
75
|
+
web_repl_service_1.WebReplService,
|
|
76
|
+
],
|
|
77
|
+
exports: [web_repl_service_1.WebReplService, constants_1.WEB_REPL_OPTIONS],
|
|
78
|
+
};
|
|
79
|
+
})
|
|
80
|
+
.build();
|
|
81
|
+
exports.ConfigurableModuleClass = definition.ConfigurableModuleClass, exports.MODULE_OPTIONS_TOKEN = definition.MODULE_OPTIONS_TOKEN;
|
|
82
|
+
optionsToken = exports.MODULE_OPTIONS_TOKEN;
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export declare class WebReplModule {
|
|
4
|
-
static forRoot(options: WebReplModuleOptions): DynamicModule;
|
|
5
|
-
static forRootAsync(async: WebReplModuleAsyncOptions): DynamicModule;
|
|
6
|
-
private static assemble;
|
|
7
|
-
private static sharedProviders;
|
|
1
|
+
import { ConfigurableModuleClass } from './web-repl.module-definition';
|
|
2
|
+
export declare class WebReplModule extends ConfigurableModuleClass {
|
|
8
3
|
}
|
package/dist/web-repl.module.js
CHANGED
|
@@ -5,78 +5,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
6
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
7
|
};
|
|
8
|
-
var WebReplModule_1;
|
|
9
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
9
|
exports.WebReplModule = void 0;
|
|
11
10
|
const common_1 = require("@nestjs/common");
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const in_memory_web_repl_adapter_1 = require("./adapters/in-memory-web-repl.adapter");
|
|
15
|
-
const web_repl_service_1 = require("./web-repl.service");
|
|
16
|
-
const web_repl_controller_1 = require("./web-repl.controller");
|
|
17
|
-
const build_repl_context_1 = require("./context/build-repl-context");
|
|
18
|
-
const CONTEXT_FACTORY = 'WEB_REPL_CONTEXT_FACTORY';
|
|
19
|
-
let WebReplModule = WebReplModule_1 = class WebReplModule {
|
|
20
|
-
static forRoot(options) {
|
|
21
|
-
if (!options.enabled)
|
|
22
|
-
return { module: WebReplModule_1 };
|
|
23
|
-
return this.assemble([{ provide: constants_1.WEB_REPL_OPTIONS, useValue: options }], options);
|
|
24
|
-
}
|
|
25
|
-
static forRootAsync(async) {
|
|
26
|
-
const optionsProvider = {
|
|
27
|
-
provide: constants_1.WEB_REPL_OPTIONS,
|
|
28
|
-
useFactory: async.useFactory,
|
|
29
|
-
inject: async.inject ?? [],
|
|
30
|
-
};
|
|
31
|
-
// registerController is read from resolved options only at request time
|
|
32
|
-
// in the sense that this module has no way to inspect the value
|
|
33
|
-
// useFactory will eventually produce until it runs -- and providers /
|
|
34
|
-
// controllers must be declared statically. So forRootAsync always
|
|
35
|
-
// registers the default controller; callers who need to omit it should
|
|
36
|
-
// use forRoot() with a statically-known `registerController: false`.
|
|
37
|
-
return {
|
|
38
|
-
module: WebReplModule_1,
|
|
39
|
-
imports: async.imports ?? [],
|
|
40
|
-
providers: [optionsProvider, ...this.sharedProviders()],
|
|
41
|
-
controllers: [web_repl_controller_1.WebReplController],
|
|
42
|
-
// WEB_REPL_OPTIONS must be exported alongside WebReplService: since
|
|
43
|
-
// WebReplController (and any subclass of it, per the README's
|
|
44
|
-
// "Securing it" pattern) now injects WEB_REPL_OPTIONS to enforce
|
|
45
|
-
// `enabled` at runtime (see CRITICAL 1), a caller who mounts their
|
|
46
|
-
// own guarded subclass controller on a SIBLING module (with
|
|
47
|
-
// registerController: false) needs to be able to resolve it too.
|
|
48
|
-
exports: [web_repl_service_1.WebReplService, constants_1.WEB_REPL_OPTIONS],
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
static assemble(optionProviders, options) {
|
|
52
|
-
return {
|
|
53
|
-
module: WebReplModule_1,
|
|
54
|
-
providers: [...optionProviders, ...this.sharedProviders()],
|
|
55
|
-
controllers: options.registerController === false ? [] : [web_repl_controller_1.WebReplController],
|
|
56
|
-
exports: [web_repl_service_1.WebReplService, constants_1.WEB_REPL_OPTIONS],
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
static sharedProviders() {
|
|
60
|
-
return [
|
|
61
|
-
{
|
|
62
|
-
provide: constants_1.WEB_REPL_ADAPTER,
|
|
63
|
-
useFactory: (opts) => opts.adapter ?? new in_memory_web_repl_adapter_1.InMemoryWebReplAdapter(),
|
|
64
|
-
inject: [constants_1.WEB_REPL_OPTIONS],
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
provide: CONTEXT_FACTORY,
|
|
68
|
-
useFactory: (moduleRef) => () => (0, build_repl_context_1.buildReplContext)(moduleRef),
|
|
69
|
-
inject: [core_1.ModuleRef],
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
provide: constants_1.WEB_REPL_CLOCK,
|
|
73
|
-
useValue: () => Date.now(),
|
|
74
|
-
},
|
|
75
|
-
web_repl_service_1.WebReplService,
|
|
76
|
-
];
|
|
77
|
-
}
|
|
11
|
+
const web_repl_module_definition_1 = require("./web-repl.module-definition");
|
|
12
|
+
let WebReplModule = class WebReplModule extends web_repl_module_definition_1.ConfigurableModuleClass {
|
|
78
13
|
};
|
|
79
14
|
exports.WebReplModule = WebReplModule;
|
|
80
|
-
exports.WebReplModule = WebReplModule =
|
|
15
|
+
exports.WebReplModule = WebReplModule = __decorate([
|
|
81
16
|
(0, common_1.Module)({})
|
|
82
17
|
], WebReplModule);
|
package/dist/web-repl.service.js
CHANGED
|
@@ -64,12 +64,12 @@ let WebReplService = class WebReplService {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
async onModuleInit() {
|
|
67
|
-
// CRITICAL: `enabled` is the product's only safety rail.
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
67
|
+
// CRITICAL: `enabled` is the product's only safety rail. Both
|
|
68
|
+
// register() and registerAsync() always register this
|
|
69
|
+
// service/controller (providers/controllers must be declared
|
|
70
|
+
// statically) -- so this runtime check is what keeps a config-driven
|
|
71
|
+
// `enabled: false` (e.g. from registerAsync) from silently subscribing
|
|
72
|
+
// to the command bus and executing arbitrary code.
|
|
73
73
|
if (!this.enabled)
|
|
74
74
|
return;
|
|
75
75
|
await this.adapter.subscribe(constants_1.TOPICS.cmd, (m) => this.onCmd(this.parse(m)));
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -41,7 +41,7 @@ import { WebReplModule } from 'nestjs-web-repl';
|
|
|
41
41
|
|
|
42
42
|
@Module({
|
|
43
43
|
imports: [
|
|
44
|
-
WebReplModule.
|
|
44
|
+
WebReplModule.register({
|
|
45
45
|
enabled: process.env.REPL_ENABLED === 'true',
|
|
46
46
|
}),
|
|
47
47
|
],
|
|
@@ -52,7 +52,7 @@ export class AppModule {}
|
|
|
52
52
|
Config-driven `enabled` (async form):
|
|
53
53
|
|
|
54
54
|
```ts
|
|
55
|
-
WebReplModule.
|
|
55
|
+
WebReplModule.registerAsync({
|
|
56
56
|
imports: [ConfigModule],
|
|
57
57
|
inject: [ConfigService],
|
|
58
58
|
useFactory: (config: ConfigService) => ({
|
|
@@ -85,12 +85,17 @@ Open `http://localhost:3000/repl/main/ui` in a browser for the interactive UI.
|
|
|
85
85
|
|
|
86
86
|
## When you need more
|
|
87
87
|
|
|
88
|
+
- **Authentication:** subclass `WebReplController`, decorate it with
|
|
89
|
+
`@UseGuards(...)`, and pass it as `controller:` to `register`/`registerAsync`.
|
|
90
|
+
See "Securing it" in the package README.
|
|
88
91
|
- **Multiple app instances** (load-balanced replicas): the default in-memory
|
|
89
|
-
adapter is per-process; supply a shared adapter
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
adapter is per-process; supply a shared adapter via the `adapter` extra
|
|
93
|
+
(a ready instance, `{ useClass, imports }`, or `{ useFactory, inject, imports }`)
|
|
94
|
+
so a command and its output reach the owning instance. See "Adapter /
|
|
95
|
+
multi-instance" in the package README.
|
|
92
96
|
- **Custom adapter:** implement the adapter interface (publish/subscribe over
|
|
93
|
-
the message topics). See "Adapter /
|
|
97
|
+
the message topics) and pass it via the `adapter` extra. See "Adapter /
|
|
98
|
+
multi-instance" in the README.
|
|
94
99
|
- **Options** (base path, TTLs, heartbeats) and **exported symbols:** see
|
|
95
100
|
"Options" and "Exports" in the README.
|
|
96
101
|
- **Runnable example gotcha:** run examples with `ts-node`, not `tsx` —
|