@shaferllc/keel 0.36.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +14 -2
  2. package/dist/core/application.d.ts +58 -2
  3. package/dist/core/application.js +99 -3
  4. package/dist/core/auth.d.ts +21 -2
  5. package/dist/core/auth.js +38 -3
  6. package/dist/core/authorization.d.ts +52 -0
  7. package/dist/core/authorization.js +97 -0
  8. package/dist/core/broadcasting.d.ts +49 -0
  9. package/dist/core/broadcasting.js +84 -0
  10. package/dist/core/broker.d.ts +398 -0
  11. package/dist/core/broker.js +602 -0
  12. package/dist/core/crypto.d.ts +51 -1
  13. package/dist/core/crypto.js +96 -3
  14. package/dist/core/database.d.ts +26 -1
  15. package/dist/core/database.js +65 -2
  16. package/dist/core/decorators.d.ts +39 -0
  17. package/dist/core/decorators.js +72 -0
  18. package/dist/core/exceptions.d.ts +77 -6
  19. package/dist/core/exceptions.js +168 -10
  20. package/dist/core/helpers.d.ts +6 -0
  21. package/dist/core/helpers.js +12 -0
  22. package/dist/core/http/kernel.js +14 -2
  23. package/dist/core/http/router.d.ts +14 -0
  24. package/dist/core/http/router.js +30 -1
  25. package/dist/core/index.d.ts +31 -8
  26. package/dist/core/index.js +17 -5
  27. package/dist/core/logger.d.ts +5 -0
  28. package/dist/core/logger.js +24 -2
  29. package/dist/core/migrations.js +3 -3
  30. package/dist/core/model.d.ts +19 -1
  31. package/dist/core/model.js +72 -4
  32. package/dist/core/provider.d.ts +13 -4
  33. package/dist/core/provider.js +12 -2
  34. package/dist/core/redis.d.ts +78 -0
  35. package/dist/core/redis.js +176 -0
  36. package/dist/core/request-logger.d.ts +26 -0
  37. package/dist/core/request-logger.js +48 -0
  38. package/dist/core/request.d.ts +17 -1
  39. package/dist/core/request.js +27 -1
  40. package/dist/core/scheduler.d.ts +60 -0
  41. package/dist/core/scheduler.js +166 -0
  42. package/dist/core/session.js +17 -2
  43. package/dist/core/storage.d.ts +57 -0
  44. package/dist/core/storage.js +98 -0
  45. package/dist/core/template.d.ts +50 -0
  46. package/dist/core/template.js +753 -0
  47. package/dist/core/testing.d.ts +54 -0
  48. package/dist/core/testing.js +141 -0
  49. package/dist/core/transformer.d.ts +89 -0
  50. package/dist/core/transformer.js +152 -0
  51. package/dist/core/validation.d.ts +20 -0
  52. package/dist/core/validation.js +52 -1
  53. package/dist/core/vite.d.ts +117 -0
  54. package/dist/core/vite.js +258 -0
  55. package/dist/vite/index.d.ts +40 -0
  56. package/dist/vite/index.js +146 -0
  57. package/package.json +16 -1
@@ -7,6 +7,9 @@
7
7
  *
8
8
  * const token = await encryption.encrypt({ userId: 1 });
9
9
  * await encryption.decrypt(token); // { userId: 1 } | null
10
+ *
11
+ * const token = await jwt.sign({ sub: "42" }, { expiresIn: "1h" });
12
+ * await jwt.verify(token); // { sub: "42", iat, exp } | null
10
13
  */
11
14
  import { config } from "./helpers.js";
12
15
  /* --------------------------- shared utilities -------------------------- */
@@ -51,13 +54,22 @@ export const hash = {
51
54
  const derived = await pbkdf2(password, salt, iterations);
52
55
  return `pbkdf2_sha256$${iterations}$${b64(salt)}$${b64(derived)}`;
53
56
  },
