@oakbun/ws 0.1.0 → 0.1.1

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 (124) hide show
  1. package/dist/index.d.ts +998 -0
  2. package/package.json +4 -4
  3. package/dist/core/src/adapter/mysql.d.ts +0 -21
  4. package/dist/core/src/adapter/mysql.d.ts.map +0 -1
  5. package/dist/core/src/adapter/postgres.d.ts +0 -16
  6. package/dist/core/src/adapter/postgres.d.ts.map +0 -1
  7. package/dist/core/src/adapter/resolve.d.ts +0 -16
  8. package/dist/core/src/adapter/resolve.d.ts.map +0 -1
  9. package/dist/core/src/adapter/sqlite.d.ts +0 -17
  10. package/dist/core/src/adapter/sqlite.d.ts.map +0 -1
  11. package/dist/core/src/adapter/types.d.ts +0 -29
  12. package/dist/core/src/adapter/types.d.ts.map +0 -1
  13. package/dist/core/src/app/audit-wiring.d.ts +0 -6
  14. package/dist/core/src/app/audit-wiring.d.ts.map +0 -1
  15. package/dist/core/src/app/body-size-limit.d.ts +0 -29
  16. package/dist/core/src/app/body-size-limit.d.ts.map +0 -1
  17. package/dist/core/src/app/compression.d.ts +0 -25
  18. package/dist/core/src/app/compression.d.ts.map +0 -1
  19. package/dist/core/src/app/cookies.d.ts +0 -17
  20. package/dist/core/src/app/cookies.d.ts.map +0 -1
  21. package/dist/core/src/app/cors.d.ts +0 -65
  22. package/dist/core/src/app/cors.d.ts.map +0 -1
  23. package/dist/core/src/app/csrf.d.ts +0 -52
  24. package/dist/core/src/app/csrf.d.ts.map +0 -1
  25. package/dist/core/src/app/health.d.ts +0 -36
  26. package/dist/core/src/app/health.d.ts.map +0 -1
  27. package/dist/core/src/app/index.d.ts +0 -282
  28. package/dist/core/src/app/index.d.ts.map +0 -1
  29. package/dist/core/src/app/logger.d.ts +0 -4
  30. package/dist/core/src/app/logger.d.ts.map +0 -1
  31. package/dist/core/src/app/middleware.d.ts +0 -20
  32. package/dist/core/src/app/middleware.d.ts.map +0 -1
  33. package/dist/core/src/app/module.d.ts +0 -273
  34. package/dist/core/src/app/module.d.ts.map +0 -1
  35. package/dist/core/src/app/plugin.d.ts +0 -112
  36. package/dist/core/src/app/plugin.d.ts.map +0 -1
  37. package/dist/core/src/app/rate-limit.d.ts +0 -76
  38. package/dist/core/src/app/rate-limit.d.ts.map +0 -1
  39. package/dist/core/src/app/request-id.d.ts +0 -52
  40. package/dist/core/src/app/request-id.d.ts.map +0 -1
  41. package/dist/core/src/app/router.d.ts +0 -6
  42. package/dist/core/src/app/router.d.ts.map +0 -1
  43. package/dist/core/src/app/scalar.d.ts +0 -40
  44. package/dist/core/src/app/scalar.d.ts.map +0 -1
  45. package/dist/core/src/app/secure-headers.d.ts +0 -48
  46. package/dist/core/src/app/secure-headers.d.ts.map +0 -1
  47. package/dist/core/src/app/system-ctx.d.ts +0 -3
  48. package/dist/core/src/app/system-ctx.d.ts.map +0 -1
  49. package/dist/core/src/app/types.d.ts +0 -277
  50. package/dist/core/src/app/types.d.ts.map +0 -1
  51. package/dist/core/src/cli/config/types.d.ts +0 -31
  52. package/dist/core/src/cli/config/types.d.ts.map +0 -1
  53. package/dist/core/src/cli/index.d.ts +0 -3
  54. package/dist/core/src/cli/index.d.ts.map +0 -1
  55. package/dist/core/src/client/error.d.ts +0 -8
  56. package/dist/core/src/client/error.d.ts.map +0 -1
  57. package/dist/core/src/client/index.d.ts +0 -33
  58. package/dist/core/src/client/index.d.ts.map +0 -1
  59. package/dist/core/src/client/proxy.d.ts +0 -88
  60. package/dist/core/src/client/proxy.d.ts.map +0 -1
  61. package/dist/core/src/client/test-client.d.ts +0 -30
  62. package/dist/core/src/client/test-client.d.ts.map +0 -1
  63. package/dist/core/src/cron/index.d.ts +0 -73
  64. package/dist/core/src/cron/index.d.ts.map +0 -1
  65. package/dist/core/src/db/index.d.ts +0 -281
  66. package/dist/core/src/db/index.d.ts.map +0 -1
  67. package/dist/core/src/db/migrations/diff.d.ts +0 -5
  68. package/dist/core/src/db/migrations/diff.d.ts.map +0 -1
  69. package/dist/core/src/db/migrations/generator.d.ts +0 -26
  70. package/dist/core/src/db/migrations/generator.d.ts.map +0 -1
  71. package/dist/core/src/db/migrations/index.d.ts +0 -9
  72. package/dist/core/src/db/migrations/index.d.ts.map +0 -1
  73. package/dist/core/src/db/migrations/introspect.d.ts +0 -8
  74. package/dist/core/src/db/migrations/introspect.d.ts.map +0 -1
  75. package/dist/core/src/db/migrations/migrator.d.ts +0 -10
  76. package/dist/core/src/db/migrations/migrator.d.ts.map +0 -1
  77. package/dist/core/src/db/migrations/runner.d.ts +0 -7
  78. package/dist/core/src/db/migrations/runner.d.ts.map +0 -1
  79. package/dist/core/src/db/migrations/tracker.d.ts +0 -7
  80. package/dist/core/src/db/migrations/tracker.d.ts.map +0 -1
  81. package/dist/core/src/db/migrations/types.d.ts +0 -70
  82. package/dist/core/src/db/migrations/types.d.ts.map +0 -1
  83. package/dist/core/src/db/sql.d.ts +0 -148
  84. package/dist/core/src/db/sql.d.ts.map +0 -1
  85. package/dist/core/src/errors/index.d.ts +0 -30
  86. package/dist/core/src/errors/index.d.ts.map +0 -1
  87. package/dist/core/src/events/handler.d.ts +0 -42
  88. package/dist/core/src/events/handler.d.ts.map +0 -1
  89. package/dist/core/src/events/index.d.ts +0 -48
  90. package/dist/core/src/events/index.d.ts.map +0 -1
  91. package/dist/core/src/hooks/executor.d.ts +0 -20
  92. package/dist/core/src/hooks/executor.d.ts.map +0 -1
  93. package/dist/core/src/hooks/types.d.ts +0 -10
  94. package/dist/core/src/hooks/types.d.ts.map +0 -1
  95. package/dist/core/src/index.d.ts +0 -83
  96. package/dist/core/src/index.d.ts.map +0 -1
  97. package/dist/core/src/model/index.d.ts +0 -24
  98. package/dist/core/src/model/index.d.ts.map +0 -1
  99. package/dist/core/src/openapi/generator.d.ts +0 -58
  100. package/dist/core/src/openapi/generator.d.ts.map +0 -1
  101. package/dist/core/src/openapi/zod-to-schema.d.ts +0 -4
  102. package/dist/core/src/openapi/zod-to-schema.d.ts.map +0 -1
  103. package/dist/core/src/resource/errors.d.ts +0 -2
  104. package/dist/core/src/resource/errors.d.ts.map +0 -1
  105. package/dist/core/src/resource/index.d.ts +0 -98
  106. package/dist/core/src/resource/index.d.ts.map +0 -1
  107. package/dist/core/src/resource/zod-table.d.ts +0 -6
  108. package/dist/core/src/resource/zod-table.d.ts.map +0 -1
  109. package/dist/core/src/schema/audit.d.ts +0 -25
  110. package/dist/core/src/schema/audit.d.ts.map +0 -1
  111. package/dist/core/src/schema/column.d.ts +0 -31
  112. package/dist/core/src/schema/column.d.ts.map +0 -1
  113. package/dist/core/src/schema/table.d.ts +0 -68
  114. package/dist/core/src/schema/table.d.ts.map +0 -1
  115. package/dist/core/src/service/index.d.ts +0 -27
  116. package/dist/core/src/service/index.d.ts.map +0 -1
  117. package/dist/ws/src/adapter.d.ts +0 -56
  118. package/dist/ws/src/adapter.d.ts.map +0 -1
  119. package/dist/ws/src/index.d.ts +0 -5
  120. package/dist/ws/src/index.d.ts.map +0 -1
  121. package/dist/ws/src/module-augment.d.ts +0 -7
  122. package/dist/ws/src/module-augment.d.ts.map +0 -1
  123. package/dist/ws/src/types.d.ts +0 -87
  124. package/dist/ws/src/types.d.ts.map +0 -1
