@c9up/ream 0.2.5 → 0.2.7

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 (94) hide show
  1. package/README.md +3 -9
  2. package/dist/HttpKernel.js +2 -2
  3. package/dist/HttpKernel.js.map +1 -1
  4. package/dist/Ignitor.d.ts +3 -3
  5. package/dist/Ignitor.d.ts.map +1 -1
  6. package/dist/Ignitor.js +50 -0
  7. package/dist/Ignitor.js.map +1 -1
  8. package/dist/bootstrap/hyper-server.js.map +1 -1
  9. package/dist/helpers/compose.d.ts +30 -0
  10. package/dist/helpers/compose.d.ts.map +1 -0
  11. package/dist/helpers/compose.js +27 -0
  12. package/dist/helpers/compose.js.map +1 -0
  13. package/dist/helpers/index.d.ts +1 -0
  14. package/dist/helpers/index.d.ts.map +1 -1
  15. package/dist/helpers/index.js +1 -0
  16. package/dist/helpers/index.js.map +1 -1
  17. package/dist/helpers/is.d.ts +69 -16
  18. package/dist/helpers/is.d.ts.map +1 -1
  19. package/dist/helpers/is.js +80 -20
  20. package/dist/helpers/is.js.map +1 -1
  21. package/dist/index.d.ts +2 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +2 -2
  24. package/dist/index.js.map +1 -1
  25. package/dist/middleware/Pipeline.d.ts +9 -3
  26. package/dist/middleware/Pipeline.d.ts.map +1 -1
  27. package/dist/middleware/Pipeline.js +10 -4
  28. package/dist/middleware/Pipeline.js.map +1 -1
  29. package/dist/rpc/RpcRouter.js +2 -2
  30. package/dist/rpc/RpcRouter.js.map +1 -1
  31. package/dist/scheduler/Scheduler.d.ts.map +1 -1
  32. package/dist/scheduler/Scheduler.js +12 -4
  33. package/dist/scheduler/Scheduler.js.map +1 -1
  34. package/dist/services/app.d.ts +6 -0
  35. package/dist/services/app.d.ts.map +1 -1
  36. package/dist/services/app.js +8 -0
  37. package/dist/services/app.js.map +1 -1
  38. package/dist/services/config.d.ts +15 -0
  39. package/dist/services/config.d.ts.map +1 -0
  40. package/dist/services/config.js +18 -0
  41. package/dist/services/config.js.map +1 -0
  42. package/dist/services/encryption.d.ts +23 -0
  43. package/dist/services/encryption.d.ts.map +1 -0
  44. package/dist/services/encryption.js +33 -0
  45. package/dist/services/encryption.js.map +1 -0
  46. package/dist/services/logger.d.ts +29 -0
  47. package/dist/services/logger.d.ts.map +1 -0
  48. package/dist/services/logger.js +40 -0
  49. package/dist/services/logger.js.map +1 -0
  50. package/dist/services/urlBuilder.d.ts +21 -0
  51. package/dist/services/urlBuilder.d.ts.map +1 -0
  52. package/dist/services/urlBuilder.js +34 -0
  53. package/dist/services/urlBuilder.js.map +1 -0
  54. package/dist/session/Session.js.map +1 -1
  55. package/dist/testing/ApiResponse.d.ts +44 -44
  56. package/dist/testing/ApiResponse.d.ts.map +1 -1
  57. package/dist/testing/ApiResponse.js +48 -48
  58. package/dist/testing/ApiResponse.js.map +1 -1
  59. package/dist/testing/RequestBuilder.d.ts +39 -39
  60. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  61. package/dist/testing/RequestBuilder.js +49 -49
  62. package/dist/testing/RequestBuilder.js.map +1 -1
  63. package/dist/testing/TestClient.d.ts +23 -8
  64. package/dist/testing/TestClient.d.ts.map +1 -1
  65. package/dist/testing/TestClient.js +52 -11
  66. package/dist/testing/TestClient.js.map +1 -1
  67. package/dist/testing/testUtils.d.ts +62 -0
  68. package/dist/testing/testUtils.d.ts.map +1 -0
  69. package/dist/testing/testUtils.js +77 -0
  70. package/dist/testing/testUtils.js.map +1 -0
  71. package/events.win32-x64-msvc.node +0 -0
  72. package/index.win32-x64-msvc.node +0 -0
  73. package/package.json +21 -1
  74. package/scheduler.win32-x64-msvc.node +0 -0
  75. package/src/HttpKernel.ts +2 -2
  76. package/src/Ignitor.ts +59 -4
  77. package/src/bootstrap/hyper-server.ts +1 -1
  78. package/src/helpers/compose.ts +84 -0
  79. package/src/helpers/index.ts +1 -0
  80. package/src/helpers/is.ts +88 -22
  81. package/src/index.ts +2 -2
  82. package/src/middleware/Pipeline.ts +10 -4
  83. package/src/rpc/RpcRouter.ts +2 -2
  84. package/src/scheduler/Scheduler.ts +10 -4
  85. package/src/services/app.ts +9 -0
  86. package/src/services/config.ts +24 -0
  87. package/src/services/encryption.ts +42 -0
  88. package/src/services/logger.ts +49 -0
  89. package/src/services/urlBuilder.ts +41 -0
  90. package/src/session/Session.ts +2 -2
  91. package/src/testing/ApiResponse.ts +52 -52
  92. package/src/testing/RequestBuilder.ts +55 -55
  93. package/src/testing/TestClient.ts +55 -13
  94. package/src/testing/testUtils.ts +90 -0