54
- /** Verify a password against a stored hash. */
57
+ /** Verify a password against a stored hash. Returns false for any malformed hash. */
55
58
  async verify(hashed, password) {
56
59
  const [algo, iter, salt64, hash64] = hashed.split("$");
57
60
  if (algo !== "pbkdf2_sha256" || !iter || !salt64 || !hash64)
58
61
  return false;
59
- const derived = await pbkdf2(password, fromB64(salt64), Number(iter));
60
- return safeEqual(b64(derived), hash64);
62
+ const iterations = Number(iter);
63
+ if (!Number.isInteger(iterations) || iterations < 1)
64
+ return false;
65
+ try {
66
+ const derived = await pbkdf2(password, fromB64(salt64), iterations);
67
+ return safeEqual(b64(derived), hash64);
68
+ }
69
+ catch {
70
+ // Malformed base64 salt/hash, etc. — treat as a non-match, never throw.
71
+ return false;
72
+ }
61
73
  },
62
74
  /** Whether a hash was made with fewer iterations than the current default. */
63
75
  needsRehash(hashed, iterations = DEFAULT_ITERATIONS) {
@@ -95,3 +107,84 @@ export const encryption = {
95
107
  }
96
108
  },
97
109
  };
110
+ const DURATION = /^(\d+)\s*(s|m|h|d)$/;
111
+ const UNIT = { s: 1, m: 60, h: 3600, d: 86400 };
112
+ /** Coerce a lifetime to seconds — a bare number passes through; `"1h"` → 3600. */
113
+ function seconds(value) {
114
+ if (typeof value === "number")
115
+ return value;
116
+ const match = DURATION.exec(value.trim());
117
+ if (!match)
118
+ throw new Error(`Invalid duration "${value}" (use e.g. 30, "30s", "15m", "1h", "7d").`);
119
+ return Number(match[1]) * UNIT[match[2]];
120
+ }
121
+ /* base64url — JWT segments use the URL-safe alphabet with no padding. */
122
+ function b64url(bytes) {
123
+ return b64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
124
+ }
125
+ function fromB64url(str) {
126
+ const pad = str.length % 4 ? "=".repeat(4 - (str.length % 4)) : "";
127
+ return fromB64(str.replace(/-/g, "+").replace(/_/g, "/") + pad);
128
+ }
129
+ function b64urlJson(value) {
130
+ return b64url(new TextEncoder().encode(JSON.stringify(value)));
131
+ }
132
+ /** HMAC-SHA256 the signing input, returned base64url — the JWT signature. */
133
+ async function hmacSha256(data, secret) {
134
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
135
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));
136
+ return b64url(new Uint8Array(sig));
137
+ }
138
+ const JWT_HEADER = b64urlJson({ alg: "HS256", typ: "JWT" });
139
+ export const jwt = {
140
+ /** Sign a payload into an HS256 JWT. Adds `iat`, and `exp` when `expiresIn` is set. */
141
+ async sign(payload, options = {}) {
142
+ const now = Math.floor(Date.now() / 1000);
143
+ const claims = { ...payload, iat: now };
144
+ if (options.subject !== undefined)
145
+ claims.sub = options.subject;
146
+ if (options.issuer !== undefined)
147
+ claims.iss = options.issuer;
148
+ if (options.audience !== undefined)
149
+ claims.aud = options.audience;
150
+ if (options.expiresIn !== undefined)
151
+ claims.exp = now + seconds(options.expiresIn);
152
+ const body = `${JWT_HEADER}.${b64urlJson(claims)}`;
153
+ const sig = await hmacSha256(body, options.secret ?? appKey());
154
+ return `${body}.${sig}`;
155
+ },
156
+ /**
157
+ * Verify an HS256 JWT and return its payload, or `null` if the token is
158
+ * malformed, tampered, expired, not-yet-valid, or fails an issuer/audience
159
+ * check. Only HS256 is accepted — `alg: none` and asymmetric algs are refused,
160
+ * closing the classic JWT algorithm-confusion hole.
161
+ */
162
+ async verify(token, options = {}) {
163
+ try {
164
+ const parts = token.split(".");
165
+ if (parts.length !== 3)
166
+ return null;
167
+ const [header, body, sig] = parts;
168
+ const alg = JSON.parse(new TextDecoder().decode(fromB64url(header))).alg;
169
+ if (alg !== "HS256")
170
+ return null;
171
+ const expected = await hmacSha256(`${header}.${body}`, options.secret ?? appKey());
172
+ if (!safeEqual(sig, expected))
173
+ return null;
174
+ const claims = JSON.parse(new TextDecoder().decode(fromB64url(body)));
175
+ const now = Math.floor(Date.now() / 1000);
176
+ if (typeof claims.exp === "number" && now >= claims.exp)
177
+ return null;
178
+ if (typeof claims.nbf === "number" && now < claims.nbf)
179
+ return null;
180
+ if (options.issuer !== undefined && claims.iss !== options.issuer)
181
+ return null;
182
+ if (options.audience !== undefined && claims.aud !== options.audience)
183
+ return null;
184
+ return claims;
185
+ }
186
+ catch {
187
+ return null;
188
+ }
189
+ },
190
+ };
@@ -14,10 +14,18 @@ export interface WriteResult {
14
14
  rowsAffected: number;
15
15
  insertId?: number | string;
16
16
  }
