@lastshotlabs/bunshot 0.0.13 → 0.0.18

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 (123) hide show
  1. package/README.md +2816 -1747
  2. package/dist/adapters/memoryAuth.d.ts +7 -0
  3. package/dist/adapters/memoryAuth.js +177 -2
  4. package/dist/adapters/mongoAuth.js +94 -0
  5. package/dist/adapters/sqliteAuth.d.ts +9 -0
  6. package/dist/adapters/sqliteAuth.js +190 -2
  7. package/dist/app.d.ts +120 -2
  8. package/dist/app.js +104 -4
  9. package/dist/entrypoints/queue.d.ts +2 -2
  10. package/dist/entrypoints/queue.js +1 -1
  11. package/dist/index.d.ts +24 -8
  12. package/dist/index.js +15 -5
  13. package/dist/lib/appConfig.d.ts +81 -0
  14. package/dist/lib/appConfig.js +30 -0
  15. package/dist/lib/authAdapter.d.ts +54 -0
  16. package/dist/lib/authRateLimit.d.ts +2 -0
  17. package/dist/lib/authRateLimit.js +4 -0
  18. package/dist/lib/clientIp.d.ts +14 -0
  19. package/dist/lib/clientIp.js +52 -0
  20. package/dist/lib/constants.d.ts +4 -0
  21. package/dist/lib/constants.js +4 -0
  22. package/dist/lib/context.d.ts +2 -0
  23. package/dist/lib/createDtoMapper.d.ts +33 -0
  24. package/dist/lib/createDtoMapper.js +69 -0
  25. package/dist/lib/crypto.d.ts +11 -0
  26. package/dist/lib/crypto.js +22 -0
  27. package/dist/lib/emailVerification.d.ts +4 -0
  28. package/dist/lib/emailVerification.js +20 -12
  29. package/dist/lib/jwt.d.ts +1 -1
  30. package/dist/lib/jwt.js +19 -6
  31. package/dist/lib/mfaChallenge.d.ts +42 -0
  32. package/dist/lib/mfaChallenge.js +293 -0
  33. package/dist/lib/oauth.d.ts +14 -1
  34. package/dist/lib/oauth.js +19 -1
  35. package/dist/lib/oauthCode.d.ts +15 -0
  36. package/dist/lib/oauthCode.js +90 -0
  37. package/dist/lib/queue.d.ts +33 -0
  38. package/dist/lib/queue.js +98 -0
  39. package/dist/lib/resetPassword.js +12 -16
  40. package/dist/lib/roles.d.ts +4 -0
  41. package/dist/lib/roles.js +27 -0
  42. package/dist/lib/session.d.ts +12 -0
  43. package/dist/lib/session.js +165 -5
  44. package/dist/lib/tenant.d.ts +15 -0
  45. package/dist/lib/tenant.js +65 -0
  46. package/dist/lib/ws.js +5 -1
  47. package/dist/lib/zodToMongoose.d.ts +38 -0
  48. package/dist/lib/zodToMongoose.js +84 -0
  49. package/dist/middleware/bearerAuth.js +4 -3
  50. package/dist/middleware/botProtection.js +2 -2
  51. package/dist/middleware/cacheResponse.d.ts +1 -0
  52. package/dist/middleware/cacheResponse.js +18 -3
  53. package/dist/middleware/cors.d.ts +2 -0
  54. package/dist/middleware/cors.js +22 -8
  55. package/dist/middleware/csrf.d.ts +18 -0
  56. package/dist/middleware/csrf.js +115 -0
  57. package/dist/middleware/rateLimit.d.ts +2 -1
  58. package/dist/middleware/rateLimit.js +7 -5
  59. package/dist/middleware/requireRole.d.ts +14 -3
  60. package/dist/middleware/requireRole.js +46 -6
  61. package/dist/middleware/tenant.d.ts +5 -0
  62. package/dist/middleware/tenant.js +116 -0
  63. package/dist/models/AuthUser.d.ts +17 -0
  64. package/dist/models/AuthUser.js +17 -0
  65. package/dist/models/TenantRole.d.ts +15 -0
  66. package/dist/models/TenantRole.js +23 -0
  67. package/dist/routes/auth.d.ts +5 -3
  68. package/dist/routes/auth.js +173 -30
  69. package/dist/routes/jobs.d.ts +2 -0
  70. package/dist/routes/jobs.js +270 -0
  71. package/dist/routes/mfa.d.ts +5 -0
  72. package/dist/routes/mfa.js +616 -0
  73. package/dist/routes/oauth.js +378 -23
  74. package/dist/schemas/auth.d.ts +2 -0
  75. package/dist/schemas/auth.js +22 -1
  76. package/dist/server.d.ts +6 -0
  77. package/dist/server.js +19 -3
  78. package/dist/services/auth.d.ts +18 -5
  79. package/dist/services/auth.js +112 -18
  80. package/dist/services/mfa.d.ts +84 -0
  81. package/dist/services/mfa.js +543 -0
  82. package/dist/ws/index.js +3 -2
  83. package/docs/sections/adding-middleware/full.md +35 -0
  84. package/docs/sections/adding-models/full.md +125 -0
  85. package/docs/sections/adding-models/overview.md +13 -0
  86. package/docs/sections/adding-routes/full.md +182 -0
  87. package/docs/sections/adding-routes/overview.md +23 -0
  88. package/docs/sections/auth-flow/full.md +634 -0
  89. package/docs/sections/auth-flow/overview.md +10 -0
  90. package/docs/sections/cli/full.md +30 -0
  91. package/docs/sections/configuration/full.md +155 -0
  92. package/docs/sections/configuration/overview.md +17 -0
  93. package/docs/sections/configuration-example/full.md +117 -0
  94. package/docs/sections/configuration-example/overview.md +30 -0
  95. package/docs/sections/documentation/full.md +171 -0
  96. package/docs/sections/environment-variables/full.md +55 -0
  97. package/docs/sections/exports/full.md +92 -0
  98. package/docs/sections/extending-context/full.md +59 -0
  99. package/docs/sections/header.md +3 -0
  100. package/docs/sections/installation/full.md +6 -0
  101. package/docs/sections/jobs/full.md +140 -0
  102. package/docs/sections/jobs/overview.md +15 -0
  103. package/docs/sections/mongodb-connections/full.md +45 -0
  104. package/docs/sections/mongodb-connections/overview.md +7 -0
  105. package/docs/sections/multi-tenancy/full.md +66 -0
  106. package/docs/sections/multi-tenancy/overview.md +15 -0
  107. package/docs/sections/oauth/full.md +189 -0
  108. package/docs/sections/oauth/overview.md +16 -0
  109. package/docs/sections/package-development/full.md +7 -0
  110. package/docs/sections/peer-dependencies/full.md +47 -0
  111. package/docs/sections/quick-start/full.md +43 -0
  112. package/docs/sections/response-caching/full.md +117 -0
  113. package/docs/sections/response-caching/overview.md +13 -0
  114. package/docs/sections/roles/full.md +136 -0
  115. package/docs/sections/roles/overview.md +12 -0
  116. package/docs/sections/running-without-redis/full.md +16 -0
  117. package/docs/sections/running-without-redis-or-mongodb/full.md +60 -0
  118. package/docs/sections/stack/full.md +10 -0
  119. package/docs/sections/websocket/full.md +101 -0
  120. package/docs/sections/websocket/overview.md +5 -0
  121. package/docs/sections/websocket-rooms/full.md +97 -0
  122. package/docs/sections/websocket-rooms/overview.md +5 -0
  123. package/package.json +30 -9
