@shaferllc/keel 0.59.0 → 0.66.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.
@@ -0,0 +1,78 @@
1
+ /**
2
+ * CSRF protection for server-rendered apps. A synchronizer token lives in the
3
+ * session; state-changing requests (`POST`/`PUT`/`PATCH`/`DELETE`) must echo it
4
+ * back, or they're rejected with `419 Page Expired`. Needs `sessionMiddleware()`.
5
+ *
6
+ * this.use(sessionMiddleware());
7
+ * this.use(csrf());
8
+ *
9
+ * Put the token in forms with `csrfField()`; SPAs get it for free — `csrf()` also
10
+ * writes an `XSRF-TOKEN` cookie that axios/fetch libraries send back as the
11
+ * `X-XSRF-TOKEN` header automatically. The token is also read from a `_token` or
12
+ * `_csrf` field, or the `X-CSRF-Token` header.
13
+ */
14
+ import { setCookie } from "hono/cookie";
15
+ import { HttpException } from "./exceptions.js";
16
+ import { session } from "./session.js";
17
+ import { request } from "./request.js";
18
+ const KEY = "_csrf";
19
+ const UNSAFE = new Set(["POST", "PUT", "PATCH", "DELETE"]);
20
+ function randomToken() {
21
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
22
+ let s = "";
23
+ for (const b of bytes)
24
+ s += String.fromCharCode(b);
25
+ return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
26
+ }
27
+ function safeEqual(a, b) {
28
+ if (a.length !== b.length)
29
+ return false;
30
+ let result = 0;
31
+ for (let i = 0; i < a.length; i++)
32
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
33
+ return result === 0;
34
+ }
35
+ /** The current request's CSRF token, minting and storing one if absent. */
36
+ export function csrfToken() {
37
+ const sess = session();
38
+ let token = sess.get(KEY, undefined);
39
+ if (!token) {
40
+ token = randomToken();
41
+ sess.put(KEY, token);
42
+ }
43
+ return token;
44
+ }
45
+ /** A hidden form input carrying the CSRF token — drop it into any `<form method="POST">`. */
46
+ export function csrfField() {
47
+ return `<input type="hidden" name="_token" value="${csrfToken()}">`;
48
+ }
49
+ async function submitted(c) {
50
+ const header = c.req.header("x-csrf-token") ?? c.req.header("x-xsrf-token");
51
+ if (header)
52
+ return header;
53
+ const all = await request.all(); // shares keel's body cache — safe to read
54
+ return (all._token ?? all._csrf);
55
+ }
56
+ function isExcepted(path, except) {
57
+ return except.some((rule) => typeof rule === "string"
58
+ ? rule.endsWith("*")
59
+ ? path.startsWith(rule.slice(0, -1))
60
+ : path === rule
61
+ : rule.test(path));
62
+ }
63
+ export function csrf(options = {}) {
64
+ const except = options.except ?? [];
65
+ return async (c, next) => {
66
+ const token = csrfToken(); // ensures a token exists in the session
67
+ if (options.cookie !== false) {
68
+ setCookie(c, "XSRF-TOKEN", token, { path: "/", sameSite: "Lax" }); // readable by JS on purpose
69
+ }
70
+ if (UNSAFE.has(c.req.method.toUpperCase()) && !isExcepted(c.req.path, except)) {
71
+ const provided = await submitted(c);
72
+ if (!provided || !safeEqual(provided, token)) {
73
+ throw new HttpException(419, "CSRF token mismatch");
74
+ }
75
+ }
76
+ await next();
77
+ };
78
+ }
@@ -31,16 +31,40 @@ export interface Connection {
31
31
  }
32
32
  export type Dialect = "sqlite" | "mysql" | "postgres";
33
33
  export type Operator = "=" | "!=" | "<" | "<=" | ">" | ">=" | "like";
34
- /** Register the database connection (and dialect) used by `db()`. */
34
+ /** A registered connection — the driver bridge plus its SQL dialect. */
35
+ interface Source {
36
+ conn: Connection;
37
+ dialect: Dialect;
38
+ }
39
+ /** Register the default connection (and dialect) used by `db()`. */
35
40
  export declare function setConnection(conn: Connection, driverDialect?: Dialect): void;
