@shaferllc/keel 0.12.0 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +30 -3
  2. package/dist/core/application.js +9 -0
  3. package/dist/core/auth.d.ts +41 -0
  4. package/dist/core/auth.js +70 -0
  5. package/dist/core/cache.d.ts +41 -0
  6. package/dist/core/cache.js +81 -0
  7. package/dist/core/casts.d.ts +19 -0
  8. package/dist/core/casts.js +69 -0
  9. package/dist/core/crypto.d.ts +24 -0
  10. package/dist/core/crypto.js +97 -0
  11. package/dist/core/database.d.ts +58 -0
  12. package/dist/core/database.js +140 -0
  13. package/dist/core/debug.d.ts +12 -0
  14. package/dist/core/debug.js +55 -0
  15. package/dist/core/events.d.ts +21 -0
  16. package/dist/core/events.js +46 -0
  17. package/dist/core/exceptions.d.ts +10 -1
  18. package/dist/core/exceptions.js +10 -1
  19. package/dist/core/factory.d.ts +84 -0
  20. package/dist/core/factory.js +154 -0
  21. package/dist/core/helpers.d.ts +13 -0
  22. package/dist/core/helpers.js +24 -0
  23. package/dist/core/http/kernel.js +21 -2
  24. package/dist/core/http/router.d.ts +32 -8
  25. package/dist/core/http/router.js +74 -5
  26. package/dist/core/index.d.ts +33 -2
  27. package/dist/core/index.js +18 -1
  28. package/dist/core/logger.d.ts +29 -0
  29. package/dist/core/logger.js +49 -0
  30. package/dist/core/mail.d.ts +94 -0
  31. package/dist/core/mail.js +157 -0
  32. package/dist/core/migrations.d.ts +76 -0
  33. package/dist/core/migrations.js +211 -0
  34. package/dist/core/model.d.ts +75 -0
  35. package/dist/core/model.js +202 -0
  36. package/dist/core/notification.d.ts +82 -0
  37. package/dist/core/notification.js +129 -0
  38. package/dist/core/queue.d.ts +73 -0
  39. package/dist/core/queue.js +88 -0
  40. package/dist/core/rate-limit.d.ts +23 -0
  41. package/dist/core/rate-limit.js +50 -0
  42. package/dist/core/relations.d.ts +90 -0
  43. package/dist/core/relations.js +229 -0
  44. package/dist/core/request.d.ts +53 -1
  45. package/dist/core/request.js +187 -0
  46. package/dist/core/session.d.ts +46 -0
  47. package/dist/core/session.js +112 -0
  48. package/dist/core/static.d.ts +27 -0
  49. package/dist/core/static.js +61 -0
  50. package/package.json +2 -1
@@ -7,6 +7,13 @@
7
7
  */
8
8
  import type { Context as HonoContext, MiddlewareHandler } from "hono";
9
9
  import type { Container, Constructor } from "../container.js";
10
+ export interface UrlOptions {
11
+ qs?: Record<string, string | number>;
12
+ }
13
+ export interface SignedUrlOptions extends UrlOptions {
14
+ /** Expiry in seconds from now. */
15
+ expiresIn?: number;
16
+ }
10
17
  /** The request context handed to every route handler and middleware. */
11
18
  export type Ctx = HonoContext;
12
19
  /** A route-parameter constraint: a regex, a source string, or `{ match }`. */
@@ -34,13 +41,15 @@ export type ControllerRef = Constructor | LazyController;
34
41
  export type ControllerAction = [ControllerRef] | [ControllerRef, string];
35
42
  /** A function, a controller action, or a ready-made Response. */
36
43
  export type RouteHandler = HandlerFn | ControllerAction | Response;
44
+ /** A middleware handler, or the name of one registered with `router.named()`. */
45
+ export type MiddlewareRef = MiddlewareHandler | string;
37
46
  export type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD";
