@c9up/ream 0.2.4 → 0.2.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 (112) hide show
  1. package/README.md +3 -9
  2. package/dist/Application.d.ts +85 -5
  3. package/dist/Application.d.ts.map +1 -1
  4. package/dist/Application.js +171 -21
  5. package/dist/Application.js.map +1 -1
  6. package/dist/ErrorBoundary.d.ts +1 -7
  7. package/dist/ErrorBoundary.d.ts.map +1 -1
  8. package/dist/ErrorBoundary.js +31 -31
  9. package/dist/ErrorBoundary.js.map +1 -1
  10. package/dist/HttpKernel.js +2 -2
  11. package/dist/HttpKernel.js.map +1 -1
  12. package/dist/Ignitor.d.ts +18 -49
  13. package/dist/Ignitor.d.ts.map +1 -1
  14. package/dist/Ignitor.js +211 -173
  15. package/dist/Ignitor.js.map +1 -1
  16. package/dist/env/Env.d.ts +6 -0
  17. package/dist/env/Env.d.ts.map +1 -1
  18. package/dist/env/Env.js +6 -0
  19. package/dist/env/Env.js.map +1 -1
  20. package/dist/events/Emitter.d.ts +0 -6
  21. package/dist/events/Emitter.d.ts.map +1 -1
  22. package/dist/events/Emitter.js +42 -42
  23. package/dist/events/Emitter.js.map +1 -1
  24. package/dist/events/native.d.ts +13 -11
  25. package/dist/events/native.d.ts.map +1 -1
  26. package/dist/events/native.js.map +1 -1
  27. package/dist/helpers/compose.d.ts +30 -0
  28. package/dist/helpers/compose.d.ts.map +1 -0
  29. package/dist/helpers/compose.js +27 -0
  30. package/dist/helpers/compose.js.map +1 -0
  31. package/dist/helpers/index.d.ts +1 -0
  32. package/dist/helpers/index.d.ts.map +1 -1
  33. package/dist/helpers/index.js +1 -0
  34. package/dist/helpers/index.js.map +1 -1
  35. package/dist/helpers/is.d.ts +69 -16
  36. package/dist/helpers/is.d.ts.map +1 -1
  37. package/dist/helpers/is.js +79 -16
  38. package/dist/helpers/is.js.map +1 -1
  39. package/dist/index.d.ts +4 -3
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -3
  42. package/dist/index.js.map +1 -1
  43. package/dist/middleware/Pipeline.d.ts +9 -3
  44. package/dist/middleware/Pipeline.d.ts.map +1 -1
  45. package/dist/middleware/Pipeline.js +10 -4
  46. package/dist/middleware/Pipeline.js.map +1 -1
  47. package/dist/native/generated.d.ts +243 -0
  48. package/dist/native/generated.d.ts.map +1 -0
  49. package/dist/native/generated.js +7 -0
  50. package/dist/native/generated.js.map +1 -0
  51. package/dist/rpc/RpcRouter.js +2 -2
  52. package/dist/rpc/RpcRouter.js.map +1 -1
  53. package/dist/services/app.d.ts +6 -0
  54. package/dist/services/app.d.ts.map +1 -1
  55. package/dist/services/app.js +8 -0
  56. package/dist/services/app.js.map +1 -1
  57. package/dist/services/config.d.ts +15 -0
  58. package/dist/services/config.d.ts.map +1 -0
  59. package/dist/services/config.js +18 -0
  60. package/dist/services/config.js.map +1 -0
  61. package/dist/services/encryption.d.ts +23 -0
  62. package/dist/services/encryption.d.ts.map +1 -0
  63. package/dist/services/encryption.js +33 -0
  64. package/dist/services/encryption.js.map +1 -0
  65. package/dist/services/urlBuilder.d.ts +21 -0
  66. package/dist/services/urlBuilder.d.ts.map +1 -0
  67. package/dist/services/urlBuilder.js +34 -0
  68. package/dist/services/urlBuilder.js.map +1 -0
  69. package/dist/session/drivers/CookieDriver.d.ts +1 -1
  70. package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
  71. package/dist/session/drivers/CookieDriver.js +4 -4
  72. package/dist/session/drivers/CookieDriver.js.map +1 -1
  73. package/dist/testing/ApiResponse.d.ts +44 -44
  74. package/dist/testing/ApiResponse.d.ts.map +1 -1
  75. package/dist/testing/ApiResponse.js +48 -48
  76. package/dist/testing/ApiResponse.js.map +1 -1
  77. package/dist/testing/RequestBuilder.d.ts +39 -39
  78. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  79. package/dist/testing/RequestBuilder.js +49 -49
  80. package/dist/testing/RequestBuilder.js.map +1 -1
  81. package/dist/testing/TestClient.d.ts +5 -5
  82. package/dist/testing/TestClient.d.ts.map +1 -1
  83. package/dist/testing/TestClient.js +7 -7
  84. package/dist/testing/TestClient.js.map +1 -1
  85. package/events.win32-x64-msvc.node +0 -0
  86. package/index.win32-x64-msvc.node +0 -0
  87. package/package.json +15 -2
  88. package/scheduler.win32-x64-msvc.node +0 -0
  89. package/scripts/build-napi-types.mjs +68 -0
  90. package/scripts/generate-napi-types.mjs +171 -0
  91. package/src/Application.ts +221 -21
  92. package/src/ErrorBoundary.ts +31 -31
  93. package/src/HttpKernel.ts +2 -2
  94. package/src/Ignitor.ts +228 -176
  95. package/src/env/Env.ts +6 -0
  96. package/src/events/Emitter.ts +42 -42
  97. package/src/events/native.ts +21 -14
  98. package/src/helpers/compose.ts +84 -0
  99. package/src/helpers/index.ts +1 -0
  100. package/src/helpers/is.ts +87 -16
  101. package/src/index.ts +3 -3
  102. package/src/middleware/Pipeline.ts +10 -4
  103. package/src/native/generated.ts +291 -0
  104. package/src/rpc/RpcRouter.ts +2 -2
  105. package/src/services/app.ts +9 -0
  106. package/src/services/config.ts +24 -0
  107. package/src/services/encryption.ts +42 -0
  108. package/src/services/urlBuilder.ts +41 -0
  109. package/src/session/drivers/CookieDriver.ts +4 -4
  110. package/src/testing/ApiResponse.ts +52 -52
  111. package/src/testing/RequestBuilder.ts +55 -55
  112. package/src/testing/TestClient.ts +8 -8
