@c9up/ream 0.1.11 → 0.1.13

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.
@@ -33,6 +33,13 @@ export type HttpSender = (method: HttpMethod, path: string, init: {
33
33
  headers: Record<string, string>;
34
34
  body: Buffer;
35
35
  }) => Promise<TestResponse>;
36
+ /** Options for a multipart file part — mirrors Adonis/japa's `.file()` options. */
37
+ export interface FilePart {
38
+ /** Filename advertised in the part's `Content-Disposition`. Defaults to the field name. */
39
+ filename?: string;
40
+ /** MIME type of the part. Defaults to `application/octet-stream`. */
41
+ contentType?: string;
42
+ }
36
43
  export declare class RequestBuilder {
37
44
  #private;
38
45
  constructor(sender: HttpSender, method: HttpMethod, path: string, authStrategy?: AuthStrategy | null);
@@ -41,6 +48,20 @@ export declare class RequestBuilder {
41
48
  json(data: unknown): this;
42
49
  body(data: Buffer | string, contentType?: string): this;
43
50
  form(data: Record<string, string>): this;
51
+ /**
52
+ * Add a `multipart/form-data` text field — mirrors Adonis/japa's `.field()`.
53
+ * Calling `.field()` or `.file()` switches the body to multipart (use
54
+ * `.form()` for `application/x-www-form-urlencoded`). The boundary +
55
+ * `Content-Type` header are produced at send time.
56
+ */
57
+ field(name: string, value: string): this;
58
+ /**
59
+ * Attach a file as a `multipart/form-data` part — mirrors Adonis/japa's
60
+ * `.file(field, contents, { filename, contentType })`. `contents` is a
61
+ * Buffer (binary) or a string (encoded as utf8). Server-side
62
+ * `request.file(field, …)` reads the resulting part.
63
+ */
64
+ file(field: string, contents: Buffer | string, options?: FilePart): this;
44
65
  cookies(map: Record<string, string>): this;
45
66
  cookie(name: string, value: string): this;
46
67
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"RequestBuilder.d.ts","sourceRoot":"","sources":["../../src/testing/RequestBuilder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAEnD;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,WAAW;IAC1B,qFAAqF;IACrF,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;IACnB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACtC;AAED,MAAM,WAAW,YAAY;IAC3B,8FAA8F;IAC9F,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC1F,oDAAoD;IACpD,UAAU,CAAC,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAC5F;AAUD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAA;AAEpE;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,CACvB,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE;IACJ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,EAAE,MAAM,CAAA;CACb,KACE,OAAO,CAAC,YAAY,CAAC,CAAA;AAE1B,qBAAa,cAAc;;gBAYvB,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,MAAM,EACZ,YAAY,GAAE,YAAY,GAAG,IAAW;IAQ1C,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAO1C,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAKzC,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAMzB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAMvD,IAAI,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IASxC,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAK1C,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAKzC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IAKpC,+EAA+E;IAC/E,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAKrC,uEAAuE;IACvE,IAAI,IAAI,OAAO,CAAC,YAAY,CAAC;IAKvB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUzC,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBjE,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuBlE,UAAU,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAoDnD;AAWD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAcxE"}
1
+ {"version":3,"file":"RequestBuilder.d.ts","sourceRoot":"","sources":["../../src/testing/RequestBuilder.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,7 @@ export class RequestBuilder {
11
11
  #path;
12
12
  #headers = {};
13
13
  #body = Buffer.alloc(0);
14
+ #multipart = [];
14
15
  #cookies = {};
15
16
  #authStrategy;
16
17
  #pendingAuth = null;
@@ -48,6 +49,32 @@ export class RequestBuilder {
48
49
  this.#headers['content-type'] = 'application/x-www-form-urlencoded';
49
50
  return this;
50
51
  }
52
+ /**
53
+ * Add a `multipart/form-data` text field — mirrors Adonis/japa's `.field()`.
54
+ * Calling `.field()` or `.file()` switches the body to multipart (use
55
+ * `.form()` for `application/x-www-form-urlencoded`). The boundary +
56
+ * `Content-Type` header are produced at send time.
57
+ */
58
+ field(name, value) {
59
+ this.#multipart.push({ kind: 'field', name, value });
60
+ return this;
61
+ }
62
+ /**
63
+ * Attach a file as a `multipart/form-data` part — mirrors Adonis/japa's
64
+ * `.file(field, contents, { filename, contentType })`. `contents` is a
65
+ * Buffer (binary) or a string (encoded as utf8). Server-side
66
+ * `request.file(field, …)` reads the resulting part.
67
+ */
68
+ file(field, contents, options = {}) {
69
+ this.#multipart.push({
70
+ kind: 'file',
71
+ name: field,
72
+ filename: options.filename ?? field,
73
+ contentType: options.contentType ?? 'application/octet-stream',
74
+ content: typeof contents === 'string' ? Buffer.from(contents, 'utf8') : contents,
75
+ });
76
+ return this;
77
+ }
51
78
  cookies(map) {
52
79
  Object.assign(this.#cookies, map);
53
80
  return this;
@@ -157,6 +184,13 @@ export class RequestBuilder {
157
184
  }
158
185
  }
159
186
  }
187
+ // Multipart parts (.field()/.file()) take precedence over any body set by
188
+ // json()/form()/body(); encode them with a fresh boundary at send time.
189
+ if (this.#multipart.length > 0) {
190
+ const boundary = `----ReamRequestBuilder${crypto.randomUUID().replace(/-/g, '')}`;
191
+ this.#body = encodeMultipart(this.#multipart, boundary);
192
+ this.#headers['content-type'] = `multipart/form-data; boundary=${boundary}`;
193
+ }
160
194
  // Serialise cookies into a single `Cookie:` header.
161
195
  const cookieEntries = Object.entries(this.#cookies);
162
196
  if (cookieEntries.length > 0) {
@@ -171,6 +205,29 @@ export class RequestBuilder {
171
205
  function capBody(s, max = 512) {
172
206
  return s.length <= max ? s : `${s.slice(0, max)}...[truncated]`;
173
207
  }
208
+ /** Strip CR/LF and escape quotes so a name/filename can't break the headers. */
209
+ function sanitizeHeaderParam(value) {
210
+ return value.replace(/[\r\n]/g, '').replace(/"/g, '%22');
211
+ }
212
+ /** Encode multipart/form-data parts (RFC 7578) into a single body Buffer. */
213
+ function encodeMultipart(parts, boundary) {
214
+ const CRLF = '\r\n';
215
+ const chunks = [];
216
+ for (const part of parts) {
217
+ if (part.kind === 'field') {
218
+ chunks.push(Buffer.from(`--${boundary}${CRLF}` +
219
+ `Content-Disposition: form-data; name="${sanitizeHeaderParam(part.name)}"${CRLF}${CRLF}` +
220
+ `${part.value}${CRLF}`, 'utf8'));
221
+ }
222
+ else {
223
+ chunks.push(Buffer.from(`--${boundary}${CRLF}` +
224
+ `Content-Disposition: form-data; name="${sanitizeHeaderParam(part.name)}"; filename="${sanitizeHeaderParam(part.filename)}"${CRLF}` +
225
+ `Content-Type: ${part.contentType}${CRLF}${CRLF}`, 'utf8'), part.content, Buffer.from(CRLF, 'utf8'));
226
+ }
227
+ }
228
+ chunks.push(Buffer.from(`--${boundary}--${CRLF}`, 'utf8'));
229
+ return Buffer.concat(chunks);
230
+ }
174
231
  /** Narrowing guard — a non-null, non-array object usable as a string map. */
175
232
  function isRecord(x) {
176
233
  return typeof x === 'object' && x !== null && !Array.isArray(x);
@@ -1 +1 @@
1
- {"version":3,"file":"RequestBuilder.js","sourceRoot":"","sources":["../../src/testing/RequestBuilder.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"RequestBuilder.js","sourceRoot":"","sources":["../../src/testing/RequestBuilder.ts"],"names":[],"mappings":"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"}
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/ream",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Ream — Rust-powered Node.js application framework",
5
5
  "license": "MIT",
6
6
  "type": "module",
Binary file
package/src/Ignitor.ts CHANGED
@@ -13,12 +13,10 @@
13
13
  * @implements FR17, FR20, FR23
14
14
  */
15
15
 
16
- import { readFileSync } from 'node:fs'
17
- import { fileURLToPath } from 'node:url'
18
- import { parseEnv } from 'node:util'
19
16
  import { Application } from './Application.js'
20
17
  import type { ErrorEvent } from './ErrorBoundary.js'
21
18
  import { ErrorBoundary } from './ErrorBoundary.js'
19
+ import { loadEnvFiles } from './env/loadEnvFiles.js'
22
20
  import { ReamError } from './errors/ReamError.js'
23
21
  import type { Emitter } from './events/Emitter.js'
24
22
  import { startHotReload } from './HotReload.js'
@@ -302,28 +300,7 @@ export class Ignitor {
302
300
  */
303
301
  #loadEnvironmentFiles(): void {
304
302
  if (!this.appRoot) return
305
- const nodeEnv = process.env.NODE_ENV
306
- // Most-specific first; with "existing wins", earlier files take precedence.
307
- const files = [
308
- nodeEnv ? `.env.${nodeEnv}.local` : null,
309
- this.environment === 'test' ? null : '.env.local',
310
- nodeEnv ? `.env.${nodeEnv}` : null,
311
- '.env',
312
- ].filter((name): name is string => name !== null)
313
-
314
- for (const name of files) {
315
- let contents: string
316
- try {
317
- contents = readFileSync(fileURLToPath(new URL(name, this.appRoot)), 'utf8')
318
- } catch {
319
- continue // file absent — nothing to load
320
- }
321
- for (const [key, value] of Object.entries(parseEnv(contents))) {
322
- if (typeof value === 'string' && process.env[key] === undefined) {
323
- process.env[key] = value
324
- }
325
- }
326
- }
303
+ loadEnvFiles(this.appRoot, { skipEnvLocal: this.environment === 'test' })
327
304
  }
328
305
 
329
306
  private async phaseRegister(): Promise<void> {
package/src/env/Env.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { loadEnvFiles } from './loadEnvFiles.js'
2
+ import { type SchemaNode, schema } from './schema.js'
3
+
4
+ /**
5
+ * Type-safe environment access — mirrors `@adonisjs/core/env`:
6
+ *
7
+ * // start/env.ts
8
+ * import { Env } from '@c9up/ream'
9
+ * export default await Env.create(new URL('../', import.meta.url), {
10
+ * HOST: Env.schema.string({ format: 'host' }),
11
+ * PORT: Env.schema.number(),
12
+ * NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const),
13
+ * DATABASE_URL: Env.schema.string.optional(),
14
+ * })
15
+ *
16
+ * // config/database.ts
17
+ * import env from '#start/env'
18
+ * const port = env.get('PORT') // typed `number`
19
+ *
20
+ * `create` loads the `.env*` files (so config read at import sees them, in EVERY
21
+ * flow incl. tests), validates each variable against the schema, and refuses to
22
+ * start with a descriptive error if a required variable is missing or invalid.
23
+ */
24
+
25
+ /** The validated output type of a single schema node. */
26
+ type EnvOutput<N> = N extends SchemaNode<infer T> ? T : never
27
+
28
+ /** Map a schema object to its `{ key: validatedType }` record. */
29
+ type EnvRecord<S extends Record<string, SchemaNode<unknown>>> = {
30
+ [K in keyof S]: EnvOutput<S[K]>
31
+ }
32
+
33
+ /** Raised when one or more environment variables fail validation. */
34
+ export class EnvValidationException extends Error {
35
+ /** Human-readable, newline-joined list of every failure (cf. Adonis `error.help`). */
36
+ readonly help: string
37
+ constructor(failures: string[]) {
38
+ super('Environment variables validation failed')
39
+ this.name = 'E_INVALID_ENV_VARIABLES'
40
+ this.help = failures.join('\n')
41
+ }
42
+ }
43
+
44
+ export class Env<Values extends Record<string, unknown>> {
45
+ /** The validation schema surface — `Env.schema.string()`, `.number()`, … */
46
+ static readonly schema = schema
47
+
48
+ readonly #values: Values
49
+
50
+ private constructor(values: Values) {
51
+ this.#values = values
52
+ }
53
+
54
+ /**
55
+ * Load `.env*` files, validate `process.env` against `schema`, and return a
56
+ * typed `Env`. Throws {@link EnvValidationException} (aggregating every
57
+ * failure) if validation fails.
58
+ */
59
+ static async create<S extends Record<string, SchemaNode<unknown>>>(
60
+ appRoot: URL,
61
+ schema: S,
62
+ ): Promise<Env<EnvRecord<S>>> {
63
+ loadEnvFiles(appRoot, { skipEnvLocal: process.env.NODE_ENV === 'test' })
64
+
65
+ const failures: string[] = []
66
+ const collected: Record<string, unknown> = {}
67
+ for (const key of Object.keys(schema)) {
68
+ try {
69
+ collected[key] = schema[key].validate(key, process.env[key])
70
+ } catch (err) {
71
+ failures.push(err instanceof Error ? err.message : String(err))
72
+ }
73
+ }
74
+ if (failures.length > 0) throw new EnvValidationException(failures)
75
+
76
+ // The ONE isolated type-erasure boundary (the same place zod / @adonisjs/env
77
+ // cast internally): every entry of `collected` was produced by its schema
78
+ // node's validator, so the object structurally IS `EnvRecord<S>` — TS just
79
+ // can't carry that proof through the dynamic `Object.keys` loop. Load-bearing.
80
+ return new Env(collected as EnvRecord<S>)
81
+ }
82
+
83
+ /** Read a validated variable. Required keys are always present; optional keys may be `undefined`. */
84
+ get<K extends keyof Values>(key: K): Values[K]
85
+ get<K extends keyof Values>(key: K, fallback: Values[K]): Values[K]
86
+ get<K extends keyof Values>(key: K, fallback?: Values[K]): Values[K] {
87
+ const value = this.#values[key]
88
+ if (value === undefined && fallback !== undefined) return fallback
89
+ return value
90
+ }
91
+ }
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { parseEnv } from 'node:util'
4
+
5
+ /**
6
+ * Load `.env` files into `process.env` — the shared primitive behind both the
7
+ * Ignitor (HTTP/console boot) and `Env.create()` (config/test flow), mirroring
8
+ * AdonisJS which loads env in every flow.
9
+ *
10
+ * Order is most-specific-first and "already-present wins", so the shell / CI
11
+ * always overrides the files. Missing files are skipped silently. Uses Node's
12
+ * built-in parser — no dependency.
13
+ *
14
+ * @param appRoot Directory URL the `.env*` files are resolved against.
15
+ * @param options.skipEnvLocal Skip `.env.local` (the test flow does, so a
16
+ * developer's local overrides don't leak into tests).
17
+ */
18
+ export function loadEnvFiles(appRoot: URL, options: { skipEnvLocal?: boolean } = {}): void {
19
+ const nodeEnv = process.env.NODE_ENV
20
+ const files = [
21
+ nodeEnv ? `.env.${nodeEnv}.local` : null,
22
+ options.skipEnvLocal ? null : '.env.local',
23
+ nodeEnv ? `.env.${nodeEnv}` : null,
24
+ '.env',
25
+ ].filter((name): name is string => name !== null)
26
+
27
+ for (const name of files) {
28
+ let contents: string
29
+ try {
30
+ contents = readFileSync(fileURLToPath(new URL(name, appRoot)), 'utf8')
31
+ } catch {
32
+ continue // file absent — nothing to load
33
+ }
34
+ for (const [key, value] of Object.entries(parseEnv(contents))) {
35
+ if (typeof value === 'string' && process.env[key] === undefined) {
36
+ process.env[key] = value
37
+ }
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Env validation schema — mirrors `@adonisjs/core/env`'s `Env.schema` surface:
3
+ *
4
+ * Env.schema.string({ format: 'host' })
5
+ * Env.schema.number()
6
+ * Env.schema.boolean()
7
+ * Env.schema.enum(['development', 'production', 'test'] as const)
8
+ * Env.schema.string.optional() // shortcut form
9
+ * Env.schema.enum([...]).optional() // node form
10
+ *
11
+ * Each node validates + coerces a raw `process.env` string into its typed
12
+ * value (or throws {@link EnvVarError}); `.optional()` accepts an absent/empty
13
+ * value as `undefined`.
14
+ */
15
+
16
+ /** A single failed-variable validation — collected by `Env.create`. */
17
+ export class EnvVarError extends Error {
18
+ constructor(message: string) {
19
+ super(message)
20
+ this.name = 'EnvVarError'
21
+ }
22
+ }
23
+
24
+ /** A validator: parse + coerce a raw env value (or `undefined`) into `T`. */
25
+ export interface SchemaNode<T> {
26
+ validate(name: string, value: string | undefined): T
27
+ }
28
+
29
+ /** A required node that can be turned optional — `.optional()` widens `T` to `T | undefined`. */
30
+ export interface RequiredNode<T> extends SchemaNode<T> {
31
+ optional(): SchemaNode<T | undefined>
32
+ }
33
+
34
+ export interface StringOptions {
35
+ /** Validate the string against a known format. */
36
+ format?: 'host' | 'url' | 'email'
37
+ /** For `format: 'url'` — require a TLD (default true). */
38
+ tld?: boolean
39
+ /** For `format: 'url'` — require a protocol (default true). */
40
+ protocol?: boolean
41
+ }
42
+
43
+ const TRUE_VALUES = new Set(['true', '1'])
44
+ const FALSE_VALUES = new Set(['false', '0'])
45
+
46
+ /**
47
+ * Build a node from a `parse` that only ever sees a present, non-empty string.
48
+ * Required `validate` throws on absent/empty; `.optional()` returns `undefined`.
49
+ */
50
+ function makeNode<T>(parse: (name: string, raw: string) => T): RequiredNode<T> {
51
+ return {
52
+ validate(name, value) {
53
+ if (value === undefined || value === '') {
54
+ throw new EnvVarError(`Missing required environment variable "${name}"`)
55
+ }
56
+ return parse(name, value)
57
+ },
58
+ optional() {
59
+ return {
60
+ validate(name, value) {
61
+ if (value === undefined || value === '') return undefined
62
+ return parse(name, value)
63
+ },
64
+ }
65
+ },
66
+ }
67
+ }
68
+
69
+ function isHost(value: string): boolean {
70
+ if (value === 'localhost') return true
71
+ if (/^(\d{1,3}\.){3}\d{1,3}$/.test(value)) {
72
+ return value.split('.').every((octet) => Number(octet) <= 255)
73
+ }
74
+ if (value.includes(':') && /^[0-9a-fA-F:]+$/.test(value)) return true // loose IPv6
75
+ return /^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.(?!-)[A-Za-z0-9-]{1,63}(?<!-))*$/.test(value)
76
+ }
77
+
78
+ function isUrl(value: string, options: StringOptions): boolean {
79
+ const candidate = options.protocol === false && !value.includes('://') ? `http://${value}` : value
80
+ let url: URL
81
+ try {
82
+ url = new URL(candidate)
83
+ } catch {
84
+ return false
85
+ }
86
+ if (options.tld !== false && !url.hostname.includes('.') && url.hostname !== 'localhost') {
87
+ return false
88
+ }
89
+ return true
90
+ }
91
+
92
+ function isEmail(value: string): boolean {
93
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
94
+ }
95
+
96
+ function checkFormat(name: string, value: string, options?: StringOptions): void {
97
+ if (!options?.format) return
98
+ const ok =
99
+ options.format === 'host'
100
+ ? isHost(value)
101
+ : options.format === 'url'
102
+ ? isUrl(value, options)
103
+ : isEmail(value)
104
+ if (!ok) {
105
+ throw new EnvVarError(
106
+ `Environment variable "${name}" must be a valid ${options.format}, got "${value}"`,
107
+ )
108
+ }
109
+ }
110
+
111
+ function stringParse(options?: StringOptions): (name: string, raw: string) => string {
112
+ return (name, raw) => {
113
+ checkFormat(name, raw, options)
114
+ return raw
115
+ }
116
+ }
117
+
118
+ /** `string(opts?)` is callable AND exposes `.optional(opts?)` — both Adonis forms. */
119
+ const string: ((options?: StringOptions) => RequiredNode<string>) & {
120
+ optional(options?: StringOptions): SchemaNode<string | undefined>
121
+ } = Object.assign((options?: StringOptions) => makeNode(stringParse(options)), {
122
+ optional: (options?: StringOptions) => makeNode(stringParse(options)).optional(),
123
+ })
124
+
125
+ function number(): RequiredNode<number> {
126
+ return makeNode((name, raw) => {
127
+ const parsed = Number(raw)
128
+ if (Number.isNaN(parsed)) {
129
+ throw new EnvVarError(`Environment variable "${name}" must be a number, got "${raw}"`)
130
+ }
131
+ return parsed
132
+ })
133
+ }
134
+
135
+ function boolean(): RequiredNode<boolean> {
136
+ return makeNode((name, raw) => {
137
+ if (TRUE_VALUES.has(raw)) return true
138
+ if (FALSE_VALUES.has(raw)) return false
139
+ throw new EnvVarError(
140
+ `Environment variable "${name}" must be a boolean (true/false/1/0), got "${raw}"`,
141
+ )
142
+ })
143
+ }
144
+
145
+ function enumNode<V extends readonly string[]>(values: V): RequiredNode<V[number]> {
146
+ return makeNode((name, raw) => {
147
+ const match = values.find((candidate) => candidate === raw)
148
+ if (match === undefined) {
149
+ throw new EnvVarError(
150
+ `Environment variable "${name}" must be one of [${values.join(', ')}], got "${raw}"`,
151
+ )
152
+ }
153
+ return match
154
+ })
155
+ }
156
+
157
+ /** The `Env.schema` surface. */
158
+ export const schema = {
159
+ string,
160
+ number,
161
+ boolean,
162
+ enum: enumNode,
163
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,14 @@ export { Application } from './Application.js'
12
12
  export { ConfigStore, defineModuleConfig, env } from './ConfigLoader.js'
13
13
  export { Container } from './container/Container.js'
14
14
  export type { Binding, ServiceFactory, ServiceScope, ServiceToken } from './container/types.js'
15
+ export { Env, EnvValidationException } from './env/Env.js'
16
+ export { loadEnvFiles } from './env/loadEnvFiles.js'
17
+ export {
18
+ EnvVarError,
19
+ type RequiredNode,
20
+ type SchemaNode,
21
+ type StringOptions,
22
+ } from './env/schema.js'
15
23
 
16
24
  // ─── HTTP (new — AdonisJS-compatible) ───────────────────────
17
25
 
@@ -51,12 +51,26 @@ export type HttpSender = (
51
51
  },
52
52
  ) => Promise<TestResponse>
53
53
 
54
+ /** Options for a multipart file part — mirrors Adonis/japa's `.file()` options. */
55
+ export interface FilePart {
56
+ /** Filename advertised in the part's `Content-Disposition`. Defaults to the field name. */
57
+ filename?: string
58
+ /** MIME type of the part. Defaults to `application/octet-stream`. */
59
+ contentType?: string
60
+ }
61
+
62
+ /** One encoded multipart/form-data part — a text field or an uploaded file. */
63
+ type MultipartPart =
64
+ | { kind: 'field'; name: string; value: string }
65
+ | { kind: 'file'; name: string; filename: string; contentType: string; content: Buffer }
66
+
54
67
  export class RequestBuilder {
55
68
  #sender: HttpSender
56
69
  #method: HttpMethod
57
70
  #path: string
58
71
  #headers: Record<string, string> = {}
59
72
  #body: Buffer = Buffer.alloc(0)
73
+ #multipart: MultipartPart[] = []
60
74
  #cookies: Record<string, string> = {}
61
75
  #authStrategy: AuthStrategy | null
62
76
  #pendingAuth: AuthSubject | null = null
@@ -107,6 +121,34 @@ export class RequestBuilder {
107
121
  return this
108
122
  }
109
123
 
124
+ /**
125
+ * Add a `multipart/form-data` text field — mirrors Adonis/japa's `.field()`.
126
+ * Calling `.field()` or `.file()` switches the body to multipart (use
127
+ * `.form()` for `application/x-www-form-urlencoded`). The boundary +
128
+ * `Content-Type` header are produced at send time.
129
+ */
130
+ field(name: string, value: string): this {
131
+ this.#multipart.push({ kind: 'field', name, value })
132
+ return this
133
+ }
134
+
135
+ /**
136
+ * Attach a file as a `multipart/form-data` part — mirrors Adonis/japa's
137
+ * `.file(field, contents, { filename, contentType })`. `contents` is a
138
+ * Buffer (binary) or a string (encoded as utf8). Server-side
139
+ * `request.file(field, …)` reads the resulting part.
140
+ */
141
+ file(field: string, contents: Buffer | string, options: FilePart = {}): this {
142
+ this.#multipart.push({
143
+ kind: 'file',
144
+ name: field,
145
+ filename: options.filename ?? field,
146
+ contentType: options.contentType ?? 'application/octet-stream',
147
+ content: typeof contents === 'string' ? Buffer.from(contents, 'utf8') : contents,
148
+ })
149
+ return this
150
+ }
151
+
110
152
  cookies(map: Record<string, string>): this {
111
153
  Object.assign(this.#cookies, map)
112
154
  return this
@@ -231,6 +273,14 @@ export class RequestBuilder {
231
273
  }
232
274
  }
233
275
 
276
+ // Multipart parts (.field()/.file()) take precedence over any body set by
277
+ // json()/form()/body(); encode them with a fresh boundary at send time.
278
+ if (this.#multipart.length > 0) {
279
+ const boundary = `----ReamRequestBuilder${crypto.randomUUID().replace(/-/g, '')}`
280
+ this.#body = encodeMultipart(this.#multipart, boundary)
281
+ this.#headers['content-type'] = `multipart/form-data; boundary=${boundary}`
282
+ }
283
+
234
284
  // Serialise cookies into a single `Cookie:` header.
235
285
  const cookieEntries = Object.entries(this.#cookies)
236
286
  if (cookieEntries.length > 0) {
@@ -248,6 +298,42 @@ function capBody(s: string, max = 512): string {
248
298
  return s.length <= max ? s : `${s.slice(0, max)}...[truncated]`
249
299
  }
250
300
 
301
+ /** Strip CR/LF and escape quotes so a name/filename can't break the headers. */
302
+ function sanitizeHeaderParam(value: string): string {
303
+ return value.replace(/[\r\n]/g, '').replace(/"/g, '%22')
304
+ }
305
+
306
+ /** Encode multipart/form-data parts (RFC 7578) into a single body Buffer. */
307
+ function encodeMultipart(parts: MultipartPart[], boundary: string): Buffer {
308
+ const CRLF = '\r\n'
309
+ const chunks: Buffer[] = []
310
+ for (const part of parts) {
311
+ if (part.kind === 'field') {
312
+ chunks.push(
313
+ Buffer.from(
314
+ `--${boundary}${CRLF}` +
315
+ `Content-Disposition: form-data; name="${sanitizeHeaderParam(part.name)}"${CRLF}${CRLF}` +
316
+ `${part.value}${CRLF}`,
317
+ 'utf8',
318
+ ),
319
+ )
320
+ } else {
321
+ chunks.push(
322
+ Buffer.from(
323
+ `--${boundary}${CRLF}` +
324
+ `Content-Disposition: form-data; name="${sanitizeHeaderParam(part.name)}"; filename="${sanitizeHeaderParam(part.filename)}"${CRLF}` +
325
+ `Content-Type: ${part.contentType}${CRLF}${CRLF}`,
326
+ 'utf8',
327
+ ),
328
+ part.content,
329
+ Buffer.from(CRLF, 'utf8'),
330
+ )
331
+ }
332
+ }
333
+ chunks.push(Buffer.from(`--${boundary}--${CRLF}`, 'utf8'))
334
+ return Buffer.concat(chunks)
335
+ }
336
+
251
337
  /** Narrowing guard — a non-null, non-array object usable as a string map. */
252
338
  function isRecord(x: unknown): x is Record<string, unknown> {
253
339
  return typeof x === 'object' && x !== null && !Array.isArray(x)