@shaferllc/keel 0.12.0 → 0.35.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.
- package/README.md +28 -3
- package/dist/core/application.js +9 -0
- package/dist/core/auth.d.ts +41 -0
- package/dist/core/auth.js +70 -0
- package/dist/core/cache.d.ts +41 -0
- package/dist/core/cache.js +81 -0
- package/dist/core/casts.d.ts +19 -0
- package/dist/core/casts.js +69 -0
- package/dist/core/crypto.d.ts +24 -0
- package/dist/core/crypto.js +97 -0
- package/dist/core/database.d.ts +58 -0
- package/dist/core/database.js +140 -0
- package/dist/core/debug.d.ts +12 -0
- package/dist/core/debug.js +55 -0
- package/dist/core/events.d.ts +21 -0
- package/dist/core/events.js +46 -0
- package/dist/core/exceptions.d.ts +10 -1
- package/dist/core/exceptions.js +10 -1
- package/dist/core/factory.d.ts +84 -0
- package/dist/core/factory.js +154 -0
- package/dist/core/helpers.d.ts +13 -0
- package/dist/core/helpers.js +24 -0
- package/dist/core/http/kernel.js +21 -2
- package/dist/core/http/router.d.ts +32 -8
- package/dist/core/http/router.js +74 -5
- package/dist/core/index.d.ts +31 -2
- package/dist/core/index.js +17 -1
- package/dist/core/logger.d.ts +29 -0
- package/dist/core/logger.js +49 -0
- package/dist/core/mail.d.ts +94 -0
- package/dist/core/mail.js +157 -0
- package/dist/core/migrations.d.ts +76 -0
- package/dist/core/migrations.js +211 -0
- package/dist/core/model.d.ts +75 -0
- package/dist/core/model.js +202 -0
- package/dist/core/queue.d.ts +73 -0
- package/dist/core/queue.js +88 -0
- package/dist/core/rate-limit.d.ts +23 -0
- package/dist/core/rate-limit.js +50 -0
- package/dist/core/relations.d.ts +90 -0
- package/dist/core/relations.js +229 -0
- package/dist/core/request.d.ts +53 -1
- package/dist/core/request.js +187 -0
- package/dist/core/session.d.ts +46 -0
- package/dist/core/session.js +112 -0
- package/dist/core/static.d.ts +27 -0
- package/dist/core/static.js +61 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -165,14 +165,33 @@ See [docs/architecture.md](./docs/architecture.md) for the full picture.
|
|
|
165
165
|
| [The Service Container](./docs/container.md) | Binding and resolving services, DI |
|
|
166
166
|
| [Service Providers](./docs/providers.md) | The register/boot lifecycle |
|
|
167
167
|
| [Configuration](./docs/configuration.md) | `config/*.ts`, dot-notation, `env()` |
|
|
168
|
-
| [Routing](./docs/routing.md) | Closures, controller tuples,
|
|
168
|
+
| [Routing](./docs/routing.md) | Closures, controller tuples, groups, resources, domains |
|
|
169
|
+
| [URL Builder](./docs/url-builder.md) | Named-route URLs, signed URLs |
|
|
170
|
+
| [Hashing & Encryption](./docs/hashing.md) | Password hashing, AES value encryption |
|
|
171
|
+
| [Controllers](./docs/controllers.md) | Classes, DI, single-action, lazy-loaded |
|
|
172
|
+
| [Request & Response](./docs/request-response.md) | Input, cookies, output, `abort()` |
|
|
173
|
+
| [Sessions](./docs/sessions.md) | Cookie-backed sessions, flash messages |
|
|
174
|
+
| [Authentication](./docs/authentication.md) | Session auth, guards, user provider |
|
|
175
|
+
| [Database](./docs/database.md) | Driver-agnostic query builder |
|
|
176
|
+
| [Models](./docs/models.md) | Active-record: find/create/save, casts, relations |
|
|
177
|
+
| [Migrations](./docs/migrations.md) | Schema builder + migrator, dialect-aware |
|
|
178
|
+
| [Factories & Seeders](./docs/factories.md) | Built-in Faker, model factories, seeders |
|
|
179
|
+
| [Mail](./docs/mail.md) | Fluent mailer, pluggable transports, edge-safe |
|
|
180
|
+
| [Queues & Jobs](./docs/queues.md) | Dispatch jobs, pluggable drivers, workers |
|
|
181
|
+
| [Events](./docs/events.md) | Emit/listen decoupling, async listeners |
|
|
182
|
+
| [Cache](./docs/cache.md) | TTLs, the remember pattern, pluggable stores |
|
|
183
|
+
| [Logger](./docs/logger.md) | Leveled structured logging, child loggers |
|
|
184
|
+
| [Static Files](./docs/static-files.md) | serveStatic(), caching, dot-file safety |
|
|
169
185
|
| [Views](./docs/views.md) | Hono JSX components, layouts, the View service |
|
|
170
186
|
| [Middleware](./docs/middleware.md) | Global middleware, writing your own |
|
|
187
|
+
| [Rate Limiting](./docs/rate-limiting.md) | rateLimiter() middleware, per-key buckets |
|
|
171
188
|
| [Errors](./docs/errors.md) | HTTP exceptions, debug page, custom handlers |
|
|
189
|
+
| [Debugging](./docs/debugging.md) | dump() and dd() (dump-and-die) |
|
|
172
190
|
| [Validation](./docs/validation.md) | `validate()` with Zod, auto-422 field errors |
|
|
173
191
|
| [Inertia](./docs/inertia.md) | Server-side Inertia.js adapter |
|
|
174
192
|
| [The Console](./docs/console.md) | `serve`, `routes`, `make:*` generators |
|
|
175
193
|
| [Architecture](./docs/architecture.md) | Container, kernel, request lifecycle |
|
|
194
|
+
| [Built on Hono](./docs/hono.md) | The Hono layer underneath, and using it directly |
|
|
176
195
|
|
|
177
196
|
## Testing
|
|
178
197
|
|
|
@@ -207,8 +226,14 @@ config, and the console. On deck:
|
|
|
207
226
|
- [x] First-class routing (groups, resources, named routes) — **v0.11.0**
|
|
208
227
|
- [x] Domain routing, matchers, Inertia adapter — **v0.12.0**
|
|
209
228
|
- [x] Test suite (~99% coverage)
|
|
210
|
-
- [
|
|
211
|
-
- [
|
|
229
|
+
- [x] Query builder (driver-agnostic) — **v0.28.0**
|
|
230
|
+
- [x] Active-record Model layer — **v0.29.0**
|
|
231
|
+
- [x] Migrations (schema builder) — **v0.30.0**
|
|
232
|
+
- [x] Model relationships (hasMany / belongsTo / belongsToMany) — **v0.31.0**
|
|
233
|
+
- [x] Factories & seeders (built-in Faker) — **v0.32.0**
|
|
234
|
+
- [x] Model attribute casts + mass-assignment guarding — **v0.33.0**
|
|
235
|
+
- [x] Mail (fluent mailer, pluggable transports) — **v0.34.0**
|
|
236
|
+
- [x] Queues / background jobs (pluggable drivers) — **v0.35.0**
|
|
212
237
|
- [ ] Publish `src/core` as the `@keel/core` package
|
|
213
238
|
|
|
214
239
|
See [CHANGELOG.md](./CHANGELOG.md) for release history.
|
package/dist/core/application.js
CHANGED
|
@@ -11,6 +11,9 @@ import { Container } from "./container.js";
|
|
|
11
11
|
import { Config } from "./config.js";
|
|
12
12
|
import { Router } from "./http/router.js";
|
|
13
13
|
import { View } from "./view.js";
|
|
14
|
+
import { Events } from "./events.js";
|
|
15
|
+
import { Cache } from "./cache.js";
|
|
16
|
+
import { Logger } from "./logger.js";
|
|
14
17
|
import { setApplication } from "./helpers.js";
|
|
15
18
|
export class Application extends Container {
|
|
16
19
|
basePath;
|
|
@@ -26,6 +29,12 @@ export class Application extends Container {
|
|
|
26
29
|
this.singleton(Config, () => new Config());
|
|
27
30
|
this.singleton(Router, (app) => new Router(app));
|
|
28
31
|
this.singleton(View, () => new View());
|
|
32
|
+
this.singleton(Events, () => new Events());
|
|
33
|
+
this.singleton(Cache, () => new Cache());
|
|
34
|
+
this.singleton(Logger, (app) => new Logger({
|
|
35
|
+
level: app.config().get("logger.level", "info"),
|
|
36
|
+
pretty: Boolean(app.config().get("app.debug", false)),
|
|
37
|
+
}));
|
|
29
38
|
}
|
|
30
39
|
path(...segments) {
|
|
31
40
|
return [this.basePath, ...segments].join("/");
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-based authentication. Builds on the session store — `login()` stashes
|
|
3
|
+
* the user id in the session, `user()` loads the full user through a provider
|
|
4
|
+
* you register. Pair it with `hash` for password checks.
|
|
5
|
+
*
|
|
6
|
+
* const user = await findUserByEmail(email);
|
|
7
|
+
* if (user && (await hash.verify(user.password, password))) {
|
|
8
|
+
* auth().login(user.id);
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* auth().check(); // logged in?
|
|
12
|
+
* await auth().user(); // the full user (via your provider)
|
|
13
|
+
*/
|
|
14
|
+
import type { MiddlewareHandler } from "hono";
|
|
15
|
+
/** Loads a user by the id stored in the session. Register with setUserProvider. */
|
|
16
|
+
export type UserProvider = (id: string) => unknown | Promise<unknown>;
|
|
17
|
+
/** Tell Keel how to load the authenticated user from its id. */
|
|
18
|
+
export declare function setUserProvider(fn: UserProvider): void;
|
|
19
|
+
export declare class Auth {
|
|
20
|
+
/** Log a user in by id (stored in the session). */
|
|
21
|
+
login(id: string | number): void;
|
|
22
|
+
/** Log the current user out. */
|
|
23
|
+
logout(): void;
|
|
24
|
+
/** The authenticated user's id, or null. */
|
|
25
|
+
id(): string | null;
|
|
26
|
+
/** Whether a user is authenticated. */
|
|
27
|
+
check(): boolean;
|
|
28
|
+
/** Whether the request is unauthenticated. */
|
|
29
|
+
guest(): boolean;
|
|
30
|
+
/** Load the authenticated user via the registered provider. */
|
|
31
|
+
user<User = unknown>(): Promise<User | null>;
|
|
32
|
+
}
|
|
33
|
+
/** The auth accessor for the current request. */
|
|
34
|
+
export declare function auth(): Auth;
|
|
35
|
+
/**
|
|
36
|
+
* A guard middleware: rejects unauthenticated requests with a 401, or redirects
|
|
37
|
+
* if `redirectTo` is set.
|
|
38
|
+
*/
|
|
39
|
+
export declare function authGuard(options?: {
|
|
40
|
+
redirectTo?: string;
|
|
41
|
+
}): MiddlewareHandler;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-based authentication. Builds on the session store — `login()` stashes
|
|
3
|
+
* the user id in the session, `user()` loads the full user through a provider
|
|
4
|
+
* you register. Pair it with `hash` for password checks.
|
|
5
|
+
*
|
|
6
|
+
* const user = await findUserByEmail(email);
|
|
7
|
+
* if (user && (await hash.verify(user.password, password))) {
|
|
8
|
+
* auth().login(user.id);
|
|
9
|
+
* }
|
|
10
|
+
*
|
|
11
|
+
* auth().check(); // logged in?
|
|
12
|
+
* await auth().user(); // the full user (via your provider)
|
|
13
|
+
*/
|
|
14
|
+
import { session } from "./session.js";
|
|
15
|
+
const KEY = "auth_id";
|
|
16
|
+
let provider;
|
|
17
|
+
/** Tell Keel how to load the authenticated user from its id. */
|
|
18
|
+
export function setUserProvider(fn) {
|
|
19
|
+
provider = fn;
|
|
20
|
+
}
|
|
21
|
+
export class Auth {
|
|
22
|
+
/** Log a user in by id (stored in the session). */
|
|
23
|
+
login(id) {
|
|
24
|
+
session().put(KEY, String(id));
|
|
25
|
+
}
|
|
26
|
+
/** Log the current user out. */
|
|
27
|
+
logout() {
|
|
28
|
+
session().forget(KEY);
|
|
29
|
+
}
|
|
30
|
+
/** The authenticated user's id, or null. */
|
|
31
|
+
id() {
|
|
32
|
+
return session().get(KEY, null);
|
|
33
|
+
}
|
|
34
|
+
/** Whether a user is authenticated. */
|
|
35
|
+
check() {
|
|
36
|
+
return this.id() != null;
|
|
37
|
+
}
|
|
38
|
+
/** Whether the request is unauthenticated. */
|
|
39
|
+
guest() {
|
|
40
|
+
return !this.check();
|
|
41
|
+
}
|
|
42
|
+
/** Load the authenticated user via the registered provider. */
|
|
43
|
+
async user() {
|
|
44
|
+
const id = this.id();
|
|
45
|
+
if (id == null)
|
|
46
|
+
return null;
|
|
47
|
+
if (!provider) {
|
|
48
|
+
throw new Error("No user provider. Call setUserProvider((id) => findUser(id)).");
|
|
49
|
+
}
|
|
50
|
+
return (await provider(id));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** The auth accessor for the current request. */
|
|
54
|
+
export function auth() {
|
|
55
|
+
return new Auth();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A guard middleware: rejects unauthenticated requests with a 401, or redirects
|
|
59
|
+
* if `redirectTo` is set.
|
|
60
|
+
*/
|
|
61
|
+
export function authGuard(options = {}) {
|
|
62
|
+
return async (c, next) => {
|
|
63
|
+
if (new Auth().guest()) {
|
|
64
|
+
if (options.redirectTo)
|
|
65
|
+
return c.redirect(options.redirectTo);
|
|
66
|
+
return c.json({ error: "Unauthenticated", status: 401 }, 401);
|
|
67
|
+
}
|
|
68
|
+
await next();
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small cache with TTLs and the `remember` pattern. Memory-backed by default
|
|
3
|
+
* (per-process / per-isolate), with a pluggable store so you can swap in Redis,
|
|
4
|
+
* KV, or anything else. Bound as a singleton on the application; reach it with
|
|
5
|
+
* the global `cache()` helper.
|
|
6
|
+
*
|
|
7
|
+
* const stats = await cache().remember("stats", 60, () => computeStats());
|
|
8
|
+
*/
|
|
9
|
+
export interface CacheStore {
|
|
10
|
+
get(key: string): Promise<unknown> | unknown;
|
|
11
|
+
set(key: string, value: unknown, ttlMs?: number): Promise<void> | void;
|
|
12
|
+
delete(key: string): Promise<void> | void;
|
|
13
|
+
clear(): Promise<void> | void;
|
|
14
|
+
}
|
|
15
|
+
/** The default in-memory store. */
|
|
16
|
+
export declare class MemoryStore implements CacheStore {
|
|
17
|
+
private data;
|
|
18
|
+
get(key: string): unknown;
|
|
19
|
+
set(key: string, value: unknown, ttlMs?: number): void;
|
|
20
|
+
delete(key: string): void;
|
|
21
|
+
clear(): void;
|
|
22
|
+
}
|
|
23
|
+
export declare class Cache {
|
|
24
|
+
private store;
|
|
25
|
+
constructor(store?: CacheStore);
|
|
26
|
+
get<T = unknown>(key: string, fallback?: T): Promise<T>;
|
|
27
|
+
/** Store a value, optionally expiring after `ttlSeconds`. */
|
|
28
|
+
put(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
|
|
29
|
+
has(key: string): Promise<boolean>;
|
|
30
|
+
forget(key: string): Promise<void>;
|
|
31
|
+
/** Read and remove a value. */
|
|
32
|
+
pull<T = unknown>(key: string, fallback?: T): Promise<T>;
|
|
33
|
+
flush(): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Return the cached value, or compute it with `factory`, cache it for
|
|
36
|
+
* `ttlSeconds`, and return it.
|
|
37
|
+
*/
|
|
38
|
+
remember<T>(key: string, ttlSeconds: number, factory: () => T | Promise<T>): Promise<T>;
|
|
39
|
+
/** Like remember(), but cached forever (no TTL). */
|
|
40
|
+
rememberForever<T>(key: string, factory: () => T | Promise<T>): Promise<T>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small cache with TTLs and the `remember` pattern. Memory-backed by default
|
|
3
|
+
* (per-process / per-isolate), with a pluggable store so you can swap in Redis,
|
|
4
|
+
* KV, or anything else. Bound as a singleton on the application; reach it with
|
|
5
|
+
* the global `cache()` helper.
|
|
6
|
+
*
|
|
7
|
+
* const stats = await cache().remember("stats", 60, () => computeStats());
|
|
8
|
+
*/
|
|
9
|
+
/** The default in-memory store. */
|
|
10
|
+
export class MemoryStore {
|
|
11
|
+
data = new Map();
|
|
12
|
+
get(key) {
|
|
13
|
+
const entry = this.data.get(key);
|
|
14
|
+
if (!entry)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (entry.expires && entry.expires < Date.now()) {
|
|
17
|
+
this.data.delete(key);
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return entry.value;
|
|
21
|
+
}
|
|
22
|
+
set(key, value, ttlMs) {
|
|
23
|
+
this.data.set(key, { value, expires: ttlMs ? Date.now() + ttlMs : 0 });
|
|
24
|
+
}
|
|
25
|
+
delete(key) {
|
|
26
|
+
this.data.delete(key);
|
|
27
|
+
}
|
|
28
|
+
clear() {
|
|
29
|
+
this.data.clear();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class Cache {
|
|
33
|
+
store;
|
|
34
|
+
constructor(store = new MemoryStore()) {
|
|
35
|
+
this.store = store;
|
|
36
|
+
}
|
|
37
|
+
async get(key, fallback) {
|
|
38
|
+
const value = await this.store.get(key);
|
|
39
|
+
return (value === undefined ? fallback : value);
|
|
40
|
+
}
|
|
41
|
+
/** Store a value, optionally expiring after `ttlSeconds`. */
|
|
42
|
+
async put(key, value, ttlSeconds) {
|
|
43
|
+
await this.store.set(key, value, ttlSeconds ? ttlSeconds * 1000 : undefined);
|
|
44
|
+
}
|
|
45
|
+
async has(key) {
|
|
46
|
+
return (await this.store.get(key)) !== undefined;
|
|
47
|
+
}
|
|
48
|
+
async forget(key) {
|
|
49
|
+
await this.store.delete(key);
|
|
50
|
+
}
|
|
51
|
+
/** Read and remove a value. */
|
|
52
|
+
async pull(key, fallback) {
|
|
53
|
+
const value = await this.get(key, fallback);
|
|
54
|
+
await this.forget(key);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
async flush() {
|
|
58
|
+
await this.store.clear();
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Return the cached value, or compute it with `factory`, cache it for
|
|
62
|
+
* `ttlSeconds`, and return it.
|
|
63
|
+
*/
|
|
64
|
+
async remember(key, ttlSeconds, factory) {
|
|
65
|
+
const existing = await this.store.get(key);
|
|
66
|
+
if (existing !== undefined)
|
|
67
|
+
return existing;
|
|
68
|
+
const value = await factory();
|
|
69
|
+
await this.put(key, value, ttlSeconds);
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
/** Like remember(), but cached forever (no TTL). */
|
|
73
|
+
async rememberForever(key, factory) {
|
|
74
|
+
const existing = await this.store.get(key);
|
|
75
|
+
if (existing !== undefined)
|
|
76
|
+
return existing;
|
|
77
|
+
const value = await factory();
|
|
78
|
+
await this.put(key, value);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attribute casting for models. A model's `static casts` maps columns to a type;
|
|
3
|
+
* values are cast to real JS types when read (from the database or `fill`) and
|
|
4
|
+
* back to storable primitives when written. This is what lets a `boolean` column
|
|
5
|
+
* round-trip as `true`/`false` (not `1`/`0`) and a `json` column as an object.
|
|
6
|
+
*
|
|
7
|
+
* class User extends Model {
|
|
8
|
+
* static casts = { active: "boolean", meta: "json", joined_at: "date" };
|
|
9
|
+
* }
|
|
10
|
+
*/
|
|
11
|
+
import type { Row } from "./database.js";
|
|
12
|
+
export type CastType = "int" | "integer" | "float" | "number" | "boolean" | "bool" | "string" | "json" | "array" | "date";
|
|
13
|
+
export type Casts = Record<string, CastType>;
|
|
14
|
+
/** Raw storage value -> JS value. Tolerant of already-cast input. */
|
|
15
|
+
export declare function castGet(value: unknown, type: CastType): unknown;
|
|
16
|
+
/** JS value -> storable primitive (numbers, strings, 0/1, null). */
|
|
17
|
+
export declare function castSet(value: unknown, type: CastType): unknown;
|
|
18
|
+
/** Apply a caster to just the keys named in `casts`, leaving the rest as-is. */
|
|
19
|
+
export declare function applyCasts(row: Row, casts: Casts, caster: (value: unknown, type: CastType) => unknown): Row;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attribute casting for models. A model's `static casts` maps columns to a type;
|
|
3
|
+
* values are cast to real JS types when read (from the database or `fill`) and
|
|
4
|
+
* back to storable primitives when written. This is what lets a `boolean` column
|
|
5
|
+
* round-trip as `true`/`false` (not `1`/`0`) and a `json` column as an object.
|
|
6
|
+
*
|
|
7
|
+
* class User extends Model {
|
|
8
|
+
* static casts = { active: "boolean", meta: "json", joined_at: "date" };
|
|
9
|
+
* }
|
|
10
|
+
*/
|
|
11
|
+
/** Raw storage value -> JS value. Tolerant of already-cast input. */
|
|
12
|
+
export function castGet(value, type) {
|
|
13
|
+
if (value === null || value === undefined)
|
|
14
|
+
return value;
|
|
15
|
+
switch (type) {
|
|
16
|
+
case "int":
|
|
17
|
+
case "integer":
|
|
18
|
+
return Math.trunc(Number(value));
|
|
19
|
+
case "float":
|
|
20
|
+
case "number":
|
|
21
|
+
return Number(value);
|
|
22
|
+
case "boolean":
|
|
23
|
+
case "bool":
|
|
24
|
+
return value === true || value === 1 || value === "1" || value === "true";
|
|
25
|
+
case "string":
|
|
26
|
+
return String(value);
|
|
27
|
+
case "json":
|
|
28
|
+
case "array":
|
|
29
|
+
return typeof value === "string" ? JSON.parse(value) : value;
|
|
30
|
+
case "date":
|
|
31
|
+
return value instanceof Date ? value : new Date(value);
|
|
32
|
+
default:
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** JS value -> storable primitive (numbers, strings, 0/1, null). */
|
|
37
|
+
export function castSet(value, type) {
|
|
38
|
+
if (value === null || value === undefined)
|
|
39
|
+
return value;
|
|
40
|
+
switch (type) {
|
|
41
|
+
case "int":
|
|
42
|
+
case "integer":
|
|
43
|
+
return Math.trunc(Number(value));
|
|
44
|
+
case "float":
|
|
45
|
+
case "number":
|
|
46
|
+
return Number(value);
|
|
47
|
+
case "boolean":
|
|
48
|
+
case "bool":
|
|
49
|
+
return value ? 1 : 0;
|
|
50
|
+
case "string":
|
|
51
|
+
return String(value);
|
|
52
|
+
case "json":
|
|
53
|
+
case "array":
|
|
54
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
55
|
+
case "date":
|
|
56
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
57
|
+
default:
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Apply a caster to just the keys named in `casts`, leaving the rest as-is. */
|
|
62
|
+
export function applyCasts(row, casts, caster) {
|
|
63
|
+
const out = { ...row };
|
|
64
|
+
for (const [key, type] of Object.entries(casts)) {
|
|
65
|
+
if (key in out)
|
|
66
|
+
out[key] = caster(out[key], type);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password hashing and value encryption — both built on the Web Crypto API, so
|
|
3
|
+
* they work the same on Node and the edge (no native bindings, no bcrypt).
|
|
4
|
+
*
|
|
5
|
+
* const hashed = await hash.make(password);
|
|
6
|
+
* await hash.verify(hashed, password); // boolean
|
|
7
|
+
*
|
|
8
|
+
* const token = await encryption.encrypt({ userId: 1 });
|
|
9
|
+
* await encryption.decrypt(token); // { userId: 1 } | null
|
|
10
|
+
*/
|
|
11
|
+
export declare const hash: {
|
|
12
|
+
/** Hash a password (PBKDF2-SHA256 with a random salt). */
|
|
13
|
+
make(password: string, iterations?: number): Promise<string>;
|
|
14
|
+
/** Verify a password against a stored hash. */
|
|
15
|
+
verify(hashed: string, password: string): Promise<boolean>;
|
|
16
|
+
/** Whether a hash was made with fewer iterations than the current default. */
|
|
17
|
+
needsRehash(hashed: string, iterations?: number): boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare const encryption: {
|
|
20
|
+
/** Encrypt any JSON-serializable value (AES-GCM), keyed by config('app.key'). */
|
|
21
|
+
encrypt(value: unknown): Promise<string>;
|
|
22
|
+
/** Decrypt a value; returns null if the payload is tampered or invalid. */
|
|
23
|
+
decrypt<T = unknown>(payload: string): Promise<T | null>;
|
|
24
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password hashing and value encryption — both built on the Web Crypto API, so
|
|
3
|
+
* they work the same on Node and the edge (no native bindings, no bcrypt).
|
|
4
|
+
*
|
|
5
|
+
* const hashed = await hash.make(password);
|
|
6
|
+
* await hash.verify(hashed, password); // boolean
|
|
7
|
+
*
|
|
8
|
+
* const token = await encryption.encrypt({ userId: 1 });
|
|
9
|
+
* await encryption.decrypt(token); // { userId: 1 } | null
|
|
10
|
+
*/
|
|
11
|
+
import { config } from "./helpers.js";
|
|
12
|
+
/* --------------------------- shared utilities -------------------------- */
|
|
13
|
+
function b64(bytes) {
|
|
14
|
+
let s = "";
|
|
15
|
+
for (const byte of bytes)
|
|
16
|
+
s += String.fromCharCode(byte);
|
|
17
|
+
return btoa(s);
|
|
18
|
+
}
|
|
19
|
+
function fromB64(str) {
|
|
20
|
+
const s = atob(str);
|
|
21
|
+
const arr = new Uint8Array(s.length);
|
|
22
|
+
for (let i = 0; i < s.length; i++)
|
|
23
|
+
arr[i] = s.charCodeAt(i);
|
|
24
|
+
return arr;
|
|
25
|
+
}
|
|
26
|
+
function safeEqual(a, b) {
|
|
27
|
+
if (a.length !== b.length)
|
|
28
|
+
return false;
|
|
29
|
+
let result = 0;
|
|
30
|
+
for (let i = 0; i < a.length; i++)
|
|
31
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
32
|
+
return result === 0;
|
|
33
|
+
}
|
|
34
|
+
function appKey() {
|
|
35
|
+
const key = config("app.key", "");
|
|
36
|
+
if (!key)
|
|
37
|
+
throw new Error("Encryption requires config('app.key'). Set APP_KEY.");
|
|
38
|
+
return key;
|
|
39
|
+
}
|
|
40
|
+
/* ------------------------------- hashing ------------------------------- */
|
|
41
|
+
const DEFAULT_ITERATIONS = 100_000;
|
|
42
|
+
async function pbkdf2(password, salt, iterations) {
|
|
43
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveBits"]);
|
|
44
|
+
const bits = await crypto.subtle.deriveBits({ name: "PBKDF2", salt: salt, iterations, hash: "SHA-256" }, key, 256);
|
|
45
|
+
return new Uint8Array(bits);
|
|
46
|
+
}
|
|
47
|
+
export const hash = {
|
|
48
|
+
/** Hash a password (PBKDF2-SHA256 with a random salt). */
|
|
49
|
+
async make(password, iterations = DEFAULT_ITERATIONS) {
|
|
50
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
51
|
+
const derived = await pbkdf2(password, salt, iterations);
|
|
52
|
+
return `pbkdf2_sha256$${iterations}$${b64(salt)}$${b64(derived)}`;
|
|
53
|
+
},
|
|
54
|
+
/** Verify a password against a stored hash. */
|
|
55
|
+
async verify(hashed, password) {
|
|
56
|
+
const [algo, iter, salt64, hash64] = hashed.split("$");
|
|
57
|
+
if (algo !== "pbkdf2_sha256" || !iter || !salt64 || !hash64)
|
|
58
|
+
return false;
|
|
59
|
+
const derived = await pbkdf2(password, fromB64(salt64), Number(iter));
|
|
60
|
+
return safeEqual(b64(derived), hash64);
|
|
61
|
+
},
|
|
62
|
+
/** Whether a hash was made with fewer iterations than the current default. */
|
|
63
|
+
needsRehash(hashed, iterations = DEFAULT_ITERATIONS) {
|
|
64
|
+
const iter = Number(hashed.split("$")[1]);
|
|
65
|
+
return !iter || iter < iterations;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
/* ----------------------------- encryption ------------------------------ */
|
|
69
|
+
async function aesKey() {
|
|
70
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(appKey()));
|
|
71
|
+
return crypto.subtle.importKey("raw", digest, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
72
|
+
}
|
|
73
|
+
export const encryption = {
|
|
74
|
+
/** Encrypt any JSON-serializable value (AES-GCM), keyed by config('app.key'). */
|
|
75
|
+
async encrypt(value) {
|
|
76
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
77
|
+
const data = new TextEncoder().encode(JSON.stringify(value));
|
|
78
|
+
const cipher = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, await aesKey(), data));
|
|
79
|
+
const packed = new Uint8Array(iv.length + cipher.length);
|
|
80
|
+
packed.set(iv);
|
|
81
|
+
packed.set(cipher, iv.length);
|
|
82
|
+
return b64(packed);
|
|
83
|
+
},
|
|
84
|
+
/** Decrypt a value; returns null if the payload is tampered or invalid. */
|
|
85
|
+
async decrypt(payload) {
|
|
86
|
+
try {
|
|
87
|
+
const bytes = fromB64(payload);
|
|
88
|
+
const iv = bytes.slice(0, 12);
|
|
89
|
+
const cipher = bytes.slice(12);
|
|
90
|
+
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, await aesKey(), cipher);
|
|
91
|
+
return JSON.parse(new TextDecoder().decode(plain));
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, driver-agnostic query builder. It generates parameterized SQL and
|
|
3
|
+
* runs it through a `Connection` you provide — so it works with any driver:
|
|
4
|
+
* Cloudflare D1, Neon/Postgres, PlanetScale, Turso/libSQL, better-sqlite3, pg.
|
|
5
|
+
* The core stays edge-safe because Keel never imports a database driver.
|
|
6
|
+
*
|
|
7
|
+
* setConnection(myConnection, "postgres");
|
|
8
|
+
* const active = await db("users").where("active", true).orderBy("name").get();
|
|
9
|
+
* const user = await db("users").where("id", 1).first();
|
|
10
|
+
* await db("users").insert({ email });
|
|
11
|
+
*/
|
|
12
|
+
export type Row = Record<string, unknown>;
|
|
13
|
+
export interface WriteResult {
|
|
14
|
+
rowsAffected: number;
|
|
15
|
+
insertId?: number | string;
|
|
16
|
+
}
|
|
17
|
+
/** The bridge to your database driver. */
|
|
18
|
+
export interface Connection {
|
|
19
|
+
/** Run a SELECT (or any row-returning query) and return the rows. */
|
|
20
|
+
select<T = Row>(sql: string, bindings: unknown[]): Promise<T[]>;
|
|
21
|
+
/** Run an INSERT/UPDATE/DELETE and return write metadata. */
|
|
22
|
+
write(sql: string, bindings: unknown[]): Promise<WriteResult>;
|
|
23
|
+
}
|
|
24
|
+
export type Dialect = "sqlite" | "mysql" | "postgres";
|
|
25
|
+
export type Operator = "=" | "!=" | "<" | "<=" | ">" | ">=" | "like";
|
|
26
|
+
/** Register the database connection (and dialect) used by `db()`. */
|
|
27
|
+
export declare function setConnection(conn: Connection, driverDialect?: Dialect): void;
|
|
28
|
+
export declare class QueryBuilder<T extends Row = Row> {
|
|
29
|
+
private table;
|
|
30
|
+
private wheres;
|
|
31
|
+
private orders;
|
|
32
|
+
private columns;
|
|
33
|
+
private _limit?;
|
|
34
|
+
private _offset?;
|
|
35
|
+
constructor(table: string);
|
|
36
|
+
select(...columns: string[]): this;
|
|
37
|
+
where(column: string, value: unknown): this;
|
|
38
|
+
where(column: string, operator: Operator, value: unknown): this;
|
|
39
|
+
orWhere(column: string, value: unknown): this;
|
|
40
|
+
orWhere(column: string, operator: Operator, value: unknown): this;
|
|
41
|
+
whereIn(column: string, values: unknown[]): this;
|
|
42
|
+
whereNull(column: string): this;
|
|
43
|
+
whereNotNull(column: string): this;
|
|
44
|
+
orderBy(column: string, direction?: "asc" | "desc"): this;
|
|
45
|
+
limit(n: number): this;
|
|
46
|
+
offset(n: number): this;
|
|
47
|
+
private whereClause;
|
|
48
|
+
get(): Promise<T[]>;
|
|
49
|
+
first(): Promise<T | null>;
|
|
50
|
+
count(): Promise<number>;
|
|
51
|
+
exists(): Promise<boolean>;
|
|
52
|
+
insert(data: Row): Promise<WriteResult>;
|
|
53
|
+
insertGetId(data: Row): Promise<number | string | undefined>;
|
|
54
|
+
update(data: Row): Promise<WriteResult>;
|
|
55
|
+
delete(): Promise<WriteResult>;
|
|
56
|
+
}
|
|
57
|
+
/** Start a query against a table. */
|
|
58
|
+
export declare function db<T extends Row = Row>(table: string): QueryBuilder<T>;
|