package/dist/Ignitor.js CHANGED
@@ -31,11 +31,31 @@ import { CookieSigner } from './security/CookieSigner.js';
31
31
  import { SignedUrl } from './security/SignedUrl.js';
32
32
  import { Server } from './server/Server.js';
33
33
  import { clearApp, setApp } from './services/app.js';
34
+ import { clearEncryption, setEncryption } from './services/encryption.js';
34
35
  import { clearRouter, setRouter } from './services/router.js';
35
36
  import { clearServer, setServer } from './services/server.js';
36
37
  import { registerSessionTemplateTags } from './session/templateTags.js';
38
+ /**
39
+ * Every option the `tests` block accepts.
40
+ *
41
+ * Kept beside the interface because it is the same list: TypeScript's
42
+ * excess-property check only fires on an object literal, so a key that reached
43
+ * `defineConfig` through a variable or a spread is invisible to it.
44
+ */
45
+ const TESTS_OPTIONS = ['bootstrap', 'forceExit', 'suites', 'timeout'];
37
46
  /** defineConfig helper — like AdonisJS defineConfig(). */
38
47
  export function defineConfig(config) {
48
+ // A key nobody reads is worse than a rejected one: an option that was
49
+ // renamed, removed between versions, or simply mistyped sits in the rc file
50
+ // doing nothing, and the run behaves as if it had never been written. Say so
51
+ // instead, and list what the block does accept — which is also the answer to
52
+ // "then where did my option go".
53
+ const unknown = Object.keys(config.tests ?? {}).filter((key) => !TESTS_OPTIONS.includes(key));
54
+ if (unknown.length > 0) {
55
+ throw new ReamError('E_REAMRC_UNKNOWN_OPTION', `\`tests\` has no option named ${unknown.map((k) => `\`${k}\``).join(', ')}.`, {
56
+ hint: `The \`tests\` block accepts: ${TESTS_OPTIONS.join(', ')}. An option removed between versions is gone from the rc file too — the runner is configured in the bootstrap module now.`,
57
+ });
58
+ }
39
59
  return config;
40
60
  }
41
61
  /**
@@ -44,30 +64,35 @@ export function defineConfig(config) {
44
64
  * Lifecycle: register → boot → start → ready → shutdown
45
65
  */
