@hadoku/worker-utils 0.0.0-stage → 1.6.19

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.
@@ -0,0 +1,157 @@
1
+ import { z } from '@hono/zod-openapi';
2
+
3
+ /**
4
+ * Shared OpenAPI Zod Schemas
5
+ *
6
+ * Common schemas used across multiple worker APIs for consistent error handling,
7
+ * health checks, and response formats.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { ErrorResponseSchema, createHealthResponseSchema } from '../util/openapi-schemas';
12
+ *
13
+ * // Use generic error schema
14
+ * const route = createRoute({
15
+ * responses: {
16
+ * 400: { content: { 'application/json': { schema: ErrorResponseSchema } } }
17
+ * }
18
+ * });
19
+ *
20
+ * // Create service-specific health schema
21
+ * const HealthSchema = createHealthResponseSchema('my-api');
22
+ * ```
23
+ */
24
+
25
+ /**
26
+ * Standard error response with success: false pattern
27
+ * Used by: oss-recon-api
28
+ */
29
+ declare const ErrorResponseSchema: z.ZodObject<{
30
+ success: z.ZodLiteral<false>;
31
+ error: z.ZodString;
32
+ }, z.core.$strip>;
33
+ /**
34
+ * Alternative error response without success field
35
+ * Used by: task-api, prompt-api
36
+ */
37
+ declare const SimpleErrorResponseSchema: z.ZodObject<{
38
+ error: z.ZodString;
39
+ message: z.ZodOptional<z.ZodString>;
40
+ }, z.core.$strip>;
41
+ /**
42
+ * Detailed error response with success pattern
43
+ * Used by: prompt-api
44
+ */
45
+ declare const DetailedErrorResponseSchema: z.ZodObject<{
46
+ success: z.ZodLiteral<false>;
47
+ error: z.ZodString;
48
+ message: z.ZodString;
49
+ }, z.core.$strip>;
50
+ /**
51
+ * Rate limit / throttle error response
52
+ * Used by: task-api
53
+ */
54
+ declare const ThrottleErrorResponseSchema: z.ZodObject<{
55
+ error: z.ZodLiteral<"Rate limit exceeded">;
56
+ message: z.ZodString;
57
+ retryAfter: z.ZodNumber;
58
+ }, z.core.$strip>;
59
+ /**
60
+ * Create a service-specific health response schema
61
+ *
62
+ * @param serviceName - The name of the service (e.g., 'task-api', 'prompt-api')
63
+ * @param options - Optional configuration for additional checks
64
+ * @returns Zod schema for health response
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * const HealthSchema = createHealthResponseSchema('task-api', { includeChecks: true });
69
+ * ```
70
+ */
71
+ declare function createHealthResponseSchema<T extends string>(serviceName: T, options?: {
72
+ includeChecks?: boolean;
73
+ }): z.ZodObject<{
74
+ status: z.ZodEnum<{
75
+ error: "error";
76
+ ok: "ok";
77
+ degraded: "degraded";
78
+ }>;
79
+ timestamp: z.ZodString;
80
+ service: z.ZodLiteral<T>;
81
+ }, z.core.$strip>;
82
+ /**
83
+ * Health response with success wrapper pattern
84
+ * Used by: oss-recon-api
85
+ *
86
+ * NOTE: these two services live in external @wolffm packages that still emit
87
+ * the legacy `status: 'healthy'` convention (and a {success,data} envelope),
88
+ * so this schema intentionally does NOT use HEALTH_STATUSES. Migrate those
89
+ * packages to healthCheck() to fold them onto the canonical contract.
90
+ */
91
+ declare function createWrappedHealthResponseSchema<T extends string>(serviceName: T): z.ZodObject<{
92
+ success: z.ZodLiteral<true>;
93
+ data: z.ZodObject<{
94
+ status: z.ZodLiteral<"healthy">;
95
+ service: z.ZodLiteral<T>;
96
+ timestamp: z.ZodString;
97
+ }, z.core.$strip>;
98
+ }, z.core.$strip>;
99
+ /**
100
+ * Create a success response wrapper schema
101
+ * Used by: prompt-api, oss-recon-api
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * const UserResponseSchema = createSuccessResponseSchema(
106
+ * z.object({ user: UserSchema })
107
+ * ).openapi('UserResponse');
108
+ * ```
109
+ */
110
+ declare function createSuccessResponseSchema<T extends z.ZodTypeAny>(dataSchema: T): z.ZodObject<{
111
+ success: z.ZodLiteral<true>;
112
+ data: T;
113
+ }, z.core.$strip>;
114
+ /**
115
+ * Create a delete confirmation response schema
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * const DeleteBoardResponseSchema = createDeleteResponseSchema('boardId', 'work-board');
120
+ * ```
121
+ */
122
+ declare function createDeleteResponseSchema(idField?: string, example?: string): z.ZodObject<{
123
+ [x: string]: z.ZodString | z.ZodLiteral<true>;
124
+ deleted: z.ZodLiteral<true>;
125
+ }, z.core.$strip>;
126
+ /**
127
+ * Zod validation error response
128
+ * Matches the defaultHook error format used in OpenAPIHono
129
+ */
130
+ declare const ValidationErrorResponseSchema: z.ZodObject<{
131
+ error: z.ZodLiteral<"Validation Error">;
132
+ message: z.ZodString;
133
+ }, z.core.$strip>;
134
+ /**
135
+ * Pre-built health schemas for known services
136
+ * Import these directly if you don't need customization
137
+ */
138
+ declare const TaskApiHealthResponseSchema: z.ZodObject<{
139
+ status: z.ZodEnum<{
140
+ error: "error";
141
+ ok: "ok";
142
+ degraded: "degraded";
143
+ }>;
144
+ timestamp: z.ZodString;
145
+ service: z.ZodLiteral<"task-api-adapter">;
146
+ }, z.core.$strip>;
147
+ declare const PromptApiHealthResponseSchema: z.ZodObject<{
148
+ status: z.ZodEnum<{
149
+ error: "error";
150
+ ok: "ok";
151
+ degraded: "degraded";
152
+ }>;
153
+ timestamp: z.ZodString;
154
+ service: z.ZodLiteral<"prompt-api">;
155
+ }, z.core.$strip>;
156
+
157
+ export { DetailedErrorResponseSchema, ErrorResponseSchema, PromptApiHealthResponseSchema, SimpleErrorResponseSchema, TaskApiHealthResponseSchema, ThrottleErrorResponseSchema, ValidationErrorResponseSchema, createDeleteResponseSchema, createHealthResponseSchema, createSuccessResponseSchema, createWrappedHealthResponseSchema };
@@ -0,0 +1,79 @@
1
+ // openapi-schemas.ts
2
+ import { z } from "@hono/zod-openapi";
3
+ var HEALTH_STATUS_VALUES = ["ok", "degraded", "error"];
4
+ var ErrorResponseSchema = z.object({
5
+ success: z.literal(false),
6
+ error: z.string().openapi({ example: "Bad Request" })
7
+ }).openapi("ErrorResponse");
8
+ var SimpleErrorResponseSchema = z.object({
9
+ error: z.string().openapi({ example: "Bad Request" }),
10
+ message: z.string().optional().openapi({ example: "Missing required field: title" })
11
+ }).openapi("SimpleErrorResponse");
12
+ var DetailedErrorResponseSchema = z.object({
13
+ success: z.literal(false),
14
+ error: z.string().openapi({ example: "Validation Error" }),
15
+ message: z.string().openapi({ example: "Invalid input data" })
16
+ }).openapi("DetailedErrorResponse");
17
+ var ThrottleErrorResponseSchema = z.object({
18
+ error: z.literal("Rate limit exceeded"),
19
+ message: z.string().openapi({ example: "Too many requests" }),
20
+ retryAfter: z.number().openapi({ example: 60 })
21
+ }).openapi("ThrottleErrorResponse");
22
+ function createHealthResponseSchema(serviceName, options) {
23
+ const baseSchema = z.object({
24
+ status: z.enum(HEALTH_STATUS_VALUES).openapi({ example: "ok" }),
25
+ timestamp: z.string().openapi({ example: "2024-01-15T10:30:00.000Z" }),
26
+ service: z.literal(serviceName)
27
+ });
28
+ if (options?.includeChecks) {
29
+ return baseSchema.extend({
30
+ checks: z.object({
31
+ kv: z.boolean().openapi({ example: true })
32
+ })
33
+ }).openapi(`${serviceName}HealthResponse`);
34
+ }
35
+ return baseSchema.openapi(`${serviceName}HealthResponse`);
36
+ }
37
+ function createWrappedHealthResponseSchema(serviceName) {
38
+ return z.object({
39
+ success: z.literal(true),
40
+ data: z.object({
41
+ status: z.literal("healthy"),
42
+ service: z.literal(serviceName),
43
+ timestamp: z.string().openapi({ example: "2024-01-15T10:30:00.000Z" })
44
+ })
45
+ }).openapi(`${serviceName}HealthResponse`);
46
+ }
47
+ function createSuccessResponseSchema(dataSchema) {
48
+ return z.object({
49
+ success: z.literal(true),
50
+ data: dataSchema
51
+ });
52
+ }
53
+ function createDeleteResponseSchema(idField = "id", example = "01HXYZ123ABC") {
54
+ return z.object({
55
+ deleted: z.literal(true),
56
+ [idField]: z.string().openapi({ example })
57
+ }).openapi("DeleteResponse");
58
+ }
59
+ var ValidationErrorResponseSchema = z.object({
60
+ error: z.literal("Validation Error"),
61
+ message: z.string().openapi({ example: "id: Required, title: String must contain at least 1 character(s)" })
62
+ }).openapi("ValidationErrorResponse");
63
+ var TaskApiHealthResponseSchema = createHealthResponseSchema("task-api-adapter", {
64
+ includeChecks: true
65
+ });
66
+ var PromptApiHealthResponseSchema = createHealthResponseSchema("prompt-api");
67
+ export {
68
+ DetailedErrorResponseSchema,
69
+ ErrorResponseSchema,
70
+ PromptApiHealthResponseSchema,
71
+ SimpleErrorResponseSchema,
72
+ TaskApiHealthResponseSchema,
73
+ ThrottleErrorResponseSchema,
74
+ ValidationErrorResponseSchema,
75
+ createDeleteResponseSchema,
76
+ createHealthResponseSchema,
77
+ createSuccessResponseSchema,
78
+ createWrappedHealthResponseSchema
79
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Sliding window rate limiting using Cloudflare KV
3
+ *
4
+ * Generic implementation that accepts configuration per-consumer.
5
+ * Each worker provides its own limits via RateLimitConfig.
6
+ */
7
+ interface RateLimitConfig {
8
+ /** Maximum requests allowed per window */
9
+ maxRequests: number;
10
+ /** Window duration in seconds */
11
+ windowSeconds: number;
12
+ /** KV TTL in seconds (should be >= windowSeconds) */
13
+ kvTtlSeconds: number;
14
+ /** KV key prefix (default: 'rate-limit') */
15
+ keyPrefix?: string;
16
+ }
17
+ interface RateLimitResult {
18
+ allowed: boolean;
19
+ remaining: number;
20
+ resetAt: number;
21
+ reason?: string;
22
+ }
23
+ /**
24
+ * Check if an identifier (IP, user key, etc.) is rate limited
25
+ */
26
+ declare function checkRateLimit(kv: KVNamespace, identifier: string, config: RateLimitConfig): Promise<RateLimitResult>;
27
+ /**
28
+ * Record a request for rate limiting.
29
+ * Call after checkRateLimit returns allowed: true.
30
+ */
31
+ declare function recordRequest(kv: KVNamespace, identifier: string, config: RateLimitConfig): Promise<void>;
32
+ /**
33
+ * Reset rate limit for an identifier (admin function)
34
+ */
35
+ declare function resetRateLimit(kv: KVNamespace, identifier: string, config?: {
36
+ keyPrefix?: string;
37
+ }): Promise<void>;
38
+
39
+ export { type RateLimitConfig, type RateLimitResult, checkRateLimit, recordRequest, resetRateLimit };
@@ -0,0 +1,59 @@
1
+ // rate-limit.ts
2
+ async function checkRateLimit(kv, identifier, config) {
3
+ const prefix = config.keyPrefix ?? "rate-limit";
4
+ const key = `${prefix}:${identifier}`;
5
+ const windowMs = config.windowSeconds * 1e3;
6
+ const now = Date.now();
7
+ const existingData = await kv.get(key, "text");
8
+ const existing = existingData ? JSON.parse(existingData) : null;
9
+ if (!existing || now - existing.windowStart >= windowMs) {
10
+ return {
11
+ allowed: true,
12
+ remaining: config.maxRequests - 1,
13
+ resetAt: now + windowMs
14
+ };
15
+ }
16
+ if (existing.count >= config.maxRequests) {
17
+ const resetAt = existing.windowStart + windowMs;
18
+ const timeRemaining = resetAt - now;
19
+ const display = timeRemaining >= 12e4 ? `${Math.ceil(timeRemaining / 6e4)} minute(s)` : `${Math.ceil(timeRemaining / 1e3)} second(s)`;
20
+ return {
21
+ allowed: false,
22
+ remaining: 0,
23
+ resetAt,
24
+ reason: `Rate limit exceeded. Try again in ${display}.`
25
+ };
26
+ }
27
+ return {
28
+ allowed: true,
29
+ remaining: config.maxRequests - existing.count - 1,
30
+ resetAt: existing.windowStart + windowMs
31
+ };
32
+ }
33
+ async function recordRequest(kv, identifier, config) {
34
+ const prefix = config.keyPrefix ?? "rate-limit";
35
+ const key = `${prefix}:${identifier}`;
36
+ const windowMs = config.windowSeconds * 1e3;
37
+ const now = Date.now();
38
+ const existingData = await kv.get(key, "text");
39
+ const existing = existingData ? JSON.parse(existingData) : null;
40
+ let entry;
41
+ if (!existing || now - existing.windowStart >= windowMs) {
42
+ entry = { count: 1, windowStart: now };
43
+ } else {
44
+ entry = { count: existing.count + 1, windowStart: existing.windowStart };
45
+ }
46
+ await kv.put(key, JSON.stringify(entry), {
47
+ expirationTtl: config.kvTtlSeconds
48
+ });
49
+ }
50
+ async function resetRateLimit(kv, identifier, config) {
51
+ const prefix = config?.keyPrefix ?? "rate-limit";
52
+ const key = `${prefix}:${identifier}`;
53
+ await kv.delete(key);
54
+ }
55
+ export {
56
+ checkRateLimit,
57
+ recordRequest,
58
+ resetRateLimit
59
+ };
@@ -0,0 +1,159 @@
1
+ import { Context } from 'hono';
2
+ import { HealthStatus } from './types.js';
3
+
4
+ /**
5
+ * Standard response helpers for Cloudflare Workers
6
+ *
7
+ * Consistent response formatting for:
8
+ * - Success responses
9
+ * - Error responses
10
+ * - Health checks
11
+ * - Not found responses
12
+ * - Validation errors
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import { ok, created, badRequest, notFound, healthCheck } from '../util';
17
+ *
18
+ * // Success response
19
+ * return ok(c, { tasks: [...] });
20
+ *
21
+ * // Created with custom message
22
+ * return created(c, { task }, 'Task created successfully');
23
+ *
24
+ * // Error response
25
+ * return badRequest(c, 'Invalid input');
26
+ *
27
+ * // Health check
28
+ * return healthCheck(c, 'task-api', { kv: true, db: false });
29
+ * ```
30
+ */
31
+
32
+ /**
33
+ * Send a success response (200)
34
+ *
35
+ * @param c - Hono context
36
+ * @param data - Response data
37
+ * @param message - Optional success message
38
+ * @returns JSON response
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * return ok(c, { tasks: [...] });
43
+ * return ok(c, { count: 10 }, 'Retrieved successfully');
44
+ * ```
45
+ */
46
+ declare function ok<T = any>(c: Context, data: T, message?: string): Response;
47
+ /**
48
+ * Send a created response (201)
49
+ *
50
+ * @param c - Hono context
51
+ * @param data - Created resource data
52
+ * @param message - Optional success message
53
+ * @returns JSON response
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * return created(c, { task: {...} });
58
+ * return created(c, { id: '123' }, 'Task created successfully');
59
+ * ```
60
+ */
61
+ declare function created<T = any>(c: Context, data: T, message?: string): Response;
62
+ /**
63
+ * Send a bad request error (400)
64
+ *
65
+ * @param c - Hono context
66
+ * @param error - Error message
67
+ * @param details - Additional error details
68
+ * @returns JSON response
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * return badRequest(c, 'Missing required field: id');
73
+ * return badRequest(c, 'Validation failed', { errors: [...] });
74
+ * ```
75
+ */
76
+ declare function badRequest(c: Context, error: string, details?: unknown): Response;
77
+ /**
78
+ * Send a not found error (404)
79
+ *
80
+ * @param c - Hono context
81
+ * @param resource - Resource type that wasn't found
82
+ * @returns JSON response
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * return notFound(c, 'Task');
87
+ * return notFound(c, 'Board');
88
+ * ```
89
+ */
90
+ declare function notFound(c: Context, resource?: string): Response;
91
+ /**
92
+ * Send an internal server error (500)
93
+ *
94
+ * @param c - Hono context
95
+ * @param message - Error message
96
+ * @param details - Additional error details (omit in production)
97
+ * @returns JSON response
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * return serverError(c, 'Failed to process request');
102
+ * return serverError(c, 'Database error', { error: err.message });
103
+ * ```
104
+ */
105
+ declare function serverError(c: Context, message?: string, details?: unknown): Response;
106
+ /**
107
+ * Send validation errors (400)
108
+ *
109
+ * @param c - Hono context
110
+ * @param errors - Array of validation errors
111
+ * @param message - Optional error message
112
+ * @returns JSON response
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * const validation = validateFields(body, rules);
117
+ * if (!validation.valid) {
118
+ * return validationError(c, validation.errors);
119
+ * }
120
+ * ```
121
+ */
122
+ declare function validationError(c: Context, errors: {
123
+ field: string;
124
+ message: string;
125
+ }[], message?: string): Response;
126
+ /**
127
+ * Send a health check response
128
+ *
129
+ * @param c - Hono context
130
+ * @param serviceName - Name of the service
131
+ * @param checks - Optional health check results
132
+ * @param options - Additional options
133
+ * @returns JSON response
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * // Simple health check
138
+ * return healthCheck(c, 'task-api');
139
+ *
140
+ * // With dependency checks
141
+ * return healthCheck(c, 'task-api', {
142
+ * kv: await checkKV(),
143
+ * api: await checkAPI()
144
+ * });
145
+ *
146
+ * // Degraded status
147
+ * return healthCheck(c, 'task-api', { kv: false }, {
148
+ * status: 'degraded'
149
+ * });
150
+ * ```
151
+ */
152
+ declare function healthCheck(c: Context, serviceName: string, checks?: Record<string, boolean | string>, options?: {
153
+ status?: HealthStatus;
154
+ version?: string;
155
+ remediation?: string;
156
+ gauges?: Record<string, number | null>;
157
+ }): Response;
158
+
159
+ export { badRequest, created, healthCheck, notFound, ok, serverError, validationError };
@@ -0,0 +1,80 @@
1
+ // responses.ts
2
+ function ok(c, data, message) {
3
+ const response = {
4
+ data,
5
+ ...message && { message },
6
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
7
+ };
8
+ return c.json(response, 200);
9
+ }
10
+ function created(c, data, message) {
11
+ const response = {
12
+ data,
13
+ message: message || "Resource created successfully",
14
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
15
+ };
16
+ return c.json(response, 201);
17
+ }
18
+ function badRequest(c, error, details) {
19
+ const response = {
20
+ error,
21
+ ...details !== void 0 ? { details } : {},
22
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
23
+ };
24
+ return c.json(response, 400);
25
+ }
26
+ function notFound(c, resource = "Resource") {
27
+ const response = {
28
+ error: `${resource} not found`,
29
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
30
+ };
31
+ return c.json(response, 404);
32
+ }
33
+ function serverError(c, message = "Internal server error", details) {
34
+ const response = {
35
+ error: message,
36
+ ...details !== void 0 ? { details } : {},
37
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
38
+ };
39
+ return c.json(response, 500);
40
+ }
41
+ function validationError(c, errors, message = "Validation failed") {
42
+ const response = {
43
+ error: message,
44
+ details: { errors },
45
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
46
+ };
47
+ return c.json(response, 400);
48
+ }
49
+ function healthCheck(c, serviceName, checks, options = {}) {
50
+ let status = options.status || "ok";
51
+ if (checks && !options.status) {
52
+ const hasFailures = Object.values(checks).some((v) => v === false);
53
+ const allFailed = Object.values(checks).every((v) => v === false);
54
+ if (allFailed) {
55
+ status = "error";
56
+ } else if (hasFailures) {
57
+ status = "degraded";
58
+ }
59
+ }
60
+ const response = {
61
+ status,
62
+ service: serviceName,
63
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
64
+ ...options.version && { version: options.version },
65
+ ...checks && { checks },
66
+ ...options.remediation && { remediation: options.remediation },
67
+ ...options.gauges && { gauges: options.gauges }
68
+ };
69
+ const statusCode = status === "ok" ? 200 : 503;
70
+ return c.json(response, statusCode);
71
+ }
72
+ export {
73
+ badRequest,
74
+ created,
75
+ healthCheck,
76
+ notFound,
77
+ ok,
78
+ serverError,
79
+ validationError
80
+ };