38
47
  export interface RouteDefinition {
39
48
  methods: Method[];
40
49
  path: string;
41
50
  handler: RouteHandler;
42
51
  name?: string;
43
- middleware: MiddlewareHandler[];
52
+ middleware: MiddlewareRef[];
44
53
  wheres: Record<string, string>;
45
54
  /** Host pattern this route is bound to, e.g. ":tenant.example.com". */
46
55
  domain?: string;
@@ -54,9 +63,9 @@ export declare class Route {
54
63
  /** Alias for name(). */
55
64
  as(name: string): this;
56
65
  /** Attach middleware that runs only for this route (after group middleware). */
57
- middleware(mw: MiddlewareHandler | MiddlewareHandler[]): this;
66
+ middleware(mw: MiddlewareRef | MiddlewareRef[]): this;
58
67
  /** Alias for middleware(), matching AdonisJS. */
59
- use(mw: MiddlewareHandler | MiddlewareHandler[]): this;
68
+ use(mw: MiddlewareRef | MiddlewareRef[]): this;
60
69
  /** Constrain a route parameter with a regex, source string, or matcher. */
61
70
  where(param: string, matcher: Matcher): this;
62
71
  /** Bind this route to a host pattern (supports `:subdomain` segments). */
@@ -67,9 +76,9 @@ export declare class RouteGroup {
67
76
  private routes;
68
77
  constructor(routes: RouteDefinition[]);
69
78
  prefix(prefix: string): this;
70
- middleware(mw: MiddlewareHandler | MiddlewareHandler[]): this;
79
+ middleware(mw: MiddlewareRef | MiddlewareRef[]): this;
71
80
  /** Alias for middleware(), matching AdonisJS. */
72
- use(mw: MiddlewareHandler | MiddlewareHandler[]): this;
81
+ use(mw: MiddlewareRef | MiddlewareRef[]): this;
73
82
  /** Constrain a parameter across every route in the group. */
74
83
  where(param: string, matcher: Matcher): this;
75
84
  as(namePrefix: string): this;
@@ -91,7 +100,7 @@ export declare class RouteResource {
91
100
  /** Rename a route parameter, e.g. `.params({ posts: "post" })`. */
92
101
  params(map: Record<string, string>): this;
93
102
  /** Attach middleware to specific actions (or "*" for all). */
94
- use(actions: string[] | "*", mw: MiddlewareHandler | MiddlewareHandler[]): this;
103
+ use(actions: string[] | "*", mw: MiddlewareRef | MiddlewareRef[]): this;
95
104
  }
96
105
  /** Fluent matcher for `on(path)` convenience routes. */
97
106
  declare class RouteMatcher {
@@ -118,6 +127,7 @@ export declare class Router {
118
127
  private group_prefix;
119
128
  private group_mw;
120
129
  private globalWheres;
130
+ private namedMiddleware;
121
131
  /** Built-in parameter matchers: `router.matchers.number()`. */
122
132
  readonly matchers: {
123
133
  number: () => RegExp;
@@ -126,6 +136,13 @@ export declare class Router {
126
136
  alpha: () => RegExp;
127
137
  };
128
138
  constructor(container: Container);
139
+ /**
140
+ * Register named middleware, referenceable by name in `.middleware()` /
141
+ * `.use()`: `router.named({ auth, admin })` then `route.use("auth")`.
142
+ */
143
+ named(map: Record<string, MiddlewareHandler>): this;
144
+ /** Resolve a middleware reference (name or function) to a handler. */
145
+ resolveMiddleware(ref: MiddlewareRef): MiddlewareHandler;
129
146
  get(path: string, handler: RouteHandler): Route;
130
147
  post(path: string, handler: RouteHandler): Route;
131
148
  put(path: string, handler: RouteHandler): Route;
@@ -152,8 +169,15 @@ export declare class Router {
152
169
  where(param: string, matcher: Matcher): this;
153
170
  /** All registered routes (excluding those trimmed to zero methods). */
154
171
  all(): RouteDefinition[];
155
- /** Generate a URL for a named route, substituting `:params`. */
156
- url(name: string, params?: Record<string, string | number>): string;
172
+ /** Generate a URL for a named route, substituting `:params` and query string. */
173
+ url(name: string, params?: Record<string, string | number>, options?: UrlOptions): string;
174
+ /**
175
+ * A tamper-proof URL for a named route, signed with `config('app.key')`.
176
+ * Verify the incoming request with `router.hasValidSignature()`.
177
+ */
178
+ signedUrl(name: string, params?: Record<string, string | number>, options?: SignedUrlOptions): Promise<string>;
179
+ /** Whether the current request carries a valid (unexpired) signature. */
180
+ hasValidSignature(): Promise<boolean>;
157
181
  /** Turn a route handler into an executable function, resolving controllers. */
158
182
  resolve(handler: RouteHandler): HandlerFn;
159
183
  }
@@ -5,9 +5,22 @@
5
5
  * Fluent, AdonisJS-inspired API: named routes, per-route and group middleware,
6
6
  * prefixes, param constraints, resource routes, and URL generation.
7
7
  */
8
- import { view } from "../helpers.js";
9
- import { redirect as makeRedirect } from "../request.js";
8
+ import { view, config } from "../helpers.js";
9
+ import { redirect as makeRedirect, request } from "../request.js";
10
10
  import { inertia } from "../inertia.js";
11
+ /** HMAC-SHA256 hex signature (Web Crypto — works on Node and the edge). */
12
+ async function hmac(key, data) {
13
+ const enc = new TextEncoder();
14
+ const cryptoKey = await crypto.subtle.importKey("raw", enc.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
15
+ const sig = await crypto.subtle.sign("HMAC", cryptoKey, enc.encode(data));
16
+ return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
17
+ }
18
+ function appKey() {
19
+ const key = config("app.key", "");
20
+ if (!key)
21
+ throw new Error("Signed URLs require config('app.key'). Set APP_KEY.");
22
+ return key;
23
+ }
11
24
  function matcherSource(m) {
12
25
  if (typeof m === "string")
13
26
  return m;
@@ -206,11 +219,30 @@ export class Router {
206
219
  group_prefix = "";
207
220
  group_mw = [];
208
221
  globalWheres = {};
222
+ namedMiddleware = {};
209
223
  /** Built-in parameter matchers: `router.matchers.number()`. */
210
224
  matchers = matchers;
211
225
  constructor(container) {
212
226
  this.container = container;
213
227
  }
228
+ /**
229
+ * Register named middleware, referenceable by name in `.middleware()` /
230
+ * `.use()`: `router.named({ auth, admin })` then `route.use("auth")`.
231
+ */
232
+ named(map) {
233
+ Object.assign(this.namedMiddleware, map);
234
+ return this;
235
+ }
236
+ /** Resolve a middleware reference (name or function) to a handler. */
237
+ resolveMiddleware(ref) {
238
+ if (typeof ref !== "string")
239
+ return ref;
240
+ const mw = this.namedMiddleware[ref];
241
+ if (!mw) {
242
+ throw new Error(`No named middleware [${ref}]. Register it with router.named({ ${ref}: … }).`);
243
+ }
244
+ return mw;
245
+ }
214
246
  get(path, handler) {
215
247
  return this.add(["GET"], path, handler);
216
248
  }
@@ -305,8 +337,8 @@ export class Router {
305
337
  }
306
338
  return this.routes.filter((r) => r.methods.length > 0);
307
339
  }
308
- /** Generate a URL for a named route, substituting `:params`. */
309
- url(name, params = {}) {
340
+ /** Generate a URL for a named route, substituting `:params` and query string. */
341
+ url(name, params = {}, options = {}) {
310
342
  const def = this.routes.find((r) => r.name === name);
311
343
  if (!def)
312
344
  throw new Error(`No route named [${name}].`);
@@ -314,7 +346,44 @@ export class Router {
314
346
  for (const [k, v] of Object.entries(params)) {
315
347
  path = path.replace(new RegExp(`:${k}\\??`), encodeURIComponent(String(v)));
316
348
  }
317
- return path.replace(/\/:[^/]+\?/g, "").replace(/:[^/]+/g, "");
349
+ path = path.replace(/\/:[^/]+\?/g, "").replace(/:[^/]+/g, "");
350
+ if (options.qs && Object.keys(options.qs).length) {
351
+ const qs = new URLSearchParams(Object.fromEntries(Object.entries(options.qs).map(([k, v]) => [k, String(v)])));
352
+ return `${path}?${qs}`;
353
+ }
354
+ return path;
355
+ }
356
+ /**
357
+ * A tamper-proof URL for a named route, signed with `config('app.key')`.
358
+ * Verify the incoming request with `router.hasValidSignature()`.
359
+ */
360
+ async signedUrl(name, params = {}, options = {}) {
361
+ const qs = new URLSearchParams();
362
+ for (const [k, v] of Object.entries(options.qs ?? {}))
363
+ qs.set(k, String(v));
364
+ if (options.expiresIn) {
365
+ qs.set("expires", String(Math.floor(Date.now() / 1000) + options.expiresIn));
366
+ }
367
+ let url = this.url(name, params);
368
+ const query = qs.toString();
369
+ if (query)
370
+ url += `?${query}`;
371
+ const signature = await hmac(appKey(), url);
372
+ return `${url}${query ? "&" : "?"}signature=${signature}`;
373
+ }
374
+ /** Whether the current request carries a valid (unexpired) signature. */
375
+ async hasValidSignature() {
376
+ const url = new URL(request.raw.url);
377
+ const signature = url.searchParams.get("signature");
378
+ if (!signature)
379
+ return false;
380
+ url.searchParams.delete("signature");
381
+ const expires = url.searchParams.get("expires");
382
+ if (expires && Number(expires) < Math.floor(Date.now() / 1000))
383
+ return false;
384
+ const base = url.pathname + (url.search || "");
385
+ const expected = await hmac(appKey(), base);
386
+ return signature.length === expected.length && signature === expected;
318
387
  }
319
388
  /** Turn a route handler into an executable function, resolving controllers. */
320
389
  resolve(handler) {
@@ -4,7 +4,34 @@ export type { Token, Constructor, Factory } from "./container.js";
4
4
  export { Application } from "./application.js";
5
5
  export type { BootOptions } from "./application.js";
6
6
  export { Config, env } from "./config.js";
7
- export { app, config, view, bind, singleton, instance, make, bound, } from "./helpers.js";
7
+ export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, } from "./helpers.js";
8
+ export { Logger } from "./logger.js";
9
+ export type { LogLevel, LoggerOptions } from "./logger.js";
10
+ export { Events } from "./events.js";
11
+ export type { Listener } from "./events.js";
12
+ export { Cache, MemoryStore } from "./cache.js";
13
+ export type { CacheStore } from "./cache.js";
14
+ export { serveStatic } from "./static.js";
15
+ export type { StaticOptions } from "./static.js";
16
+ export { dump, dd } from "./debug.js";
17
+ export { hash, encryption } from "./crypto.js";
18
+ export { rateLimiter } from "./rate-limit.js";
19
+ export type { RateLimiterOptions } from "./rate-limit.js";
20
+ export { db, setConnection, QueryBuilder } from "./database.js";
21
+ export type { Connection, WriteResult, Row, Dialect, Operator } from "./database.js";
22
+ export { Model } from "./model.js";
23
+ export type { CastType, Casts } from "./casts.js";
24
+ export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations.js";
25
+ export { Faker, Factory as ModelFactory, factory, Seeder, seed } from "./factory.js";
26
+ export type { Definition } from "./factory.js";
27
+ export { Mailer, PendingMail, ArrayTransport, LogTransport, fetchTransport, mail, setMailer, getMailer, } from "./mail.js";
28
+ export type { Message, Transport, MailerOptions, FetchTransportOptions } from "./mail.js";
29
+ export { Job, Queue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, } from "./queue.js";
30
+ export type { Dispatchable, JobOptions, QueueDriver, Drainable, QueuedJob } from "./queue.js";
31
+ export { Notification, Notifier, MailChannel, DatabaseChannel, ArrayChannel, routeFor, notify, setNotifier, getNotifier, } from "./notification.js";
32
+ export type { Notifiable, MailContent, Channel } from "./notification.js";
33
+ export { SchemaBuilder, Migrator, TableBuilder, Column } from "./migrations.js";
34
+ export type { Migration } from "./migrations.js";
8
35
  export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
9
36
  export type { ConfigData } from "./config.js";
10
37
  export { View } from "./view.js";
@@ -12,10 +39,14 @@ export type { Renderable, ViewConfig } from "./view.js";
12
39
  export { ServiceProvider } from "./provider.js";
13
40
  export type { ProviderClass } from "./provider.js";
14
41
  export { Router, Route, RouteGroup, RouteResource, matchers } from "./http/router.js";
15
- export type { Ctx, RouteHandler, RouteDefinition, Method, Matcher } from "./http/router.js";
42
+ export type { Ctx, RouteHandler, RouteDefinition, Method, Matcher, MiddlewareRef, UrlOptions, SignedUrlOptions, } from "./http/router.js";
16
43
  export { Inertia, inertia, inertiaPageAttr } from "./inertia.js";
17
44
  export type { InertiaPage, InertiaOptions } from "./inertia.js";
18
45
  export { HttpKernel } from "./http/kernel.js";
19
46
  export { HttpException, NotFoundException, UnauthorizedException, ForbiddenException, ValidationException, STATUS_TEXT, } from "./exceptions.js";
20
47
  export { validate } from "./validation.js";
21
48
  export type { Schema } from "./validation.js";
49
+ export { Session, session, sessionMiddleware } from "./session.js";
50
+ export type { SessionOptions } from "./session.js";
51
+ export { Auth, auth, authGuard, setUserProvider } from "./auth.js";
52
+ export type { UserProvider } from "./auth.js";
@@ -2,7 +2,22 @@
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, } from "./helpers.js";
5
+ export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, } from "./helpers.js";
6
+ export { Logger } from "./logger.js";
7
+ export { Events } from "./events.js";
8
+ export { Cache, MemoryStore } from "./cache.js";
9
+ export { serveStatic } from "./static.js";
10
+ export { dump, dd } from "./debug.js";
11
+ export { hash, encryption } from "./crypto.js";
12
+ export { rateLimiter } from "./rate-limit.js";
13
+ export { db, setConnection, QueryBuilder } from "./database.js";
14
+ export { Model } from "./model.js";
15
+ export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations.js";
16
+ export { Faker, Factory as ModelFactory, factory, Seeder, seed } from "./factory.js";
17
+ export { Mailer, PendingMail, ArrayTransport, LogTransport, fetchTransport, mail, setMailer, getMailer, } from "./mail.js";
18
+ export { Job, Queue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, } from "./queue.js";
19
+ export { Notification, Notifier, MailChannel, DatabaseChannel, ArrayChannel, routeFor, notify, setNotifier, getNotifier, } from "./notification.js";
20
+ export { SchemaBuilder, Migrator, TableBuilder, Column } from "./migrations.js";
6
21
  export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
7
22
  export { View } from "./view.js";
8
23
  export { ServiceProvider } from "./provider.js";
@@ -11,3 +26,5 @@ export { Inertia, inertia, inertiaPageAttr } from "./inertia.js";
11
26
  export { HttpKernel } from "./http/kernel.js";
12
27
  export { HttpException, NotFoundException, UnauthorizedException, ForbiddenException, ValidationException, STATUS_TEXT, } from "./exceptions.js";
13
28
  export { validate } from "./validation.js";
29
+ export { Session, session, sessionMiddleware } from "./session.js";
30
+ export { Auth, auth, authGuard, setUserProvider } from "./auth.js";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * A small leveled logger. Structured JSON by default (one line per event, ready
3
+ * for log aggregators); pretty single-line output in debug. Bound as a
4
+ * singleton on the application — reach it with the global `logger()` helper.
5
+ *
6
+ * logger().info("user registered", { userId: user.id });
7
+ * logger().error("payment failed", { orderId, error });
8
+ */
9
+ export type LogLevel = "debug" | "info" | "warn" | "error";
10
+ export interface LoggerOptions {
11
+ /** Minimum level to emit. Default: "info". */
12
+ level?: LogLevel;
13
+ /** Pretty single-line output instead of JSON. Default: false. */
14
+ pretty?: boolean;
15
+ /** Fields merged into every log line. */
16
+ bindings?: Record<string, unknown>;
17
+ }
18
+ export declare class Logger {
19
+ private options;
20
+ private threshold;
21
+ constructor(options?: LoggerOptions);
22
+ private write;
23
+ debug(message: string, context?: Record<string, unknown>): void;
24
+ info(message: string, context?: Record<string, unknown>): void;
25
+ warn(message: string, context?: Record<string, unknown>): void;
26
+ error(message: string, context?: Record<string, unknown>): void;
27
+ /** A child logger with additional bound fields (e.g. a request id). */
28
+ child(bindings: Record<string, unknown>): Logger;
29
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * A small leveled logger. Structured JSON by default (one line per event, ready
3
+ * for log aggregators); pretty single-line output in debug. Bound as a
4
+ * singleton on the application — reach it with the global `logger()` helper.
5
+ *
6
+ * logger().info("user registered", { userId: user.id });
7
+ * logger().error("payment failed", { orderId, error });
8
+ */
9
+ const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
10
+ export class Logger {
11
+ options;
12
+ threshold;
13
+ constructor(options = {}) {
14
+ this.options = options;
15
+ this.threshold = LEVELS[options.level ?? "info"];
16
+ }
17
+ write(level, message, context) {
18
+ if (LEVELS[level] < this.threshold)
19
+ return;
20
+ const time = new Date().toISOString();
21
+ if (this.options.pretty) {
22
+ const extra = context ? " " + JSON.stringify(context) : "";
23
+ const fn = level === "warn" ? console.warn : level === "error" ? console.error : console.log;
24
+ fn(`[${time}] ${level.toUpperCase().padEnd(5)} ${message}${extra}`);
25
+ }
26
+ else {
27
+ console.log(JSON.stringify({ level, time, msg: message, ...this.options.bindings, ...context }));
28
+ }
29
+ }
30
+ debug(message, context) {
31
+ this.write("debug", message, context);
32
+ }
33
+ info(message, context) {
34
+ this.write("info", message, context);
35
+ }
36
+ warn(message, context) {
37
+ this.write("warn", message, context);
38
+ }
39
+ error(message, context) {
40
+ this.write("error", message, context);
41
+ }
42
+ /** A child logger with additional bound fields (e.g. a request id). */
43
+ child(bindings) {
44
+ return new Logger({
45
+ ...this.options,
46
+ bindings: { ...this.options.bindings, ...bindings },
47
+ });
48
+ }
49
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * A small, edge-safe mailer. You compose a message with a fluent builder and
3
+ * send it through a pluggable `Transport` — the same shape as the database
4
+ * layer (`setMailer` / `mail()` mirror `setConnection` / `db()`). The core
5
+ * imports no SDK: the built-in transports use `fetch`, `console`, or memory, so
6
+ * it runs on Node and the edge.
7
+ *
8
+ * setMailer(fetchTransport({ url, headers }), { from: "hi@app.com" });
9
+ *
10
+ * await mail()
11
+ * .to("ada@example.com")
12
+ * .subject("Welcome")
13
+ * .html("<h1>Hi</h1>")
14
+ * .send();
15
+ *
16
+ * In tests, register an `ArrayTransport` and assert on `transport.sent`.
17
+ */
18
+ /** A normalized, ready-to-send message. */
19
+ export interface Message {
20
+ to: string[];
21
+ from?: string;
22
+ cc?: string[];
23
+ bcc?: string[];
24
+ replyTo?: string;
25
+ subject: string;
26
+ text?: string;
27
+ html?: string;
28
+ headers?: Record<string, string>;
29
+ }
30
+ /** The bridge to your email provider. */
31
+ export interface Transport {
32
+ send(message: Message): Promise<void>;
33
+ }
34
+ export interface MailerOptions {
35
+ /** Default `from` address for messages that don't set one. */
36
+ from?: string;
37
+ }
38
+ /** A fluent, immutable-ish builder — chain setters, then `send()`. */
39
+ export declare class PendingMail {
40
+ private mailer;
41
+ private message;
42
+ constructor(mailer: Mailer);
43
+ to(...addresses: string[]): this;
44
+ from(address: string): this;
45
+ cc(...addresses: string[]): this;
46
+ bcc(...addresses: string[]): this;
47
+ replyTo(address: string): this;
48
+ subject(subject: string): this;
49
+ text(text: string): this;
50
+ html(html: string): this;
51
+ header(name: string, value: string): this;
52
+ /** Seed several fields at once (merges into what's been chained). */
53
+ fill(partial: Partial<{
54
+ to: string | string[];
55
+ cc: string | string[];
56
+ bcc: string | string[];
57
+ } & Omit<Message, "to" | "cc" | "bcc">>): this;
58
+ /** Freeze and hand the message to the mailer. */
59
+ send(): Promise<Message>;
60
+ }
61
+ export declare class Mailer {
62
+ private transport;
63
+ private options;
64
+ constructor(transport: Transport, options?: MailerOptions);
65
+ /** Start composing a message. */
66
+ message(): PendingMail;
67
+ /** Validate, apply defaults, and dispatch through the transport. */
68
+ send(message: Message): Promise<Message>;
69
+ }
70
+ /** Collects sent messages in memory — the default, and ideal for tests. */
71
+ export declare class ArrayTransport implements Transport {
72
+ readonly sent: Message[];
73
+ send(message: Message): Promise<void>;
74
+ }
75
+ /** Logs each message instead of delivering it — handy in local development. */
76
+ export declare class LogTransport implements Transport {
77
+ send(message: Message): Promise<void>;
78
+ }
79
+ export interface FetchTransportOptions {
80
+ /** Provider endpoint to POST to. */
81
+ url: string;
82
+ /** Extra headers (e.g. `Authorization`). `Content-Type: application/json` is added. */
83
+ headers?: Record<string, string>;
84
+ /** Map a message to the provider's request body. Defaults to the message itself. */
85
+ body?: (message: Message) => unknown;
86
+ }
87
+ /** A generic HTTP transport for provider APIs (Resend, Postmark, …), via `fetch`. */
88
+ export declare function fetchTransport(options: FetchTransportOptions): Transport;
89
+ /** Register the default mailer used by `mail()`. */
90
+ export declare function setMailer(transport: Transport, options?: MailerOptions): Mailer;
91
+ /** The default mailer instance. */
92
+ export declare function getMailer(): Mailer;
93
+ /** Start a message on the default mailer: `mail().to(…).subject(…).send()`. */
94
+ export declare function mail(): PendingMail;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * A small, edge-safe mailer. You compose a message with a fluent builder and
3
+ * send it through a pluggable `Transport` — the same shape as the database
4
+ * layer (`setMailer` / `mail()` mirror `setConnection` / `db()`). The core
5
+ * imports no SDK: the built-in transports use `fetch`, `console`, or memory, so
6
+ * it runs on Node and the edge.
7
+ *
8
+ * setMailer(fetchTransport({ url, headers }), { from: "hi@app.com" });
9
+ *
10
+ * await mail()
11
+ * .to("ada@example.com")
12
+ * .subject("Welcome")
13
+ * .html("<h1>Hi</h1>")
14
+ * .send();
15
+ *
16
+ * In tests, register an `ArrayTransport` and assert on `transport.sent`.
17
+ */
18
+ import { logger } from "./helpers.js";
19
+ function list(value) {
20
+ if (value === undefined)
21
+ return [];
22
+ return Array.isArray(value) ? value : [value];
23
+ }
24
+ /** A fluent, immutable-ish builder — chain setters, then `send()`. */
25
+ export class PendingMail {
26
+ mailer;
27
+ message = { to: [], subject: "" };
28
+ constructor(mailer) {
29
+ this.mailer = mailer;
30
+ }
31
+ to(...addresses) {
32
+ this.message.to.push(...addresses);
33
+ return this;
34
+ }
35
+ from(address) {
36
+ this.message.from = address;
37
+ return this;
38
+ }
39
+ cc(...addresses) {
40
+ (this.message.cc ??= []).push(...addresses);
41
+ return this;
42
+ }
43
+ bcc(...addresses) {
44
+ (this.message.bcc ??= []).push(...addresses);
45
+ return this;
46
+ }
47
+ replyTo(address) {
48
+ this.message.replyTo = address;
49
+ return this;
50
+ }
51
+ subject(subject) {
52
+ this.message.subject = subject;
53
+ return this;
54
+ }
55
+ text(text) {
56
+ this.message.text = text;
57
+ return this;
58
+ }
59
+ html(html) {
60
+ this.message.html = html;
61
+ return this;
62
+ }
63
+ header(name, value) {
64
+ (this.message.headers ??= {})[name] = value;
65
+ return this;
66
+ }
67
+ /** Seed several fields at once (merges into what's been chained). */
68
+ fill(partial) {
69
+ const { to, cc, bcc, ...rest } = partial;
70
+ if (to)
71
+ this.message.to.push(...list(to));
72
+ if (cc)
73
+ (this.message.cc ??= []).push(...list(cc));
74
+ if (bcc)
75
+ (this.message.bcc ??= []).push(...list(bcc));
76
+ Object.assign(this.message, rest);
77
+ return this;
78
+ }
79
+ /** Freeze and hand the message to the mailer. */
80
+ async send() {
81
+ return this.mailer.send(this.message);
82
+ }
83
+ }
84
+ export class Mailer {
85
+ transport;
86
+ options;
87
+ constructor(transport, options = {}) {
88
+ this.transport = transport;
89
+ this.options = options;
90
+ }
91
+ /** Start composing a message. */
92
+ message() {
93
+ return new PendingMail(this);
94
+ }
95
+ /** Validate, apply defaults, and dispatch through the transport. */
96
+ async send(message) {
97
+ const final = { ...message, from: message.from ?? this.options.from };
98
+ if (!final.to.length)
99
+ throw new Error("Mail: at least one recipient (to) is required.");
100
+ if (!final.subject)
101
+ throw new Error("Mail: a subject is required.");
102
+ if (!final.text && !final.html)
103
+ throw new Error("Mail: a text or html body is required.");
104
+ if (!final.from)
105
+ throw new Error("Mail: a from address is required (set one or a default).");
106
+ await this.transport.send(final);
107
+ return final;
108
+ }
109
+ }
110
+ /* ------------------------------- transports ------------------------------- */
111
+ /** Collects sent messages in memory — the default, and ideal for tests. */
112
+ export class ArrayTransport {
113
+ sent = [];
114
+ async send(message) {
115
+ this.sent.push(message);
116
+ }
117
+ }
118
+ /** Logs each message instead of delivering it — handy in local development. */
119
+ export class LogTransport {
120
+ async send(message) {
121
+ logger().info("mail sent", {
122
+ to: message.to,
123
+ from: message.from,
124
+ subject: message.subject,
125
+ });
126
+ }
127
+ }
128
+ /** A generic HTTP transport for provider APIs (Resend, Postmark, …), via `fetch`. */
129
+ export function fetchTransport(options) {
130
+ return {
131
+ async send(message) {
132
+ const res = await fetch(options.url, {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json", ...options.headers },
135
+ body: JSON.stringify(options.body ? options.body(message) : message),
136
+ });
137
+ if (!res.ok) {
138
+ throw new Error(`Mail: transport responded ${res.status} ${res.statusText}`);
139
+ }
140
+ },
141
+ };
142
+ }
143
+ /* --------------------------------- global --------------------------------- */
144
+ let mailer = new Mailer(new ArrayTransport());
145
+ /** Register the default mailer used by `mail()`. */
146
+ export function setMailer(transport, options = {}) {
147
+ mailer = new Mailer(transport, options);
148
+ return mailer;
149
+ }
150
+ /** The default mailer instance. */
151
+ export function getMailer() {
152
+ return mailer;
153
+ }
154
+ /** Start a message on the default mailer: `mail().to(…).subject(…).send()`. */
155
+ export function mail() {
156
+ return mailer.message();
157
+ }