41
+ /**
42
+ * Register a *named* connection alongside any others — the way to use more than
43
+ * one database. Point a query or model at it by name; nothing else changes.
44
+ *
45
+ * addConnection("reporting", pgConn, "postgres");
46
+ * await db("events", "reporting").where("kind", "signup").count();
47
+ */
48
+ export declare function addConnection(name: string, conn: Connection, driverDialect?: Dialect): void;
49
+ /** Choose which registered connection `db()` and models use when none is named. */
50
+ export declare function setDefaultConnection(name: string): void;
51
+ /** The names of every registered connection. */
52
+ export declare function connectionNames(): string[];
53
+ /** Unregister every connection — a test helper for a clean slate. */
54
+ export declare function clearConnections(): void;
36
55
  export declare class QueryBuilder<T extends Row = Row> {
37
56
  private table;
57
+ private getSource;
38
58
  private wheres;
39
59
  private orders;
40
60
  private columns;
41
61
  private _limit?;
42
62
  private _offset?;
43
- constructor(table: string);
63
+ constructor(table: string, getSource: () => Source);
64
+ /** Run a row-returning query on this builder's connection, dialect-adjusted. */
65
+ private runSelect;
66
+ /** Run a write on this builder's connection, dialect-adjusted. */
67
+ private runWrite;
44
68
  select(...columns: string[]): this;
45
69
  where(column: string, value: unknown): this;
46
70
  where(column: string, operator: Operator, value: unknown): this;
@@ -79,5 +103,26 @@ export declare class QueryBuilder<T extends Row = Row> {
79
103
  update(data: Row): Promise<WriteResult>;
80
104
  delete(): Promise<WriteResult>;
81
105
  }
82
- /** Start a query against a table. */
83
- export declare function db<T extends Row = Row>(table: string): QueryBuilder<T>;
106
+ /** Start a query against a table, on the default connection or a named one. */
107
+ export declare function db<T extends Row = Row>(table: string, connectionName?: string): QueryBuilder<T>;
108
+ /** A handle to one registered connection — query it, or run raw SQL on it. */
109
+ export interface ConnectionHandle {
110
+ /** Start a query against a table on this connection. */
111
+ table<T extends Row = Row>(table: string): QueryBuilder<T>;
112
+ /** Run a raw row-returning query (`?` placeholders, dialect-adjusted). */
113
+ select(sql: string, bindings?: unknown[]): Promise<Row[]>;
114
+ /** Run a raw write (`?` placeholders, dialect-adjusted). */
115
+ write(sql: string, bindings?: unknown[]): Promise<WriteResult>;
116
+ /** This connection's SQL dialect. */
117
+ readonly dialect: Dialect;
118
+ }
119
+ /**
120
+ * Get a handle to a named connection (or the default). Use it to run several
121
+ * queries against one database without repeating the name, or to reach the raw
122
+ * `select`/`write` bridge.
123
+ *
124
+ * const reporting = connection("reporting");
125
+ * await reporting.table("events").where("kind", "signup").count();
126
+ */
127
+ export declare function connection(name?: string): ConnectionHandle;
128
+ export {};
@@ -9,20 +9,56 @@
9
9
  * const user = await db("users").where("id", 1).first();
10
10
  * await db("users").insert({ email });
11
11
  */
12
- let connection;
13
- let dialect = "sqlite";
14
- /** Register the database connection (and dialect) used by `db()`. */
12
+ /**
13
+ * The connection registry. An app can talk to several databases at once —
14
+ * register each by name, then route a query with `db(table, name)`, a whole
15
+ * model with `static connection`, or a handle from `connection(name)`. The
16
+ * unnamed default lives under `"default"` so `db(table)` and `setConnection()`
17
+ * keep working unchanged.
18
+ */
19
+ const registry = new Map();
20
+ let defaultConnection = "default";
21
+ /** Register the default connection (and dialect) used by `db()`. */
15
22
  export function setConnection(conn, driverDialect = "sqlite") {
16
- connection = conn;
17
- dialect = driverDialect;
23
+ registry.set("default", { conn, dialect: driverDialect });
24
+ defaultConnection = "default";
25
+ }
26
+ /**
27
+ * Register a *named* connection alongside any others — the way to use more than
28
+ * one database. Point a query or model at it by name; nothing else changes.
29
+ *
30
+ * addConnection("reporting", pgConn, "postgres");
31
+ * await db("events", "reporting").where("kind", "signup").count();
32
+ */
33
+ export function addConnection(name, conn, driverDialect = "sqlite") {
34
+ registry.set(name, { conn, dialect: driverDialect });
35
+ }
36
+ /** Choose which registered connection `db()` and models use when none is named. */
37
+ export function setDefaultConnection(name) {
38
+ if (!registry.has(name))
39
+ throw new Error(`No database connection "${name}" to make default.`);
40
+ defaultConnection = name;
41
+ }
42
+ /** The names of every registered connection. */
43
+ export function connectionNames() {
44
+ return [...registry.keys()];
18
45
  }
19
- function conn() {
20
- if (!connection)
21
- throw new Error("No database connection. Call setConnection(conn, dialect).");
22
- return connection;
46
+ /** Unregister every connection — a test helper for a clean slate. */
47
+ export function clearConnections() {
48
+ registry.clear();
49
+ defaultConnection = "default";
23
50
  }
24
- /** Render `?` placeholders for the active dialect (Postgres uses $1, $2, …). */
25
- function placeholders(sql) {
51
+ /** Resolve a connection by name (or the default); throws if it isn't registered. */
52
+ function resolve(name) {
53
+ const source = registry.get(name ?? defaultConnection);
54
+ if (!source) {
55
+ throw new Error(`No database connection${name ? ` "${name}"` : ""}. ` +
56
+ `Call setConnection(conn, dialect) or addConnection(name, conn, dialect).`);
57
+ }
58
+ return source;
59
+ }
60
+ /** Render `?` placeholders for a dialect (Postgres uses $1, $2, …). */
61
+ function placeholders(sql, dialect) {
26
62
  if (dialect !== "postgres")
27
63
  return sql;
28
64
  let i = 0;
@@ -30,13 +66,28 @@ function placeholders(sql) {
30
66
  }
31
67
  export class QueryBuilder {
32
68
  table;
69
+ getSource;
33
70
  wheres = [];
34
71
  orders = [];
35
72
  columns = "*";
36
73
  _limit;
37
74
  _offset;
38
- constructor(table) {
75
+ // The connection is resolved lazily, when a query actually runs — so building
76
+ // a query never throws, and an unregistered connection surfaces as a rejected
77
+ // read/write rather than a synchronous error at construction.
78
+ constructor(table, getSource) {
39
79
  this.table = table;
80
+ this.getSource = getSource;
81
+ }
82
+ /** Run a row-returning query on this builder's connection, dialect-adjusted. */
83
+ runSelect(sql, bindings) {
84
+ const source = this.getSource();
85
+ return source.conn.select(placeholders(sql, source.dialect), bindings);
86
+ }
87
+ /** Run a write on this builder's connection, dialect-adjusted. */
88
+ runWrite(sql, bindings) {
89
+ const source = this.getSource();
90
+ return source.conn.write(placeholders(sql, source.dialect), bindings);
40
91
  }
41
92
  select(...columns) {
42
93
  this.columns = columns.length ? columns.join(", ") : "*";
@@ -114,7 +165,7 @@ export class QueryBuilder {
114
165
  sql += ` LIMIT ${this._limit}`;
115
166
  if (this._offset != null)
116
167
  sql += ` OFFSET ${this._offset}`;
117
- return (await conn().select(placeholders(sql), where.bindings));
168
+ return (await this.runSelect(sql, where.bindings));
118
169
  }
119
170
  async first() {
120
171
  this._limit = 1;
@@ -123,7 +174,7 @@ export class QueryBuilder {
123
174
  }
124
175
  async count() {
125
176
  const where = this.whereClause();
126
- const rows = (await conn().select(placeholders(`SELECT COUNT(*) AS count FROM ${this.table}${where.sql}`), where.bindings));
177
+ const rows = (await this.runSelect(`SELECT COUNT(*) AS count FROM ${this.table}${where.sql}`, where.bindings));
127
178
  return Number(rows[0]?.count ?? 0);
128
179
  }
129
180
  async exists() {
@@ -131,7 +182,7 @@ export class QueryBuilder {
131
182
  }
132
183
  async aggregate(fn, column) {
133
184
  const where = this.whereClause();
134
- const rows = (await conn().select(placeholders(`SELECT ${fn}(${column}) AS agg FROM ${this.table}${where.sql}`), where.bindings));
185
+ const rows = (await this.runSelect(`SELECT ${fn}(${column}) AS agg FROM ${this.table}${where.sql}`, where.bindings));
135
186
  return Number(rows[0]?.agg ?? 0);
136
187
  }
137
188
  sum(column) {
@@ -178,7 +229,7 @@ export class QueryBuilder {
178
229
  const sql = `INSERT INTO ${this.table} (${keys.join(", ")}) VALUES (${keys
179
230
  .map(() => "?")
180
231
  .join(", ")})`;
181
- return conn().write(placeholders(sql), Object.values(data));
232
+ return this.runWrite(sql, Object.values(data));
182
233
  }
183
234
  async insertGetId(data) {
184
235
  return (await this.insert(data)).insertId;
@@ -187,17 +238,34 @@ export class QueryBuilder {
187
238
  const keys = Object.keys(data);
188
239
  const set = keys.map((k) => `${k} = ?`).join(", ");
189
240
  const where = this.whereClause();
190
- return conn().write(placeholders(`UPDATE ${this.table} SET ${set}${where.sql}`), [
241
+ return this.runWrite(`UPDATE ${this.table} SET ${set}${where.sql}`, [
191
242
  ...Object.values(data),
192
243
  ...where.bindings,
193
244
  ]);
194
245
  }
195
246
  async delete() {
196
247
  const where = this.whereClause();
197
- return conn().write(placeholders(`DELETE FROM ${this.table}${where.sql}`), where.bindings);
248
+ return this.runWrite(`DELETE FROM ${this.table}${where.sql}`, where.bindings);
198
249
  }
199
250
  }
200
- /** Start a query against a table. */
201
- export function db(table) {
202
- return new QueryBuilder(table);
251
+ /** Start a query against a table, on the default connection or a named one. */
252
+ export function db(table, connectionName) {
253
+ return new QueryBuilder(table, () => resolve(connectionName));
254
+ }
255
+ /**
256
+ * Get a handle to a named connection (or the default). Use it to run several
257
+ * queries against one database without repeating the name, or to reach the raw
258
+ * `select`/`write` bridge.
259
+ *
260
+ * const reporting = connection("reporting");
261
+ * await reporting.table("events").where("kind", "signup").count();
262
+ */
263
+ export function connection(name) {
264
+ const source = resolve(name);
265
+ return {
266
+ table: (t) => new QueryBuilder(t, () => source),
267
+ select: (sql, bindings = []) => source.conn.select(placeholders(sql, source.dialect), bindings),
268
+ write: (sql, bindings = []) => source.conn.write(placeholders(sql, source.dialect), bindings),
269
+ dialect: source.dialect,
270
+ };
203
271
  }
@@ -38,6 +38,12 @@ export declare function instance<T>(token: Token<T>, value: T): T;
38
38
  export declare function make<T>(token: Token<T>): T;
39
39
  /** Whether a token is bound or has a cached instance. */
40
40
  export declare function bound(token: Token): boolean;
41
+ /** Register an alias token that resolves to another token. */
42
+ export declare function alias<T>(aliasToken: Token<T>, target: Token<T>): void;
43
+ /** Temporarily replace a binding with a fake (tests). Undo with `restore()`. */
44
+ export declare function swap<T>(token: Token<T>, factory: Factory<T>): void;
45
+ /** Undo a `swap()` — restore the original binding. No token restores every swap. */
46
+ export declare function restore(token?: Token): void;
41
47
  /** The application's event emitter. */
42
48
  export declare function events(): Events;
43
49
  /** Emit an event, awaiting every listener. */
@@ -65,6 +65,18 @@ export function make(token) {
65
65
  export function bound(token) {
66
66
  return app().bound(token);
67
67
  }
68
+ /** Register an alias token that resolves to another token. */
69
+ export function alias(aliasToken, target) {
70
+ app().alias(aliasToken, target);
71
+ }
72
+ /** Temporarily replace a binding with a fake (tests). Undo with `restore()`. */
73
+ export function swap(token, factory) {
74
+ app().swap(token, factory);
75
+ }
76
+ /** Undo a `swap()` — restore the original binding. No token restores every swap. */
77
+ export function restore(token) {
78
+ app().restore(token);
79
+ }
68
80
  /* ------------------------------- events -------------------------------- */
69
81
  /** The application's event emitter. */
70
82
  export function events() {
@@ -4,7 +4,7 @@ export type { Token, Constructor, Factory } from "./container.js";
4
4
  export { Application } from "./application.js";
5
5
  export type { BootOptions, LifecycleHook, Configurator } from "./application.js";
6
6
  export { Config, env } from "./config.js";
7
- export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
7
+ export { app, config, view, bind, singleton, instance, make, bound, alias, swap, restore, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
8
8
  export { Logger } from "./logger.js";
9
9
  export type { LogLevel, LoggerOptions } from "./logger.js";
10
10
  export { requestLogger, requestLog } from "./request-logger.js";
@@ -19,11 +19,17 @@ export { Storage, MemoryDisk, storage, setDisk } from "./storage.js";
19
19
  export type { Disk, Contents } from "./storage.js";
20
20
  export { dump, dd } from "./debug.js";
21
21
  export { hash, encryption, jwt } from "./crypto.js";
22
- export type { JwtPayload, JwtSignOptions, JwtVerifyOptions } from "./crypto.js";
22
+ export type { JwtPayload, JwtSignOptions, JwtVerifyOptions, EncryptOptions } from "./crypto.js";
23
23
  export { rateLimiter } from "./rate-limit.js";
24
24
  export type { RateLimiterOptions } from "./rate-limit.js";
25
- export { db, setConnection, QueryBuilder } from "./database.js";
26
- export type { Connection, WriteResult, Row, Dialect, Operator, Paginated } from "./database.js";
25
+ export { cors } from "./cors.js";
26
+ export type { CorsOptions } from "./cors.js";
27
+ export { securityHeaders } from "./shield.js";
28
+ export type { SecurityHeadersOptions, HstsOptions } from "./shield.js";
29
+ export { csrf, csrfToken, csrfField } from "./csrf.js";
30
+ export type { CsrfOptions } from "./csrf.js";
31
+ export { db, connection, setConnection, addConnection, setDefaultConnection, connectionNames, clearConnections, QueryBuilder, } from "./database.js";
32
+ export type { Connection, ConnectionHandle, WriteResult, Row, Dialect, Operator, Paginated } from "./database.js";
27
33
  export { Model } from "./model.js";
28
34
  export type { CastType, Casts } from "./casts.js";
29
35
  export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations.js";
@@ -63,10 +69,14 @@ export { validate, validateRequest, validated } from "./validation.js";
63
69
  export type { Schema, RequestSchemas } from "./validation.js";
64
70
  export { Session, session, sessionMiddleware } from "./session.js";
65
71
  export type { SessionOptions } from "./session.js";
66
- export { Auth, auth, authGuard, bearerAuth, setUserProvider } from "./auth.js";
67
- export type { UserProvider } from "./auth.js";
68
- export { define, policy, gateBefore, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
69
- export type { GateCallback, BeforeCallback } from "./authorization.js";
72
+ export { Auth, auth, authGuard, bearerAuth, basicAuth, tokenAuth, token, tokenCan, setUserProvider } from "./auth.js";
73
+ export type { UserProvider, BasicVerifier } from "./auth.js";
74
+ export { createToken, verifyToken, revokeToken, revokeTokens, listTokens, tokenAllows, tokenDenies, setTokensTable, } from "./tokens.js";
75
+ export type { AccessToken, CreateTokenOptions, IssuedToken } from "./tokens.js";
76
+ export { social, github, google, discord, oauthDriver, oauthState, OAuthDriver, OAuthError, twitter, oauth1Driver, oauth1Signature, OAuth1Driver, } from "./social.js";
77
+ export type { SocialUser, OAuthToken, OAuthConfig, ProviderSpec, RedirectOptions, OAuth1Config, OAuth1Token, OAuth1ProviderSpec, } from "./social.js";
78
+ export { define, policy, gateBefore, gateAfter, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
79
+ export type { GateCallback, BeforeCallback, AfterCallback } from "./authorization.js";
70
80
  export { Transformer } from "./transformer.js";
71
81
  export type { Attributes, DocumentOptions } from "./transformer.js";
72
82
  export { Broker, Service, LocalTransporter, ServiceNotFoundError, RequestTimeoutError, broker, setBroker, } from "./broker.js";
@@ -2,7 +2,7 @@
2
2
  export { Container } from "./container.js";
3
3
  export { Application } from "./application.js";
4
4
  export { Config, env } from "./config.js";
5
- export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
5
+ export { app, config, view, bind, singleton, instance, make, bound, alias, swap, restore, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
6
6
  export { Logger } from "./logger.js";
7
7
  export { requestLogger, requestLog } from "./request-logger.js";
8
8
  export { Events } from "./events.js";
@@ -12,7 +12,10 @@ export { Storage, MemoryDisk, storage, setDisk } from "./storage.js";
12
12
  export { dump, dd } from "./debug.js";
13
13
  export { hash, encryption, jwt } from "./crypto.js";
14
14
  export { rateLimiter } from "./rate-limit.js";
15
- export { db, setConnection, QueryBuilder } from "./database.js";
15
+ export { cors } from "./cors.js";
16
+ export { securityHeaders } from "./shield.js";
17
+ export { csrf, csrfToken, csrfField } from "./csrf.js";
18
+ export { db, connection, setConnection, addConnection, setDefaultConnection, connectionNames, clearConnections, QueryBuilder, } from "./database.js";
16
19
  export { Model } from "./model.js";
17
20
  export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations.js";
18
21
  export { Faker, Factory as ModelFactory, factory, Seeder, seed } from "./factory.js";
@@ -35,8 +38,10 @@ export { TestClient, TestResponse, testClient } from "./testing.js";
35
38
  export { HttpException, BadRequestException, UnauthorizedException, PaymentRequiredException, ForbiddenException, NotFoundException, MethodNotAllowedException, NotAcceptableException, RequestTimeoutException, ConflictException, LengthRequiredException, ValidationException, TooManyRequestsException, ServerErrorException, NotImplementedException, BadGatewayException, ServiceUnavailableException, createError, STATUS_TEXT, } from "./exceptions.js";
36
39
  export { validate, validateRequest, validated } from "./validation.js";
37
40
  export { Session, session, sessionMiddleware } from "./session.js";
38
- export { Auth, auth, authGuard, bearerAuth, setUserProvider } from "./auth.js";
39
- export { define, policy, gateBefore, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
41
+ export { Auth, auth, authGuard, bearerAuth, basicAuth, tokenAuth, token, tokenCan, setUserProvider } from "./auth.js";
42
+ export { createToken, verifyToken, revokeToken, revokeTokens, listTokens, tokenAllows, tokenDenies, setTokensTable, } from "./tokens.js";
43
+ export { social, github, google, discord, oauthDriver, oauthState, OAuthDriver, OAuthError, twitter, oauth1Driver, oauth1Signature, OAuth1Driver, } from "./social.js";
44
+ export { define, policy, gateBefore, gateAfter, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
40
45
  export { Transformer } from "./transformer.js";
41
46
  export { Broker, Service, LocalTransporter, ServiceNotFoundError, RequestTimeoutError, broker, setBroker, } from "./broker.js";
42
47
  export { Vite, viteTags, viteAsset, viteReactRefresh } from "./vite.js";
@@ -22,6 +22,8 @@ type ModelClass<T extends Model> = (new (attributes?: Row) => T) & typeof Model;
22
22
  export declare class Model {
23
23
  static table: string;
24
24
  static primaryKey: string;
25
+ /** Which registered connection this model uses; the default when unset. */
26
+ static connection: string | undefined;
25
27
  /** Columns mass-assignable via `create`/`fill` (allowlist). */
26
28
  static fillable: string[];
27
29
  /** Columns NOT mass-assignable (denylist). Ignored when `fillable` is set. */
@@ -34,6 +34,8 @@ function serialize(value) {
34
34
  export class Model {
35
35
  static table = "";
36
36
  static primaryKey = "id";
37
+ /** Which registered connection this model uses; the default when unset. */
38
+ static connection = undefined;
37
39
  /** Columns mass-assignable via `create`/`fill` (allowlist). */
38
40
  static fillable = [];
39
41
  /** Columns NOT mass-assignable (denylist). Ignored when `fillable` is set. */
@@ -51,7 +53,7 @@ export class Model {
51
53
  /* ------------------------------ static -------------------------------- */
52
54
  /** A raw query builder scoped to this model's table. */
53
55
  static query() {
54
- return db(this.table);
56
+ return db(this.table, this.connection);
55
57
  }
56
58
  /** Keep only the attributes mass-assignment allows (fillable / guarded). */
57
59
  static filterFillable(attributes) {
@@ -86,11 +88,11 @@ export class Model {
86
88
  return out;
87
89
  }
88
90
  static async all() {
89
- const rows = await db(this.table).get();
91
+ const rows = await db(this.table, this.connection).get();
90
92
  return rows.map((row) => new this(row));
91
93
  }
92
94
  static async find(id) {
93
- const row = await db(this.table).where(this.primaryKey, id).first();
95
+ const row = await db(this.table, this.connection).where(this.primaryKey, id).first();
94
96
  return row ? new this(row) : null;
95
97
  }
96
98
  static async findOrFail(id) {
@@ -100,18 +102,18 @@ export class Model {
100
102
  return model;
101
103
  }
102
104
  static async first() {
103
- const row = await db(this.table).first();
105
+ const row = await db(this.table, this.connection).first();
104
106
  return row ? new this(row) : null;
105
107
  }
106
108
  /** Fetch models matching a simple equality condition. */
107
109
  static async where(column, value) {
108
- const rows = await db(this.table).where(column, value).get();
110
+ const rows = await db(this.table, this.connection).where(column, value).get();
109
111
  return rows.map((row) => new this(row));
110
112
  }
111
113
  static async create(attributes) {
112
114
  const filtered = this.filterFillable(attributes);
113
115
  const write = this.stampTimestamps(this.toDatabase(filtered), true);
114
- const id = await db(this.table).insertGetId(write);
116
+ const id = await db(this.table, this.connection).insertGetId(write);
115
117
  const model = new this(filtered);
116
118
  if (this.timestamps) {
117
119
  model[this.createdAtColumn] = write[this.createdAtColumn];
@@ -123,7 +125,7 @@ export class Model {
123
125
  }
124
126
  /** A page of models plus pagination metadata. */
125
127
  static async paginate(page = 1, perPage = 15) {
126
- const result = await db(this.table).paginate(page, perPage);
128
+ const result = await db(this.table, this.connection).paginate(page, perPage);
127
129
  return { ...result, data: result.data.map((row) => new this(row)) };
128
130
  }
129
131
  /** Find the first row matching `match`, or create one from `{ ...match, ...values }`. */
@@ -145,7 +147,7 @@ export class Model {
145
147
  }
146
148
  /** A query scoped to every column/value in `match`. */
147
149
  static matching(match) {
148
- let q = db(this.table);
150
+ let q = db(this.table, this.connection);
149
151
  for (const [column, value] of Object.entries(match))
150
152
  q = q.where(column, value);
151
153
  return q;
@@ -209,7 +211,7 @@ export class Model {
209
211
  /** Insert (no primary key) or update (has one). */
210
212
  async save() {
211
213
  const ctor = this.ctor();
212
- const { table, primaryKey } = ctor;
214
+ const { table, primaryKey, connection } = ctor;
213
215
  const idValue = this[primaryKey];
214
216
  const forInsert = idValue == null;
215
217
  const data = ctor.stampTimestamps(ctor.toDatabase({ ...this }), forInsert);
@@ -221,10 +223,10 @@ export class Model {
221
223
  }
222
224
  delete data[primaryKey];
223
225
  if (!forInsert) {
224
- await db(table).where(primaryKey, idValue).update(data);
226
+ await db(table, connection).where(primaryKey, idValue).update(data);
225
227
  }
226
228
  else {
227
- const id = await db(table).insertGetId(data);
229
+ const id = await db(table, connection).insertGetId(data);
228
230
  if (id != null)
229
231
  this[primaryKey] = id;
230
232
  }
@@ -237,14 +239,14 @@ export class Model {
237
239
  /** Reload this model's columns from the database. */
238
240
  async refresh() {
239
241
  const ctor = this.ctor();
240
- const row = await db(ctor.table).where(ctor.primaryKey, this[ctor.primaryKey]).first();
242
+ const row = await db(ctor.table, ctor.connection).where(ctor.primaryKey, this[ctor.primaryKey]).first();
241
243
  if (row)
242
244
  Object.assign(this, applyCasts(row, ctor.casts, castGet));
243
245
  return this;
244
246
  }
245
247
  async delete() {
246
- const { table, primaryKey } = this.ctor();
247
- await db(table).where(primaryKey, this[primaryKey]).delete();
248
+ const { table, primaryKey, connection } = this.ctor();
249
+ await db(table, connection).where(primaryKey, this[primaryKey]).delete();
248
250
  }
249
251
  /** Merge mass-assignable attributes into the model (cast, not saved). */
250
252
  fill(attributes) {
@@ -6,6 +6,11 @@
6
6
  * here — nothing is guaranteed to be registered yet.
7
7
  * boot(): called after every provider has registered. Safe to resolve
8
8
  * and wire things together here.
9
+ * ready(): called once the whole app has booted (after every provider's
10
+ * boot() and any onReady hooks). For work that needs a fully-live
11
+ * app — attaching to the running server, warming a cache.
12
+ * shutdown(): called during graceful termination, in reverse registration
13
+ * order (LIFO). Close connections, flush queues, cancel timers.
9
14
  *
10
15
  * Register with options to make a provider reusable:
11
16
  *
@@ -21,5 +26,7 @@ export declare abstract class ServiceProvider<O = Record<string, unknown>> {
21
26
  constructor(app: Application, options?: O);
22
27
  register(): void | Promise<void>;
23
28
  boot(): void | Promise<void>;
29
+ ready(): void | Promise<void>;
30
+ shutdown(): void | Promise<void>;
24
31
  }
25
32
  export type ProviderClass = new (app: Application, options?: any) => ServiceProvider;
@@ -6,6 +6,11 @@
6
6
  * here — nothing is guaranteed to be registered yet.
7
7
  * boot(): called after every provider has registered. Safe to resolve
8
8
  * and wire things together here.
9
+ * ready(): called once the whole app has booted (after every provider's
10
+ * boot() and any onReady hooks). For work that needs a fully-live
11
+ * app — attaching to the running server, warming a cache.
12
+ * shutdown(): called during graceful termination, in reverse registration
13
+ * order (LIFO). Close connections, flush queues, cancel timers.
9
14
  *
10
15
  * Register with options to make a provider reusable:
11
16
  *
@@ -23,4 +28,6 @@ export class ServiceProvider {
23
28
  }
24
29
  register() { }
25
30
  boot() { }
31
+ ready() { }
32
+ shutdown() { }
26
33
  }
@@ -35,10 +35,12 @@ export function rateLimiter(options = {}) {
35
35
  }
36
36
  bucket.count++;
37
37
  const remaining = Math.max(0, max - bucket.count);
38
+ const resetSeconds = Math.ceil(bucket.reset / 1000);
38
39
  if (bucket.count > max) {
39
40
  return c.json({ error: options.message ?? "Too Many Requests", status: 429 }, 429, {
40
41
  "X-RateLimit-Limit": String(max),
41
42
  "X-RateLimit-Remaining": "0",
43
+ "X-RateLimit-Reset": String(resetSeconds),
42
44
  "Retry-After": String(Math.ceil((bucket.reset - now) / 1000)),
43
45
  });
44
46
  }
@@ -46,5 +48,6 @@ export function rateLimiter(options = {}) {
46
48
  // Set headers on the final response (survives any handler type).
47
49
  c.header("X-RateLimit-Limit", String(max));
48
50
  c.header("X-RateLimit-Remaining", String(remaining));
51
+ c.header("X-RateLimit-Reset", String(resetSeconds));
49
52
  };
50
53
  }