46
66
  export class Ignitor {
47
- app;
48
- router;
49
- server;
50
- middleware;
67
+ #app;
68
+ #router;
69
+ #server;
70
+ #middleware;
51
71
  /** Where each data package registers its migration runner. See `MigrationRegistry`. */
52
- migrations;
53
- errorBoundary;
54
- _httpServer;
55
- config;
56
- appRoot;
57
- environment = 'unknown';
58
- reamrc;
59
- providers = [];
60
- errorListeners = [];
61
- phase = 'created';
62
- hotReloadCleanup;
72
+ #migrations;
73
+ #errorBoundary;
74
+ /**
75
+ * The APP_KEY-backed signer, when there is one. Held so `stop()` can release
76
+ * the `services/encryption` locator against the exact instance it installed
77
+ * — an unguarded clear would tear down a newer Ignitor's binding.
78
+ */
79
+ #signer;
80
+ #_httpServer;
81
+ #config;
82
+ #appRoot;
83
+ #reamrc;
84
+ #providers = [];
85
+ #errorListeners = [];
86
+ #phase = 'created';
87
+ #hotReloadCleanup;
63
88
  #shutdownHandle;
64
89
  #host;
65
90
  #console;
66
91
  // Inline configuration (for simple use or testing)
67
- inlineRoutes;
68
- inlineMiddleware = [];
69
- inlineNamedMiddleware = [];
70
- inlineProviderFactories = [];
92
+ #inlineRoutes;
93
+ #inlineMiddleware = [];
94
+ #inlineNamedMiddleware = [];
95
+ #inlineProviderFactories = [];
71
96
  /**
72
97
  * Create the Ignitor.
73
98
  *
@@ -79,46 +104,46 @@ export class Ignitor {
79
104
  */
80
105
  constructor(appRootOrConfig, config) {
81
106
  if (appRootOrConfig instanceof URL) {
82
- this.appRoot = appRootOrConfig;
83
- this.config = config ?? {};
107
+ this.#appRoot = appRootOrConfig;
108
+ this.#config = config ?? {};
84
109
  }
85
110
  else {
86
- this.config = appRootOrConfig ?? {};
111
+ this.#config = appRootOrConfig ?? {};
87
112
  }
88
- this.app = new Application();
89
- this.router = new Router();
90
- this.server = new Server(this.router);
91
- this.middleware = new MiddlewareRegistry();
92
- this.migrations = new MigrationRegistry();
93
- this.errorBoundary = new ErrorBoundary((event) => this.handleError(event), this.isDevMode());
113
+ this.#app = new Application();
114
+ this.#router = new Router();
115
+ this.#server = new Server(this.#router);
116
+ this.#middleware = new MiddlewareRegistry();
117
+ this.#migrations = new MigrationRegistry();
118
+ this.#errorBoundary = new ErrorBoundary((event) => this.#handleError(event), this.isDevMode());
94
119
  // Register framework services in container
95
- this.app.container.singleton('router', () => this.router);
96
- this.app.container.singleton('server', () => this.server);
97
- this.app.container.singleton('middleware', () => this.middleware);
120
+ this.#app.container.singleton('router', () => this.#router);
121
+ this.#app.container.singleton('server', () => this.#server);
122
+ this.#app.container.singleton('middleware', () => this.#middleware);
98
123
  // Always bound, even with no data package: an EMPTY registry is what lets
99
124
  // the CLI tell "this app has no migrations" from "this app predates the
100
125
  // registry" and say something useful in each case.
101
- this.app.container.singleton('migrations', () => this.migrations);
102
- this.app.container.singleton('app', () => this.app);
126
+ this.#app.container.singleton('migrations', () => this.#migrations);
127
+ this.#app.container.singleton('app', () => this.#app);
103
128
  // AdonisJS binds the config store too, so code that only needs settings can
104
129
  // resolve `config` instead of taking the whole application.
105
- this.app.container.singleton('config', () => this.app.config);
130
+ this.#app.container.singleton('config', () => this.#app.config);
106
131
  // The HttpContext CLASS, so an integration provider can extend it the way
107
132
  // AdonisJS does — `HttpContext.getter('view', …)` is how the view layer
108
133
  // attaches `ctx.view`. Exported from the barrel too, but a provider that
109
134
  // must stay agnostic reaches it through the container instead of importing
110
135
  // ream at runtime.
111
- this.app.container.singleton('HttpContext', () => HttpContext);
136
+ this.#app.container.singleton('HttpContext', () => HttpContext);
112
137
  // `appRoot` is the URL passed to `new Ignitor(new URL('../', import.meta.url))`.
113
138
  // Providers resolve it through the container so they can interpret
114
139
  // relative paths in config files (e.g. `pages.root: './resources/pages'`)
115
140
  // against the project root — same convention `modules.path` uses.
116
- if (this.appRoot) {
117
- const root = this.appRoot;
118
- this.app.container.singleton('appRoot', () => root);
141
+ if (this.#appRoot) {
142
+ const root = this.#appRoot;
143
+ this.#app.container.singleton('appRoot', () => root);
119
144
  // Also expose it on the Application for the AdonisJS-style path helpers
120
145
  // (`app.makePath`, `app.configPath`, `app.migrationsPath`, …).
121
- this.app.setAppRoot(root);
146
+ this.#app.setAppRoot(root);
122
147
  }
123
148
  // Encryption / cookie-signing service (AdonisJS `APP_KEY` idiom). Registered
124
149
  // only when APP_KEY is set; HttpContext hands it to Response/Request so
@@ -130,17 +155,19 @@ export class Ignitor {
130
155
  throw new Error('APP_KEY is too short — use at least a 16-character (ideally 32-byte) random key for cookie signing/encryption.');
131
156
  }
132
157
  const signer = new CookieSigner(appKey);
133
- this.app.container.singleton('encryption', () => signer);
158
+ this.#app.container.singleton('encryption', () => signer);
159
+ this.#signer = signer;
160
+ setEncryption(signer);
134
161
  // Signed-URL helper (same APP_KEY): the router signs via makeSignedUrl,
135
162
  // HttpContext hands it to the request so hasValidSignature() can verify.
136
163
  const signedUrl = new SignedUrl({ secret: appKey });
137
- this.app.container.singleton('signedUrl', () => signedUrl);
138
- this.router.setSignedUrl(signedUrl);
164
+ this.#app.container.singleton('signedUrl', () => signedUrl);
165
+ this.#router.setSignedUrl(signedUrl);
139
166
  }
140
167
  // Set service singletons so route/kernel files can import them
141
- setApp(this.app);
142
- setRouter(this.router);
143
- setServer(this.server);
168
+ setApp(this.#app);
169
+ setRouter(this.#router);
170
+ setServer(this.#server);
144
171
  }
145
172
  // ─── Configuration ────────────────────────────────────────
146
173
  /**
@@ -148,93 +175,99 @@ export class Ignitor {
148
175
  * Like AdonisJS: .tap((app) => { app.booting(...) })
149
176
  */
150
177
  tap(callback) {
151
- callback(this.app);
178
+ callback(this.#app);
152
179
  return this;
153
180
  }
154
- /** Set the application environment. */
181
+ /**
182
+ * Set the application environment.
183
+ *
184
+ * Stored on the Application, which is where a provider reads it
185
+ * (`app.getEnvironment()`, the AdonisJS shape). The Ignitor keeps the setter
186
+ * because that is where the entry point declares it.
187
+ */
155
188
  setEnvironment(env) {
156
- this.environment = env;
189
+ this.#app.setEnvironment(env);
157
190
  return this;
158
191
  }
159
192
  getEnvironment() {
160
- return this.environment;
193
+ return this.#app.getEnvironment();
161
194
  }
162
195
  /**
163
196
  * Load the reamrc config (equivalent to adonisrc.ts).
164
197
  */
165
198
  useRcFile(reamrc) {
166
- this.reamrc = reamrc;
199
+ this.#reamrc = reamrc;
167
200
  // Hand the app its directory overrides now rather than at boot: a path
168
201
  // helper called from a provider's register() must already see them.
169
- this.app.rcContents(reamrc);
202
+ this.#app.rcContents(reamrc);
170
203
  return this;
171
204
  }
172
205
  // === Inline configuration (simple mode / testing) ===
173
206
  /** Define routes inline (simple mode). */
174
207
  routes(callback) {
175
- this.inlineRoutes = callback;
208
+ this.#inlineRoutes = callback;
176
209
  return this;
177
210
  }
178
211
  /** Add global middleware inline. */
179
212
  use(mw) {
180
- this.inlineMiddleware.push(mw);
213
+ this.#inlineMiddleware.push(mw);
181
214
  return this;
182
215
  }
183
216
  /** Register a named middleware inline. */
184
217
  named(name, mw) {
185
- this.inlineNamedMiddleware.push([name, mw]);
218
+ this.#inlineNamedMiddleware.push([name, mw]);
186
219
  return this;
187
220
  }
188
221
  /** Register a provider inline (for testing or simple apps). */
189
222
  provider(factory) {
190
- this.inlineProviderFactories.push(factory);
223
+ this.#inlineProviderFactories.push(factory);
191
224
  return this;
192
225
  }
193
226
  /** Listen for error events. */
194
227
  onError(listener) {
195
- this.errorListeners.push(listener);
228
+ this.#errorListeners.push(listener);
196
229
  return this;
197
230
  }
198
231
  /** Set a config value. */
199
232
  configure(key, value) {
200
- this.app.config.set(key, value);
233
+ this.#app.config.set(key, value);
201
234
  return this;
202
235
  }
203
236
  // ─── Mode selection ───────────────────────────────────────
204
237
  /** Configure for HTTP server mode. */
205
238
  httpServer() {
206
- this.environment = 'web';
239
+ this.#app.setEnvironment('web');
207
240
  return this;
208
241
  }
209
242
  /** Configure for CLI/console mode. Returns a ConsoleKernel for dispatching commands. */
210
243
  console() {
211
- this.environment = 'console';
244
+ this.#app.setEnvironment('console');
212
245
  return new ConsoleKernel(this);
213
246
  }
214
247
  /** Configure for test mode. */
215
248
  testMode() {
216
- this.environment = 'test';
249
+ this.#app.setEnvironment('test');
217
250
  return this;
218
251
  }
219
252
  // ─── Lifecycle ────────────────────────────────────────────
220
253
  async start() {
221
254
  // Double-start guard: a second start() would re-run phaseRegister and
222
255
  // instantiate + register every reamrc provider a second time.
223
- if (this.phase !== 'created') {
224
- throw new ReamError('IGNITOR_ALREADY_STARTED', `start() called while in phase '${this.phase}' — an Ignitor boots once`, { hint: 'Create a new Ignitor instance instead of restarting this one.' });
256
+ if (this.#phase !== 'created') {
257
+ throw new ReamError('IGNITOR_ALREADY_STARTED', `start() called while in phase '${this.#phase}' — an Ignitor boots once`, { hint: 'Create a new Ignitor instance instead of restarting this one.' });
225
258
  }
226
259
  this.#loadEnvironmentFiles();
227
260
  try {
228
- await this.phaseRegister();
229
- await this.phaseBoot();
230
- await this.phaseStart();
261
+ await this.#phaseRegister();
262
+ await this.#phaseBoot();
263
+ await this.#phaseStart();
231
264
  // Console documents its `consoleApp` service as available once the application has
232
265
  // booted, so the locator is installed here rather than on first use —
233
266
  // `import consoleApp from '@c9up/ream/services/console'` must not throw in a running
234
267
  // app. Only the façade is built (three small modules); loading the
235
268
  // commands themselves stays lazy, inside `consoleApp.boot()`.
236
269
  await this.consoleApp();
237
- await this.phaseReady();
270
+ await this.#phaseReady();
238
271
  }
239
272
  catch (err) {
240
273
  // A throw mid-boot (e.g. a provider ready() failing AFTER the HTTP port
@@ -261,9 +294,9 @@ export class Ignitor {
261
294
  * `process.env` win, so the shell / CI always overrides the files.
262
295
  */
263
296
  #loadEnvironmentFiles() {
264
- if (!this.appRoot)
297
+ if (!this.#appRoot)
265
298
  return;
266
- loadEnvFiles(this.appRoot, { skipEnvLocal: this.environment === 'test' });
299
+ loadEnvFiles(this.#appRoot, { skipEnvLocal: this.#app.getEnvironment() === 'test' });
267
300
  }
268
301
  /**
269
302
  * Give the template engine `@error` / `@errors` / `@inputError` /
@@ -272,11 +305,11 @@ export class Ignitor {
272
305
  * The engine is duck-typed out of the container so ream carries no dependency
273
306
  * on it, and an app with no template layer simply skips this.
274
307
  */
275
- async registerSessionTemplateTags() {
308
+ async #registerSessionTemplateTags() {
276
309
  for (const token of ['inker', 'view']) {
277
- if (!this.app.container.has(token))
310
+ if (!this.#app.container.has(token))
278
311
  continue;
279
- const binding = await this.app.container.resolve(token);
312
+ const binding = await this.#app.container.resolve(token);
280
313
  for (const candidate of [binding, Reflect.get(Object(binding), '_templates')]) {
281
314
  const register = Reflect.get(Object(candidate), 'registerTag');
282
315
  if (typeof register !== 'function')
@@ -290,91 +323,91 @@ export class Ignitor {
290
323
  }
291
324
  }
292
325
  }
293
- async phaseRegister() {
326
+ async #phaseRegister() {
294
327
  // Auto-load config/*.ts files into app.config
295
- await this.autoloadConfig();
328
+ await this.#autoloadConfig();
296
329
  // Load providers from reamrc
297
- if (this.reamrc?.providers) {
298
- for (const providerEntry of this.reamrc.providers) {
330
+ if (this.#reamrc?.providers) {
331
+ for (const providerEntry of this.#reamrc.providers) {
299
332
  const providerImport = typeof providerEntry === 'function' ? providerEntry : providerEntry.file;
300
333
  const env = typeof providerEntry === 'function' ? undefined : providerEntry.environment;
301
334
  // Skip providers not matching current environment
302
- if (env && !env.includes(this.environment))
335
+ if (env && !env.includes(this.#app.getEnvironment()))
303
336
  continue;
304
337
  const mod = await providerImport();
305
338
  const ProviderClass = mod.default;
306
- const instance = new ProviderClass(this.app);
307
- this.providers.push(instance);
308
- this.app.register(instance);
339
+ const instance = new ProviderClass(this.#app);
340
+ this.#providers.push(instance);
341
+ this.#app.register(instance);
309
342
  }
310
343
  }
311
344
  // Register inline providers
312
- for (const factory of this.inlineProviderFactories) {
313
- const instance = factory(this.app);
314
- this.providers.push(instance);
315
- this.app.register(instance);
345
+ for (const factory of this.#inlineProviderFactories) {
346
+ const instance = factory(this.#app);
347
+ this.#providers.push(instance);
348
+ this.#app.register(instance);
316
349
  }
317
- this.phase = 'registered';
350
+ this.#phase = 'registered';
318
351
  }
319
- async phaseBoot() {
320
- await this.app.boot();
321
- this.phase = 'booted';
352
+ async #phaseBoot() {
353
+ await this.#app.boot();
354
+ this.#phase = 'booted';
322
355
  }
323
- async phaseStart() {
356
+ async #phaseStart() {
324
357
  // Import preload files (routes.ts, kernel.ts, etc.)
325
- if (this.reamrc?.preloads) {
326
- for (const preloadEntry of this.reamrc.preloads) {
358
+ if (this.#reamrc?.preloads) {
359
+ for (const preloadEntry of this.#reamrc.preloads) {
327
360
  const preloadImport = typeof preloadEntry === 'function' ? preloadEntry : preloadEntry.file;
328
361
  const env = typeof preloadEntry === 'function'
329
362
  ? undefined
330
363
  : preloadEntry.environment;
331
- if (env && !env.includes(this.environment))
364
+ if (env && !env.includes(this.#app.getEnvironment()))
332
365
  continue;
333
366
  await preloadImport();
334
367
  }
335
368
  }
336
369
  // Auto-load module files (routes.ts, etc.) from modules directory
337
- await this.autoloadModules();
370
+ await this.#autoloadModules();
338
371
  // Apply inline configuration
339
- for (const mw of this.inlineMiddleware) {
340
- this.middleware.use(mw);
372
+ for (const mw of this.#inlineMiddleware) {
373
+ this.#middleware.use(mw);
341
374
  }
342
- for (const [name, mw] of this.inlineNamedMiddleware) {
343
- this.middleware.register(name, mw);
375
+ for (const [name, mw] of this.#inlineNamedMiddleware) {
376
+ this.#middleware.register(name, mw);
344
377
  }
345
- if (this.inlineRoutes) {
346
- this.inlineRoutes(this.router);
378
+ if (this.#inlineRoutes) {
379
+ this.#inlineRoutes(this.#router);
347
380
  }
348
381
  // Call start() on providers
349
- for (const provider of this.providers) {
382
+ for (const provider of this.#providers) {
350
383
  await callProviderPhase(provider, 'start');
351
384
  }
352
- this.phase = 'started';
385
+ this.#phase = 'started';
353
386
  }
354
- async phaseReady() {
387
+ async #phaseReady() {
355
388
  // Boot the Server (resolves lazy error handler etc.)
356
- await this.server.boot();
389
+ await this.#server.boot();
357
390
  // Start HTTP server if in web mode
358
- if (this.environment === 'web' && this.config.serverFactory) {
391
+ if (this.#app.getEnvironment() === 'web' && this.#config.serverFactory) {
359
392
  // Build the HttpKernel with server middleware + router middleware.
360
393
  // The streaming backend is resolved lazily — the HyperServer is
361
394
  // created a few lines down, after the kernel — so the kernel
362
- // closes over the `this._httpServer` slot and reads it on every
395
+ // closes over the `this.#_httpServer` slot and reads it on every
363
396
  // request through the factory.
364
397
  const kernel = createHttpKernel({
365
- router: this.router,
366
- middleware: this.middleware,
367
- container: this.app.container,
368
- exceptionHandler: this.server.getErrorHandler() ?? new ExceptionHandler(!this.app.inProduction),
369
- serverMiddleware: this.server.getServerMiddleware(),
370
- routerMiddleware: this.router.getRouterMiddleware(),
398
+ router: this.#router,
399
+ middleware: this.#middleware,
400
+ container: this.#app.container,
401
+ exceptionHandler: this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
402
+ serverMiddleware: this.#server.getServerMiddleware(),
403
+ routerMiddleware: this.#router.getRouterMiddleware(),
371
404
  onError: (error, ctx) => {
372
- this.errorBoundary.serviceError('HttpKernel', error, ctx.id);
405
+ this.#errorBoundary.serviceError('HttpKernel', error, ctx.id);
373
406
  },
374
407
  // AdonisJS `config/app.ts` → `http: { allowMethodSpoofing: true }`.
375
- allowMethodSpoofing: this.app.config.get('app.http.allowMethodSpoofing') === true,
408
+ allowMethodSpoofing: this.#app.config.get('app.http.allowMethodSpoofing') === true,
376
409
  streamBackend: () => {
377
- const server = this._httpServer;
410
+ const server = this.#_httpServer;
378
411
  if (server &&
379
412
  typeof server.registerStream === 'function' &&
380
413
  typeof server.writeStream === 'function' &&
@@ -393,33 +426,33 @@ export class Ignitor {
393
426
  return undefined;
394
427
  },
395
428
  });
396
- const desiredPort = this.config.port ?? 3000;
429
+ const desiredPort = this.#config.port ?? 3000;
397
430
  // Port-scan fallback (+1..+19) is a DEV convenience only. In production
398
431
  // a silent drift to :3001 while the LB targets :3000 is an outage —
399
432
  // bind the configured port and let EADDRINUSE fail loudly. The probe
400
433
  // also has an inherent TOCTOU; acceptable in dev, not in prod.
401
- const availablePort = this.app.inProduction
434
+ const availablePort = this.#app.inProduction
402
435
  ? desiredPort
403
436
  : await findAvailablePort(desiredPort);
404
437
  this.#host =
405
- this.config.host ?? process.env.HOST ?? (this.app.inProduction ? '0.0.0.0' : 'localhost');
406
- this._httpServer = this.config.serverFactory(availablePort, this.#host);
407
- this._httpServer.onRequest(kernel);
438
+ this.#config.host ?? process.env.HOST ?? (this.#app.inProduction ? '0.0.0.0' : 'localhost');
439
+ this.#_httpServer = this.#config.serverFactory(availablePort, this.#host);
440
+ this.#_httpServer.onRequest(kernel);
408
441
  // Pre-resolve client IPs in Rust from the trusted-proxy CIDRs before
409
442
  // listen. Security filtering itself lives in @c9up/blackhole.
410
- const trustedProxies = this.server.getTrustedProxies();
443
+ const trustedProxies = this.#server.getTrustedProxies();
411
444
  if (trustedProxies.length > 0 &&
412
- typeof this._httpServer.configureTrustedProxies === 'function') {
413
- this._httpServer.configureTrustedProxies([...trustedProxies]);
445
+ typeof this.#_httpServer.configureTrustedProxies === 'function') {
446
+ this.#_httpServer.configureTrustedProxies([...trustedProxies]);
414
447
  }
415
- await this._httpServer.listen();
448
+ await this.#_httpServer.listen();
416
449
  // Wire OS-signal graceful shutdown. Without it the process never closes
417
450
  // the HTTP server on SIGTERM/SIGINT, so live keep-alive / SSE sockets keep
418
451
  // the event loop alive — a `ream dev` restart or Ctrl+C ends in a watcher
419
452
  // force-kill, and an orchestrator's rolling deploy drops in-flight work.
420
453
  // onShutdown = this.stop(), which closes the port (aborting connections),
421
454
  // shuts providers down (DB pools), and releases the locators.
422
- if (this.config.gracefulShutdown !== false) {
455
+ if (this.#config.gracefulShutdown !== false) {
423
456
  this.#shutdownHandle = installGracefulShutdown({
424
457
  onShutdown: () => this.stop(),
425
458
  logger: {
@@ -429,26 +462,26 @@ export class Ignitor {
429
462
  });
430
463
  }
431
464
  }
432
- else if (this.environment === 'web' && !this.config.serverFactory) {
465
+ else if (this.#app.getEnvironment() === 'web' && !this.#config.serverFactory) {
433
466
  throw new ReamError('IGNITOR_NO_SERVER_FACTORY', 'httpServer() requires a serverFactory in config', {
434
467
  hint: 'Example: new Ignitor({ serverFactory: (port, host) => new HyperServer(port, host) })',
435
468
  });
436
469
  }
437
470
  // Install error boundary
438
- this.errorBoundary.install();
471
+ this.#errorBoundary.install();
439
472
  // Publish the flash-message tags to the template engine, if one is
440
473
  // installed. After boot(), so the engine's own provider has bound it;
441
474
  // before ready(), so a provider that pre-renders finds the tags in place.
442
- await this.registerSessionTemplateTags();
475
+ await this.#registerSessionTemplateTags();
443
476
  // Call ready() on providers
444
- for (const provider of this.providers) {
477
+ for (const provider of this.#providers) {
445
478
  await callProviderPhase(provider, 'ready');
446
479
  }
447
480
  // Core domain event: the app finished booting (all providers ready).
448
481
  // Emitted once through the bus when events are wired — zero hot-path cost.
449
- if (this.app.container.has('events')) {
450
- const bus = await this.app.container.resolve('events');
451
- bus.emit('app:ready', { environment: this.environment });
482
+ if (this.#app.container.has('events')) {
483
+ const bus = await this.#app.container.resolve('events');
484
+ bus.emit('app:ready', { environment: this.#app.getEnvironment() });
452
485
  }
453
486
  // Dev-mode change watcher. IMPORTANT: this does NOT attempt an in-process
454
487
  // reload. The previous implementation cleared the router + service registry
@@ -462,11 +495,11 @@ export class Ignitor {
462
495
  // the Ignitor without a supervisor. A future true HMR needs a loader-hook
463
496
  // (hot-hook style) that can invalidate the ESM cache — plug it in here.
464
497
  if (this.isDevMode()) {
465
- const watchDirs = this.config.watchDirs ?? ['app', 'start'];
466
- this.hotReloadCleanup = startHotReload({
498
+ const watchDirs = this.#config.watchDirs ?? ['app', 'start'];
499
+ this.#hotReloadCleanup = startHotReload({
467
500
  watchDirs,
468
501
  onReload: () => {
469
- this.handleError({
502
+ this.#handleError({
470
503
  type: 'system.info',
471
504
  source: 'HotReload',
472
505
  message: 'File change detected — a process restart is required to apply it (`ream dev` restarts automatically).',
@@ -475,7 +508,7 @@ export class Ignitor {
475
508
  });
476
509
  },
477
510
  logger: {
478
- info: (msg) => this.handleError({
511
+ info: (msg) => this.#handleError({
479
512
  type: 'system.info',
480
513
  source: 'HotReload',
481
514
  message: msg,
@@ -485,19 +518,19 @@ export class Ignitor {
485
518
  },
486
519
  });
487
520
  }
488
- this.phase = 'ready';
521
+ this.#phase = 'ready';
489
522
  }
490
523
  /**
491
524
  * Auto-load config/*.ts files into app.config store.
492
525
  * Each file's default export is stored under its filename (without extension).
493
526
  * e.g. config/database.ts → app.config.get('database')
494
527
  */
495
- async autoloadConfig() {
528
+ async #autoloadConfig() {
496
529
  const { readdirSync, existsSync } = await import('node:fs');
497
530
  const { join, basename } = await import('node:path');
498
531
  const { fileURLToPath, pathToFileURL } = await import('node:url');
499
- const configDir = this.appRoot
500
- ? join(fileURLToPath(this.appRoot), 'config')
532
+ const configDir = this.#appRoot
533
+ ? join(fileURLToPath(this.#appRoot), 'config')
501
534
  : join(process.cwd(), 'config');
502
535
  if (!existsSync(configDir))
503
536
  return;
@@ -518,15 +551,15 @@ export class Ignitor {
518
551
  // is what AdonisJS does (`fsImportAll`). Spread rather than stored as-is:
519
552
  // an ESM namespace is sealed and null-prototype, so a later merge into
520
553
  // this entry would fail on it.
521
- this.app.config.set(key, 'default' in mod ? mod.default : { ...mod });
554
+ this.#app.config.set(key, 'default' in mod ? mod.default : { ...mod });
522
555
  }
523
556
  }
524
557
  /**
525
558
  * Auto-load module files (routes.ts, etc.) from the modules directory.
526
559
  * Scans modules.path for subdirectories and imports matching files.
527
560
  */
528
- async autoloadModules() {
529
- const modulesConfig = this.reamrc?.modules;
561
+ async #autoloadModules() {
562
+ const modulesConfig = this.#reamrc?.modules;
530
563
  if (!modulesConfig?.path)
531
564
  return;
532
565
  const { readdirSync, existsSync } = await import('node:fs');
@@ -534,8 +567,8 @@ export class Ignitor {
534
567
  const { fileURLToPath } = await import('node:url');
535
568
  const { pathToFileURL } = await import('node:url');
536
569
  // Resolve modules path relative to app root or cwd
537
- const basePath = this.appRoot
538
- ? join(fileURLToPath(this.appRoot), modulesConfig.path)
570
+ const basePath = this.#appRoot
571
+ ? join(fileURLToPath(this.#appRoot), modulesConfig.path)
539
572
  : resolve(modulesConfig.path);
540
573
  if (!existsSync(basePath))
541
574
  return;
@@ -592,22 +625,27 @@ export class Ignitor {
592
625
  this.#shutdownHandle = undefined;
593
626
  });
594
627
  await attempt(() => {
595
- if (this.hotReloadCleanup)
596
- this.hotReloadCleanup();
628
+ if (this.#hotReloadCleanup)
629
+ this.#hotReloadCleanup();
597
630
  });
598
631
  await attempt(async () => {
599
- if (this._httpServer)
600
- await this._httpServer.close();
632
+ if (this.#_httpServer)
633
+ await this.#_httpServer.close();
601
634
  });
602
- await attempt(() => this.errorBoundary.uninstall());
603
- await attempt(() => this.app.shutdown());
635
+ await attempt(() => this.#errorBoundary.uninstall());
636
+ await attempt(() => this.#app.shutdown());
604
637
  // Release the process-wide service locators (ownership-guarded): a stopped
605
638
  // app must not keep its container/router reachable through the module
606
639
  // singletons, and must not clobber a newer Ignitor's bindings.
607
- clearApp(this.app);
608
- clearRouter(this.router);
609
- clearServer(this.server);
610
- this.phase = 'shutdown';
640
+ clearApp(this.#app);
641
+ clearRouter(this.#router);
642
+ clearServer(this.#server);
643
+ // `signedUrl` is what the router was handed; the signer behind
644
+ // `services/encryption` is the same object, so releasing it here keeps the
645
+ // locator from outliving the app that built it.
646
+ if (this.#signer)
647
+ clearEncryption(this.#signer);
648
+ this.#phase = 'shutdown';
611
649
  if (errors.length === 1)
612
650
  throw errors[0];
613
651
  if (errors.length > 1) {
@@ -616,7 +654,7 @@ export class Ignitor {
616
654
  }
617
655
  // ─── Accessors ────────────────────────────────────────────
618
656
  async port() {
619
- return this._httpServer ? this._httpServer.port() : 0;
657
+ return this.#_httpServer ? this.#_httpServer.port() : 0;
620
658
  }
621
659
  /**
622
660
  * The address the HTTP server was bound to. Resolved at start; undefined
@@ -628,18 +666,18 @@ export class Ignitor {
628
666
  return this.#host;
629
667
  }
630
668
  getApp() {
631
- return this.app;
669
+ return this.#app;
632
670
  }
633
671
  /**
634
672
  * The application root passed to `new Ignitor(new URL('../', import.meta.url))`.
635
673
  * Undefined for inline/test ignitors created without one.
636
674
  */
637
675
  getAppRoot() {
638
- return this.appRoot;
676
+ return this.#appRoot;
639
677
  }
640
678
  /** Has the application already been started? */
641
679
  isStarted() {
642
- return this.phase === 'started' || this.phase === 'ready';
680
+ return this.#phase === 'started' || this.#phase === 'ready';
643
681
  }
644
682
  /**
645
683
  * The `consoleApp` façade — the programmatic console.
@@ -658,22 +696,22 @@ export class Ignitor {
658
696
  const consoleApp = new Console({ kernel, load });
659
697
  this.#console = consoleApp;
660
698
  setConsole(consoleApp);
661
- this.app.container.singleton('console', () => consoleApp);
699
+ this.#app.container.singleton('console', () => consoleApp);
662
700
  return consoleApp;
663
701
  }
664
702
  /** The loaded rc file, if `useRcFile()` was called. */
665
703
  getRcFile() {
666
- return this.reamrc;
704
+ return this.#reamrc;
667
705
  }
668
706
  /** The custom module importer, when the app provided one. */
669
707
  getImporter() {
670
- return this.config.importer;
708
+ return this.#config.importer;
671
709
  }
672
710
  getRouter() {
673
- return this.router;
711
+ return this.#router;
674
712
  }
675
713
  getServer() {
676
- return this.server;
714
+ return this.#server;
677
715
  }
678
716
  /**
679
717
  * Get the kernel callback (for serverless / testing). Memoized — building a
@@ -681,24 +719,24 @@ export class Ignitor {
681
719
  */
682
720
  getKernel() {
683
721
  this.#kernel ??= createHttpKernel({
684
- router: this.router,
685
- middleware: this.middleware,
686
- container: this.app.container,
687
- exceptionHandler: this.server.getErrorHandler() ?? new ExceptionHandler(!this.app.inProduction),
688
- serverMiddleware: this.server.getServerMiddleware(),
689
- routerMiddleware: this.router.getRouterMiddleware(),
722
+ router: this.#router,
723
+ middleware: this.#middleware,
724
+ container: this.#app.container,
725
+ exceptionHandler: this.#server.getErrorHandler() ?? new ExceptionHandler(!this.#app.inProduction),
726
+ serverMiddleware: this.#server.getServerMiddleware(),
727
+ routerMiddleware: this.#router.getRouterMiddleware(),
690
728
  });
691
729
  return this.#kernel;
692
730
  }
693
731
  #kernel;
694
732
  isDevMode() {
695
- return this.app.inDev;
733
+ return this.#app.inDev;
696
734
  }
697
735
  getPhase() {
698
- return this.phase;
736
+ return this.#phase;
699
737
  }
700
- handleError(event) {
701
- for (const listener of this.errorListeners) {
738
+ #handleError(event) {
739
+ for (const listener of this.#errorListeners) {
702
740
  try {
703
741
  listener(event);
704
742
  }
@@ -713,7 +751,7 @@ export class Ignitor {
713
751
  * shutdown logger so drain progress / timeouts are observable.
714
752
  */
715
753
  #emitSystemInfo(source, message, kind) {
716
- this.handleError({
754
+ this.#handleError({
717
755
  type: kind === 'error' ? 'system.error' : 'system.info',
718
756
  source,
719
757
  message,