17
+ /** A page of results plus pagination metadata, returned by `paginate()`. */
18
+ export interface Paginated<T> {
19
+ data: T[];
20
+ total: number;
21
+ perPage: number;
22
+ currentPage: number;
23
+ lastPage: number;
24
+ }
17
25
  /** The bridge to your database driver. */
18
26
  export interface Connection {
19
27
  /** Run a SELECT (or any row-returning query) and return the rows. */
20
- select<T = Row>(sql: string, bindings: unknown[]): Promise<T[]>;
28
+ select(sql: string, bindings: unknown[]): Promise<Row[]>;
21
29
  /** Run an INSERT/UPDATE/DELETE and return write metadata. */
22
30
  write(sql: string, bindings: unknown[]): Promise<WriteResult>;
23
31
  }
@@ -41,7 +49,13 @@ export declare class QueryBuilder<T extends Row = Row> {
41
49
  whereIn(column: string, values: unknown[]): this;
42
50
  whereNull(column: string): this;
43
51
  whereNotNull(column: string): this;
52
+ whereNotIn(column: string, values: unknown[]): this;
53
+ whereBetween(column: string, [min, max]: [unknown, unknown]): this;
54
+ whereLike(column: string, pattern: string): this;
44
55
  orderBy(column: string, direction?: "asc" | "desc"): this;
56
+ /** Newest-first / oldest-first by a timestamp column (default `created_at`). */
57
+ latest(column?: string): this;
58
+ oldest(column?: string): this;
45
59
  limit(n: number): this;
46
60
  offset(n: number): this;
47
61
  private whereClause;
@@ -49,6 +63,17 @@ export declare class QueryBuilder<T extends Row = Row> {
49
63
  first(): Promise<T | null>;
50
64
  count(): Promise<number>;
51
65
  exists(): Promise<boolean>;
66
+ private aggregate;
67
+ sum(column: string): Promise<number>;
68
+ avg(column: string): Promise<number>;
69
+ min(column: string): Promise<number>;
70
+ max(column: string): Promise<number>;
71
+ /** The value of a single column from the first matching row (or null). */
72
+ value<V = unknown>(column: string): Promise<V | null>;
73
+ /** An array of a single column across all matching rows. */
74
+ pluck<V = unknown>(column: string): Promise<V[]>;
75
+ /** A page of results plus pagination metadata. */
76
+ paginate(page?: number, perPage?: number): Promise<Paginated<T>>;
52
77
  insert(data: Row): Promise<WriteResult>;
53
78
  insertGetId(data: Row): Promise<number | string | undefined>;
54
79
  update(data: Row): Promise<WriteResult>;
@@ -65,10 +65,30 @@ export class QueryBuilder {
65
65
  this.wheres.push({ boolean: "AND", sql: `${column} IS NOT NULL`, bindings: [] });
66
66
  return this;
67
67
  }
68
+ whereNotIn(column, values) {
69
+ const marks = values.map(() => "?").join(", ");
70
+ this.wheres.push({ boolean: "AND", sql: `${column} NOT IN (${marks})`, bindings: values });
71
+ return this;
72
+ }
73
+ whereBetween(column, [min, max]) {
74
+ this.wheres.push({ boolean: "AND", sql: `${column} BETWEEN ? AND ?`, bindings: [min, max] });
75
+ return this;
76
+ }
77
+ whereLike(column, pattern) {
78
+ this.wheres.push({ boolean: "AND", sql: `${column} LIKE ?`, bindings: [pattern] });
79
+ return this;
80
+ }
68
81
  orderBy(column, direction = "asc") {
69
82
  this.orders.push(`${column} ${direction.toUpperCase()}`);
70
83
  return this;
71
84
  }
85
+ /** Newest-first / oldest-first by a timestamp column (default `created_at`). */
86
+ latest(column = "created_at") {
87
+ return this.orderBy(column, "desc");
88
+ }
89
+ oldest(column = "created_at") {
90
+ return this.orderBy(column, "asc");
91
+ }
72
92
  limit(n) {
73
93
  this._limit = n;
74
94
  return this;
@@ -94,7 +114,7 @@ export class QueryBuilder {
94
114
  sql += ` LIMIT ${this._limit}`;
95
115
  if (this._offset != null)
96
116
  sql += ` OFFSET ${this._offset}`;
97
- return conn().select(placeholders(sql), where.bindings);
117
+ return (await conn().select(placeholders(sql), where.bindings));
98
118
  }
99
119
  async first() {
100
120
  this._limit = 1;
@@ -103,12 +123,55 @@ export class QueryBuilder {
103
123
  }
104
124
  async count() {
105
125
  const where = this.whereClause();
106
- const rows = await conn().select(placeholders(`SELECT COUNT(*) AS count FROM ${this.table}${where.sql}`), where.bindings);
126
+ const rows = (await conn().select(placeholders(`SELECT COUNT(*) AS count FROM ${this.table}${where.sql}`), where.bindings));
107
127
  return Number(rows[0]?.count ?? 0);
108
128
  }
109
129
  async exists() {
110
130
  return (await this.count()) > 0;
111
131
  }
132
+ async aggregate(fn, column) {
133
+ const where = this.whereClause();
134
+ const rows = (await conn().select(placeholders(`SELECT ${fn}(${column}) AS agg FROM ${this.table}${where.sql}`), where.bindings));
135
+ return Number(rows[0]?.agg ?? 0);
136
+ }
137
+ sum(column) {
138
+ return this.aggregate("SUM", column);
139
+ }
140
+ avg(column) {
141
+ return this.aggregate("AVG", column);
142
+ }
143
+ min(column) {
144
+ return this.aggregate("MIN", column);
145
+ }
146
+ max(column) {
147
+ return this.aggregate("MAX", column);
148
+ }
149
+ /** The value of a single column from the first matching row (or null). */
150
+ async value(column) {
151
+ this.columns = column;
152
+ const row = await this.first();
153
+ return row ? row[column] : null;
154
+ }
155
+ /** An array of a single column across all matching rows. */
156
+ async pluck(column) {
157
+ this.columns = column;
158
+ const rows = await this.get();
159
+ return rows.map((row) => row[column]);
160
+ }
161
+ /** A page of results plus pagination metadata. */
162
+ async paginate(page = 1, perPage = 15) {
163
+ const total = await this.count();
164
+ this._limit = perPage;
165
+ this._offset = (Math.max(1, page) - 1) * perPage;
166
+ const data = await this.get();
167
+ return {
168
+ data,
169
+ total,
170
+ perPage,
171
+ currentPage: page,
172
+ lastPage: Math.max(1, Math.ceil(total / perPage)),
173
+ };
174
+ }
112
175
  /* ------------------------------- writes ------------------------------ */
113
176
  async insert(data) {
114
177
  const keys = Object.keys(data);
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Request decorators — attach named, computed values to the current request,
3
+ * resolved lazily and memoized for the life of that request. Inspired by
4
+ * Fastify's `decorateRequest`, but without its footguns: you register a resolver
5
+ * once, and Keel computes it on first access and caches it per request (keyed off
6
+ * the Hono context via a WeakMap, so there's no shared-state leak between
7
+ * requests and no V8 shape deopt to design around).
8
+ *
9
+ * decorateRequest("user", async (c) => findUser(c.req.header("authorization")));
10
+ *
11
+ * // anywhere in the request — computed once, then cached:
12
+ * const user = await decorated<User>("user");
13
+ *
14
+ * (Decorating the *application* is already the service container's job —
15
+ * `bind` / `singleton` / `instance` / `make`, with `bound()` as `hasDecorator`.)
16
+ */
17
+ import type { Context } from "hono";
18
+ /** Resolves a decorator's value from the current request context. */
19
+ export type RequestResolver<T = unknown> = (c: Context) => T | Promise<T>;
20
+ /**
21
+ * Register a request decorator. Throws if `name` is already registered — a
22
+ * collision guard, like Fastify's.
23
+ */
24
+ export declare function decorateRequest<T>(name: string, resolver: RequestResolver<T>): void;
25
+ /** Whether a request decorator has been registered. */
26
+ export declare function hasRequestDecorator(name: string): boolean;
27
+ /**
28
+ * The value of a decorator for the current request — computed on first access
29
+ * via its resolver, then cached for the rest of the request. Always returns a
30
+ * promise (resolvers may be async).
31
+ */
32
+ export declare function decorated<T = unknown>(name: string): Promise<T>;
33
+ /**
34
+ * Imperatively set a decorator's value for the current request, overriding the
35
+ * resolver — e.g. from an auth middleware that already resolved the user.
36
+ */
37
+ export declare function setRequestValue<T>(name: string, value: T): void;
38
+ /** Clear all registered decorators (test helper). */
39
+ export declare function clearRequestDecorators(): void;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Request decorators — attach named, computed values to the current request,
3
+ * resolved lazily and memoized for the life of that request. Inspired by
4
+ * Fastify's `decorateRequest`, but without its footguns: you register a resolver
5
+ * once, and Keel computes it on first access and caches it per request (keyed off
6
+ * the Hono context via a WeakMap, so there's no shared-state leak between
7
+ * requests and no V8 shape deopt to design around).
8
+ *
9
+ * decorateRequest("user", async (c) => findUser(c.req.header("authorization")));
10
+ *
11
+ * // anywhere in the request — computed once, then cached:
12
+ * const user = await decorated<User>("user");
13
+ *
14
+ * (Decorating the *application* is already the service container's job —
15
+ * `bind` / `singleton` / `instance` / `make`, with `bound()` as `hasDecorator`.)
16
+ */
17
+ import { ctx } from "./request.js";
18
+ const resolvers = new Map();
19
+ // Per-request memo, keyed by the context object so it's GC'd with the request.
20
+ const memo = new WeakMap();
21
+ function bag(c) {
22
+ let b = memo.get(c);
23
+ if (!b) {
24
+ b = new Map();
25
+ memo.set(c, b);
26
+ }
27
+ return b;
28
+ }
29
+ /**
30
+ * Register a request decorator. Throws if `name` is already registered — a
31
+ * collision guard, like Fastify's.
32
+ */
33
+ export function decorateRequest(name, resolver) {
34
+ if (resolvers.has(name)) {
35
+ throw new Error(`Request decorator "${name}" is already registered.`);
36
+ }
37
+ resolvers.set(name, resolver);
38
+ }
39
+ /** Whether a request decorator has been registered. */
40
+ export function hasRequestDecorator(name) {
41
+ return resolvers.has(name);
42
+ }
43
+ /**
44
+ * The value of a decorator for the current request — computed on first access
45
+ * via its resolver, then cached for the rest of the request. Always returns a
46
+ * promise (resolvers may be async).
47
+ */
48
+ export function decorated(name) {
49
+ const c = ctx();
50
+ const b = bag(c);
51
+ if (b.has(name))
52
+ return Promise.resolve(b.get(name));
53
+ const resolver = resolvers.get(name);
54
+ if (!resolver) {
55
+ throw new Error(`No request decorator "${name}". Register it with decorateRequest().`);
56
+ }
57
+ // Memoize the promise so concurrent access computes once.
58
+ const result = Promise.resolve(resolver(c));
59
+ b.set(name, result);
60
+ return result;
61
+ }
62
+ /**
63
+ * Imperatively set a decorator's value for the current request, overriding the
64
+ * resolver — e.g. from an auth middleware that already resolved the user.
65
+ */
66
+ export function setRequestValue(name, value) {
67
+ bag(ctx()).set(name, value);
68
+ }
69
+ /** Clear all registered decorators (test helper). */
70
+ export function clearRequestDecorators() {
71
+ resolvers.clear();
72
+ }
@@ -17,19 +17,90 @@ export declare class HttpException extends Error {
17
17
  readonly headers?: Record<string, string>;
18
18
  /** A machine-readable error code, e.g. "E_VALIDATION". */
19
19
  code?: string;
20
- constructor(status: number, message?: string, headers?: Record<string, string>);
20
+ /** Structured context attached to the error; surfaced in the JSON body as `data`. */
21
+ data?: unknown;
22
+ constructor(status: number, message?: string, headers?: Record<string, string>, data?: unknown);
23
+ /**
24
+ * The error as a plain object, matching the JSON body the HTTP kernel renders:
25
+ * `{ error, status }` plus `code` and `data` when present. Subclasses that add
26
+ * fields (e.g. `ValidationException.errors`) override this to include them.
27
+ */
28
+ toJSON(): Record<string, unknown>;
21
29
  }
22
- export declare class NotFoundException extends HttpException {
23
- constructor(message?: string);
30
+ /** 400 the request was malformed or failed validation. */
31
+ export declare class BadRequestException extends HttpException {
32
+ constructor(message?: string, data?: unknown);
24
33
  }
25
34
  export declare class UnauthorizedException extends HttpException {
26
- constructor(message?: string);
35
+ constructor(message?: string, data?: unknown);
36
+ }
37
+ /** 402 — payment is required to access the resource. */
38
+ export declare class PaymentRequiredException extends HttpException {
39
+ constructor(message?: string, data?: unknown);
27
40
  }
28
41
  export declare class ForbiddenException extends HttpException {
29
- constructor(message?: string);
42
+ constructor(message?: string, data?: unknown);
43
+ }
44
+ export declare class NotFoundException extends HttpException {
45
+ constructor(message?: string, data?: unknown);
46
+ }
47
+ /** 405 — the HTTP method isn't allowed for this resource. */
48
+ export declare class MethodNotAllowedException extends HttpException {
49
+ constructor(message?: string, data?: unknown);
50
+ }
51
+ /** 406 — no representation matches the request's `Accept` header. */
52
+ export declare class NotAcceptableException extends HttpException {
53
+ constructor(message?: string, data?: unknown);
54
+ }
55
+ /** 408 — the client took too long to send the request. */
56
+ export declare class RequestTimeoutException extends HttpException {
57
+ constructor(message?: string, data?: unknown);
58
+ }
59
+ /** 409 — the request conflicts with the current state (e.g. a duplicate). */
60
+ export declare class ConflictException extends HttpException {
61
+ constructor(message?: string, data?: unknown);
62
+ }
63
+ /** 411 — a `Content-Length` header is required and was missing. */
64
+ export declare class LengthRequiredException extends HttpException {
65
+ constructor(message?: string, data?: unknown);
30
66
  }
31
- /** 422 with per-field messages. Pairs with a future validation layer. */
67
+ /** 422 with per-field messages. Pairs with the validation layer. */
32
68
  export declare class ValidationException extends HttpException {
33
69
  readonly errors: Record<string, string[]>;
34
70
  constructor(errors: Record<string, string[]>, message?: string);
71
+ toJSON(): Record<string, unknown>;
35
72
  }
73
+ /** 429 — the client has sent too many requests in a given window. */
74
+ export declare class TooManyRequestsException extends HttpException {
75
+ constructor(message?: string, data?: unknown);
76
+ }
77
+ /** 500 — a catch-all server error you want to raise deliberately. */
78
+ export declare class ServerErrorException extends HttpException {
79
+ constructor(message?: string, data?: unknown);
80
+ }
81
+ /** 501 — the requested functionality isn't implemented. */
82
+ export declare class NotImplementedException extends HttpException {
83
+ constructor(message?: string, data?: unknown);
84
+ }
85
+ /** 502 — an upstream server returned an invalid response. */
86
+ export declare class BadGatewayException extends HttpException {
87
+ constructor(message?: string, data?: unknown);
88
+ }
89
+ /** 503 — the service is temporarily unavailable (down, overloaded). */
90
+ export declare class ServiceUnavailableException extends HttpException {
91
+ constructor(message?: string, data?: unknown);
92
+ }
93
+ /**
94
+ * Mint a reusable, coded `HttpException` subclass — the ergonomic way to define
95
+ * app-specific errors with a stable, machine-readable `code`. Inspired by
96
+ * `@fastify/error`. The `message` may carry `%s` placeholders, filled in order
97
+ * from the constructor arguments.
98
+ *
99
+ * const InsufficientFunds = createError("E_FUNDS", "Balance too low: need %s", 402);
100
+ * throw new InsufficientFunds("$40");
101
+ * // → 402 { error: "Balance too low: need $40", status: 402, code: "E_FUNDS" }
102
+ *
103
+ * The returned class extends `HttpException`, so it renders through the same
104
+ * path — `code` lands in the JSON body — and passes `instanceof HttpException`.
105
+ */
106
+ export declare function createError(code: string, message: string, status?: number): new (...args: (string | number)[]) => HttpException;