@@ -0,0 +1,998 @@
1
+ import * as bun from 'bun';
2
+ import { ZodTypeAny } from 'zod';
3
+ import * as oakbun from 'oakbun';
4
+ import { AuthPayload, VelnWsAdapter, WsRouteShape as WsRouteShape$1, BaseCtx as BaseCtx$1 } from 'oakbun';
5
+
6
+ type BindingValue = string | number | bigint | boolean | null | Uint8Array;
7
+ interface ExecuteResult {
8
+ rowsAffected: number;
9
+ lastInsertId?: number | bigint;
10
+ }
11
+ /** Emitted by the adapter after every query() or execute() call. */
12
+ interface QueryLogEntry {
13
+ /** The SQL string that was executed. */
14
+ sql: string;
15
+ /** The bound parameter values. */
16
+ params: BindingValue[];
17
+ /** Wall-clock duration in milliseconds (performance.now() resolution). */
18
+ durationMs: number;
19
+ /** Whether the call was query() (returns rows) or execute() (DML/DDL). */
20
+ type: 'query' | 'execute';
21
+ }
22
+ interface VelnAdapter {
23
+ query<T = Record<string, unknown>>(sql: string, params?: BindingValue[]): Promise<T[]>;
24
+ execute(sql: string, params?: BindingValue[]): Promise<ExecuteResult>;
25
+ transaction<T>(fn: (tx: VelnAdapter) => Promise<T>): Promise<T>;
26
+ close(): Promise<void>;
27
+ /**
28
+ * Optional query observer. When set, the adapter calls this after every
29
+ * query() and execute() with timing and SQL details.
30
+ * Set by dbPlugin when query logging is enabled — do not call directly.
31
+ */
32
+ onQuery?: (entry: QueryLogEntry) => void;
33
+ }
34
+
35
+ type SqlType = 'INTEGER' | 'TEXT' | 'REAL' | 'BOOLEAN' | 'TIMESTAMP' | 'JSON' | 'UUID' | 'BLOB';
36
+ interface ColumnDef {
37
+ type: SqlType;
38
+ nullable: boolean;
39
+ primaryKey: boolean;
40
+ autoIncrement: boolean;
41
+ unique: boolean;
42
+ defaultValue?: unknown;
43
+ defaultFn?: () => unknown;
44
+ }
45
+ declare class Column<T> {
46
+ readonly def: Readonly<ColumnDef>;
47
+ readonly _: T;
48
+ constructor(def: Readonly<ColumnDef>);
49
+ nullable(): Column<T | null>;
50
+ primaryKey(): Column<T>;
51
+ unique(): Column<T>;
52
+ default(value: NonNullable<T>): Column<T>;
53
+ defaultFn(fn: () => NonNullable<T>): Column<T>;
54
+ }
55
+
56
+ type SchemaMap = Record<string, Column<any>>;
57
+ type InferRow<T> = T extends TableDef<infer R, any, any> ? R : T extends SchemaMap ? {
58
+ [K in keyof T]: T[K] extends Column<infer C> ? C : never;
59
+ } : never;
60
+ type IsOptionalOnInsert<C extends Column<any>> = C['def']['primaryKey'] extends true ? true : C['def']['defaultValue'] extends undefined ? C['def']['defaultFn'] extends undefined ? C['def']['nullable'] extends true ? true : false : true : true;
61
+ type InferInsertFromSchema<S extends SchemaMap> = {
62
+ [K in keyof S as IsOptionalOnInsert<S[K]> extends true ? never : K]: S[K] extends Column<infer T> ? NonNullable<T> : never;
63
+ } & {
64
+ [K in keyof S as IsOptionalOnInsert<S[K]> extends true ? K : never]?: S[K] extends Column<infer T> ? T : never;
65
+ };
66
+ type InferInsert<T> = T extends TableDef<any, infer S, any> ? InferInsertFromSchema<S> : T extends SchemaMap ? InferInsertFromSchema<T> : never;
67
+ interface TableHookHandlers<T> {
68
+ beforeInsert?: (data: Partial<T>) => Partial<T> | void | Promise<Partial<T> | void>;
69
+ afterInsert?: (result: T, input: Partial<T>) => void | Promise<void>;
70
+ beforeUpdate?: (current: T, patch: Partial<T>) => Partial<T> | void | Promise<Partial<T> | void>;
71
+ afterUpdate?: (result: T, before: T) => void | Promise<void>;
72
+ beforeDelete?: (current: T) => void | Promise<void>;
73
+ afterDelete?: (deleted: T) => void | Promise<void>;
74
+ }
75
+ interface TableEventMap {
76
+ afterInsert?: string;
77
+ afterUpdate?: string;
78
+ afterDelete?: string;
79
+ }
80
+ type InferTableEvents<T, M extends TableEventMap> = (M['afterInsert'] extends string ? {
81
+ [_ in M['afterInsert']]: T;
82
+ } : Record<never, never>) & (M['afterUpdate'] extends string ? {
83
+ [_ in M['afterUpdate']]: {
84
+ before: T;
85
+ after: T;
86
+ };
87
+ } : Record<never, never>) & (M['afterDelete'] extends string ? {
88
+ [_ in M['afterDelete']]: T;
89
+ } : Record<never, never>);
90
+ interface TableDef<T, S extends SchemaMap = SchemaMap, TEvents extends TableEventMap = TableEventMap> {
91
+ readonly name: string;
92
+ readonly schema: S;
93
+ readonly primaryKey: keyof T & string;
94
+ readonly hooks: TableHookHandlers<T>[];
95
+ readonly events: TEvents;
96
+ readonly _eventMap: InferTableEvents<T, TEvents>;
97
+ }
98
+
99
+ interface ModuleHookHandlers<T, TCtx = unknown> {
100
+ beforeInsert?: (ctx: TCtx, data: Partial<T>) => Partial<T> | void | Promise<Partial<T> | void>;
101
+ afterInsert?: (ctx: TCtx, result: T, input: Partial<T>) => void | Promise<void>;
102
+ beforeUpdate?: (ctx: TCtx, current: T, patch: Partial<T>) => Partial<T> | void | Promise<Partial<T> | void>;
103
+ afterUpdate?: (ctx: TCtx, result: T, before: T) => void | Promise<void>;
104
+ beforeDelete?: (ctx: TCtx, current: T) => void | Promise<void>;
105
+ afterDelete?: (ctx: TCtx, deleted: T) => void | Promise<void>;
106
+ }
107
+
108
+ type EventHandler = (payload: unknown, ctx: unknown) => Promise<void> | void;
109
+ declare class RequestEventQueue {
110
+ private readonly buffer;
111
+ collect(name: string, payload: unknown): void;
112
+ flush(ctx: unknown, bus: EventBusAdapter | InMemoryEventBus): Promise<void>;
113
+ /** Drain — returns collected events and clears the buffer.
114
+ * Used by the TX path to hand events off to TransactionResult. */
115
+ drain(): PendingEvent[];
116
+ /** Number of buffered events — useful in tests. */
117
+ get size(): number;
118
+ }
119
+ interface VelnEvents {
120
+ }
121
+ type EventBusErrorHandler = (event: string, error: unknown) => void;
122
+ interface EventBusOptions {
123
+ /** Called when an event handler throws. Defaults to console.error. */
124
+ onError?: EventBusErrorHandler;
125
+ }
126
+ /**
127
+ * EventBusAdapter — minimal interface for event bus implementations.
128
+ *
129
+ * Default: InMemoryEventBus (single-process, zero latency)
130
+ *
131
+ * For multi-worker deployments: BroadcastChannelAdapter (@oakbun/broadcast, roadmap)
132
+ * For multi-server deployments: RedisAdapter (@oakbun/redis, roadmap)
133
+ *
134
+ * NOTE: EventBus is single-process by default. Events fired on instance A
135
+ * will NOT reach instance B without a distributed adapter.
136
+ */
137
+ interface EventBusAdapter {
138
+ on(event: string, handler: (payload: unknown) => void): void;
139
+ emit(event: string, payload: unknown): Promise<void>;
140
+ }
141
+ declare class InMemoryEventBus {
142
+ private readonly handlers;
143
+ private readonly _onError;
144
+ constructor(options?: EventBusOptions);
145
+ on<K extends keyof VelnEvents>(event: K, handler: (payload: VelnEvents[K], ctx: unknown) => Promise<void> | void): this;
146
+ on(event: string, handler: EventHandler): this;
147
+ _emit(event: string, payload: unknown, ctx: unknown): void;
148
+ emit(event: string, payload: unknown): Promise<void>;
149
+ flush(events: PendingEvent[], ctx: unknown): Promise<void>;
150
+ }
151
+ /** @deprecated Use InMemoryEventBus instead. EventBus will be removed in a future version. */
152
+ declare const EventBus: typeof InMemoryEventBus;
153
+ type EventBus = InMemoryEventBus;
154
+
155
+ declare class HookExecutor {
156
+ private readonly registry;
157
+ private _adapter?;
158
+ constructor();
159
+ setAdapter(adapter: VelnAdapter): void;
160
+ getAdapter(): VelnAdapter | undefined;
161
+ registerModuleHook<T, TCtx>(tableName: string, handlers: ModuleHookHandlers<T, TCtx>): void;
162
+ runBeforeInsert<T>(table: TableDef<T>, ctx: unknown, data: Partial<T>): Promise<Partial<T>>;
163
+ runBeforeUpdate<T>(table: TableDef<T>, ctx: unknown, current: T, patch: Partial<T>): Promise<Partial<T>>;
164
+ runBeforeDelete<T>(table: TableDef<T>, ctx: unknown, current: T): Promise<void>;
165
+ runAfterInsert<T>(table: TableDef<T>, ctx: unknown, result: T, input: Partial<T>, queue?: RequestEventQueue): Promise<void>;
166
+ runAfterUpdate<T>(table: TableDef<T>, ctx: unknown, result: T, before: T, queue?: RequestEventQueue): Promise<void>;
167
+ runAfterDelete<T>(table: TableDef<T>, ctx: unknown, deleted: T, queue?: RequestEventQueue): Promise<void>;
168
+ private _moduleHandlers;
169
+ }
170
+
171
+ interface JoinClause {
172
+ type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL';
173
+ table: string;
174
+ on: string;
175
+ }
176
+ /** SQL dialect — used for ILIKE fallback on non-Postgres adapters. */
177
+ type SqlDialect = 'sqlite' | 'postgres' | 'mysql';
178
+ /** Explicit operator condition for a single column. */
179
+ type WhereOp<T> = {
180
+ op: '=';
181
+ value: T;
182
+ } | {
183
+ op: '!=';
184
+ value: T;
185
+ } | {
186
+ op: '>';
187
+ value: T;
188
+ } | {
189
+ op: '>=';
190
+ value: T;
191
+ } | {
192
+ op: '<';
193
+ value: T;
194
+ } | {
195
+ op: '<=';
196
+ value: T;
197
+ } | {
198
+ op: 'IN';
199
+ value: T[];
200
+ } | {
201
+ op: 'NOT IN';
202
+ value: T[];
203
+ } | {
204
+ op: 'LIKE';
205
+ value: string;
206
+ } | {
207
+ op: 'ILIKE';
208
+ value: string;
209
+ } | {
210
+ op: 'IS NULL';
211
+ } | {
212
+ op: 'IS NOT NULL';
213
+ };
214
+ /** Per-field condition — shorthand (plain value = equality) or explicit operator. */
215
+ type FieldCondition<T> = T | WhereOp<T>;
216
+ /** Map of column conditions — each field is optional. */
217
+ type WhereConditions<TRow> = {
218
+ [K in keyof TRow]?: FieldCondition<TRow[K]>;
219
+ };
220
+ /**
221
+ * Full WHERE input — either a flat conditions map, OR-group, or AND-group.
222
+ * OR/AND values are recursively WhereInput allowing nesting.
223
+ */
224
+ type WhereInput<TRow> = WhereConditions<TRow> | {
225
+ OR: WhereInput<TRow>[];
226
+ } | {
227
+ AND: WhereInput<TRow>[];
228
+ };
229
+ /** A single aggregate expression: FN("col") AS alias. */
230
+ interface AggregateClause {
231
+ alias: string;
232
+ fn: 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
233
+ col?: string;
234
+ }
235
+ interface SelectOptions {
236
+ limit?: number;
237
+ offset?: number;
238
+ orderBy?: {
239
+ col: string;
240
+ dir: 'ASC' | 'DESC';
241
+ }[];
242
+ columns?: string[];
243
+ groupBy?: string[];
244
+ aggregates?: AggregateClause[];
245
+ having?: WhereInput<Record<string, unknown>>;
246
+ }
247
+
248
+ interface QueryLog {
249
+ /** Total number of queries executed during this request. */
250
+ queries: number;
251
+ /** Cumulative wall-clock duration of all queries in ms. */
252
+ totalMs: number;
253
+ /** Individual query entries — populated only when logQueries is true. */
254
+ entries: QueryLogEntry[];
255
+ /** Warning threshold — exceeded → N+1 warning. */
256
+ threshold: number;
257
+ /** Whether individual query entries should be captured (for logQueries). */
258
+ logQueries: boolean;
259
+ }
260
+ interface PendingEvent {
261
+ name: string;
262
+ payload: unknown;
263
+ }
264
+ interface TransactionResult<T> {
265
+ result: T;
266
+ events: PendingEvent[];
267
+ }
268
+ declare class BoundVelnDB {
269
+ private readonly hooks;
270
+ private readonly ctx;
271
+ private readonly queue?;
272
+ /** Per-request query counter — incremented for every query() and execute() call on this instance. */
273
+ _queryCount: number;
274
+ private readonly adapter;
275
+ constructor(adapter: VelnAdapter, hooks: HookExecutor, ctx: unknown, queue?: RequestEventQueue | undefined, queryLog?: QueryLog);
276
+ from<T, S extends SchemaMap>(table: TableDef<T, S>): SelectBuilder<T, S>;
277
+ /**
278
+ * Start a JOIN query from the given table name.
279
+ * Returns a JoinBuilder — call .join()/.leftJoin() etc. to add clauses,
280
+ * then .select() to execute and get Record<string, unknown>[] results.
281
+ *
282
+ * @example
283
+ * const rows = await db.join('orders')
284
+ * .columns(['orders.id', 'users.name'])
285
+ * .join('users', 'orders.user_id = users.id')
286
+ * .where('orders.status = ?', ['pending'])
287
+ * .select()
288
+ */
289
+ join(tableName: string): JoinBuilder;
290
+ into<T, S extends SchemaMap>(table: TableDef<T, S>): InsertBuilder<T, S>;
291
+ /**
292
+ * DataLoader-pattern relation fetch — single IN-query, no N+1.
293
+ * Returns a Map keyed by the foreign-key value; each entry is an array of
294
+ * matching child rows (for one-to-many relations).
295
+ *
296
+ * @example
297
+ * const posts = await db.from(postsTable).select()
298
+ * const authorMap = await db.loadRelation(posts, 'authorId', usersTable, 'id')
299
+ * // → SELECT * FROM "users" WHERE "id" IN (1, 2, 3)
300
+ * const withAuthors = posts.map(p => ({ ...p, author: authorMap.get(p.authorId)?.[0] ?? null }))
301
+ */
302
+ loadRelation<TParent extends Record<string, unknown>, TChild, TFk extends keyof TParent & string, TPk extends keyof TChild & string>(parents: TParent[], foreignKey: TFk, childTable: TableDef<TChild>, primaryKey: TPk): Promise<Map<TParent[TFk], TChild[]>>;
303
+ /**
304
+ * Convenience variant of loadRelation for belongs-to (many-to-one) relations.
305
+ * Returns Map<fkValue, TChild> — single child per key instead of an array.
306
+ *
307
+ * @example
308
+ * const authorMap = await db.loadRelationOne(posts, 'authorId', usersTable, 'id')
309
+ * const author = authorMap.get(post.authorId) ?? null
310
+ */
311
+ loadRelationOne<TParent extends Record<string, unknown>, TChild, TFk extends keyof TParent & string, TPk extends keyof TChild & string>(parents: TParent[], foreignKey: TFk, childTable: TableDef<TChild>, primaryKey: TPk): Promise<Map<TParent[TFk], TChild>>;
312
+ transaction<T>(fn: (db: BoundVelnDB) => Promise<T>): Promise<TransactionResult<T>>;
313
+ /**
314
+ * Execute raw SQL and return typed rows.
315
+ *
316
+ * Without a schema the return type is `Record<string, unknown>[]`.
317
+ * With a schema (e.g. a Zod object) every row is validated at runtime
318
+ * and the return type is inferred from the schema.
319
+ *
320
+ * @example
321
+ * // Untyped
322
+ * const rows = await ctx.db.raw('SELECT * FROM orders WHERE amount > ?', [100])
323
+ *
324
+ * // Typed + validated
325
+ * const schema = z.object({ id: z.number(), amount: z.number() })
326
+ * const rows = await ctx.db.raw('SELECT id, amount FROM orders WHERE amount > ?', [100], schema)
327
+ */
328
+ raw<T = Record<string, unknown>>(sql: string, params?: BindingValue[], schema?: {
329
+ parse: (row: unknown) => T;
330
+ }): Promise<T[]>;
331
+ }
332
+ declare class SelectBuilder<T, S extends SchemaMap> {
333
+ private readonly adapter;
334
+ private readonly hooks;
335
+ private readonly ctx;
336
+ private readonly queue;
337
+ private readonly table;
338
+ private readonly conditions;
339
+ private readonly _options;
340
+ private readonly _rawWhere;
341
+ private readonly _dialect;
342
+ constructor(adapter: VelnAdapter, hooks: HookExecutor, ctx: unknown, queue: RequestEventQueue | undefined, table: TableDef<T, S>, conditions: WhereInput<T>, _options?: SelectOptions, _rawWhere?: {
343
+ sql: string;
344
+ params: BindingValue[];
345
+ }[], _dialect?: SqlDialect);
346
+ private _cloneWith;
347
+ private _clone;
348
+ /**
349
+ * Add WHERE conditions. Accepts:
350
+ * - Plain equality map: `.where({ role: 'admin' })`
351
+ * - Operator condition: `.where({ age: { op: '>=', value: 18 } })`
352
+ * - OR group: `.where({ OR: [{ role: 'admin' }, { role: 'mod' }] })`
353
+ * - AND group: `.where({ AND: [...] })`
354
+ *
355
+ * Multiple `.where()` calls are combined with AND.
356
+ */
357
+ where(conditions: WhereInput<T>): SelectBuilder<T, S>;
358
+ /**
359
+ * Append a raw SQL WHERE fragment, combined with AND.
360
+ * Use for conditions the builder cannot express.
361
+ *
362
+ * @example
363
+ * .whereRaw('"score" > "threshold"', [])
364
+ * .whereRaw('"created_at" > ?', ['2024-01-01'])
365
+ */
366
+ whereRaw(sql: string, params: BindingValue[]): SelectBuilder<T, S>;
367
+ /** Limit the number of rows returned. Bound as a parameter — never interpolated. */
368
+ limit(n: number): SelectBuilder<T, S>;
369
+ /** Skip the first n rows. Bound as a parameter — never interpolated. */
370
+ offset(n: number): SelectBuilder<T, S>;
371
+ /** Add an ORDER BY clause. Multiple calls accumulate in order. */
372
+ orderBy(col: keyof T & string, dir?: 'ASC' | 'DESC'): SelectBuilder<T, S>;
373
+ /**
374
+ * Convenience helper for cursor-based pagination.
375
+ * page(1, 10) → LIMIT 10 OFFSET 0
376
+ * page(2, 10) → LIMIT 10 OFFSET 10
377
+ */
378
+ page(page: number, size: number): SelectBuilder<T, S>;
379
+ /**
380
+ * Restrict which columns are returned.
381
+ * SELECT "id", "name" FROM "table" — instead of SELECT *
382
+ *
383
+ * Return type is narrowed to Pick<T, K> for full type safety.
384
+ */
385
+ columns<K extends keyof T & string>(...cols: K[]): SelectBuilder<Pick<T, K>, S>;
386
+ /**
387
+ * Add a GROUP BY clause. Multiple columns are comma-separated.
388
+ * Combine with .aggregate() to get grouped aggregate results.
389
+ */
390
+ groupBy(...cols: (keyof T & string)[]): SelectBuilder<T, S>;
391
+ /**
392
+ * Add a HAVING clause — filters aggregate groups.
393
+ * Uses the same WhereInput system as .where() (supports operators, OR/AND).
394
+ *
395
+ * @example
396
+ * .groupBy('role').aggregate({ cnt: { fn: 'COUNT' } }).having({ cnt: { op: '>', value: 1 } })
397
+ */
398
+ having(conditions: WhereInput<Record<string, unknown>>): SelectBuilder<T, S>;
399
+ /**
400
+ * Execute a GROUP BY + aggregate query.
401
+ * Returns typed rows with group-by columns + aggregate aliases.
402
+ *
403
+ * @example
404
+ * const rows = await db.from(orders)
405
+ * .groupBy('status')
406
+ * .aggregate({ total: { fn: 'SUM', col: 'amount' }, cnt: { fn: 'COUNT' } })
407
+ * // rows: { status: string; total: number; cnt: number }[]
408
+ */
409
+ aggregate<TAgg extends Record<string, number | string | null>>(aggregates: {
410
+ [K in keyof TAgg]: {
411
+ fn: 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX';
412
+ col?: keyof T & string;
413
+ };
414
+ }): Promise<(Partial<T> & TAgg)[]>;
415
+ /** COUNT(*) or COUNT("col") — returns the count as a number. */
416
+ count(col?: keyof T & string): Promise<number>;
417
+ /** SUM("col") — returns the sum as a number (0 if no rows). */
418
+ sum(col: keyof T & string): Promise<number>;
419
+ /** AVG("col") — returns the average as a number (0 if no rows). */
420
+ avg(col: keyof T & string): Promise<number>;
421
+ /** MIN("col") — returns the minimum value. */
422
+ min(col: keyof T & string): Promise<number | string | null>;
423
+ /** MAX("col") — returns the maximum value. */
424
+ max(col: keyof T & string): Promise<number | string | null>;
425
+ private _scalarAggregate;
426
+ private _scalarAggregateRaw;
427
+ select(): Promise<T[]>;
428
+ first(): Promise<T | null>;
429
+ update(patch: Partial<T>): Promise<T>;
430
+ delete(): Promise<T>;
431
+ }
432
+ declare class InsertBuilder<T, S extends SchemaMap> {
433
+ private readonly adapter;
434
+ private readonly hooks;
435
+ private readonly ctx;
436
+ private readonly queue;
437
+ private readonly table;
438
+ constructor(adapter: VelnAdapter, hooks: HookExecutor, ctx: unknown, queue: RequestEventQueue | undefined, table: TableDef<T, S>);
439
+ insert(data: InferInsert<S>): Promise<T>;
440
+ /** Serialize values for SQLite storage. Date → ISO string. */
441
+ private _serializeForInsert;
442
+ }
443
+ declare class JoinBuilder {
444
+ private readonly adapter;
445
+ private readonly tableName;
446
+ private readonly _columns;
447
+ private readonly _joins;
448
+ private readonly _where;
449
+ private readonly _params;
450
+ private readonly _options;
451
+ constructor(adapter: VelnAdapter, tableName: string, _columns: string[], _joins: JoinClause[], _where: string, _params: BindingValue[], _options?: SelectOptions);
452
+ private _cloneOpts;
453
+ /** Restrict the selected columns (e.g. ['orders.id', 'users.name']). */
454
+ columns(cols: string[]): JoinBuilder;
455
+ /** Add an INNER JOIN clause. */
456
+ join(table: string, on: string): JoinBuilder;
457
+ /** Add a LEFT JOIN clause. */
458
+ leftJoin(table: string, on: string): JoinBuilder;
459
+ /** Add a RIGHT JOIN clause. */
460
+ rightJoin(table: string, on: string): JoinBuilder;
461
+ /** Add a FULL JOIN clause. */
462
+ fullJoin(table: string, on: string): JoinBuilder;
463
+ /**
464
+ * Set a typed WHERE clause from a conditions object.
465
+ * Each key becomes "key" = ? — values are bound safely.
466
+ *
467
+ * @example
468
+ * .where({ status: 'pending', userId })
469
+ */
470
+ where(conditions: Record<string, BindingValue>): JoinBuilder;
471
+ /**
472
+ * Set a raw WHERE clause.
473
+ * Use ? as placeholder; pass bind values as the second argument.
474
+ *
475
+ * @example
476
+ * .where('orders.status = ? AND orders.user_id = ?', ['pending', userId])
477
+ */
478
+ where(sql: string, params: BindingValue[]): JoinBuilder;
479
+ /** Limit the number of rows returned. Bound as a parameter — never interpolated. */
480
+ limit(n: number): JoinBuilder;
481
+ /** Skip the first n rows. Bound as a parameter — never interpolated. */
482
+ offset(n: number): JoinBuilder;
483
+ /**
484
+ * Add an ORDER BY clause. Multiple calls accumulate in order.
485
+ * @param col Column reference, e.g. 'orders.created_at' or 'name'.
486
+ */
487
+ orderBy(col: string, dir?: 'ASC' | 'DESC'): JoinBuilder;
488
+ /**
489
+ * Convenience helper for cursor-based pagination.
490
+ * page(1, 10) → LIMIT 10 OFFSET 0
491
+ * page(2, 10) → LIMIT 10 OFFSET 10
492
+ */
493
+ page(page: number, size: number): JoinBuilder;
494
+ /**
495
+ * Execute the query and return raw rows (no deserialization).
496
+ *
497
+ * @remarks
498
+ * Type parameter T is a manual cast — not validated at runtime.
499
+ * For runtime validation, use db.raw() with a Zod schema instead.
500
+ */
501
+ select<T = Record<string, unknown>>(): Promise<T[]>;
502
+ /**
503
+ * Execute the query and return the first row, or null.
504
+ *
505
+ * @remarks
506
+ * Type parameter T is a manual cast — not validated at runtime.
507
+ */
508
+ first<T = Record<string, unknown>>(): Promise<T | null>;
509
+ private _addJoin;
510
+ }
511
+
512
+ interface WsCtx<TData = unknown> {
513
+ /** Matched path params, e.g. { roomId: '42' } for /rooms/:roomId */
514
+ params: Record<string, string>;
515
+ /** Parsed query string from the upgrade request */
516
+ query: Record<string, string | string[]>;
517
+ /**
518
+ * The Bun ServerWebSocket — use ws.send(), ws.close(), ws.subscribe(), etc.
519
+ * Typed with `data: WsCtxData` so ws.data carries ctx.user etc.
520
+ */
521
+ ws: bun.ServerWebSocket<WsCtxData>;
522
+ /**
523
+ * Validated & typed message payload.
524
+ * Only set inside onMessage() when a message schema was defined.
525
+ * Undefined in open / close / drain.
526
+ */
527
+ data: TData;
528
+ /** Set by jwtPlugin when registered on the app. undefined otherwise. */
529
+ user?: AuthPayload;
530
+ /** Set by dbPlugin when registered on the app. undefined otherwise. */
531
+ db?: BoundVelnDB;
532
+ }
533
+ interface WsCtxData {
534
+ _wsPath: string;
535
+ params: Record<string, string>;
536
+ query: Record<string, string | string[]>;
537
+ user?: AuthPayload;
538
+ db?: BoundVelnDB;
539
+ [key: string]: unknown;
540
+ _data?: unknown;
541
+ }
542
+ interface WsHandlers<TMsg = unknown> {
543
+ /** Called when a client opens a connection. ctx.data is undefined here. */
544
+ open?: (ctx: WsCtx<undefined>) => void | Promise<void>;
545
+ /**
546
+ * Called for each incoming message.
547
+ * If a message schema was provided, ctx.data is the validated & typed payload.
548
+ */
549
+ message?: (ctx: WsCtx<TMsg>, raw: string | Uint8Array) => void | Promise<void>;
550
+ /**
551
+ * Called when a client disconnects.
552
+ * code: WebSocket close code; reason: human-readable string.
553
+ */
554
+ close?: (ctx: WsCtx<undefined>, code: number, reason: string) => void | Promise<void>;
555
+ /** Called when the send buffer is drained and more data can be written. */
556
+ drain?: (ctx: WsCtx<undefined>) => void | Promise<void>;
557
+ }
558
+ /** Stored internally — one entry per registered WS path. */
559
+ interface WsRoute {
560
+ /** Registered path pattern, e.g. '/chat' or '/rooms/:id' */
561
+ path: string;
562
+ /** Optional Zod schema to parse incoming messages */
563
+ messageSchema?: ZodTypeAny;
564
+ handlers: WsHandlers<any>;
565
+ /** Reference to the originating module (null for app-level) */
566
+ _module: unknown | null;
567
+ /** Index signature — satisfies WsRouteShape from core */
568
+ [key: string]: unknown;
569
+ }
570
+ /**
571
+ * WsRouteHandler — passed to app.ws() / module.ws().
572
+ *
573
+ * Usage (no schema):
574
+ * app.ws('/chat', { open(ctx) { ... }, message(ctx, raw) { ... } })
575
+ *
576
+ * Usage (with Zod schema — message is typed):
577
+ * app.ws('/chat', {
578
+ * message: z.object({ text: z.string() }),
579
+ * handlers: {
580
+ * message(ctx, raw) { ctx.data.text // ← typed }
581
+ * }
582
+ * })
583
+ */
584
+ type WsRouteHandler<TMsg = unknown> = WsHandlers<TMsg> | WsRouteHandlerWithSchema<TMsg>;
585
+ interface WsRouteHandlerWithSchema<TMsg = unknown> {
586
+ /** Zod schema for incoming messages. Parsed before onMessage is called. */
587
+ message: ZodTypeAny;
588
+ /** Lifecycle callbacks — message(ctx) receives the validated & typed payload */
589
+ handlers: WsHandlers<TMsg>;
590
+ }
591
+
592
+ declare module 'oakbun' {
593
+ interface ModuleBuilder<TCtx, TPrefix extends string, TRoutes> {
594
+ ws<TMsg = unknown>(path: string, handler: WsRouteHandler<TMsg>): ModuleBuilder<TCtx, TPrefix, TRoutes>;
595
+ }
596
+ }
597
+
598
+ declare const _baseAuditFields: {
599
+ readonly id: oakbun.Column<number>;
600
+ readonly tableName: oakbun.Column<string>;
601
+ readonly operation: oakbun.Column<string>;
602
+ readonly actor: oakbun.Column<string | null>;
603
+ readonly before: oakbun.Column<string | null>;
604
+ readonly after: oakbun.Column<string | null>;
605
+ readonly changedAt: oakbun.Column<Date>;
606
+ };
607
+ type BaseAuditSchema = typeof _baseAuditFields;
608
+ type AuditTableDef<S extends SchemaMap = BaseAuditSchema> = TableDef<InferRow<BaseAuditSchema & S>, BaseAuditSchema & S>;
609
+ interface AuditConfig<TCtx, TRow, S extends SchemaMap = BaseAuditSchema> {
610
+ /** The audit table to write into. */
611
+ storeIn: AuditTableDef<S>;
612
+ /** Extract actor identifier from request context. Return null for anonymous. */
613
+ actor: (ctx: TCtx) => string | null | undefined;
614
+ /** Field names to replace with '[REDACTED]' in before/after snapshots. */
615
+ redact?: (keyof TRow & string)[];
616
+ /** Called when an audit write fails. Defaults to console.error. */
617
+ onError?: (err: unknown) => void;
618
+ }
619
+
620
+ type ModelInstance<TDef> = TDef & {
621
+ readonly db: BoundVelnDB;
622
+ };
623
+ interface ModelDef<TName extends string, TDef> {
624
+ readonly _modelName: TName;
625
+ readonly _factory: (db: BoundVelnDB) => ModelInstance<TDef>;
626
+ }
627
+
628
+ type Dep<TKey extends string, TDef> = ModelDef<TKey, TDef> | ServiceDef<TKey, TDef>;
629
+ interface ServiceDef<TKey extends string, TDef> {
630
+ readonly _serviceKey: TKey;
631
+ readonly _deps: ReadonlyArray<Dep<string, unknown>>;
632
+ readonly _options: BaseOptions;
633
+ readonly _factory: (deps: Record<string, unknown>) => TDef;
634
+ }
635
+
636
+ type EventCallback = (payload: unknown) => void | Promise<void>;
637
+ type RawHandler = (payload: unknown, ctx: Record<string, unknown>) => void | Promise<void>;
638
+ interface EventHandlerDef {
639
+ readonly _handlers: Map<string, EventCallback>;
640
+ readonly _rawHandlers: Map<string, RawHandler>;
641
+ readonly _logger: Logger;
642
+ readonly _services: ReadonlyArray<ServiceDef<string, unknown>>;
643
+ }
644
+
645
+ interface CronCtx {
646
+ db: BoundVelnDB;
647
+ [key: string]: unknown;
648
+ }
649
+
650
+ interface CronDef<TServices extends Record<string, unknown> = Record<never, never>> {
651
+ readonly _name: string;
652
+ readonly _expression: string;
653
+ readonly _timezone: string | undefined;
654
+ readonly _runOnStart: boolean;
655
+ readonly _ttlMs: number | undefined;
656
+ readonly _logger: Logger;
657
+ readonly _mode: 'process' | 'os';
658
+ readonly _handler: ((ctx: CronCtx & TServices, logger: Logger) => Promise<void> | void) | undefined;
659
+ readonly _services: ReadonlyArray<ServiceDef<string, unknown>>;
660
+ readonly _script: string | undefined;
661
+ readonly _onError: ((err: unknown) => void) | undefined;
662
+ use<TKey extends string, TDef>(service: ServiceDef<TKey, TDef>): CronDef<TServices & Record<TKey, TDef>>;
663
+ }
664
+
665
+ interface HookDeclaration<T, TCtx> {
666
+ table: TableDef<T, any>;
667
+ handlers: ModuleHookHandlers<T, TCtx>;
668
+ }
669
+ interface AuditDeclaration<T, TCtx, S extends SchemaMap> {
670
+ table: TableDef<T, any>;
671
+ config: AuditConfig<TCtx, T, S>;
672
+ }
673
+ interface ServiceDeclaration<TKey extends string, TDef> {
674
+ readonly service: ServiceDef<TKey, TDef>;
675
+ }
676
+ interface VelnModule {
677
+ prefix: string;
678
+ routes: Route<any>[];
679
+ wsRoutes: WsRouteShape[];
680
+ hookDeclarations: HookDeclaration<any, any>[];
681
+ auditDeclarations: AuditDeclaration<any, any, any>[];
682
+ serviceDeclarations: ReadonlyArray<ServiceDeclaration<string, unknown>>;
683
+ plugins: Plugin<any, any>[];
684
+ guards: Guard<any>[];
685
+ onRequestHooks: OnRequestHook<any>[];
686
+ onBeforeHandleHooks: OnBeforeHandleHook<any>[];
687
+ onResponseHooks: OnResponseHook<any>[];
688
+ onError?: ErrorHandler<any>;
689
+ eventHandlerDefs: EventHandlerDef[];
690
+ cronDefs: CronDef<Record<string, unknown>>[];
691
+ visibility: 'public' | 'hidden';
692
+ meta?: {
693
+ tag?: string;
694
+ description?: string;
695
+ };
696
+ options?: BaseOptions;
697
+ }
698
+
699
+ interface CookieOptions {
700
+ httpOnly?: boolean;
701
+ secure?: boolean;
702
+ sameSite?: 'Strict' | 'Lax' | 'None';
703
+ maxAge?: number;
704
+ path?: string;
705
+ domain?: string;
706
+ }
707
+ interface CookieJar {
708
+ get(name: string): string | undefined;
709
+ set(name: string, value: string, options?: CookieOptions): void;
710
+ delete(name: string): void;
711
+ /** Framework-internal: returns all pending Set-Cookie header values */
712
+ _pending(): string[];
713
+ }
714
+
715
+ /** Minimal WS route shape that Core knows about. Full type lives in @veln/ws. */
716
+ interface WsRouteShape {
717
+ path: string;
718
+ _module: unknown | null;
719
+ [key: string]: unknown;
720
+ }
721
+ interface RouteSchema {
722
+ params?: ZodTypeAny;
723
+ query?: ZodTypeAny;
724
+ body?: ZodTypeAny;
725
+ response?: ZodTypeAny;
726
+ }
727
+ /** Additional response code definition for OpenAPI docs (e.g. 401, 404). */
728
+ interface RouteResponseDoc {
729
+ description: string;
730
+ }
731
+ interface RouteDocs {
732
+ /** Human-readable route summary shown in Scalar / Swagger UI. Auto-generated if absent. */
733
+ summary?: string;
734
+ /** Longer description rendered below the summary. Markdown supported. */
735
+ description?: string;
736
+ /** Unique operationId. Auto-generated if absent (e.g. "listUsers", "getUserById"). */
737
+ operationId?: string;
738
+ /**
739
+ * Additional HTTP response codes to document in the OpenAPI spec.
740
+ * The 200 success response is always generated automatically.
741
+ * Use this to document error responses like 401, 403, 404, 422, etc.
742
+ *
743
+ * @example
744
+ * docs: {
745
+ * responses: {
746
+ * 401: { description: 'Unauthorized' },
747
+ * 404: { description: 'Not found' },
748
+ * }
749
+ * }
750
+ */
751
+ responses?: Record<number, RouteResponseDoc>;
752
+ }
753
+ /** Controls a streaming response. Passed to the ctx.stream() writer callback. */
754
+ interface StreamController {
755
+ /** Push a string or binary chunk to the stream. */
756
+ send(chunk: string | Uint8Array): void;
757
+ /** Close the stream. Must be called to end the response. */
758
+ close(): void;
759
+ }
760
+ /** Options for ctx.stream(). */
761
+ interface StreamOptions {
762
+ /**
763
+ * Content-Type header for the streaming response.
764
+ * Defaults to `'text/plain'`.
765
+ * Use `'text/event-stream'` for SSE, `'application/x-ndjson'` for NDJSON.
766
+ */
767
+ contentType?: string;
768
+ /** Additional headers to include in the response. */
769
+ headers?: Record<string, string>;
770
+ /** HTTP status code. Defaults to 200. */
771
+ status?: number;
772
+ }
773
+ /**
774
+ * Controls a Server-Sent Events stream.
775
+ * Passed to the ctx.sse() writer callback.
776
+ *
777
+ * SSE wire format:
778
+ * event: <name>\ndata: <json>\n\n — named event
779
+ * data: <json>\n\n — unnamed event
780
+ * : <text>\n\n — comment / keepalive
781
+ * id: <value>\n — event ID for reconnect
782
+ * retry: <ms>\n — reconnect interval
783
+ */
784
+ interface SseController {
785
+ /** Send a named event with a JSON-serializable payload. */
786
+ event(name: string, data: unknown): Promise<void>;
787
+ /** Send an unnamed data event. */
788
+ data(data: unknown): Promise<void>;
789
+ /** Send an SSE comment (e.g. keepalive ping). */
790
+ comment(text?: string): Promise<void>;
791
+ /** Set the last event ID (used by the browser for reconnect). */
792
+ id(id: string): Promise<void>;
793
+ /** Tell the browser how long to wait before reconnecting (milliseconds). */
794
+ retry(ms: number): Promise<void>;
795
+ }
796
+ interface Logger {
797
+ info(msg: string, ...args: unknown[]): void;
798
+ warn(msg: string, ...args: unknown[]): void;
799
+ error(msg: string, ...args: unknown[]): void;
800
+ debug(msg: string, ...args: unknown[]): void;
801
+ }
802
+ interface LogOptions {
803
+ /** Minimum level to emit. Defaults to 'info'. */
804
+ level?: 'debug' | 'info' | 'warn' | 'error';
805
+ /** Keys whose values are replaced with '***' in structured data. Case-insensitive. */
806
+ mask?: string[];
807
+ /** Suppress all output — useful in tests. */
808
+ silent?: boolean;
809
+ }
810
+ interface BaseOptions {
811
+ log?: LogOptions;
812
+ }
813
+ interface BaseCtx {
814
+ req: Request;
815
+ params: Record<string, string>;
816
+ query: Record<string, string | string[]>;
817
+ body?: unknown;
818
+ json: <T>(data: T, status?: number) => Response;
819
+ text: (data: string, status?: number) => Response;
820
+ html: (data: string, status?: number) => Response;
821
+ /**
822
+ * Returns a streaming Response backed by a ReadableStream.
823
+ *
824
+ * The callback receives a StreamController — call `send(chunk)` to push data
825
+ * and `close()` to end the stream. Errors thrown inside are caught automatically.
826
+ *
827
+ * Set `contentType` for SSE (`'text/event-stream'`) or NDJSON (`'application/x-ndjson'`).
828
+ * Compression is automatically skipped for streaming responses.
829
+ *
830
+ * @example
831
+ * return ctx.stream((stream) => {
832
+ * stream.send('data: hello\n\n')
833
+ * stream.send('data: world\n\n')
834
+ * stream.close()
835
+ * }, { contentType: 'text/event-stream' })
836
+ */
837
+ stream: (writer: (controller: StreamController) => void | Promise<void>, options?: StreamOptions) => Response;
838
+ /**
839
+ * Returns a Server-Sent Events Response.
840
+ *
841
+ * The callback receives an SseController — call `event()`, `data()`,
842
+ * `comment()`, `id()`, or `retry()` to push SSE frames, then let the
843
+ * callback return (or the async iterator complete) to close the stream.
844
+ *
845
+ * @example
846
+ * return ctx.sse(async (sse) => {
847
+ * await sse.event('connected', { userId: '42' })
848
+ * for await (const update of source()) {
849
+ * await sse.event('update', update)
850
+ * await sse.comment('keepalive')
851
+ * }
852
+ * })
853
+ */
854
+ sse: (writer: (controller: SseController) => void | Promise<void>) => Response;
855
+ events?: EventBus;
856
+ logger?: Logger;
857
+ db?: BoundVelnDB;
858
+ cookie: CookieJar;
859
+ emit: <K extends keyof VelnEvents>(event: K, payload: VelnEvents[K]) => void;
860
+ _requestQueue?: RequestEventQueue;
861
+ _queryLog?: QueryLog;
862
+ _startTime?: number;
863
+ }
864
+ type Guard<TCtx> = (ctx: TCtx) => Response | null | Promise<Response | null>;
865
+ type ErrorHandler<TCtx = BaseCtx> = (err: unknown, ctx: TCtx) => Response | Promise<Response>;
866
+ interface RouteHandler<TCtx> {
867
+ handler: (ctx: TCtx) => Response | Promise<Response>;
868
+ }
869
+ type OnRequestFn<TCtx extends BaseCtx = BaseCtx> = (ctx: TCtx) => Response | void | Promise<Response | void>;
870
+ type OnBeforeHandleFn<TCtx extends BaseCtx = BaseCtx> = (ctx: TCtx) => Response | void | Promise<Response | void>;
871
+ type OnResponseFn<TCtx extends BaseCtx = BaseCtx> = (ctx: TCtx, response: Response) => Response | void | Promise<Response | void>;
872
+ interface OnRequestHook<TCtx extends BaseCtx = BaseCtx> {
873
+ readonly _phase: 'onRequest';
874
+ readonly _fn: OnRequestFn<TCtx>;
875
+ }
876
+ interface OnBeforeHandleHook<TCtx extends BaseCtx = BaseCtx> {
877
+ readonly _phase: 'onBeforeHandle';
878
+ readonly _fn: OnBeforeHandleFn<TCtx>;
879
+ }
880
+ interface OnResponseHook<TCtx extends BaseCtx = BaseCtx> {
881
+ readonly _phase: 'onResponse';
882
+ readonly _fn: OnResponseFn<TCtx>;
883
+ }
884
+ interface Route<TCtx = BaseCtx> {
885
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
886
+ path: string;
887
+ summary?: string;
888
+ description?: string;
889
+ /** OpenAPI documentation override set via the `docs` option on route registration. */
890
+ docs?: RouteDocs;
891
+ handler: RouteHandler<TCtx>;
892
+ guards: Guard<TCtx>[];
893
+ onError?: ErrorHandler<TCtx>;
894
+ schema?: RouteSchema;
895
+ visibility?: 'public' | 'hidden';
896
+ moduleGuardOptOut?: true;
897
+ _module?: VelnModule;
898
+ _pluginName?: string;
899
+ }
900
+
901
+ /** A single navigation item contributed by a plugin via .nav(). */
902
+ interface NavItem {
903
+ label: string;
904
+ route: string;
905
+ icon?: string;
906
+ order?: number;
907
+ children?: NavItem[];
908
+ }
909
+ interface Plugin<TCtx, TAdd extends object> {
910
+ name: string;
911
+ /**
912
+ * Optional list of plugin names that must be registered before this plugin.
913
+ * app.plugin() validates this at registration time and throws PLUGIN_MISSING_DEP
914
+ * if a required plugin is not yet registered.
915
+ *
916
+ * Example: eventBusPlugin sets requires: ['db'] to enforce registration order.
917
+ */
918
+ requires?: string[];
919
+ /**
920
+ * Optional list of modules this plugin contributes.
921
+ * app.plugin() calls app.register() on each entry automatically.
922
+ *
923
+ * Can also be set to a factory function for typed ctx inference (Option A, Spec 04).
924
+ * The factory is called once with a dummy ctx to extract the module list —
925
+ * it is NEVER called at request time.
926
+ */
927
+ modules?: VelnModule[];
928
+ /**
929
+ * Optional permission gate for all routes contributed via .modules().
930
+ * app.plugin() checks ctx.user before running plugin.request() for those routes.
931
+ * User must have at least one of the listed permissions — checked via AuthAdapter.hasPermission().
932
+ * No user → 401. User without any matching permission → 403.
933
+ */
934
+ permissions?: string[];
935
+ /**
936
+ * Optional nav items contributed by this plugin.
937
+ * GET /nav returns these filtered by the plugin's permissions for the current user.
938
+ */
939
+ nav?: NavItem[];
940
+ install?: (hooks: HookExecutor) => Promise<void> | void;
941
+ request: (ctx: TCtx) => Promise<TCtx & TAdd> | (TCtx & TAdd);
942
+ teardown?: () => Promise<void> | void;
943
+ }
944
+
945
+ interface WsRateLimitConfig {
946
+ /** Maximum messages per window per connection. Default: 60 */
947
+ max?: number;
948
+ /** Window duration in milliseconds. Default: 1000 (1 second) */
949
+ windowMs?: number;
950
+ }
951
+ declare class VelnWsAdapterImpl implements VelnWsAdapter {
952
+ private readonly _routes;
953
+ private readonly _rateLimitMap;
954
+ private readonly _rateLimitMax;
955
+ private readonly _rateLimitWindowMs;
956
+ constructor(rateLimit?: WsRateLimitConfig);
957
+ registerRoute(path: string, route: WsRouteShape$1): void;
958
+ getRoute(path: string): WsRoute | undefined;
959
+ /**
960
+ * ws() — register a typed WebSocket route on this adapter.
961
+ *
962
+ * Usage:
963
+ * const ws = createWsAdapter()
964
+ * app.registerWsAdapter(ws)
965
+ *
966
+ * ws.route('/chat', {
967
+ * open(ctx) { ctx.ws.send('welcome') },
968
+ * message(ctx, raw) { ctx.ws.send(raw) },
969
+ * })
970
+ *
971
+ * // With Zod message schema:
972
+ * ws.route('/chat', {
973
+ * message: z.object({ text: z.string() }),
974
+ * handlers: { message(ctx) { ctx.data.text } },
975
+ * })
976
+ */
977
+ route<TMsg = unknown>(path: string, handler: WsRouteHandler<TMsg>): this;
978
+ handleUpgrade(req: Request, server: bun.Server, baseCtx: BaseCtx$1, plugins: ReadonlyArray<Plugin<any, any>>, installedRef: {
979
+ value: boolean;
980
+ }, installedModulePlugins: Set<string>): Promise<Response | null>;
981
+ getWebsocketConfig(): bun.WebSocketHandler<Record<string, unknown>>;
982
+ private _buildCtx;
983
+ }
984
+ /**
985
+ * createWsAdapter() — creates a VelnWsAdapter for use with Veln apps.
986
+ *
987
+ * Usage:
988
+ * import { createWsAdapter } from '@oakbun/ws'
989
+ * app.registerWsAdapter(createWsAdapter())
990
+ *
991
+ * app.ws('/chat', {
992
+ * open(ctx) { ctx.ws.send('welcome') },
993
+ * message(ctx, raw) { ctx.ws.send(raw) },
994
+ * })
995
+ */
996
+ declare function createWsAdapter(rateLimit?: WsRateLimitConfig): VelnWsAdapterImpl;
997
+
998
+ export { VelnWsAdapterImpl, type WsCtx, type WsCtxData, type WsHandlers, type WsRateLimitConfig, type WsRoute, type WsRouteHandler, type WsRouteHandlerWithSchema, createWsAdapter };