@lastshotlabs/bunshot 0.0.10 → 0.0.16

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 (100) hide show
  1. package/README.md +2510 -1580
  2. package/dist/adapters/memoryAuth.d.ts +4 -0
  3. package/dist/adapters/memoryAuth.js +131 -2
  4. package/dist/adapters/mongoAuth.js +56 -0
  5. package/dist/adapters/sqliteAuth.d.ts +6 -0
  6. package/dist/adapters/sqliteAuth.js +137 -2
  7. package/dist/app.d.ts +107 -2
  8. package/dist/app.js +83 -4
  9. package/dist/entrypoints/queue.d.ts +2 -2
  10. package/dist/entrypoints/queue.js +1 -1
  11. package/dist/index.d.ts +15 -5
  12. package/dist/index.js +10 -3
  13. package/dist/lib/appConfig.d.ts +46 -0
  14. package/dist/lib/appConfig.js +20 -0
  15. package/dist/lib/authAdapter.d.ts +30 -0
  16. package/dist/lib/constants.d.ts +2 -0
  17. package/dist/lib/constants.js +2 -0
  18. package/dist/lib/context.d.ts +2 -0
  19. package/dist/lib/createDtoMapper.d.ts +33 -0
  20. package/dist/lib/createDtoMapper.js +69 -0
  21. package/dist/lib/createRoute.d.ts +61 -0
  22. package/dist/lib/createRoute.js +147 -0
  23. package/dist/lib/jwt.d.ts +1 -1
  24. package/dist/lib/jwt.js +2 -2
  25. package/dist/lib/mfaChallenge.d.ts +20 -0
  26. package/dist/lib/mfaChallenge.js +184 -0
  27. package/dist/lib/queue.d.ts +33 -0
  28. package/dist/lib/queue.js +98 -0
  29. package/dist/lib/roles.d.ts +4 -0
  30. package/dist/lib/roles.js +27 -0
  31. package/dist/lib/session.d.ts +12 -0
  32. package/dist/lib/session.js +163 -5
  33. package/dist/lib/tenant.d.ts +15 -0
  34. package/dist/lib/tenant.js +65 -0
  35. package/dist/lib/zodToMongoose.d.ts +38 -0
  36. package/dist/lib/zodToMongoose.js +84 -0
  37. package/dist/middleware/cacheResponse.js +4 -1
  38. package/dist/middleware/rateLimit.d.ts +2 -1
  39. package/dist/middleware/rateLimit.js +5 -2
  40. package/dist/middleware/requireRole.d.ts +14 -3
  41. package/dist/middleware/requireRole.js +46 -6
  42. package/dist/middleware/tenant.d.ts +5 -0
  43. package/dist/middleware/tenant.js +116 -0
  44. package/dist/models/AuthUser.d.ts +8 -0
  45. package/dist/models/AuthUser.js +8 -0
  46. package/dist/models/TenantRole.d.ts +15 -0
  47. package/dist/models/TenantRole.js +23 -0
  48. package/dist/routes/auth.d.ts +5 -3
  49. package/dist/routes/auth.js +253 -80
  50. package/dist/routes/jobs.d.ts +2 -0
  51. package/dist/routes/jobs.js +270 -0
  52. package/dist/routes/mfa.d.ts +1 -0
  53. package/dist/routes/mfa.js +409 -0
  54. package/dist/routes/oauth.js +107 -16
  55. package/dist/server.js +9 -0
  56. package/dist/services/auth.d.ts +21 -2
  57. package/dist/services/auth.js +97 -17
  58. package/dist/services/mfa.d.ts +37 -0
  59. package/dist/services/mfa.js +276 -0
  60. package/docs/sections/adding-middleware/full.md +35 -0
  61. package/docs/sections/adding-models/full.md +125 -0
  62. package/docs/sections/adding-models/overview.md +13 -0
  63. package/docs/sections/adding-routes/full.md +182 -0
  64. package/docs/sections/adding-routes/overview.md +23 -0
  65. package/docs/sections/auth-flow/full.md +456 -0
  66. package/docs/sections/auth-flow/overview.md +10 -0
  67. package/docs/sections/cli/full.md +30 -0
  68. package/docs/sections/configuration/full.md +135 -0
  69. package/docs/sections/configuration/overview.md +17 -0
  70. package/docs/sections/configuration-example/full.md +99 -0
  71. package/docs/sections/configuration-example/overview.md +30 -0
  72. package/docs/sections/documentation/full.md +171 -0
  73. package/docs/sections/environment-variables/full.md +55 -0
  74. package/docs/sections/exports/full.md +83 -0
  75. package/docs/sections/extending-context/full.md +59 -0
  76. package/docs/sections/header.md +3 -0
  77. package/docs/sections/installation/full.md +6 -0
  78. package/docs/sections/jobs/full.md +140 -0
  79. package/docs/sections/jobs/overview.md +15 -0
  80. package/docs/sections/mongodb-connections/full.md +45 -0
  81. package/docs/sections/mongodb-connections/overview.md +7 -0
  82. package/docs/sections/multi-tenancy/full.md +62 -0
  83. package/docs/sections/multi-tenancy/overview.md +15 -0
  84. package/docs/sections/oauth/full.md +119 -0
  85. package/docs/sections/oauth/overview.md +16 -0
  86. package/docs/sections/package-development/full.md +7 -0
  87. package/docs/sections/peer-dependencies/full.md +43 -0
  88. package/docs/sections/quick-start/full.md +43 -0
  89. package/docs/sections/response-caching/full.md +115 -0
  90. package/docs/sections/response-caching/overview.md +13 -0
  91. package/docs/sections/roles/full.md +136 -0
  92. package/docs/sections/roles/overview.md +12 -0
  93. package/docs/sections/running-without-redis/full.md +16 -0
  94. package/docs/sections/running-without-redis-or-mongodb/full.md +60 -0
  95. package/docs/sections/stack/full.md +10 -0
  96. package/docs/sections/websocket/full.md +100 -0
  97. package/docs/sections/websocket/overview.md +5 -0
  98. package/docs/sections/websocket-rooms/full.md +97 -0
  99. package/docs/sections/websocket-rooms/overview.md +5 -0
  100. package/package.json +19 -10
