duron 0.1.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/LICENSE +7 -0
- package/README.md +140 -0
- package/dist/action-job.d.ts +24 -0
- package/dist/action-job.d.ts.map +1 -0
- package/dist/action-job.js +108 -0
- package/dist/action-manager.d.ts +21 -0
- package/dist/action-manager.d.ts.map +1 -0
- package/dist/action-manager.js +78 -0
- package/dist/action.d.ts +129 -0
- package/dist/action.d.ts.map +1 -0
- package/dist/action.js +87 -0
- package/dist/adapters/adapter.d.ts +92 -0
- package/dist/adapters/adapter.d.ts.map +1 -0
- package/dist/adapters/adapter.js +424 -0
- package/dist/adapters/postgres/drizzle.config.d.ts +3 -0
- package/dist/adapters/postgres/drizzle.config.d.ts.map +1 -0
- package/dist/adapters/postgres/drizzle.config.js +10 -0
- package/dist/adapters/postgres/pglite.d.ts +13 -0
- package/dist/adapters/postgres/pglite.d.ts.map +1 -0
- package/dist/adapters/postgres/pglite.js +36 -0
- package/dist/adapters/postgres/postgres.d.ts +51 -0
- package/dist/adapters/postgres/postgres.d.ts.map +1 -0
- package/dist/adapters/postgres/postgres.js +867 -0
- package/dist/adapters/postgres/schema.d.ts +581 -0
- package/dist/adapters/postgres/schema.d.ts.map +1 -0
- package/dist/adapters/postgres/schema.default.d.ts +577 -0
- package/dist/adapters/postgres/schema.default.d.ts.map +1 -0
- package/dist/adapters/postgres/schema.default.js +3 -0
- package/dist/adapters/postgres/schema.js +87 -0
- package/dist/adapters/schemas.d.ts +516 -0
- package/dist/adapters/schemas.d.ts.map +1 -0
- package/dist/adapters/schemas.js +184 -0
- package/dist/client.d.ts +85 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +416 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +22 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +75 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/server.d.ts +1193 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +516 -0
- package/dist/step-manager.d.ts +46 -0
- package/dist/step-manager.d.ts.map +1 -0
- package/dist/step-manager.js +216 -0
- package/dist/utils/checksum.d.ts +2 -0
- package/dist/utils/checksum.d.ts.map +1 -0
- package/dist/utils/checksum.js +6 -0
- package/dist/utils/p-retry.d.ts +19 -0
- package/dist/utils/p-retry.d.ts.map +1 -0
- package/dist/utils/p-retry.js +130 -0
- package/dist/utils/wait-for-abort.d.ts +5 -0
- package/dist/utils/wait-for-abort.d.ts.map +1 -0
- package/dist/utils/wait-for-abort.js +32 -0
- package/migrations/postgres/0000_lethal_speed_demon.sql +64 -0
- package/migrations/postgres/meta/0000_snapshot.json +606 -0
- package/migrations/postgres/meta/_journal.json +13 -0
- package/package.json +88 -0
- package/src/action-job.ts +201 -0
- package/src/action-manager.ts +166 -0
- package/src/action.ts +247 -0
- package/src/adapters/adapter.ts +969 -0
- package/src/adapters/postgres/drizzle.config.ts +11 -0
- package/src/adapters/postgres/pglite.ts +86 -0
- package/src/adapters/postgres/postgres.ts +1346 -0
- package/src/adapters/postgres/schema.default.ts +5 -0
- package/src/adapters/postgres/schema.ts +119 -0
- package/src/adapters/schemas.ts +320 -0
- package/src/client.ts +859 -0
- package/src/constants.ts +37 -0
- package/src/errors.ts +205 -0
- package/src/index.ts +14 -0
- package/src/server.ts +718 -0
- package/src/step-manager.ts +471 -0
- package/src/utils/checksum.ts +7 -0
- package/src/utils/p-retry.ts +213 -0
- package/src/utils/wait-for-abort.ts +40 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import { Elysia } from 'elysia'
|
|
2
|
+
import { jwtVerify, SignJWT } from 'jose'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
5
|
+
import type { GetJobStepsOptions, GetJobsOptions } from './adapters/adapter.js'
|
|
6
|
+
import {
|
|
7
|
+
GetActionsResultSchema,
|
|
8
|
+
GetJobStepsResultSchema,
|
|
9
|
+
GetJobsResultSchema,
|
|
10
|
+
JobSchema,
|
|
11
|
+
JobSortFieldSchema,
|
|
12
|
+
JobStatusResultSchema,
|
|
13
|
+
JobStatusSchema,
|
|
14
|
+
JobStepSchema,
|
|
15
|
+
JobStepStatusResultSchema,
|
|
16
|
+
SortOrderSchema,
|
|
17
|
+
} from './adapters/schemas.js'
|
|
18
|
+
import type { Client } from './client.js'
|
|
19
|
+
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// Custom Errors
|
|
22
|
+
// ============================================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Error thrown when a requested resource is not found.
|
|
26
|
+
*/
|
|
27
|
+
export class NotFoundError extends Error {
|
|
28
|
+
/**
|
|
29
|
+
* Create a new NotFoundError.
|
|
30
|
+
*
|
|
31
|
+
* @param message - Error message describing what was not found
|
|
32
|
+
*/
|
|
33
|
+
constructor(message: string) {
|
|
34
|
+
super(message)
|
|
35
|
+
this.name = 'NotFoundError'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Error thrown when authentication fails.
|
|
41
|
+
*/
|
|
42
|
+
export class UnauthorizedError extends Error {
|
|
43
|
+
/**
|
|
44
|
+
* Create a new UnauthorizedError.
|
|
45
|
+
*
|
|
46
|
+
* @param message - Error message describing the authentication failure
|
|
47
|
+
*/
|
|
48
|
+
constructor(message: string) {
|
|
49
|
+
super(message)
|
|
50
|
+
this.name = 'UnauthorizedError'
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Zod Validation Schemas
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
// Note: JobStatusSchema, JobSortFieldSchema, SortOrderSchema, JobSchema,
|
|
59
|
+
// JobStepSchema, GetJobStepsResultSchema, GetJobsResultSchema, and
|
|
60
|
+
// GetActionsResultSchema are imported from ./adapters/schemas.js to avoid duplication
|
|
61
|
+
|
|
62
|
+
export const GetJobStepsQuerySchema = z
|
|
63
|
+
.object({
|
|
64
|
+
page: z.coerce.number().int().min(1).optional(),
|
|
65
|
+
pageSize: z.coerce.number().int().min(1).max(1000).optional(),
|
|
66
|
+
search: z.string().optional(),
|
|
67
|
+
fUpdatedAfter: z.coerce.date().optional(),
|
|
68
|
+
})
|
|
69
|
+
.transform((data) => ({
|
|
70
|
+
page: data.page,
|
|
71
|
+
pageSize: data.pageSize,
|
|
72
|
+
search: data.search,
|
|
73
|
+
updatedAfter: data.fUpdatedAfter,
|
|
74
|
+
}))
|
|
75
|
+
|
|
76
|
+
// Reuse GetJobStepsResultSchema from schemas.ts
|
|
77
|
+
export const GetJobStepsResponseSchema = GetJobStepsResultSchema
|
|
78
|
+
|
|
79
|
+
export const GetJobsQuerySchema = z
|
|
80
|
+
.object({
|
|
81
|
+
// Pagination
|
|
82
|
+
page: z.coerce.number().int().min(1).optional(),
|
|
83
|
+
pageSize: z.coerce.number().int().min(1).max(1000).optional(),
|
|
84
|
+
|
|
85
|
+
// Filters - arrays can be passed as comma-separated or multiple params
|
|
86
|
+
fStatus: z.union([JobStatusSchema, z.array(JobStatusSchema)]).optional(),
|
|
87
|
+
fActionName: z.union([z.string(), z.array(z.string())]).optional(),
|
|
88
|
+
fGroupKey: z.union([z.string(), z.array(z.string())]).optional(),
|
|
89
|
+
fOwnerId: z.union([z.string(), z.array(z.string())]).optional(),
|
|
90
|
+
// Date filters: can be a single ISO string or JSON array [start, end] - both coerced to Date objects
|
|
91
|
+
fCreatedAt: z.union([z.coerce.date(), z.array(z.coerce.date())]).optional(),
|
|
92
|
+
fStartedAt: z.union([z.coerce.date(), z.array(z.coerce.date())]).optional(),
|
|
93
|
+
fFinishedAt: z.union([z.coerce.date(), z.array(z.coerce.date())]).optional(),
|
|
94
|
+
fUpdatedAfter: z.coerce.date().optional(),
|
|
95
|
+
fSearch: z.string().optional(),
|
|
96
|
+
|
|
97
|
+
// Sort - format: "field:asc,field:desc"
|
|
98
|
+
sort: z.string().optional(),
|
|
99
|
+
|
|
100
|
+
// JSONB filters as JSON strings
|
|
101
|
+
fInputFilter: z.record(z.string(), z.any()).optional(),
|
|
102
|
+
fOutputFilter: z.record(z.string(), z.any()).optional(),
|
|
103
|
+
})
|
|
104
|
+
.transform((data) => {
|
|
105
|
+
const filters: any = {}
|
|
106
|
+
|
|
107
|
+
if (data.fStatus) filters.status = data.fStatus
|
|
108
|
+
if (data.fActionName) filters.actionName = data.fActionName
|
|
109
|
+
if (data.fGroupKey) filters.groupKey = data.fGroupKey
|
|
110
|
+
if (data.fOwnerId) filters.ownerId = data.fOwnerId
|
|
111
|
+
if (data.fCreatedAt) filters.createdAt = data.fCreatedAt
|
|
112
|
+
if (data.fStartedAt) filters.startedAt = data.fStartedAt
|
|
113
|
+
if (data.fFinishedAt) filters.finishedAt = data.fFinishedAt
|
|
114
|
+
if (data.fUpdatedAfter) filters.updatedAfter = data.fUpdatedAfter
|
|
115
|
+
if (data.fSearch) filters.search = data.fSearch
|
|
116
|
+
if (data.fInputFilter) filters.inputFilter = data.fInputFilter
|
|
117
|
+
if (data.fOutputFilter) filters.outputFilter = data.fOutputFilter
|
|
118
|
+
|
|
119
|
+
// Parse sort string: "field:asc,field:desc" -> [{ field: 'field', order: 'asc' }, { field: 'field', order: 'desc' }]
|
|
120
|
+
let sort: Array<{ field: z.infer<typeof JobSortFieldSchema>; order: z.infer<typeof SortOrderSchema> }> | undefined
|
|
121
|
+
if (data.sort) {
|
|
122
|
+
const sortParts = data.sort
|
|
123
|
+
.split(',')
|
|
124
|
+
.map((part) => part.trim())
|
|
125
|
+
.filter((part) => part.length > 0)
|
|
126
|
+
const parsedSorts = sortParts
|
|
127
|
+
.map((part) => {
|
|
128
|
+
const [field, order] = part.split(':').map((s) => s.trim())
|
|
129
|
+
if (!field || !order) {
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
// Validate field and order
|
|
133
|
+
const fieldResult = JobSortFieldSchema.safeParse(field)
|
|
134
|
+
const orderResult = SortOrderSchema.safeParse(order.toLowerCase())
|
|
135
|
+
if (!fieldResult.success || !orderResult.success) {
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
field: fieldResult.data,
|
|
140
|
+
order: orderResult.data,
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
.filter(
|
|
144
|
+
(s): s is { field: z.infer<typeof JobSortFieldSchema>; order: z.infer<typeof SortOrderSchema> } => s !== null,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
// If no valid sorts were parsed, set to undefined
|
|
148
|
+
if (parsedSorts.length === 0) {
|
|
149
|
+
sort = undefined
|
|
150
|
+
} else {
|
|
151
|
+
sort = parsedSorts
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
page: data.page,
|
|
157
|
+
pageSize: data.pageSize,
|
|
158
|
+
filters: Object.keys(filters).length > 0 ? filters : undefined,
|
|
159
|
+
sort,
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
// Reuse GetJobsResultSchema from schemas.ts
|
|
164
|
+
export const GetJobsResponseSchema = GetJobsResultSchema
|
|
165
|
+
|
|
166
|
+
// Reuse GetActionsResultSchema from schemas.ts
|
|
167
|
+
export const GetActionsResponseSchema = GetActionsResultSchema
|
|
168
|
+
|
|
169
|
+
export const GetActionsMetadataResponseSchema = z.array(
|
|
170
|
+
z.object({
|
|
171
|
+
name: z.string(),
|
|
172
|
+
mockInput: z.any(),
|
|
173
|
+
}),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
// Export query input types for use in clients
|
|
177
|
+
export type GetJobsQueryInput = z.input<typeof GetJobsQuerySchema>
|
|
178
|
+
export type GetJobStepsQueryInput = z.input<typeof GetJobStepsQuerySchema>
|
|
179
|
+
|
|
180
|
+
export const ErrorResponseSchema = z.object({
|
|
181
|
+
error: z.string(),
|
|
182
|
+
message: z.string().optional(),
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const JobIdParamsSchema = z.object({
|
|
186
|
+
id: z.uuid(),
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const StepIdParamsSchema = z.object({
|
|
190
|
+
id: z.uuid(),
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
export const CancelJobResponseSchema = z.object({
|
|
194
|
+
success: z.boolean(),
|
|
195
|
+
message: z.string(),
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
export const RetryJobResponseSchema = z.object({
|
|
199
|
+
success: z.boolean(),
|
|
200
|
+
message: z.string(),
|
|
201
|
+
newJobId: z.string(),
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// Server Factory
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
export interface CreateServerOptions<P extends string> {
|
|
209
|
+
/**
|
|
210
|
+
* The Duron instance to use for the API endpoints
|
|
211
|
+
*/
|
|
212
|
+
client: Client<any, any>
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Optional prefix for all routes (default: '/api')
|
|
216
|
+
*/
|
|
217
|
+
prefix?: P
|
|
218
|
+
|
|
219
|
+
login?: {
|
|
220
|
+
onLogin: (body: { email: string; password: string }) => Promise<boolean>
|
|
221
|
+
jwtSecret: string | Uint8Array
|
|
222
|
+
/**
|
|
223
|
+
* Optional expiration time for the access JWT token (default: '1h')
|
|
224
|
+
*/
|
|
225
|
+
expirationTime?: string
|
|
226
|
+
/**
|
|
227
|
+
* Optional expiration time for the refresh token (default: '7d')
|
|
228
|
+
*/
|
|
229
|
+
refreshTokenExpirationTime?: string
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Creates an Elysia server instance with duron API endpoints.
|
|
235
|
+
* All endpoints use Zod for input and response validation.
|
|
236
|
+
*
|
|
237
|
+
* @param options - Configuration options
|
|
238
|
+
* @returns Elysia server instance
|
|
239
|
+
*/
|
|
240
|
+
export function createServer<P extends string>({ client, prefix, login }: CreateServerOptions<P>) {
|
|
241
|
+
// Convert string secret to Uint8Array if needed
|
|
242
|
+
const secretKey = typeof login?.jwtSecret === 'string' ? new TextEncoder().encode(login?.jwtSecret) : login?.jwtSecret
|
|
243
|
+
|
|
244
|
+
const routePrefix = (prefix ?? '/api') as P
|
|
245
|
+
|
|
246
|
+
return new Elysia({
|
|
247
|
+
prefix: routePrefix,
|
|
248
|
+
})
|
|
249
|
+
.onError(({ code, error, set }) => {
|
|
250
|
+
if (code === 'VALIDATION') {
|
|
251
|
+
set.status = 400
|
|
252
|
+
return error
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (error instanceof NotFoundError) {
|
|
256
|
+
set.status = 404
|
|
257
|
+
return {
|
|
258
|
+
error: 'Not found',
|
|
259
|
+
message: error.message,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (error instanceof UnauthorizedError) {
|
|
264
|
+
set.status = 401
|
|
265
|
+
return {
|
|
266
|
+
error: 'Unauthorized',
|
|
267
|
+
message: error.message,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Handle other errors
|
|
272
|
+
set.status = code === 'NOT_FOUND' ? 404 : 500
|
|
273
|
+
return {
|
|
274
|
+
error: 'Internal server error',
|
|
275
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
.macro('getUser', {
|
|
279
|
+
headers: z.object({
|
|
280
|
+
authorization: z.string().optional(),
|
|
281
|
+
}),
|
|
282
|
+
resolve: async ({ headers }) => {
|
|
283
|
+
if (login) {
|
|
284
|
+
const authHeader = headers.authorization
|
|
285
|
+
if (!authHeader) {
|
|
286
|
+
return {
|
|
287
|
+
user: null,
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Extract token from "Bearer <token>" format
|
|
292
|
+
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : authHeader
|
|
293
|
+
if (!token) {
|
|
294
|
+
return {
|
|
295
|
+
user: null,
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const { payload } = await jwtVerify(token, secretKey!).catch(() => {
|
|
300
|
+
return {
|
|
301
|
+
payload: null,
|
|
302
|
+
}
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
user: payload,
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
user: null,
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
})
|
|
315
|
+
.macro('auth', {
|
|
316
|
+
getUser: true,
|
|
317
|
+
beforeHandle: async ({ user }) => {
|
|
318
|
+
if (login && !user) {
|
|
319
|
+
throw new UnauthorizedError('Unauthorized')
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
})
|
|
323
|
+
.get(
|
|
324
|
+
'/jobs/:id',
|
|
325
|
+
async ({ params }) => {
|
|
326
|
+
const job = await client.getJobById(params.id)
|
|
327
|
+
if (!job) {
|
|
328
|
+
throw new NotFoundError(`Job with ID ${params.id} was not found`)
|
|
329
|
+
}
|
|
330
|
+
return job
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
params: JobIdParamsSchema,
|
|
334
|
+
response: {
|
|
335
|
+
200: JobSchema,
|
|
336
|
+
404: ErrorResponseSchema,
|
|
337
|
+
500: ErrorResponseSchema,
|
|
338
|
+
401: ErrorResponseSchema,
|
|
339
|
+
},
|
|
340
|
+
auth: true,
|
|
341
|
+
},
|
|
342
|
+
)
|
|
343
|
+
.get(
|
|
344
|
+
'/jobs/:id/steps',
|
|
345
|
+
async ({ params, query }) => {
|
|
346
|
+
const options: GetJobStepsOptions = {
|
|
347
|
+
jobId: params.id,
|
|
348
|
+
page: query.page,
|
|
349
|
+
pageSize: query.pageSize,
|
|
350
|
+
search: query.search,
|
|
351
|
+
updatedAfter: query.updatedAfter,
|
|
352
|
+
}
|
|
353
|
+
return client.getJobSteps(options)
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
params: JobIdParamsSchema,
|
|
357
|
+
query: GetJobStepsQuerySchema,
|
|
358
|
+
response: {
|
|
359
|
+
200: GetJobStepsResponseSchema,
|
|
360
|
+
400: ErrorResponseSchema,
|
|
361
|
+
500: ErrorResponseSchema,
|
|
362
|
+
401: ErrorResponseSchema,
|
|
363
|
+
},
|
|
364
|
+
auth: true,
|
|
365
|
+
},
|
|
366
|
+
)
|
|
367
|
+
.get(
|
|
368
|
+
'/jobs',
|
|
369
|
+
async ({ query }) => {
|
|
370
|
+
const options: GetJobsOptions = {
|
|
371
|
+
page: query.page,
|
|
372
|
+
pageSize: query.pageSize,
|
|
373
|
+
filters: query.filters,
|
|
374
|
+
sort: query.sort,
|
|
375
|
+
}
|
|
376
|
+
return client.getJobs(options)
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
query: GetJobsQuerySchema,
|
|
380
|
+
response: {
|
|
381
|
+
200: GetJobsResponseSchema,
|
|
382
|
+
400: ErrorResponseSchema,
|
|
383
|
+
500: ErrorResponseSchema,
|
|
384
|
+
401: ErrorResponseSchema,
|
|
385
|
+
},
|
|
386
|
+
auth: true,
|
|
387
|
+
},
|
|
388
|
+
)
|
|
389
|
+
.get(
|
|
390
|
+
'/steps/:id',
|
|
391
|
+
async ({ params }) => {
|
|
392
|
+
const step = await client.getJobStepById(params.id)
|
|
393
|
+
if (!step) {
|
|
394
|
+
throw new NotFoundError(`Step with ID ${params.id} was not found`)
|
|
395
|
+
}
|
|
396
|
+
return step
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
params: StepIdParamsSchema,
|
|
400
|
+
response: {
|
|
401
|
+
200: JobStepSchema,
|
|
402
|
+
404: ErrorResponseSchema,
|
|
403
|
+
500: ErrorResponseSchema,
|
|
404
|
+
401: ErrorResponseSchema,
|
|
405
|
+
},
|
|
406
|
+
auth: true,
|
|
407
|
+
},
|
|
408
|
+
)
|
|
409
|
+
.get(
|
|
410
|
+
'/jobs/:id/status',
|
|
411
|
+
async ({ params }) => {
|
|
412
|
+
const status = await client.getJobStatus(params.id)
|
|
413
|
+
if (!status) {
|
|
414
|
+
throw new NotFoundError(`Job with ID ${params.id} was not found`)
|
|
415
|
+
}
|
|
416
|
+
return status
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
params: JobIdParamsSchema,
|
|
420
|
+
response: {
|
|
421
|
+
200: JobStatusResultSchema,
|
|
422
|
+
404: ErrorResponseSchema,
|
|
423
|
+
500: ErrorResponseSchema,
|
|
424
|
+
401: ErrorResponseSchema,
|
|
425
|
+
},
|
|
426
|
+
auth: true,
|
|
427
|
+
},
|
|
428
|
+
)
|
|
429
|
+
.get(
|
|
430
|
+
'/steps/:id/status',
|
|
431
|
+
async ({ params }) => {
|
|
432
|
+
const status = await client.getJobStepStatus(params.id)
|
|
433
|
+
if (!status) {
|
|
434
|
+
throw new NotFoundError(`Step with ID ${params.id} was not found`)
|
|
435
|
+
}
|
|
436
|
+
return status
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
params: StepIdParamsSchema,
|
|
440
|
+
response: {
|
|
441
|
+
200: JobStepStatusResultSchema,
|
|
442
|
+
404: ErrorResponseSchema,
|
|
443
|
+
500: ErrorResponseSchema,
|
|
444
|
+
401: ErrorResponseSchema,
|
|
445
|
+
},
|
|
446
|
+
auth: true,
|
|
447
|
+
},
|
|
448
|
+
)
|
|
449
|
+
.post(
|
|
450
|
+
'/jobs/:id/cancel',
|
|
451
|
+
async ({ params }) => {
|
|
452
|
+
await client.cancelJob(params.id)
|
|
453
|
+
return {
|
|
454
|
+
success: true,
|
|
455
|
+
message: `Job ${params.id} has been cancelled`,
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
params: JobIdParamsSchema,
|
|
460
|
+
response: {
|
|
461
|
+
200: CancelJobResponseSchema,
|
|
462
|
+
400: ErrorResponseSchema,
|
|
463
|
+
500: ErrorResponseSchema,
|
|
464
|
+
401: ErrorResponseSchema,
|
|
465
|
+
},
|
|
466
|
+
auth: true,
|
|
467
|
+
},
|
|
468
|
+
)
|
|
469
|
+
.post(
|
|
470
|
+
'/jobs/:id/retry',
|
|
471
|
+
async ({ params }) => {
|
|
472
|
+
const newJobId = await client.retryJob(params.id)
|
|
473
|
+
if (!newJobId) {
|
|
474
|
+
throw new Error(`Could not retry job ${params.id}. The job may not be in a retryable state.`)
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
success: true,
|
|
478
|
+
message: `Job ${params.id} has been retried`,
|
|
479
|
+
newJobId,
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
params: JobIdParamsSchema,
|
|
484
|
+
response: {
|
|
485
|
+
200: RetryJobResponseSchema,
|
|
486
|
+
400: ErrorResponseSchema,
|
|
487
|
+
500: ErrorResponseSchema,
|
|
488
|
+
401: ErrorResponseSchema,
|
|
489
|
+
},
|
|
490
|
+
auth: true,
|
|
491
|
+
},
|
|
492
|
+
)
|
|
493
|
+
.delete(
|
|
494
|
+
'/jobs/:id',
|
|
495
|
+
async ({ params }) => {
|
|
496
|
+
const deleted = await client.deleteJob(params.id)
|
|
497
|
+
if (!deleted) {
|
|
498
|
+
throw new NotFoundError(
|
|
499
|
+
`Job with ID ${params.id} was not found or cannot be deleted (active jobs cannot be deleted)`,
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
success: true,
|
|
504
|
+
message: `Job ${params.id} has been deleted`,
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
params: JobIdParamsSchema,
|
|
509
|
+
response: {
|
|
510
|
+
200: z.object({
|
|
511
|
+
success: z.boolean(),
|
|
512
|
+
message: z.string(),
|
|
513
|
+
}),
|
|
514
|
+
404: ErrorResponseSchema,
|
|
515
|
+
500: ErrorResponseSchema,
|
|
516
|
+
401: ErrorResponseSchema,
|
|
517
|
+
},
|
|
518
|
+
auth: true,
|
|
519
|
+
},
|
|
520
|
+
)
|
|
521
|
+
.delete(
|
|
522
|
+
'/jobs',
|
|
523
|
+
async ({ query }) => {
|
|
524
|
+
const options: GetJobsOptions = {
|
|
525
|
+
page: query.page,
|
|
526
|
+
pageSize: query.pageSize,
|
|
527
|
+
filters: query.filters,
|
|
528
|
+
sort: query.sort,
|
|
529
|
+
}
|
|
530
|
+
const deletedCount = await client.deleteJobs(options)
|
|
531
|
+
return {
|
|
532
|
+
success: true,
|
|
533
|
+
message: `Deleted ${deletedCount} job(s)`,
|
|
534
|
+
deletedCount,
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
query: GetJobsQuerySchema,
|
|
539
|
+
response: {
|
|
540
|
+
200: z.object({
|
|
541
|
+
success: z.boolean(),
|
|
542
|
+
message: z.string(),
|
|
543
|
+
deletedCount: z.number(),
|
|
544
|
+
}),
|
|
545
|
+
400: ErrorResponseSchema,
|
|
546
|
+
500: ErrorResponseSchema,
|
|
547
|
+
401: ErrorResponseSchema,
|
|
548
|
+
},
|
|
549
|
+
auth: true,
|
|
550
|
+
},
|
|
551
|
+
)
|
|
552
|
+
.get(
|
|
553
|
+
'/actions',
|
|
554
|
+
async () => {
|
|
555
|
+
return client.getActions()
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
response: {
|
|
559
|
+
200: GetActionsResponseSchema,
|
|
560
|
+
400: ErrorResponseSchema,
|
|
561
|
+
500: ErrorResponseSchema,
|
|
562
|
+
401: ErrorResponseSchema,
|
|
563
|
+
},
|
|
564
|
+
auth: true,
|
|
565
|
+
},
|
|
566
|
+
)
|
|
567
|
+
.get(
|
|
568
|
+
'/actions/metadata',
|
|
569
|
+
async () => {
|
|
570
|
+
return client.getActionsMetadata()
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
response: {
|
|
574
|
+
200: GetActionsMetadataResponseSchema,
|
|
575
|
+
400: ErrorResponseSchema,
|
|
576
|
+
500: ErrorResponseSchema,
|
|
577
|
+
401: ErrorResponseSchema,
|
|
578
|
+
},
|
|
579
|
+
auth: true,
|
|
580
|
+
},
|
|
581
|
+
)
|
|
582
|
+
.post(
|
|
583
|
+
'/actions/:actionName/run',
|
|
584
|
+
async ({ params, body }) => {
|
|
585
|
+
const jobId = await client.runAction(params.actionName as any, body)
|
|
586
|
+
return {
|
|
587
|
+
success: true,
|
|
588
|
+
jobId,
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
params: z.object({
|
|
593
|
+
actionName: z.string(),
|
|
594
|
+
}),
|
|
595
|
+
body: z.any(),
|
|
596
|
+
response: {
|
|
597
|
+
200: z.object({
|
|
598
|
+
success: z.boolean(),
|
|
599
|
+
jobId: z.string(),
|
|
600
|
+
}),
|
|
601
|
+
400: ErrorResponseSchema,
|
|
602
|
+
500: ErrorResponseSchema,
|
|
603
|
+
401: ErrorResponseSchema,
|
|
604
|
+
},
|
|
605
|
+
auth: true,
|
|
606
|
+
},
|
|
607
|
+
)
|
|
608
|
+
.post(
|
|
609
|
+
'/login',
|
|
610
|
+
async ({ body }) => {
|
|
611
|
+
if (!login || !secretKey) {
|
|
612
|
+
throw new Error('Login is not configured')
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const { email, password } = body
|
|
616
|
+
|
|
617
|
+
const success = await login.onLogin({ email, password })
|
|
618
|
+
if (!success) {
|
|
619
|
+
throw new UnauthorizedError('Invalid credentials')
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Generate access token (short-lived)
|
|
623
|
+
const accessToken = await new SignJWT({ email, type: 'access' })
|
|
624
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
625
|
+
.setIssuedAt()
|
|
626
|
+
.setExpirationTime(login.expirationTime ?? '1h')
|
|
627
|
+
.sign(secretKey)
|
|
628
|
+
|
|
629
|
+
// Generate refresh token (long-lived)
|
|
630
|
+
const refreshToken = await new SignJWT({ email, type: 'refresh' })
|
|
631
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
632
|
+
.setIssuedAt()
|
|
633
|
+
.setExpirationTime(login.refreshTokenExpirationTime ?? '7d')
|
|
634
|
+
.sign(secretKey)
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
accessToken,
|
|
638
|
+
refreshToken,
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
body: z.object({
|
|
643
|
+
email: z.email(),
|
|
644
|
+
password: z.string(),
|
|
645
|
+
}),
|
|
646
|
+
response: {
|
|
647
|
+
200: z.object({
|
|
648
|
+
accessToken: z.string(),
|
|
649
|
+
refreshToken: z.string(),
|
|
650
|
+
}),
|
|
651
|
+
401: ErrorResponseSchema,
|
|
652
|
+
400: ErrorResponseSchema,
|
|
653
|
+
500: ErrorResponseSchema,
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
)
|
|
657
|
+
.post(
|
|
658
|
+
'/refresh',
|
|
659
|
+
async ({ body }) => {
|
|
660
|
+
if (!login || !secretKey) {
|
|
661
|
+
throw new Error('Login is not configured')
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const { refreshToken: providedRefreshToken } = body
|
|
665
|
+
|
|
666
|
+
if (!providedRefreshToken) {
|
|
667
|
+
throw new UnauthorizedError('Refresh token is required')
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
// Verify refresh token
|
|
672
|
+
const { payload } = await jwtVerify(providedRefreshToken, secretKey)
|
|
673
|
+
|
|
674
|
+
// Type assertion for JWT payload
|
|
675
|
+
interface RefreshTokenPayload {
|
|
676
|
+
email?: string
|
|
677
|
+
type?: string
|
|
678
|
+
}
|
|
679
|
+
const typedPayload = payload as RefreshTokenPayload
|
|
680
|
+
|
|
681
|
+
// Ensure it's a refresh token
|
|
682
|
+
if (typedPayload.type !== 'refresh') {
|
|
683
|
+
throw new UnauthorizedError('Invalid token type')
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (!typedPayload.email) {
|
|
687
|
+
throw new UnauthorizedError('Invalid token payload')
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Generate new access token
|
|
691
|
+
const accessToken = await new SignJWT({ email: typedPayload.email, type: 'access' })
|
|
692
|
+
.setProtectedHeader({ alg: 'HS256' })
|
|
693
|
+
.setIssuedAt()
|
|
694
|
+
.setExpirationTime(login.expirationTime ?? '1h')
|
|
695
|
+
.sign(secretKey)
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
accessToken,
|
|
699
|
+
}
|
|
700
|
+
} catch {
|
|
701
|
+
throw new UnauthorizedError('Invalid or expired refresh token')
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
body: z.object({
|
|
706
|
+
refreshToken: z.string(),
|
|
707
|
+
}),
|
|
708
|
+
response: {
|
|
709
|
+
200: z.object({
|
|
710
|
+
accessToken: z.string(),
|
|
711
|
+
}),
|
|
712
|
+
401: ErrorResponseSchema,
|
|
713
|
+
400: ErrorResponseSchema,
|
|
714
|
+
500: ErrorResponseSchema,
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
)
|
|
718
|
+
}
|