@@ -20,7 +20,7 @@ import {
20
20
  } from '@c9up/comet'
21
21
  import type { HttpContext } from '../http/HttpContext.js'
22
22
  import type { MiddlewareRegistry, RuntimeValidator } from '../middleware/Pipeline.js'
23
- import { compose, runValidator } from '../middleware/Pipeline.js'
23
+ import { composeMiddleware, runValidator } from '../middleware/Pipeline.js'
24
24
 
25
25
  export type RpcHandler = (ctx: HttpContext, params: unknown) => Promise<unknown> | unknown
26
26
 
@@ -252,7 +252,7 @@ export class RpcRouter {
252
252
  }
253
253
  return mw
254
254
  })
255
- await compose(fns)(ctx, async () => {})
255
+ await composeMiddleware(fns)(ctx, async () => {})
256
256
  }
257
257
 
258
258
  async #processOne(ctx: HttpContext, request: unknown): Promise<unknown> {
@@ -252,9 +252,12 @@ export class Scheduler {
252
252
  // `.then`-based detection + `Promise.resolve` wrap so non-native
253
253
  // thenables (e.g. RxJS-style) are handled uniformly.
254
254
  if (
255
+ // Object OR function: a thenable function is exotic but legal, and the
256
+ // guard this replaced accepted one. `in` narrows either.
257
+ (typeof result === 'object' || typeof result === 'function') &&
255
258
  result !== null &&
256
- result !== undefined &&
257
- typeof (result as { then?: unknown }).then === 'function'
259
+ 'then' in result &&
260
+ typeof result.then === 'function'
258
261
  ) {
259
262
  Promise.resolve(result).catch(() => {})
260
263
  }
@@ -334,9 +337,12 @@ export class Scheduler {
334
337
  // reporter never crashes the ticker.
335
338
  const result = this.#errorReporter(err, { taskName }) as unknown
336
339
  if (
340
+ // Object OR function: a thenable function is exotic but legal, and the
341
+ // guard this replaced accepted one. `in` narrows either.
342
+ (typeof result === 'object' || typeof result === 'function') &&
337
343
  result !== null &&
338
- result !== undefined &&
339
- typeof (result as { then?: unknown }).then === 'function'
344
+ 'then' in result &&
345
+ typeof result.then === 'function'
340
346
  ) {
341
347
  Promise.resolve(result).catch(() => {})
342
348
  }
@@ -26,6 +26,15 @@ export function clearApp(app: Application): void {
26
26
  if (instance === app) instance = undefined
27
27
  }
28
28
 
29
+ /**
30
+ * @internal Get the app instance directly. Used by the accessors that stand in
31
+ * for something the app OWNS (`services/config` is `app.config`), so they read
32
+ * through here instead of keeping a second copy that a re-boot could desync.
33
+ */
34
+ export function getApp(): Application | undefined {
35
+ return instance
36
+ }
37
+
29
38
  const app: Application = createServiceProxy<Application>(
30
39
  () => instance,
31
40
  'Application accessed before initialization. ' +
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Config store singleton service.
3
+ *
4
+ * Usage:
5
+ * import config from '@c9up/ream/services/config'
6
+ * const port = config.get<number>('app.port', 3333)
7
+ *
8
+ * Unlike the other accessors this one keeps no instance of its own: the store
9
+ * IS `app.config`, so it reads through the app locator. One source of truth
10
+ * means the two can never point at different stores after a re-boot.
11
+ */
12
+
13
+ import type { ConfigStore } from '../ConfigLoader.js'
14
+ import { getApp } from './app.js'
15
+ import { createServiceProxy } from './createServiceProxy.js'
16
+
17
+ const config: ConfigStore = createServiceProxy<ConfigStore>(
18
+ () => getApp()?.config,
19
+ 'Config accessed before initialization. ' +
20
+ 'The store is built during the boot phase — read it from inside a provider, ' +
21
+ 'a preload, or a request, not at module top level.',
22
+ )
23
+
24
+ export default config
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Cookie/value signing singleton service.
3
+ *
4
+ * Usage:
5
+ * import encryption from '@c9up/ream/services/encryption'
6
+ * const signed = encryption.sign('payload')
7
+ *
8
+ * Registered only when `APP_KEY` is set — there is nothing to sign with
9
+ * otherwise — so the not-ready message names that rather than the boot phase.
10
+ */
11
+
12
+ import type { CookieSigner } from '../security/CookieSigner.js'
13
+ import { createServiceProxy } from './createServiceProxy.js'
14
+
15
+ let instance: CookieSigner | undefined
16
+
17
+ /** @internal Set the signer (called by Ignitor once APP_KEY is validated). */
18
+ export function setEncryption(signer: CookieSigner): void {
19
+ instance = signer
20
+ }
21
+
22
+ /**
23
+ * @internal Unset the locator IF it still points at `signer` (called by
24
+ * Ignitor.stop()). Ownership-guarded — see services/app.ts.
25
+ */
26
+ export function clearEncryption(signer: CookieSigner): void {
27
+ if (instance === signer) instance = undefined
28
+ }
29
+
30
+ /** @internal Get the signer directly. */
31
+ export function getEncryption(): CookieSigner | undefined {
32
+ return instance
33
+ }
34
+
35
+ const encryption: CookieSigner = createServiceProxy<CookieSigner>(
36
+ () => instance,
37
+ '[E_MISSING_APP_KEY] Encryption accessed before it was registered. ' +
38
+ 'It exists only when APP_KEY is set (>= 16 characters) — check the ' +
39
+ 'environment before reaching for it, or you are reading it before boot.',
40
+ )
41
+
42
+ export default encryption
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Application logger singleton service.
3
+ *
4
+ * Usage:
5
+ * import logger from '@c9up/ream/services/logger'
6
+ * logger.info('user signed in', { userId })
7
+ *
8
+ * ream does not implement the logger — it declares the contract
9
+ * ({@link ContextLogger}) and reads whatever a provider bound as `'logger'`.
10
+ * `@c9up/spectrum` is the implementation in this universe, the way `hash` is
11
+ * sigil's and validation is rune's.
12
+ *
13
+ * `ctx.logger` stays the right thing to use inside a request: it is
14
+ * correlation-scoped. This is for everywhere else — a provider, a scheduled
15
+ * task, a console command — where there is no request to scope to.
16
+ */
17
+
18
+ import type { ChildLoggerSource } from '../http/HttpContext.js'
19
+ import { createServiceProxy } from './createServiceProxy.js'
20
+
21
+ let instance: ChildLoggerSource | undefined
22
+
23
+ /** @internal Set the logger (called by Ignitor once providers have booted). */
24
+ export function setLogger(logger: ChildLoggerSource): void {
25
+ instance = logger
26
+ }
27
+
28
+ /**
29
+ * @internal Unset the locator IF it still points at `logger` (called by
30
+ * Ignitor.stop()). Ownership-guarded — see services/app.ts.
31
+ */
32
+ export function clearLogger(logger: ChildLoggerSource): void {
33
+ if (instance === logger) instance = undefined
34
+ }
35
+
36
+ /** @internal Get the logger directly. */
37
+ export function getLogger(): ChildLoggerSource | undefined {
38
+ return instance
39
+ }
40
+
41
+ const logger: ChildLoggerSource = createServiceProxy<ChildLoggerSource>(
42
+ () => instance,
43
+ 'Logger accessed before initialization. ' +
44
+ 'ream declares the contract but implements no logger — install one (`@c9up/spectrum`) ' +
45
+ "and register its provider, which binds it as 'logger'. Inside a request, prefer " +
46
+ '`ctx.logger`, which is scoped to the correlation id.',
47
+ )
48
+
49
+ export default logger
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Named-route URL helpers, as free functions.
3
+ *
4
+ * Usage:
5
+ * import { urlFor, signedUrlFor } from '@c9up/ream/services/urlBuilder'
6
+ * const href = urlFor('users.show', { id: '42' })
7
+ * const link = signedUrlFor('email.verify', { id: '42' }, { expiresIn: '1h' })
8
+ *
9
+ * Both delegate to the router — the same code `router.urlFor` /
10
+ * `router.makeSignedUrl` run — so a route renamed in one place cannot mean two
11
+ * things. They exist as functions rather than as another proxy object because
12
+ * that is all there is to the URL builder: two calls, no state.
13
+ *
14
+ * `signedUrlFor` needs an APP_KEY-backed signer and says so if there is none.
15
+ */
16
+
17
+ import type { SignedUrlOptions } from '../router/Router.js'
18
+ import { getRouter } from './router.js'
19
+
20
+ const NOT_READY =
21
+ 'URL builder used before initialization. ' +
22
+ 'Routes are registered during the boot phase — build URLs from inside a ' +
23
+ 'controller, a view, or a preload, not at module top level.'
24
+
25
+ /** Generate a URL from a named route, filling `:param` placeholders. */
26
+ export function urlFor(name: string, params?: Record<string, string>): string {
27
+ const router = getRouter()
28
+ if (!router) throw new Error(NOT_READY)
29
+ return router.urlFor(name, params)
30
+ }
31
+
32
+ /** Generate a tamper-proof signed URL for a named route. */
33
+ export function signedUrlFor(
34
+ name: string,
35
+ params?: Record<string, string>,
36
+ options?: SignedUrlOptions,
37
+ ): string {
38
+ const router = getRouter()
39
+ if (!router) throw new Error(NOT_READY)
40
+ return router.makeSignedUrl(name, params, options)
41
+ }
@@ -56,9 +56,9 @@ export interface SessionDriverWithTagging extends SessionDriver {
56
56
  export function supportsTagging(driver: SessionDriver): driver is SessionDriverWithTagging {
57
57
  return (
58
58
  'tag' in driver &&
59
- typeof (driver as SessionDriverWithTagging).tag === 'function' &&
59
+ typeof driver.tag === 'function' &&
60
60
  'tagged' in driver &&
61
- typeof (driver as SessionDriverWithTagging).tagged === 'function'
61
+ typeof driver.tagged === 'function'
62
62
  )
63
63
  }
64
64
 
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Rich HTTP test response — `@japa/api-client`'s `ApiResponse` parity.
2
+ * Rich HTTP test response — `helix`'s `ApiResponse` parity.
3
3
  *
4
- * Wraps the raw {@link TestResponse} and exposes the full Japa response surface:
4
+ * Wraps the raw {@link TestResponse} and exposes the full helix response surface:
5
5
  * accessor METHODS (`status()`, `headers()`, `body()`, `text()`, `header()`,
6
- * `cookies()`, `redirects()`, `error()`, …) matching Japa exactly, the complete
6
+ * `cookies()`, `redirects()`, `error()`, …) matching helix exactly, the complete
7
7
  * `assert*` family, response `dump*()` debuggers, and `macro()`/`getter()` for
8
8
  * consumer extension.
9
9
  *
10
- * `json()` is kept as an additive convenience (not in Japa, but harmless for
11
- * parity — Japa code never calls it) so `res.json()` call sites need no change.
10
+ * `json()` is kept as an additive convenience (not in helix, but harmless for
11
+ * parity — helix code never calls it) so `res.json()` call sites need no change.
12
12
  */
13
13
 
14
14
  import type { Dict } from '../types/helpers.js'
@@ -109,7 +109,7 @@ function parseCookies(setCookie: string | undefined): Record<string, ResponseCoo
109
109
  return out
110
110
  }
111
111
 
112
- /** A file parsed from a `multipart/form-data` response — Japa `files()` entry. */
112
+ /** A file parsed from a `multipart/form-data` response — helix `files()` entry. */
113
113
  export interface ResponseFile {
114
114
  /** The form field name. */
115
115
  fieldName: string
@@ -123,7 +123,7 @@ export interface ResponseFile {
123
123
  content: Buffer
124
124
  }
125
125
 
126
- /** Consumer-registered response parsers (Japa `ApiRequest.addParser`), by content-type. */
126
+ /** Consumer-registered response parsers (helix `ApiRequest.addParser`), by content-type. */
127
127
  const responseParsers = new Map<string, (body: string, headers: Dict) => unknown>()
128
128
 
129
129
  /**
@@ -188,8 +188,8 @@ function splitBuffer(buf: Buffer, delim: Buffer): Buffer[] {
188
188
  }
189
189
 
190
190
  /**
191
- * Validator for `assertAgainstApiSpec()` — mirrors Japa's OpenAPI plugin, which
192
- * is a SEPARATE opt-in (`@japa/openapi-assertions`). Register one to enable spec
191
+ * Validator for `assertAgainstApiSpec()` — mirrors helix's OpenAPI plugin, which
192
+ * is a SEPARATE opt-in (`a separate OpenAPI-assertions plugin`). Register one to enable spec
193
193
  * assertions; without it, the assertion throws a clear "not configured" error
194
194
  * rather than silently passing.
195
195
  */
@@ -206,7 +206,7 @@ function jsonOf(raw: TestResponse): unknown {
206
206
  }
207
207
  }
208
208
 
209
- /** The Japa error object returned by `response.error()`. */
209
+ /** The helix error object returned by `response.error()`. */
210
210
  export interface ResponseError {
211
211
  /** The error status code (>= 400). */
212
212
  status: number
@@ -216,14 +216,14 @@ export interface ResponseError {
216
216
 
217
217
  /** Extra context the builder threads into the response. */
218
218
  export interface ApiResponseMeta {
219
- /** The (final) request method — Japa `response.method()`. */
219
+ /** The (final) request method — helix `response.method()`. */
220
220
  method?: string
221
- /** URLs followed when redirects were configured — Japa `response.redirects()`. */
221
+ /** URLs followed when redirects were configured — helix `response.redirects()`. */
222
222
  redirects?: string[]
223
223
  }
224
224
 
225
225
  /**
226
- * Every Japa status-shortcut assertion, as `methodName → code`. Consumed to
226
+ * Every helix status-shortcut assertion, as `methodName → code`. Consumed to
227
227
  * generate the `assert*` methods on both {@link ApiResponse} and the builder,
228
228
  * so the two surfaces never drift.
229
229
  */
@@ -254,21 +254,21 @@ export const STATUS_ASSERTIONS = {
254
254
  assertUnprocessableEntity: 422,
255
255
  assertLocked: 423,
256
256
  assertTooManyRequests: 429,
257
- /** ream extra (not in Japa's list, kept for convenience). */
257
+ /** ream extra (not in helix's list, kept for convenience). */
258
258
  assertInternalServerError: 500,
259
259
  } as const
260
260
 
261
- /** Consumer-registered macros/getters (Japa `ApiResponse.macro`/`.getter`). */
261
+ /** Consumer-registered macros/getters (helix `ApiResponse.macro`/`.getter`). */
262
262
  const responseMacros = new Map<string, unknown>()
263
263
  const responseGetters = new Map<string, (this: ApiResponse, res: ApiResponse) => unknown>()
264
264
 
265
265
  /**
266
266
  * The rich response. Constructed by the request builder from a raw
267
- * {@link TestResponse}; exposes the Japa accessor + assertion surface.
267
+ * {@link TestResponse}; exposes the helix accessor + assertion surface.
268
268
  *
269
269
  * PARITY NOTE: the status code / headers / raw text are METHODS — `status()`,
270
270
  * `headers()`, `text()` — and `body()` returns the content-type-parsed body,
271
- * exactly like Japa. `json()` is kept as an additive convenience.
271
+ * exactly like helix. `json()` is kept as an additive convenience.
272
272
  */
273
273
  // biome-ignore lint/suspicious/noUnsafeDeclarationMerging: the merged interface (StatusAssertions) types the status-shortcut asserts attached to the prototype from STATUS_ASSERTIONS — the same generated-method pattern as EonSchema/Macroable (AdonisJS parity); every member is implemented, so the merge is safe.
274
274
  export class ApiResponse {
@@ -283,35 +283,35 @@ export class ApiResponse {
283
283
  applyResponseExtensions(this)
284
284
  }
285
285
 
286
- // ── Japa accessors (method form) ─────────────────────────────────────────
286
+ // ── helix accessors (method form) ─────────────────────────────────────────
287
287
 
288
- /** HTTP status code — Japa `status()`. */
288
+ /** HTTP status code — helix `status()`. */
289
289
  status(): number {
290
290
  return this.#raw.status
291
291
  }
292
292
 
293
- /** Status class digit (2 for 2xx, 4 for 4xx, …) — Japa `statusType()`. */
293
+ /** Status class digit (2 for 2xx, 4 for 4xx, …) — helix `statusType()`. */
294
294
  statusType(): number {
295
295
  return Math.floor(this.#raw.status / 100)
296
296
  }
297
297
 
298
- /** All response headers (lower-cased keys) — Japa `headers()`. */
298
+ /** All response headers (lower-cased keys) — helix `headers()`. */
299
299
  headers(): Dict {
300
300
  return this.#raw.headers
301
301
  }
302
302
 
303
- /** A single response header value — Japa `header(name)`. */
303
+ /** A single response header value — helix `header(name)`. */
304
304
  header(name: string): string | undefined {
305
305
  return this.#raw.headers[name.toLowerCase()]
306
306
  }
307
307
 
308
- /** Raw response text — Japa `text()`. */
308
+ /** Raw response text — helix `text()`. */
309
309
  text(): string {
310
310
  return this.#raw.body
311
311
  }
312
312
 
313
313
  /**
314
- * The parsed response body — Japa `body()`. Parses by `Content-Type`:
314
+ * The parsed response body — helix `body()`. Parses by `Content-Type`:
315
315
  * `application/json` → object, `application/x-www-form-urlencoded` → object,
316
316
  * `multipart/form-data` → the non-file fields (files come from {@link files});
317
317
  * otherwise the raw text. A registered parser (`ApiResponse.addParser`) for the
@@ -336,45 +336,45 @@ export class ApiResponse {
336
336
  return text
337
337
  }
338
338
 
339
- /** Convenience JSON parse (additive; not in Japa). */
339
+ /** Convenience JSON parse (additive; not in helix). */
340
340
  json<T = unknown>(): T {
341
341
  return JSON.parse(this.#raw.body) as T
342
342
  }
343
343
 
344
- /** The request method that produced this response — Japa `method()`. */
344
+ /** The request method that produced this response — helix `method()`. */
345
345
  method(): string {
346
346
  return this.#method
347
347
  }
348
348
 
349
- /** Response `Content-Type` without parameters — Japa `type()`. */
349
+ /** Response `Content-Type` without parameters — helix `type()`. */
350
350
  type(): string | undefined {
351
351
  const ct = this.#raw.headers['content-type']
352
352
  return ct ? (ct.split(';')[0]?.trim() ?? ct) : undefined
353
353
  }
354
354
 
355
- /** Charset from `Content-Type`, if any — Japa `charset()`. */
355
+ /** Charset from `Content-Type`, if any — helix `charset()`. */
356
356
  charset(): string | undefined {
357
357
  const ct = this.#raw.headers['content-type'] ?? ''
358
358
  const m = ct.match(/charset=([^;]+)/i)
359
359
  return m ? m[1].trim() : undefined
360
360
  }
361
361
 
362
- /** All response cookies, parsed from `Set-Cookie` — Japa `cookies()`. */
362
+ /** All response cookies, parsed from `Set-Cookie` — helix `cookies()`. */
363
363
  cookies(): Record<string, ResponseCookie> {
364
364
  return parseCookies(this.#raw.headers['set-cookie'])
365
365
  }
366
366
 
367
- /** A single parsed response cookie — Japa `cookie(name)`. */
367
+ /** A single parsed response cookie — helix `cookie(name)`. */
368
368
  cookie(name: string): ResponseCookie | undefined {
369
369
  return this.cookies()[name]
370
370
  }
371
371
 
372
- /** URLs followed for a redirected request — Japa `redirects()`. */
372
+ /** URLs followed for a redirected request — helix `redirects()`. */
373
373
  redirects(): string[] {
374
374
  return [...this.#redirects]
375
375
  }
376
376
 
377
- /** Parsed `Link` header (`rel → url`) — Japa `links()`. */
377
+ /** Parsed `Link` header (`rel → url`) — helix `links()`. */
378
378
  links(): Record<string, string> {
379
379
  const link = this.#raw.headers.link
380
380
  if (!link) return {}
@@ -387,7 +387,7 @@ export class ApiResponse {
387
387
  }
388
388
 
389
389
  /**
390
- * Files parsed from a `multipart/form-data` response — Japa `files()`. Each
390
+ * Files parsed from a `multipart/form-data` response — helix `files()`. Each
391
391
  * entry carries the field name, filename, content-type and the raw bytes.
392
392
  * Empty for non-multipart responses.
393
393
  */
@@ -398,23 +398,23 @@ export class ApiResponse {
398
398
  return parseMultipartResponse(this.#raw.body, ctRaw).files
399
399
  }
400
400
 
401
- /** Whether the response carries a body — Japa `hasBody()`. */
401
+ /** Whether the response carries a body — helix `hasBody()`. */
402
402
  hasBody(): boolean {
403
403
  return this.#raw.body.length > 0
404
404
  }
405
405
 
406
- /** Whether the status is an error (>= 400) — Japa `hasError()`. */
406
+ /** Whether the status is an error (>= 400) — helix `hasError()`. */
407
407
  hasError(): boolean {
408
408
  return this.#raw.status >= 400
409
409
  }
410
410
 
411
- /** Whether the status is a server error (>= 500) — Japa `hasFatalError()`. */
411
+ /** Whether the status is a server error (>= 500) — helix `hasFatalError()`. */
412
412
  hasFatalError(): boolean {
413
413
  return this.#raw.status >= 500
414
414
  }
415
415
 
416
416
  /**
417
- * The error object when the response is an error, else `undefined` — Japa
417
+ * The error object when the response is an error, else `undefined` — helix
418
418
  * `error()`. Exposes `.status` and `.text` (superagent-style).
419
419
  */
420
420
  error(): ResponseError | undefined {
@@ -422,9 +422,9 @@ export class ApiResponse {
422
422
  return { status: this.#raw.status, text: this.#raw.body }
423
423
  }
424
424
 
425
- // ── Debugging (Japa response `dump*`) ────────────────────────────────────
425
+ // ── Debugging (helix response `dump*`) ────────────────────────────────────
426
426
 
427
- /** Print status + headers + body to stderr — Japa `response.dump()`. */
427
+ /** Print status + headers + body to stderr — helix `response.dump()`. */
428
428
  dump(): this {
429
429
  process.stderr.write(
430
430
  `[helix:response.dump] ${this.#method} → ${this.#raw.status}\n` +
@@ -433,26 +433,26 @@ export class ApiResponse {
433
433
  )
434
434
  return this
435
435
  }
436
- /** Print only the response body — Japa `response.dumpBody()`. */
436
+ /** Print only the response body — helix `response.dumpBody()`. */
437
437
  dumpBody(): this {
438
438
  process.stderr.write(`[helix:response.dumpBody] ${capBody(this.#raw.body, 2048)}\n`)
439
439
  return this
440
440
  }
441
- /** Print only the response headers — Japa `response.dumpHeaders()`. */
441
+ /** Print only the response headers — helix `response.dumpHeaders()`. */
442
442
  dumpHeaders(): this {
443
443
  process.stderr.write(
444
444
  `[helix:response.dumpHeaders] ${JSON.stringify(this.#raw.headers, null, 2)}\n`,
445
445
  )
446
446
  return this
447
447
  }
448
- /** Print only the response cookies — Japa `response.dumpCookies()`. */
448
+ /** Print only the response cookies — helix `response.dumpCookies()`. */
449
449
  dumpCookies(): this {
450
450
  process.stderr.write(
451
451
  `[helix:response.dumpCookies] ${JSON.stringify(this.cookies(), null, 2)}\n`,
452
452
  )
453
453
  return this
454
454
  }
455
- /** Print the error object, if any — Japa `response.dumpError()`. */
455
+ /** Print the error object, if any — helix `response.dumpError()`. */
456
456
  dumpError(): this {
457
457
  process.stderr.write(`[helix:response.dumpError] ${JSON.stringify(this.error(), null, 2)}\n`)
458
458
  return this
@@ -522,7 +522,7 @@ export class ApiResponse {
522
522
  }
523
523
 
524
524
  /**
525
- * The body used by `assertBody*`, parsed per `Content-Type` (Japa parity):
525
+ * The body used by `assertBody*`, parsed per `Content-Type` (helix parity):
526
526
  * urlencoded/multipart → the structured `body()`; JSON or an unknown/absent
527
527
  * content-type → a tolerant JSON parse (so bodyless test fakes keep working).
528
528
  */
@@ -574,7 +574,7 @@ export class ApiResponse {
574
574
  }
575
575
 
576
576
  /**
577
- * Assert the request was redirected to `path` — Japa `assertRedirectsTo`.
577
+ * Assert the request was redirected to `path` — helix `assertRedirectsTo`.
578
578
  * Checks the followed redirect chain (`redirects()`); also accepts a direct
579
579
  * 3xx `Location` match when no redirects were followed.
580
580
  */
@@ -599,9 +599,9 @@ export class ApiResponse {
599
599
  }
600
600
 
601
601
  /**
602
- * Assert the response conforms to the registered OpenAPI spec — Japa
603
- * `assertAgainstApiSpec()`. Japa ships this via the SEPARATE opt-in
604
- * `@japa/openapi-assertions` plugin; register a validator with
602
+ * Assert the response conforms to the registered OpenAPI spec — helix
603
+ * `assertAgainstApiSpec()`. helix ships this via the SEPARATE opt-in
604
+ * `a separate OpenAPI-assertions plugin` plugin; register a validator with
605
605
  * {@link ApiResponse.registerApiSpecValidator} to enable it. Without one, this
606
606
  * throws (never silently passes).
607
607
  */
@@ -609,7 +609,7 @@ export class ApiResponse {
609
609
  if (!apiSpecValidator) {
610
610
  throw new ExpectationError(
611
611
  'assertAgainstApiSpec() requires an OpenAPI validator. Register one via ' +
612
- 'ApiResponse.registerApiSpecValidator(fn) (the opt-in equivalent of @japa/openapi-assertions).',
612
+ 'ApiResponse.registerApiSpecValidator(fn) (the opt-in equivalent of a separate OpenAPI-assertions plugin).',
613
613
  )
614
614
  }
615
615
  apiSpecValidator(this)
@@ -617,9 +617,9 @@ export class ApiResponse {
617
617
  }
618
618
 
619
619
  /**
620
- * Register a shared property (Japa `ApiResponse.macro`) or lazy getter
620
+ * Register a shared property (helix `ApiResponse.macro`) or lazy getter
621
621
  * (`ApiResponse.getter`). The callback is invoked with `this` bound to the
622
- * response AND the response as its first argument, so both Japa's
622
+ * response AND the response as its first argument, so both helix's
623
623
  * `function () { return this.header('x') }` and `(res) => res.header('x')` work.
624
624
  */
625
625
  static macro(name: string, value: unknown): void {
@@ -629,7 +629,7 @@ export class ApiResponse {
629
629
  responseGetters.set(name, fn)
630
630
  }
631
631
 
632
- /** Register a response body parser for a content-type — Japa `ApiRequest.addParser`. */
632
+ /** Register a response body parser for a content-type — helix `ApiRequest.addParser`. */
633
633
  static addParser(contentType: string, fn: (body: string, headers: Dict) => unknown): void {
634
634
  responseParsers.set(contentType, fn)
635
635
  }