@@ -0,0 +1,125 @@
1
+ ## Adding Models
2
+
3
+ Import `appConnection` and register models on it. This ensures your models use the correct connection whether you're on a single DB or a separate tenant DB.
4
+
5
+ `appConnection` is a lazy proxy — calling `.model()` at the top level works fine even before `connectMongo()` has been called. Mongoose buffers any queries until the connection is established.
6
+
7
+ ```ts
8
+ // src/models/Product.ts
9
+ import { appConnection } from "@lastshotlabs/bunshot";
10
+ import { Schema } from "mongoose";
11
+ import type { HydratedDocument } from "mongoose";
12
+
13
+ interface IProduct {
14
+ name: string;
15
+ price: number;
16
+ }
17
+
18
+ export type ProductDocument = HydratedDocument<IProduct>;
19
+
20
+ const ProductSchema = new Schema<IProduct>({
21
+ name: { type: String, required: true },
22
+ price: { type: Number, required: true },
23
+ }, { timestamps: true });
24
+
25
+ export const Product = appConnection.model<IProduct>("Product", ProductSchema);
26
+ ```
27
+
28
+ > **Note:** Import types (`HydratedDocument`, `Schema`, etc.) directly from `"mongoose"` — the `appConnection` and `mongoose` exports from bunshot are runtime proxies and cannot be used as TypeScript namespaces.
29
+
30
+ ### Zod as Single Source of Truth
31
+
32
+ If you use Zod schemas for your OpenAPI spec (via `createRoute` or `modelSchemas`), you can derive your Mongoose schemas and DTO mappers from those same Zod definitions — so each entity is defined **once**.
33
+
34
+ #### `zodToMongoose` — Zod → Mongoose SchemaDefinition
35
+
36
+ Converts a Zod object schema into a Mongoose field definition. Business fields are auto-converted; DB-specific concerns (ObjectId refs, type overrides, subdocuments) are declared via config. The `id` field is automatically excluded since Mongoose provides `_id`.
37
+
38
+ ```ts
39
+ import { appConnection, zodToMongoose } from "@lastshotlabs/bunshot";
40
+ import { Schema, type HydratedDocument } from "mongoose";
41
+ import { ProductSchema } from "../schemas/product"; // your Zod schema
42
+ import type { ProductDto } from "../schemas/product";
43
+
44
+ // DB interface derives from Zod DTO type
45
+ interface IProduct extends Omit<ProductDto, "id" | "categoryId"> {
46
+ user: Types.ObjectId;
47
+ category: Types.ObjectId;
48
+ }
49
+
50
+ const ProductMongoSchema = new Schema<IProduct>(
51
+ zodToMongoose(ProductSchema, {
52
+ dbFields: {
53
+ user: { type: Schema.Types.ObjectId, ref: "UserProfile", required: true },
54
+ },
55
+ refs: {
56
+ categoryId: { dbField: "category", ref: "Category" },
57
+ },
58
+ typeOverrides: {
59
+ createdAt: { type: Date, required: true },
60
+ },
61
+ }) as Record<string, unknown>,
62
+ { timestamps: true }
63
+ );
64
+
65
+ export type ProductDocument = HydratedDocument<IProduct>;
66
+ export const Product = appConnection.model<IProduct>("Product", ProductMongoSchema);
67
+ ```
68
+
69
+ **Config options:**
70
+
71
+ | Option | Description |
72
+ |---|---|
73
+ | `dbFields` | Fields that exist only in the DB, not in the API schema (e.g., `user` ObjectId ref) |
74
+ | `refs` | API fields that map to ObjectId refs: `{ accountId: { dbField: "account", ref: "Account" } }` |
75
+ | `typeOverrides` | Override the auto-converted Mongoose type for a field (e.g., Zod `z.string()` for dates → Mongoose `Date`) |
76
+ | `subdocSchemas` | Subdocument array fields: `{ items: mongooseSubSchema }` |
77
+
78
+ **Auto-conversion mapping:**
79
+
80
+ | Zod type | Mongoose type |
81
+ |---|---|
82
+ | `z.string()` | `String` |
83
+ | `z.number()` | `Number` |
84
+ | `z.boolean()` | `Boolean` |
85
+ | `z.date()` | `Date` |
86
+ | `z.enum([...])` | `String` with `enum` |
87
+ | `.nullable()` / `.optional()` | `required: false` |
88
+
89
+ #### `createDtoMapper` — Zod → toDto mapper
90
+
91
+ Creates a generic `toDto` function from a Zod schema. The schema defines which fields exist in the DTO; the config declares how to transform DB-specific types.
92
+
93
+ ```ts
94
+ import { createDtoMapper } from "@lastshotlabs/bunshot";
95
+ import { ProductSchema, type ProductDto } from "../schemas/product";
96
+
97
+ const toDto = createDtoMapper<ProductDto>(ProductSchema, {
98
+ refs: { category: "categoryId" }, // ObjectId ref → string, with rename
99
+ dates: ["createdAt"], // Date → ISO string
100
+ });
101
+
102
+ // Use it
103
+ const product = await Product.findOne({ _id: id });
104
+ return product ? toDto(product) : null;
105
+ ```
106
+
107
+ **Auto-handled transforms:**
108
+
109
+ | Transform | Description |
110
+ |---|---|
111
+ | `_id` → `id` | Always converted via `.toString()` |
112
+ | `refs` | ObjectId fields → string (`.toString()`), with DB→API field renaming |
113
+ | `dates` | `Date` objects → ISO strings (`.toISOString()`) |
114
+ | `subdocs` | Array fields mapped with a sub-mapper (for nested documents) |
115
+ | nullable/optional | `undefined` → `null` coercion (based on Zod schema) |
116
+ | everything else | Passthrough |
117
+
118
+ **Subdocument example:**
119
+
120
+ ```ts
121
+ const itemToDto = createDtoMapper<TemplateItemDto>(TemplateItemSchema);
122
+ const toDto = createDtoMapper<TemplateDto>(TemplateSchema, {
123
+ subdocs: { items: itemToDto },
124
+ });
125
+ ```
@@ -0,0 +1,13 @@
1
+ ## Adding Models
2
+
3
+ Import `appConnection` and register Mongoose models on it. `appConnection` is a lazy proxy — `.model()` works before `connectMongo()` has been called.
4
+
5
+ ```ts
6
+ import { appConnection } from "@lastshotlabs/bunshot";
7
+ import { Schema, type HydratedDocument } from "mongoose";
8
+
9
+ const ProductSchema = new Schema({ name: String, price: Number }, { timestamps: true });
10
+ export const Product = appConnection.model("Product", ProductSchema);
11
+ ```
12
+
13
+ Bunshot also provides `zodToMongoose` (Zod -> Mongoose schema conversion) and `createDtoMapper` (DB document -> API DTO) to use Zod as the single source of truth for your models and OpenAPI spec.
@@ -0,0 +1,182 @@
1
+ ## Adding Routes
2
+
3
+ Drop a file in your `routes/` directory that exports a `router` — see the [Quick Start](#quick-start) example above. Routes are auto-discovered via glob — no registration needed. Subdirectories are supported, so you can organise by feature:
4
+
5
+ ```
6
+ routes/
7
+ products.ts
8
+ ingredients/
9
+ list.ts
10
+ detail.ts
11
+ ```
12
+
13
+ ### OpenAPI Schema Registration
14
+
15
+ Import `createRoute` from `@lastshotlabs/bunshot` (not from `@hono/zod-openapi`). The wrapper automatically registers every unnamed request body and response schema as a named entry in `components/schemas`. Schemas you already named via `registerSchema` are never overwritten.
16
+
17
+ Every Zod schema that appears in your OpenAPI spec ends up as a named entry in `components/schemas` — either auto-named by the framework or explicitly named by you. There are four registration methods, each suited to a different scenario.
18
+
19
+ ---
20
+
21
+ ### Method 1 — Route-level auto-registration (via `createRoute`)
22
+
23
+ The most common case. When you define a route with `createRoute`, every unnamed request body and response schema is automatically registered under a name derived from the HTTP method and path.
24
+
25
+ **Naming convention**
26
+
27
+ | Route | Part | Generated name |
28
+ |-------|------|----------------|
29
+ | `POST /products` | request body | `CreateProductsRequest` |
30
+ | `POST /products` | 201 response | `CreateProductsResponse` |
31
+ | `GET /products/{id}` | 200 response | `GetProductsByIdResponse` |
32
+ | `DELETE /products/{id}` | 404 response | `DeleteProductsByIdNotFoundError` |
33
+ | `PATCH /products/{id}` | request body | `UpdateProductsByIdRequest` |
34
+
35
+ HTTP methods → verbs: `GET → Get`, `POST → Create`, `PUT → Replace`, `PATCH → Update`, `DELETE → Delete`.
36
+
37
+ Status codes → suffixes: `200/201/204 → Response`, `400 → BadRequestError`, `401 → UnauthorizedError`, `403 → ForbiddenError`, `404 → NotFoundError`, `409 → ConflictError`, `422 → ValidationError`, `429 → RateLimitError`, `500 → InternalError`, `501 → NotImplementedError`, `503 → UnavailableError`. Unknown codes fall back to the number.
38
+
39
+ **Limitation:** if the same Zod object is used in two different routes, each route names it after itself — you get two identical inline shapes instead of one shared `$ref`. Use Method 2 or 3 to fix this.
40
+
41
+ ---
42
+
43
+ ### Method 2 — Directory / glob auto-discovery (via `modelSchemas`)
44
+
45
+ Use this when you have schemas shared across multiple routes. Point `modelSchemas` at one or more directories and Bunshot imports every `.ts` file **before** routes are loaded. Any exported Zod schema is registered automatically — same object referenced in multiple routes → same `$ref` in the spec.
46
+
47
+ **Naming:** export name with the trailing `Schema` suffix stripped (`LedgerItemSchema` → `"LedgerItem"`). Already-registered schemas are never overwritten.
48
+
49
+ ```ts
50
+ // src/schemas/ledgerItem.ts
51
+ import { z } from "zod";
52
+ export const LedgerItemSchema = z.object({ id: z.string(), name: z.string(), amount: z.number() });
53
+ // → auto-registered as "LedgerItem"
54
+ ```
55
+
56
+ ```ts
57
+ // src/config/index.ts
58
+ await createServer({
59
+ routesDir: import.meta.dir + "/routes",
60
+ modelSchemas: import.meta.dir + "/schemas", // string shorthand — registration: "auto"
61
+ });
62
+ ```
63
+
64
+ ```ts
65
+ // src/routes/ledger.ts AND src/routes/ledgerDetail.ts
66
+ import { LedgerItemSchema } from "@schemas/ledgerItem"; // same Zod object instance
67
+ createRoute({ responses: { 200: { content: { "application/json": { schema: LedgerItemSchema } } } } });
68
+ // → $ref: "#/components/schemas/LedgerItem" in both routes
69
+ ```
70
+
71
+ **Multiple directories and glob patterns**
72
+
73
+ ```ts
74
+ modelSchemas: [
75
+ import.meta.dir + "/schemas", // dedicated schemas dir
76
+ import.meta.dir + "/models", // co-located with DB models
77
+ import.meta.dir + "/services/**/*.schema.ts", // selective glob
78
+ ]
79
+ ```
80
+
81
+ **Full config object** — use when you need to set `registration` or mix paths and globs:
82
+
83
+ ```ts
84
+ modelSchemas: {
85
+ paths: [import.meta.dir + "/schemas", import.meta.dir + "/models"],
86
+ registration: "auto", // default — auto-registers exports with suffix stripping
87
+ }
88
+ ```
89
+
90
+ **`registration: "explicit"`** — files are imported but nothing is auto-registered. Registration is left entirely to `registerSchema` / `registerSchemas` calls inside each file. Use this when you want zero magic and full name control:
91
+
92
+ ```ts
93
+ modelSchemas: { paths: import.meta.dir + "/schemas", registration: "explicit" }
94
+ ```
95
+
96
+ ---
97
+
98
+ ### Method 3 — Batch explicit registration (via `registerSchemas`)
99
+
100
+ `registerSchemas` lets you name a group of schemas all at once. Object keys become the `components/schemas` names; the same object is returned so you can destructure and export normally. No suffix stripping — names are taken as-is.
101
+
102
+ ```ts
103
+ // src/schemas/index.ts
104
+ import { registerSchemas } from "@lastshotlabs/bunshot";
105
+ import { z } from "zod";
106
+
107
+ export const { LedgerItem, Product, ErrorResponse } = registerSchemas({
108
+ LedgerItem: z.object({ id: z.string(), name: z.string(), amount: z.number() }),
109
+ Product: z.object({ id: z.string(), price: z.number() }),
110
+ ErrorResponse: z.object({ error: z.string() }),
111
+ });
112
+ ```
113
+
114
+ Pair with `registration: "explicit"` in `modelSchemas` so the file is imported before routes, or call it inline at the top of any route file — route files are auto-discovered so the top-level call runs before the spec is served.
115
+
116
+ ---
117
+
118
+ ### Method 4 — Single explicit registration (via `registerSchema`)
119
+
120
+ `registerSchema("Name", schema)` registers one schema and returns it unchanged. Useful for a single shared type (e.g. a common error envelope) or to override the name auto-discovery would generate.
121
+
122
+ ```ts
123
+ // src/schemas/errors.ts
124
+ import { registerSchema } from "@lastshotlabs/bunshot";
125
+ import { z } from "zod";
126
+
127
+ export const ErrorResponse = registerSchema("ErrorResponse",
128
+ z.object({ error: z.string() })
129
+ );
130
+ ```
131
+
132
+ Registration is idempotent — calling `registerSchema` on an already-registered schema is a no-op. This means you can safely call it in files that are also covered by `modelSchemas` auto-discovery: whichever runs first wins, and the other is silently skipped.
133
+
134
+ ---
135
+
136
+ ### Priority and interaction
137
+
138
+ All four methods write to the same process-global registry. The rules are simple:
139
+
140
+ 1. **First write wins** — once a schema has a name, it cannot be renamed.
141
+ 2. **`modelSchemas` files are imported before routes**, so explicit calls inside them always take precedence over what `createRoute` would generate for the same object.
142
+ 3. **`registerSchema` / `registerSchemas` take precedence over auto-discovery** when they appear at module top level (they run at import time, before `maybeAutoRegister` inspects the export list).
143
+ 4. **`createRoute` never overwrites** a schema already in the registry — it only fills gaps.
144
+
145
+ **Decision guide:**
146
+
147
+ | Situation | Use |
148
+ |-----------|-----|
149
+ | Route-specific, one-off schema | `createRoute` auto-registration (Method 1) |
150
+ | Shared across routes, happy with suffix-stripped export name | `modelSchemas` auto-discovery (Method 2) |
151
+ | Shared across routes, want explicit names or batch control | `registerSchemas` (Method 3) |
152
+ | Single shared schema or custom name override | `registerSchema` (Method 4) |
153
+
154
+ **Protected routes**
155
+
156
+ Use `withSecurity` to declare security schemes on a route without breaking `c.req.valid()` type inference. (Inlining `security` directly in `createRoute({...})` causes TypeScript to collapse the handler's input types to `never`.)
157
+
158
+ ```ts
159
+ import { createRoute, withSecurity } from "@lastshotlabs/bunshot";
160
+
161
+ router.openapi(
162
+ withSecurity(
163
+ createRoute({ method: "get", path: "/me", ... }),
164
+ { cookieAuth: [] },
165
+ { userToken: [] }
166
+ ),
167
+ async (c) => {
168
+ const userId = c.get("authUserId"); // fully typed
169
+ }
170
+ );
171
+ ```
172
+
173
+ Pass each security scheme as a separate object argument. The security scheme names (`cookieAuth`, `userToken`, `bearerAuth`) are registered globally by `createApp`.
174
+
175
+ **Load order:** By default, routes load in filesystem order. If a route needs to be registered before another (e.g. for Hono's first-match-wins routing), export a `priority` number — lower values load first. Routes without a `priority` load last.
176
+
177
+ ```ts
178
+ // routes/tenants.ts — must match before generic routes
179
+ export const priority = 1;
180
+ export const router = createRouter();
181
+ // ...
182
+ ```
@@ -0,0 +1,23 @@
1
+ ## Adding Routes
2
+
3
+ Drop a file in your `routes/` directory that exports a `router` — routes are auto-discovered via glob. Subdirectories are supported.
4
+
5
+ ```ts
6
+ import { z } from "zod";
7
+ import { createRoute, createRouter } from "@lastshotlabs/bunshot";
8
+
9
+ export const router = createRouter();
10
+
11
+ router.openapi(
12
+ createRoute({
13
+ method: "get",
14
+ path: "/hello",
15
+ responses: {
16
+ 200: { content: { "application/json": { schema: z.object({ message: z.string() }) } }, description: "Hello" },
17
+ },
18
+ }),
19
+ (c) => c.json({ message: "Hello world!" }, 200)
20
+ );
21
+ ```
22
+
23
+ Import `createRoute` from `@lastshotlabs/bunshot` (not `@hono/zod-openapi`) to get automatic OpenAPI schema registration. Four registration methods are available — route-level auto-registration, directory/glob auto-discovery via `modelSchemas`, batch explicit via `registerSchemas`, and single explicit via `registerSchema`. Use `withSecurity` to add auth requirements without breaking type inference.