@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.
package/dist/index.js ADDED
@@ -0,0 +1,976 @@
1
+ // auth.ts
2
+ function getAuthContext(c, contextKey = "authContext") {
3
+ return c.get(contextKey);
4
+ }
5
+ var TIER_RANK = {
6
+ public: 0,
7
+ member: 1,
8
+ friend: 2,
9
+ service: 3,
10
+ wife: 4,
11
+ admin: 5
12
+ };
13
+ function tierAtLeast(auth, minTier) {
14
+ if (!Object.hasOwn(TIER_RANK, minTier)) {
15
+ throw new Error(`tierAtLeast: unknown required tier '${minTier}'`);
16
+ }
17
+ const caller = auth?.userType;
18
+ const callerRank = caller !== void 0 && Object.hasOwn(TIER_RANK, caller) ? TIER_RANK[caller] : 0;
19
+ return callerRank >= TIER_RANK[minTier];
20
+ }
21
+ function requireMinTier(minTier, options = {}) {
22
+ const {
23
+ contextKey = "authContext",
24
+ errorMessage = "Insufficient permissions",
25
+ errorStatus = 403
26
+ } = options;
27
+ return async (c, next) => {
28
+ if (!tierAtLeast(getAuthContext(c, contextKey), minTier)) {
29
+ return c.json({ error: errorMessage }, errorStatus);
30
+ }
31
+ await next();
32
+ };
33
+ }
34
+
35
+ // content-level.ts
36
+ var DEFAULT_CONTENT_LEVEL = 1;
37
+ var MAX_CONTENT_LEVEL_BY_TIER = {
38
+ admin: 4,
39
+ wife: 3,
40
+ service: 3,
41
+ friend: 3,
42
+ member: 3
43
+ };
44
+ function maxContentLevelForTier(tier) {
45
+ if (!tier || TIER_RANK[tier] === void 0) return DEFAULT_CONTENT_LEVEL;
46
+ return MAX_CONTENT_LEVEL_BY_TIER[tier] ?? DEFAULT_CONTENT_LEVEL;
47
+ }
48
+
49
+ // edgeAuth.ts
50
+ var TIERS = new Set(Object.keys(TIER_RANK));
51
+ function timingSafeEqual(a, b) {
52
+ if (a.length !== b.length) return false;
53
+ let diff = 0;
54
+ for (let i = 0; i < a.length; i++) {
55
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
56
+ }
57
+ return diff === 0;
58
+ }
59
+ var EDGE_IDENTITY_HEADERS = [
60
+ "X-User-Id",
61
+ "X-User-Name",
62
+ "X-Hadoku-Tier",
63
+ "X-Hadoku-Content-Level"
64
+ ];
65
+ function withoutEdgeIdentity(req) {
66
+ const headers = new Headers(req.headers);
67
+ for (const h of EDGE_IDENTITY_HEADERS) headers.delete(h);
68
+ return new Request(req, { headers });
69
+ }
70
+ var headerOrNull = (c, name) => {
71
+ const v = c.req.header(name)?.trim();
72
+ return v ? v : null;
73
+ };
74
+ function verifyEdgeAuth(c, env) {
75
+ const secret = env.EDGE_AUTH_SECRET;
76
+ if (!secret) return { ok: false, reason: "misconfigured" };
77
+ const provided = c.req.header("X-Edge-Auth") ?? "";
78
+ if (!timingSafeEqual(provided, secret)) {
79
+ return { ok: false, reason: "bad-provenance" };
80
+ }
81
+ const rawTier = c.req.header("X-Hadoku-Tier") ?? "public";
82
+ const userType = TIERS.has(rawTier) ? rawTier : "public";
83
+ const credential = c.req.header("X-User-Key") ?? null;
84
+ return {
85
+ ok: true,
86
+ userType,
87
+ credential,
88
+ userId: headerOrNull(c, "X-User-Id"),
89
+ userName: headerOrNull(c, "X-User-Name")
90
+ };
91
+ }
92
+ function createEdgeAuth(options) {
93
+ const contextKey = options?.contextKey ?? "authContext";
94
+ return async (c, next) => {
95
+ const result = verifyEdgeAuth(c, c.env);
96
+ if (!result.ok) c.req.raw = withoutEdgeIdentity(c.req.raw);
97
+ const base = result.ok ? {
98
+ userType: result.userType,
99
+ credential: result.credential,
100
+ userId: result.userId,
101
+ userName: result.userName
102
+ } : { userType: "public", credential: null, userId: null, userName: null };
103
+ const ctx = options?.extend ? { ...base, ...options.extend(base) } : base;
104
+ c.set(contextKey, ctx);
105
+ await next();
106
+ };
107
+ }
108
+
109
+ // context.ts
110
+ function extractField(c, sources, defaultValue, transform) {
111
+ for (const source of sources) {
112
+ const [type, key] = source.split(":");
113
+ let value;
114
+ switch (type.toLowerCase()) {
115
+ case "header":
116
+ value = c.req.header(key);
117
+ break;
118
+ case "query":
119
+ value = c.req.query(key);
120
+ break;
121
+ case "body":
122
+ value = void 0;
123
+ break;
124
+ case "param":
125
+ value = c.req.param(key);
126
+ break;
127
+ case "cookie": {
128
+ const cookieHeader = c.req.header("Cookie");
129
+ if (cookieHeader) {
130
+ const cookies = Object.fromEntries(
131
+ cookieHeader.split(";").map((cookie) => {
132
+ const [k, v] = cookie.trim().split("=");
133
+ return [k, v];
134
+ })
135
+ );
136
+ value = cookies[key];
137
+ }
138
+ break;
139
+ }
140
+ default:
141
+ continue;
142
+ }
143
+ if (value !== void 0 && value !== null && value !== "") {
144
+ return transform ? transform(value) : value;
145
+ }
146
+ }
147
+ return defaultValue;
148
+ }
149
+ function extractContext(c, config) {
150
+ const result = {};
151
+ for (const [fieldName, sources] of Object.entries(config.fields)) {
152
+ const defaultValue = config.defaults?.[fieldName];
153
+ const transform = config.transforms?.[fieldName];
154
+ result[fieldName] = extractField(c, sources, defaultValue, transform);
155
+ }
156
+ return result;
157
+ }
158
+ async function parseBody(c, fallback = {}) {
159
+ try {
160
+ return await c.req.json();
161
+ } catch {
162
+ return fallback;
163
+ }
164
+ }
165
+ function getRequestMetadata(c) {
166
+ const cf = c.req.raw.cf;
167
+ return {
168
+ ip: c.req.header("CF-Connecting-IP") || c.req.header("X-Forwarded-For"),
169
+ userAgent: c.req.header("User-Agent"),
170
+ country: cf?.country,
171
+ city: cf?.city,
172
+ region: cf?.region,
173
+ timezone: cf?.timezone,
174
+ colo: cf?.colo,
175
+ requestId: c.req.header("CF-Ray") || crypto.randomUUID()
176
+ };
177
+ }
178
+
179
+ // validation.ts
180
+ function validateField(value, rule) {
181
+ if (rule.required && (value === void 0 || value === null || value === "")) {
182
+ return `${rule.field} is required`;
183
+ }
184
+ if (value === void 0 || value === null) {
185
+ return null;
186
+ }
187
+ if (rule.type) {
188
+ const actualType = Array.isArray(value) ? "array" : typeof value;
189
+ if (actualType !== rule.type) {
190
+ return `${rule.field} must be of type ${rule.type}`;
191
+ }
192
+ }
193
+ if (typeof value === "string") {
194
+ if (rule.minLength !== void 0 && value.length < rule.minLength) {
195
+ return `${rule.field} must be at least ${rule.minLength} characters`;
196
+ }
197
+ if (rule.maxLength !== void 0 && value.length > rule.maxLength) {
198
+ return `${rule.field} must be at most ${rule.maxLength} characters`;
199
+ }
200
+ if (rule.pattern && !rule.pattern.test(value)) {
201
+ return `${rule.field} format is invalid`;
202
+ }
203
+ }
204
+ if (rule.custom) {
205
+ const result = rule.custom(value);
206
+ if (result !== true && typeof result === "string") {
207
+ return result;
208
+ }
209
+ if (result === false) {
210
+ return `${rule.field} is invalid`;
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+ function validateFields(data, rules) {
216
+ const errors = [];
217
+ for (const rule of rules) {
218
+ const value = data[rule.field];
219
+ const error = validateField(value, rule);
220
+ if (error) {
221
+ errors.push({ field: rule.field, message: error });
222
+ }
223
+ }
224
+ return {
225
+ valid: errors.length === 0,
226
+ errors
227
+ };
228
+ }
229
+ function requireFields(data, fields) {
230
+ for (const field of fields) {
231
+ const value = data[field];
232
+ if (value === void 0 || value === null || typeof value === "string" && value.trim() === "") {
233
+ return `Missing required field: ${field}`;
234
+ }
235
+ }
236
+ return null;
237
+ }
238
+ function isNonEmptyString(value) {
239
+ return typeof value === "string" && value.trim() !== "";
240
+ }
241
+ var CommonRules = {
242
+ /**
243
+ * ID field (required, non-empty string)
244
+ */
245
+ id: () => ({
246
+ field: "id",
247
+ required: true,
248
+ type: "string",
249
+ custom: (val) => typeof val === "string" && val.trim() !== "" || "ID cannot be empty"
250
+ }),
251
+ /**
252
+ * Title field (required, 1-500 characters)
253
+ */
254
+ title: (options = {}) => ({
255
+ field: "title",
256
+ required: true,
257
+ type: "string",
258
+ minLength: options.minLength ?? 1,
259
+ maxLength: options.maxLength ?? 500,
260
+ custom: (val) => typeof val === "string" && val.trim() !== "" || "Title cannot be empty"
261
+ }),
262
+ /**
263
+ * Email field
264
+ */
265
+ email: (required = false) => ({
266
+ field: "email",
267
+ required,
268
+ type: "string",
269
+ pattern: /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/
270
+ }),
271
+ /**
272
+ * URL field
273
+ */
274
+ url: (required = false) => ({
275
+ field: "url",
276
+ required,
277
+ type: "string",
278
+ pattern: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/
279
+ }),
280
+ /**
281
+ * Enum field (value must be in allowed list)
282
+ */
283
+ enum: (field, allowedValues, required = false) => ({
284
+ field,
285
+ required,
286
+ custom: (val) => allowedValues.includes(val) || `${field} must be one of: ${allowedValues.join(", ")}`
287
+ }),
288
+ /**
289
+ * Array field with optional element validation
290
+ */
291
+ array: (field, options = {}) => ({
292
+ field,
293
+ required: options.required,
294
+ type: "array",
295
+ custom: (val) => {
296
+ if (!Array.isArray(val)) return false;
297
+ if (options.minLength !== void 0 && val.length < options.minLength) {
298
+ return `${field} must have at least ${options.minLength} items`;
299
+ }
300
+ if (options.maxLength !== void 0 && val.length > options.maxLength) {
301
+ return `${field} must have at most ${options.maxLength} items`;
302
+ }
303
+ if (options.elementType) {
304
+ const invalidItem = val.find((item) => {
305
+ const actualType = Array.isArray(item) ? "array" : typeof item;
306
+ return actualType !== options.elementType;
307
+ });
308
+ if (invalidItem !== void 0) {
309
+ return `All ${field} items must be of type ${options.elementType}`;
310
+ }
311
+ }
312
+ return true;
313
+ }
314
+ }),
315
+ /**
316
+ * Number field with optional range validation
317
+ */
318
+ number: (field, options = {}) => ({
319
+ field,
320
+ required: options.required,
321
+ type: "number",
322
+ custom: (val) => {
323
+ if (typeof val !== "number") return false;
324
+ if (options.integer && !Number.isInteger(val)) {
325
+ return `${field} must be an integer`;
326
+ }
327
+ if (options.min !== void 0 && val < options.min) {
328
+ return `${field} must be at least ${options.min}`;
329
+ }
330
+ if (options.max !== void 0 && val > options.max) {
331
+ return `${field} must be at most ${options.max}`;
332
+ }
333
+ return true;
334
+ }
335
+ }),
336
+ /**
337
+ * Boolean field
338
+ */
339
+ boolean: (field, required = false) => ({
340
+ field,
341
+ required,
342
+ type: "boolean"
343
+ })
344
+ };
345
+
346
+ // cors.ts
347
+ import { cors } from "hono/cors";
348
+
349
+ // edge-routes.ts
350
+ var EDGE_BASE = "https://hadoku.me";
351
+ var MGMT_BASE = `${EDGE_BASE}/mgmt`;
352
+ var MONITORING_BASE = `${EDGE_BASE}/health`;
353
+ var SCRAPER_BASE = `${EDGE_BASE}/scraper`;
354
+ var CONTACT_BASE = `${EDGE_BASE}/contact`;
355
+ var RESUME_BASE = `${EDGE_BASE}/resume`;
356
+ var DISPATCH_BASE = `${EDGE_BASE}/dispatch`;
357
+ var PREFS_BASE = `${EDGE_BASE}/prefs`;
358
+ var JOBPLATFORM_BASE = `${EDGE_BASE}/jobplatform`;
359
+
360
+ // cors.ts
361
+ var DEFAULT_HADOKU_ORIGINS = [
362
+ EDGE_BASE,
363
+ "https://task-api.hadoku.me",
364
+ "http://localhost:*"
365
+ ];
366
+ function matchOrigin(origin, pattern) {
367
+ if (origin === pattern) return true;
368
+ if (pattern.includes("*")) {
369
+ const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
370
+ const regex = new RegExp(`^${regexPattern}$`);
371
+ return regex.test(origin);
372
+ }
373
+ return false;
374
+ }
375
+ function isOriginAllowed(origin, allowedOrigins) {
376
+ return allowedOrigins.some((pattern) => matchOrigin(origin, pattern));
377
+ }
378
+ function createCorsMiddleware(config) {
379
+ const {
380
+ origins,
381
+ methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
382
+ allowedHeaders,
383
+ exposedHeaders,
384
+ credentials = true,
385
+ maxAge = 86400
386
+ } = config;
387
+ return cors({
388
+ origin: (origin) => {
389
+ if (!origin) return origins[0];
390
+ return isOriginAllowed(origin, origins) ? origin : origins[0];
391
+ },
392
+ allowMethods: methods,
393
+ allowHeaders: allowedHeaders,
394
+ exposeHeaders: exposedHeaders,
395
+ credentials,
396
+ maxAge
397
+ });
398
+ }
399
+ function createHadokuCors(additionalOrigins = []) {
400
+ return createCorsMiddleware({
401
+ origins: [...DEFAULT_HADOKU_ORIGINS, ...additionalOrigins],
402
+ credentials: true
403
+ });
404
+ }
405
+ var CORSPresets = {
406
+ /**
407
+ * Development - Allow all localhost origins
408
+ */
409
+ development: {
410
+ origins: ["http://localhost:*", "http://127.0.0.1:*"],
411
+ credentials: true
412
+ },
413
+ /**
414
+ * Production - Strict origin checking
415
+ */
416
+ production: (domains) => ({
417
+ origins: domains,
418
+ credentials: true,
419
+ maxAge: 86400
420
+ }),
421
+ /**
422
+ * Public API - Allow all origins (no credentials)
423
+ */
424
+ publicApi: {
425
+ origins: ["*"],
426
+ credentials: false
427
+ },
428
+ /**
429
+ * Hadoku standard - Production + development origins
430
+ */
431
+ hadoku: {
432
+ origins: DEFAULT_HADOKU_ORIGINS,
433
+ credentials: true,
434
+ maxAge: 86400
435
+ }
436
+ };
437
+ function getCorsHeaders(origin, allowedOrigins, options = {}) {
438
+ const headers = {};
439
+ if (origin && isOriginAllowed(origin, allowedOrigins)) {
440
+ headers["Access-Control-Allow-Origin"] = origin;
441
+ if (options.credentials !== false) {
442
+ headers["Access-Control-Allow-Credentials"] = "true";
443
+ }
444
+ if (options.exposedHeaders && options.exposedHeaders.length > 0) {
445
+ headers["Access-Control-Expose-Headers"] = options.exposedHeaders.join(", ");
446
+ }
447
+ }
448
+ return headers;
449
+ }
450
+
451
+ // cloudflareWorkerLogger.ts
452
+ import { createWorkerLogger } from "@wolffm/logger/worker";
453
+ function deriveService(prefix) {
454
+ if (!prefix) return "worker";
455
+ const trimmed = prefix.trim();
456
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
457
+ return trimmed.slice(1, -1);
458
+ }
459
+ return trimmed;
460
+ }
461
+ function createLogger(config = {}) {
462
+ return createWorkerLogger({
463
+ service: deriveService(config.prefix),
464
+ minLevel: config.minLevel ?? "info"
465
+ });
466
+ }
467
+ var defaultLogger = createWorkerLogger({ service: "worker" });
468
+ function logRequest(method, path, context, level = "info") {
469
+ defaultLogger[level](`[${method} ${path}]`, context);
470
+ }
471
+ function logError(method, path, error, context) {
472
+ const message = error instanceof Error ? error.message : error;
473
+ const ctx = { ...context ?? {} };
474
+ if (error instanceof Error && error.stack) ctx.stack = error.stack;
475
+ defaultLogger.error(`[${method} ${path}] ${message}`, ctx);
476
+ }
477
+ function createRequestLogger(method, path, baseContext = {}) {
478
+ const scoped = defaultLogger.child(`${method} ${path}`);
479
+ const merge = (extra) => ({ ...baseContext, ...extra ?? {} });
480
+ return {
481
+ debug: (message, context) => scoped.debug(message, merge(context)),
482
+ info: (message, context) => scoped.info(message, merge(context)),
483
+ warn: (message, context) => scoped.warn(message, merge(context)),
484
+ error: (message, context) => scoped.error(message, merge(context))
485
+ };
486
+ }
487
+ function loggerMiddleware(config = {}, contextKey = "logger") {
488
+ return async (c, next) => {
489
+ const logger = createLogger(config);
490
+ c.set(contextKey, logger);
491
+ await next();
492
+ };
493
+ }
494
+ function formatError(error) {
495
+ if (error instanceof Error) {
496
+ return { name: error.name, message: error.message, stack: error.stack };
497
+ }
498
+ return { error: String(error) };
499
+ }
500
+ function startTimer() {
501
+ const start = Date.now();
502
+ return {
503
+ end: (message, context) => {
504
+ const duration = Date.now() - start;
505
+ defaultLogger.info(`${message} (duration: ${duration}ms)`, context);
506
+ return duration;
507
+ },
508
+ elapsed: () => Date.now() - start
509
+ };
510
+ }
511
+ function redactFields(obj, fields) {
512
+ const redacted = { ...obj };
513
+ for (const field of fields) {
514
+ if (field in redacted) redacted[field] = "[REDACTED]";
515
+ }
516
+ return redacted;
517
+ }
518
+ var SENSITIVE_FIELDS = [
519
+ "password",
520
+ "apiKey",
521
+ "api_key",
522
+ "token",
523
+ "accessToken",
524
+ "access_token",
525
+ "refreshToken",
526
+ "refresh_token",
527
+ "secret",
528
+ "privateKey",
529
+ "private_key",
530
+ "authorization",
531
+ "cookie",
532
+ "session"
533
+ ];
534
+
535
+ // rate-limit.ts
536
+ async function checkRateLimit(kv, identifier, config) {
537
+ const prefix = config.keyPrefix ?? "rate-limit";
538
+ const key = `${prefix}:${identifier}`;
539
+ const windowMs = config.windowSeconds * 1e3;
540
+ const now = Date.now();
541
+ const existingData = await kv.get(key, "text");
542
+ const existing = existingData ? JSON.parse(existingData) : null;
543
+ if (!existing || now - existing.windowStart >= windowMs) {
544
+ return {
545
+ allowed: true,
546
+ remaining: config.maxRequests - 1,
547
+ resetAt: now + windowMs
548
+ };
549
+ }
550
+ if (existing.count >= config.maxRequests) {
551
+ const resetAt = existing.windowStart + windowMs;
552
+ const timeRemaining = resetAt - now;
553
+ const display = timeRemaining >= 12e4 ? `${Math.ceil(timeRemaining / 6e4)} minute(s)` : `${Math.ceil(timeRemaining / 1e3)} second(s)`;
554
+ return {
555
+ allowed: false,
556
+ remaining: 0,
557
+ resetAt,
558
+ reason: `Rate limit exceeded. Try again in ${display}.`
559
+ };
560
+ }
561
+ return {
562
+ allowed: true,
563
+ remaining: config.maxRequests - existing.count - 1,
564
+ resetAt: existing.windowStart + windowMs
565
+ };
566
+ }
567
+ async function recordRequest(kv, identifier, config) {
568
+ const prefix = config.keyPrefix ?? "rate-limit";
569
+ const key = `${prefix}:${identifier}`;
570
+ const windowMs = config.windowSeconds * 1e3;
571
+ const now = Date.now();
572
+ const existingData = await kv.get(key, "text");
573
+ const existing = existingData ? JSON.parse(existingData) : null;
574
+ let entry;
575
+ if (!existing || now - existing.windowStart >= windowMs) {
576
+ entry = { count: 1, windowStart: now };
577
+ } else {
578
+ entry = { count: existing.count + 1, windowStart: existing.windowStart };
579
+ }
580
+ await kv.put(key, JSON.stringify(entry), {
581
+ expirationTtl: config.kvTtlSeconds
582
+ });
583
+ }
584
+ async function resetRateLimit(kv, identifier, config) {
585
+ const prefix = config?.keyPrefix ?? "rate-limit";
586
+ const key = `${prefix}:${identifier}`;
587
+ await kv.delete(key);
588
+ }
589
+
590
+ // masking.ts
591
+ var MASKING = {
592
+ /** Percentage of key to show (default: 50%) */
593
+ KEY_PERCENTAGE: 0.5,
594
+ /** Minimum characters to show for keys */
595
+ KEY_MIN_LENGTH: 4,
596
+ /** Number of characters to show for session IDs (default: 16) */
597
+ SESSION_ID_PREFIX_LENGTH: 16
598
+ };
599
+ function maskKey(key, percentage = MASKING.KEY_PERCENTAGE) {
600
+ if (!key) {
601
+ return key;
602
+ }
603
+ const showLength = Math.max(MASKING.KEY_MIN_LENGTH, Math.floor(key.length * percentage));
604
+ if (key.length <= showLength) {
605
+ return key;
606
+ }
607
+ return key.substring(0, showLength);
608
+ }
609
+ function maskSessionId(id) {
610
+ if (!id) {
611
+ return id;
612
+ }
613
+ const length = MASKING.SESSION_ID_PREFIX_LENGTH;
614
+ if (id.length <= length) {
615
+ return id;
616
+ }
617
+ return id.substring(0, length);
618
+ }
619
+ function maskIpAddress(ip) {
620
+ if (!ip) {
621
+ return void 0;
622
+ }
623
+ if (ip.includes(".")) {
624
+ const parts = ip.split(".");
625
+ if (parts.length === 4) {
626
+ return `${parts[0]}.${parts[1]}.${parts[2]}.*`;
627
+ }
628
+ }
629
+ if (ip.includes(":")) {
630
+ const lastColon = ip.lastIndexOf(":");
631
+ if (lastColon > 0) {
632
+ return ip.substring(0, lastColon + 1) + "*";
633
+ }
634
+ }
635
+ return ip;
636
+ }
637
+ function maskEmail(email) {
638
+ if (!email?.includes("@")) {
639
+ return email;
640
+ }
641
+ const [username, domain] = email.split("@");
642
+ if (!username || !domain) {
643
+ return email;
644
+ }
645
+ return `${username[0]}***@${domain}`;
646
+ }
647
+
648
+ // responses.ts
649
+ function ok(c, data, message) {
650
+ const response = {
651
+ data,
652
+ ...message && { message },
653
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
654
+ };
655
+ return c.json(response, 200);
656
+ }
657
+ function created(c, data, message) {
658
+ const response = {
659
+ data,
660
+ message: message || "Resource created successfully",
661
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
662
+ };
663
+ return c.json(response, 201);
664
+ }
665
+ function badRequest(c, error, details) {
666
+ const response = {
667
+ error,
668
+ ...details !== void 0 ? { details } : {},
669
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
670
+ };
671
+ return c.json(response, 400);
672
+ }
673
+ function notFound(c, resource = "Resource") {
674
+ const response = {
675
+ error: `${resource} not found`,
676
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
677
+ };
678
+ return c.json(response, 404);
679
+ }
680
+ function serverError(c, message = "Internal server error", details) {
681
+ const response = {
682
+ error: message,
683
+ ...details !== void 0 ? { details } : {},
684
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
685
+ };
686
+ return c.json(response, 500);
687
+ }
688
+ function validationError(c, errors, message = "Validation failed") {
689
+ const response = {
690
+ error: message,
691
+ details: { errors },
692
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
693
+ };
694
+ return c.json(response, 400);
695
+ }
696
+ function healthCheck(c, serviceName, checks, options = {}) {
697
+ let status = options.status || "ok";
698
+ if (checks && !options.status) {
699
+ const hasFailures = Object.values(checks).some((v) => v === false);
700
+ const allFailed = Object.values(checks).every((v) => v === false);
701
+ if (allFailed) {
702
+ status = "error";
703
+ } else if (hasFailures) {
704
+ status = "degraded";
705
+ }
706
+ }
707
+ const response = {
708
+ status,
709
+ service: serviceName,
710
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
711
+ ...options.version && { version: options.version },
712
+ ...checks && { checks },
713
+ ...options.remediation && { remediation: options.remediation },
714
+ ...options.gauges && { gauges: options.gauges }
715
+ };
716
+ const statusCode = status === "ok" ? 200 : 503;
717
+ return c.json(response, statusCode);
718
+ }
719
+
720
+ // openapi-schemas.ts
721
+ import { z } from "@hono/zod-openapi";
722
+ var HEALTH_STATUS_VALUES = ["ok", "degraded", "error"];
723
+ var ErrorResponseSchema = z.object({
724
+ success: z.literal(false),
725
+ error: z.string().openapi({ example: "Bad Request" })
726
+ }).openapi("ErrorResponse");
727
+ var SimpleErrorResponseSchema = z.object({
728
+ error: z.string().openapi({ example: "Bad Request" }),
729
+ message: z.string().optional().openapi({ example: "Missing required field: title" })
730
+ }).openapi("SimpleErrorResponse");
731
+ var DetailedErrorResponseSchema = z.object({
732
+ success: z.literal(false),
733
+ error: z.string().openapi({ example: "Validation Error" }),
734
+ message: z.string().openapi({ example: "Invalid input data" })
735
+ }).openapi("DetailedErrorResponse");
736
+ var ThrottleErrorResponseSchema = z.object({
737
+ error: z.literal("Rate limit exceeded"),
738
+ message: z.string().openapi({ example: "Too many requests" }),
739
+ retryAfter: z.number().openapi({ example: 60 })
740
+ }).openapi("ThrottleErrorResponse");
741
+ function createHealthResponseSchema(serviceName, options) {
742
+ const baseSchema = z.object({
743
+ status: z.enum(HEALTH_STATUS_VALUES).openapi({ example: "ok" }),
744
+ timestamp: z.string().openapi({ example: "2024-01-15T10:30:00.000Z" }),
745
+ service: z.literal(serviceName)
746
+ });
747
+ if (options?.includeChecks) {
748
+ return baseSchema.extend({
749
+ checks: z.object({
750
+ kv: z.boolean().openapi({ example: true })
751
+ })
752
+ }).openapi(`${serviceName}HealthResponse`);
753
+ }
754
+ return baseSchema.openapi(`${serviceName}HealthResponse`);
755
+ }
756
+ function createWrappedHealthResponseSchema(serviceName) {
757
+ return z.object({
758
+ success: z.literal(true),
759
+ data: z.object({
760
+ status: z.literal("healthy"),
761
+ service: z.literal(serviceName),
762
+ timestamp: z.string().openapi({ example: "2024-01-15T10:30:00.000Z" })
763
+ })
764
+ }).openapi(`${serviceName}HealthResponse`);
765
+ }
766
+ function createSuccessResponseSchema(dataSchema) {
767
+ return z.object({
768
+ success: z.literal(true),
769
+ data: dataSchema
770
+ });
771
+ }
772
+ function createDeleteResponseSchema(idField = "id", example = "01HXYZ123ABC") {
773
+ return z.object({
774
+ deleted: z.literal(true),
775
+ [idField]: z.string().openapi({ example })
776
+ }).openapi("DeleteResponse");
777
+ }
778
+ var ValidationErrorResponseSchema = z.object({
779
+ error: z.literal("Validation Error"),
780
+ message: z.string().openapi({ example: "id: Required, title: String must contain at least 1 character(s)" })
781
+ }).openapi("ValidationErrorResponse");
782
+ var TaskApiHealthResponseSchema = createHealthResponseSchema("task-api-adapter", {
783
+ includeChecks: true
784
+ });
785
+ var PromptApiHealthResponseSchema = createHealthResponseSchema("prompt-api");
786
+
787
+ // openapi-config.ts
788
+ function createValidationHook(format = "simple") {
789
+ return (result, c) => {
790
+ if (!result.success) {
791
+ const issues = result.error.issues;
792
+ const message = issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
793
+ if (format === "wrapped") {
794
+ return c.json(
795
+ {
796
+ success: false,
797
+ error: "Validation Error",
798
+ message
799
+ },
800
+ 400
801
+ );
802
+ }
803
+ return c.json(
804
+ {
805
+ error: "Validation Error",
806
+ message
807
+ },
808
+ 400
809
+ );
810
+ }
811
+ };
812
+ }
813
+ var simpleValidationHook = createValidationHook("simple");
814
+ var wrappedValidationHook = createValidationHook("wrapped");
815
+ function createErrorHandlers(format = "simple") {
816
+ const notFoundHandler = (c) => {
817
+ if (format === "wrapped") {
818
+ return c.json(
819
+ {
820
+ success: false,
821
+ error: "Not found",
822
+ message: "The requested endpoint does not exist"
823
+ },
824
+ 404
825
+ );
826
+ }
827
+ return c.json(
828
+ {
829
+ error: "Not found",
830
+ message: "The requested endpoint does not exist"
831
+ },
832
+ 404
833
+ );
834
+ };
835
+ const errorHandler = (err, c) => {
836
+ console.error("Unhandled error:", err);
837
+ if (format === "wrapped") {
838
+ return c.json(
839
+ {
840
+ success: false,
841
+ error: "Internal server error",
842
+ message: "An unexpected error occurred"
843
+ },
844
+ 500
845
+ );
846
+ }
847
+ return c.json(
848
+ {
849
+ error: "Internal server error",
850
+ message: "An unexpected error occurred"
851
+ },
852
+ 500
853
+ );
854
+ };
855
+ return { notFoundHandler, errorHandler };
856
+ }
857
+ function createOpenAPIDocConfig(options) {
858
+ const { title, version, description, production, localPort = 8787, tags = [] } = options;
859
+ return {
860
+ openapi: "3.0.0",
861
+ info: {
862
+ title,
863
+ version,
864
+ description: description.trim()
865
+ },
866
+ servers: [
867
+ {
868
+ url: production,
869
+ description: "Production"
870
+ },
871
+ {
872
+ url: `http://localhost:${localPort}`,
873
+ description: "Local development"
874
+ }
875
+ ],
876
+ tags
877
+ };
878
+ }
879
+ var CommonTags = {
880
+ Health: { name: "Health", description: "Health check endpoints" },
881
+ Admin: { name: "Admin", description: "Admin operations (requires authentication)" }
882
+ };
883
+ function okWrapped(c, data) {
884
+ return c.json({ success: true, data }, 200);
885
+ }
886
+ function createdWrapped(c, data) {
887
+ return c.json({ success: true, data }, 201);
888
+ }
889
+ function badRequestWrapped(c, message) {
890
+ return c.json({ success: false, error: "Bad Request", message }, 400);
891
+ }
892
+ function notFoundWrapped(c, resource = "Resource") {
893
+ return c.json(
894
+ {
895
+ success: false,
896
+ error: "Not Found",
897
+ message: `${resource} not found`
898
+ },
899
+ 404
900
+ );
901
+ }
902
+ function serverErrorWrapped(c, message = "Internal server error") {
903
+ return c.json({ success: false, error: "Internal Server Error", message }, 500);
904
+ }
905
+ export {
906
+ CORSPresets,
907
+ CommonRules,
908
+ CommonTags,
909
+ DEFAULT_CONTENT_LEVEL,
910
+ DEFAULT_HADOKU_ORIGINS,
911
+ DetailedErrorResponseSchema,
912
+ ErrorResponseSchema,
913
+ MASKING,
914
+ MAX_CONTENT_LEVEL_BY_TIER,
915
+ PromptApiHealthResponseSchema,
916
+ SENSITIVE_FIELDS,
917
+ SimpleErrorResponseSchema,
918
+ TIER_RANK,
919
+ TaskApiHealthResponseSchema,
920
+ ThrottleErrorResponseSchema,
921
+ ValidationErrorResponseSchema,
922
+ badRequest,
923
+ badRequestWrapped,
924
+ checkRateLimit,
925
+ createCorsMiddleware,
926
+ createDeleteResponseSchema,
927
+ createEdgeAuth,
928
+ createErrorHandlers,
929
+ createHadokuCors,
930
+ createHealthResponseSchema,
931
+ createLogger,
932
+ createOpenAPIDocConfig,
933
+ createRequestLogger,
934
+ createSuccessResponseSchema,
935
+ createValidationHook,
936
+ createWrappedHealthResponseSchema,
937
+ created,
938
+ createdWrapped,
939
+ extractContext,
940
+ extractField,
941
+ formatError,
942
+ getAuthContext,
943
+ getCorsHeaders,
944
+ getRequestMetadata,
945
+ healthCheck,
946
+ isNonEmptyString,
947
+ isOriginAllowed,
948
+ logError,
949
+ logRequest,
950
+ loggerMiddleware,
951
+ maskEmail,
952
+ maskIpAddress,
953
+ maskKey,
954
+ maskSessionId,
955
+ matchOrigin,
956
+ maxContentLevelForTier,
957
+ notFound,
958
+ notFoundWrapped,
959
+ ok,
960
+ okWrapped,
961
+ parseBody,
962
+ recordRequest,
963
+ redactFields,
964
+ requireFields,
965
+ requireMinTier,
966
+ resetRateLimit,
967
+ serverError,
968
+ serverErrorWrapped,
969
+ simpleValidationHook,
970
+ startTimer,
971
+ tierAtLeast,
972
+ validateFields,
973
+ validationError,
974
+ verifyEdgeAuth,
975
+ wrappedValidationHook
976
+ };