@@ -0,0 +1,270 @@
1
+ import { createRoute, withSecurity } from "../lib/createRoute";
2
+ import { z } from "zod";
3
+ import { createRouter } from "../lib/context";
4
+ import { userAuth } from "../middleware/userAuth";
5
+ import { requireRole } from "../middleware/requireRole";
6
+ import { createQueue } from "../lib/queue";
7
+ const tags = ["Jobs"];
8
+ const ErrorResponse = z.object({ error: z.string() });
9
+ const JobStatusResponse = z.object({
10
+ id: z.string().describe("Job ID."),
11
+ state: z.string().describe("Job state: waiting, active, completed, failed, delayed, paused."),
12
+ progress: z.union([z.number(), z.record(z.string(), z.unknown())]).describe("Job progress."),
13
+ result: z.unknown().optional().describe("Job result (when completed)."),
14
+ failedReason: z.string().optional().describe("Failure reason (when failed)."),
15
+ attemptsMade: z.number().describe("Number of attempts made."),
16
+ timestamp: z.number().describe("Unix timestamp (ms) when the job was created."),
17
+ finishedOn: z.number().optional().describe("Unix timestamp (ms) when the job finished."),
18
+ }).openapi("JobStatus");
19
+ export const createJobsRouter = (config) => {
20
+ const router = createRouter();
21
+ const allowedQueues = new Set(config.allowedQueues ?? []);
22
+ const authConfig = config.auth ?? "none";
23
+ const scopeToUser = config.scopeToUser ?? false;
24
+ // Determine if userAuth is involved (for scopeToUser and OpenAPI security schemes)
25
+ const hasUserAuth = authConfig === "userAuth" || Array.isArray(authConfig);
26
+ // Apply middleware based on config
27
+ if (authConfig === "userAuth") {
28
+ router.use("/jobs/*", userAuth);
29
+ if (config.roles?.length) {
30
+ router.use("/jobs/*", requireRole(...config.roles));
31
+ }
32
+ }
33
+ else if (Array.isArray(authConfig)) {
34
+ for (const mw of authConfig) {
35
+ router.use("/jobs/*", mw);
36
+ }
37
+ }
38
+ // "none" requires no middleware
39
+ function isQueueAllowed(queueName) {
40
+ return allowedQueues.has(queueName);
41
+ }
42
+ /** Determine OpenAPI security for a route */
43
+ function applyRouteSecurity(route) {
44
+ if (authConfig === "userAuth") {
45
+ return withSecurity(route, { cookieAuth: [] }, { userToken: [] });
46
+ }
47
+ if (Array.isArray(authConfig)) {
48
+ // Custom middleware — mark as cookieAuth/userToken if it likely includes userAuth
49
+ return withSecurity(route, { cookieAuth: [] }, { userToken: [] });
50
+ }
51
+ return route;
52
+ }
53
+ /** Map a BullMQ job to the response shape */
54
+ async function jobToResponse(job) {
55
+ const state = await job.getState();
56
+ return {
57
+ id: job.id,
58
+ state,
59
+ progress: job.progress,
60
+ result: job.returnvalue,
61
+ failedReason: job.failedReason ?? undefined,
62
+ attemptsMade: job.attemptsMade,
63
+ timestamp: job.timestamp,
64
+ finishedOn: job.finishedOn ?? undefined,
65
+ };
66
+ }
67
+ // ─── List available queues ──────────────────────────────────────────────
68
+ const listQueuesRoute = createRoute({
69
+ method: "get",
70
+ path: "/jobs",
71
+ summary: "List available queues",
72
+ description: "Returns the list of queue names exposed via the API.",
73
+ tags,
74
+ responses: {
75
+ 200: {
76
+ content: {
77
+ "application/json": {
78
+ schema: z.object({
79
+ queues: z.array(z.string()).describe("Available queue names."),
80
+ }),
81
+ },
82
+ },
83
+ description: "Available queues.",
84
+ },
85
+ },
86
+ });
87
+ router.openapi(applyRouteSecurity(listQueuesRoute), async (c) => {
88
+ return c.json({ queues: [...allowedQueues] }, 200);
89
+ });
90
+ // ─── List jobs in a queue ─────────────────────────────────────────────
91
+ const listJobsRoute = createRoute({
92
+ method: "get",
93
+ path: "/jobs/{queue}",
94
+ summary: "List jobs in a queue",
95
+ description: "Returns a paginated list of jobs in a queue, optionally filtered by state.",
96
+ tags,
97
+ request: {
98
+ params: z.object({
99
+ queue: z.string().describe("Queue name."),
100
+ }),
101
+ query: z.object({
102
+ state: z.enum(["waiting", "active", "completed", "failed", "delayed", "paused"]).optional().describe("Filter by job state."),
103
+ start: z.string().optional().describe("Start index. Default: 0."),
104
+ end: z.string().optional().describe("End index. Default: 19."),
105
+ }),
106
+ },
107
+ responses: {
108
+ 200: {
109
+ content: {
110
+ "application/json": {
111
+ schema: z.object({
112
+ jobs: z.array(JobStatusResponse),
113
+ total: z.number().describe("Total jobs matching the filter."),
114
+ }),
115
+ },
116
+ },
117
+ description: "Jobs list.",
118
+ },
119
+ 403: { content: { "application/json": { schema: ErrorResponse } }, description: "Queue not allowed." },
120
+ },
121
+ });
122
+ router.openapi(applyRouteSecurity(listJobsRoute), async (c) => {
123
+ const { queue: queueName } = c.req.valid("param");
124
+ if (!isQueueAllowed(queueName)) {
125
+ return c.json({ error: "Queue not allowed" }, 403);
126
+ }
127
+ const { state, start: startStr, end: endStr } = c.req.valid("query");
128
+ const start = startStr ? parseInt(startStr) : 0;
129
+ const end = endStr ? parseInt(endStr) : 19;
130
+ const queue = createQueue(queueName);
131
+ // Get jobs by state or all jobs
132
+ const stateFilter = state ?? "waiting";
133
+ const jobs = await queue.getJobs([stateFilter], start, end);
134
+ // Get total count for the filtered state
135
+ const counts = await queue.getJobCounts(stateFilter);
136
+ const total = counts[stateFilter] ?? 0;
137
+ // Optionally filter by userId
138
+ let filteredJobs = jobs;
139
+ if (scopeToUser && hasUserAuth) {
140
+ const userId = c.get("authUserId");
141
+ filteredJobs = jobs.filter((job) => job.data?.userId === userId);
142
+ }
143
+ const result = await Promise.all(filteredJobs.map(jobToResponse));
144
+ return c.json({ jobs: result, total }, 200);
145
+ });
146
+ // ─── Get job status ─────────────────────────────────────────────────────
147
+ const getJobRoute = createRoute({
148
+ method: "get",
149
+ path: "/jobs/{queue}/{id}",
150
+ summary: "Get job status",
151
+ description: "Returns the current state, progress, result, or failure reason for a job.",
152
+ tags,
153
+ request: {
154
+ params: z.object({
155
+ queue: z.string().describe("Queue name."),
156
+ id: z.string().describe("Job ID."),
157
+ }),
158
+ },
159
+ responses: {
160
+ 200: { content: { "application/json": { schema: JobStatusResponse } }, description: "Job status." },
161
+ 403: { content: { "application/json": { schema: ErrorResponse } }, description: "Queue not in allowedQueues." },
162
+ 404: { content: { "application/json": { schema: ErrorResponse } }, description: "Job not found." },
163
+ },
164
+ });
165
+ router.openapi(applyRouteSecurity(getJobRoute), async (c) => {
166
+ const { queue: queueName, id } = c.req.valid("param");
167
+ if (!isQueueAllowed(queueName)) {
168
+ return c.json({ error: "Queue not allowed" }, 403);
169
+ }
170
+ const queue = createQueue(queueName);
171
+ const job = await queue.getJob(id);
172
+ if (!job)
173
+ return c.json({ error: "Job not found" }, 404);
174
+ // Scope to user if configured
175
+ if (scopeToUser && hasUserAuth) {
176
+ const userId = c.get("authUserId");
177
+ if (job.data?.userId !== userId) {
178
+ return c.json({ error: "Job not found" }, 404);
179
+ }
180
+ }
181
+ return c.json(await jobToResponse(job), 200);
182
+ });
183
+ // ─── Get job logs ───────────────────────────────────────────────────────
184
+ const getJobLogsRoute = createRoute({
185
+ method: "get",
186
+ path: "/jobs/{queue}/{id}/logs",
187
+ summary: "Get job logs",
188
+ description: "Returns logs for a specific job.",
189
+ tags,
190
+ request: {
191
+ params: z.object({
192
+ queue: z.string().describe("Queue name."),
193
+ id: z.string().describe("Job ID."),
194
+ }),
195
+ },
196
+ responses: {
197
+ 200: {
198
+ content: {
199
+ "application/json": {
200
+ schema: z.object({
201
+ logs: z.array(z.string()).describe("Log entries."),
202
+ count: z.number().describe("Total log count."),
203
+ }),
204
+ },
205
+ },
206
+ description: "Job logs.",
207
+ },
208
+ 403: { content: { "application/json": { schema: ErrorResponse } }, description: "Queue not allowed." },
209
+ 404: { content: { "application/json": { schema: ErrorResponse } }, description: "Job not found." },
210
+ },
211
+ });
212
+ router.openapi(applyRouteSecurity(getJobLogsRoute), async (c) => {
213
+ const { queue: queueName, id } = c.req.valid("param");
214
+ if (!isQueueAllowed(queueName)) {
215
+ return c.json({ error: "Queue not allowed" }, 403);
216
+ }
217
+ const queue = createQueue(queueName);
218
+ const job = await queue.getJob(id);
219
+ if (!job)
220
+ return c.json({ error: "Job not found" }, 404);
221
+ const { logs, count } = await queue.getJobLogs(id);
222
+ return c.json({ logs, count }, 200);
223
+ });
224
+ // ─── Dead letter queue ────────────────────────────────────────────────
225
+ const getDlqRoute = createRoute({
226
+ method: "get",
227
+ path: "/jobs/{queue}/dead-letters",
228
+ summary: "List dead letter queue jobs",
229
+ description: "Returns paginated list of jobs in the dead letter queue for a given source queue.",
230
+ tags,
231
+ request: {
232
+ params: z.object({ queue: z.string().describe("Source queue name (DLQ name is {queue}-dlq).") }),
233
+ query: z.object({
234
+ start: z.string().optional().describe("Start index. Default: 0."),
235
+ end: z.string().optional().describe("End index. Default: 19."),
236
+ }),
237
+ },
238
+ responses: {
239
+ 200: {
240
+ content: {
241
+ "application/json": {
242
+ schema: z.object({
243
+ jobs: z.array(JobStatusResponse),
244
+ total: z.number().describe("Total jobs in DLQ."),
245
+ }),
246
+ },
247
+ },
248
+ description: "DLQ jobs.",
249
+ },
250
+ 403: { content: { "application/json": { schema: ErrorResponse } }, description: "Queue not allowed." },
251
+ },
252
+ });
253
+ router.openapi(applyRouteSecurity(getDlqRoute), async (c) => {
254
+ const { queue: queueName } = c.req.valid("param");
255
+ if (!isQueueAllowed(queueName)) {
256
+ return c.json({ error: "Queue not allowed" }, 403);
257
+ }
258
+ const { start: startStr, end: endStr } = c.req.valid("query");
259
+ const start = startStr ? parseInt(startStr) : 0;
260
+ const end = endStr ? parseInt(endStr) : 19;
261
+ const dlqQueue = createQueue(`${queueName}-dlq`);
262
+ const [jobs, total] = await Promise.all([
263
+ dlqQueue.getWaiting(start, end),
264
+ dlqQueue.getWaitingCount(),
265
+ ]);
266
+ const result = await Promise.all(jobs.map(jobToResponse));
267
+ return c.json({ jobs: result, total }, 200);
268
+ });
269
+ return router;
270
+ };
@@ -0,0 +1 @@
1
+ export declare const createMfaRouter: () => import("@hono/zod-openapi").OpenAPIHono<import("../lib/context").AppEnv, {}, "/">;