@hadoku/worker-utils 0.0.0-stage → 1.6.20

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,166 @@
1
+ import { Context } from 'hono';
2
+
3
+ /**
4
+ * Shared types for worker utilities
5
+ */
6
+
7
+ type UserType = string;
8
+ interface AuthContext {
9
+ userType: UserType;
10
+ [key: string]: unknown;
11
+ }
12
+ interface AuthConfig<TEnv = any> {
13
+ /**
14
+ * Sources to check for authentication credentials (in order of priority)
15
+ * @example ['header:X-User-Key', 'query:key', 'cookie:auth_token']
16
+ */
17
+ sources: string[];
18
+ /**
19
+ * Function to resolve userType from the extracted credential
20
+ * @param credential - The extracted auth credential
21
+ * @param env - The worker environment bindings
22
+ * @returns UserType or AuthContext object
23
+ */
24
+ resolver: (credential: string | undefined, env: TEnv) => UserType | AuthContext;
25
+ /**
26
+ * Variable name to store auth context in Hono context
27
+ * @default 'authContext'
28
+ */
29
+ contextKey?: string;
30
+ /**
31
+ * Additional fields to add to auth context
32
+ */
33
+ additionalFields?: (c: Context, env: TEnv) => Record<string, any>;
34
+ }
35
+ interface ContextExtractionConfig {
36
+ /**
37
+ * Mappings of context field name to sources
38
+ * @example { userId: ['header:X-User-Id', 'query:userId'], sessionId: ['header:X-Session-Id'] }
39
+ */
40
+ fields: Record<string, string[]>;
41
+ /**
42
+ * Default values for fields if not found
43
+ * @example { userId: 'anonymous', boardId: 'main' }
44
+ */
45
+ defaults?: Record<string, any>;
46
+ /**
47
+ * Transform functions for extracted values
48
+ * @example { userId: (val) => val?.toLowerCase() }
49
+ */
50
+ transforms?: Record<string, (value: string) => any>;
51
+ }
52
+ interface ValidationRule {
53
+ field: string;
54
+ required?: boolean;
55
+ type?: 'string' | 'number' | 'boolean' | 'object' | 'array';
56
+ minLength?: number;
57
+ maxLength?: number;
58
+ pattern?: RegExp;
59
+ custom?: (value: unknown) => boolean | string;
60
+ }
61
+ interface ValidationResult {
62
+ valid: boolean;
63
+ errors: {
64
+ field: string;
65
+ message: string;
66
+ }[];
67
+ }
68
+ type LogLevel = 'debug' | 'info' | 'warn' | 'error';
69
+ interface LogEntry {
70
+ timestamp: string;
71
+ level: LogLevel;
72
+ message: string;
73
+ context?: Record<string, any>;
74
+ }
75
+ interface LoggerConfig {
76
+ /**
77
+ * Minimum log level to output
78
+ * @default 'info'
79
+ */
80
+ minLevel?: LogLevel;
81
+ /**
82
+ * Include timestamp in logs
83
+ * @default true
84
+ */
85
+ includeTimestamp?: boolean;
86
+ /**
87
+ * Prefix for all log messages
88
+ * @example '[task-api]'
89
+ */
90
+ prefix?: string;
91
+ /**
92
+ * Custom formatter for log output
93
+ */
94
+ formatter?: (entry: LogEntry) => string;
95
+ }
96
+ interface ErrorResponse {
97
+ error: string;
98
+ details?: unknown;
99
+ timestamp?: string;
100
+ }
101
+ interface SuccessResponse<T = any> {
102
+ data: T;
103
+ message?: string;
104
+ timestamp?: string;
105
+ }
106
+ /**
107
+ * The canonical health status — the SINGLE source of truth for the health
108
+ * contract. Both the runtime helper `healthCheck()` (responses.ts) and the
109
+ * OpenAPI schema `createHealthResponseSchema()` (openapi-schemas.ts) reference
110
+ * this type, so the two can no longer drift apart (they used to: the schema
111
+ * declared `status: 'healthy'` + a numeric timestamp that healthCheck never
112
+ * emitted). openapi-schemas.ts guards its `z.enum([...])` against this type
113
+ * with `satisfies`, so a rename/removal here fails that build.
114
+ */
115
+ type HealthStatus = 'ok' | 'degraded' | 'error';
116
+ interface HealthCheckResponse {
117
+ status: HealthStatus;
118
+ service: string;
119
+ timestamp: string;
120
+ version?: string;
121
+ checks?: Record<string, boolean | string>;
122
+ /**
123
+ * What an operator should DO about a non-ok status, ideally the literal
124
+ * command to run. Surfaced verbatim into the Discord alert by
125
+ * monitoring-api's worker-health alerter, so "edge_auth: missing" at 2am
126
+ * does not mean rediscovering the fix.
127
+ */
128
+ remediation?: string;
129
+ /**
130
+ * Slow-moving numbers worth watching but not worth alerting on — storage
131
+ * consumed, row counts, percentage of a quota. Reported, never used to
132
+ * derive `status`.
133
+ */
134
+ gauges?: Record<string, number | null>;
135
+ }
136
+ interface CORSConfig {
137
+ /**
138
+ * Allowed origins (supports wildcards like 'http://localhost:*')
139
+ */
140
+ origins: string[];
141
+ /**
142
+ * Allowed HTTP methods
143
+ * @default ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
144
+ */
145
+ methods?: string[];
146
+ /**
147
+ * Allowed headers
148
+ */
149
+ allowedHeaders?: string[];
150
+ /**
151
+ * Exposed headers
152
+ */
153
+ exposedHeaders?: string[];
154
+ /**
155
+ * Allow credentials
156
+ * @default true
157
+ */
158
+ credentials?: boolean;
159
+ /**
160
+ * Max age for preflight cache
161
+ * @default 86400 (24 hours)
162
+ */
163
+ maxAge?: number;
164
+ }
165
+
166
+ export type { AuthConfig, AuthContext, CORSConfig, ContextExtractionConfig, ErrorResponse, HealthCheckResponse, HealthStatus, LogEntry, LogLevel, LoggerConfig, SuccessResponse, UserType, ValidationResult, ValidationRule };
package/dist/types.js ADDED
File without changes
@@ -0,0 +1,88 @@
1
+ import { ValidationRule, ValidationResult } from './types.js';
2
+ import 'hono';
3
+
4
+ /**
5
+ * Validate multiple fields against rules
6
+ *
7
+ * @param data - Data object to validate
8
+ * @param rules - Array of validation rules
9
+ * @returns Validation result with errors
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const result = validateFields(requestBody, [
14
+ * { field: 'id', required: true },
15
+ * { field: 'title', required: true, minLength: 1 },
16
+ * { field: 'tags', type: 'array' }
17
+ * ]);
18
+ *
19
+ * if (!result.valid) {
20
+ * console.log(result.errors);
21
+ * // [{ field: 'title', message: 'title must be at least 1 characters' }]
22
+ * }
23
+ * ```
24
+ */
25
+ declare function validateFields(data: Record<string, any>, rules: ValidationRule[]): ValidationResult;
26
+ /**
27
+ * Returns the name of the first missing or empty-string field, or null.
28
+ *
29
+ * Re-added to worker-utils 1.2.1 (matches the original 1.0.0 signature).
30
+ * Removed in 15c3376 cleanup but @wolffm/task still imports it.
31
+ */
32
+ declare function requireFields(data: Record<string, any>, fields: string[]): string | null;
33
+ /**
34
+ * Re-added in 1.2.1. Same surface @wolffm/task imports for ID validation.
35
+ */
36
+ declare function isNonEmptyString(value: unknown): boolean;
37
+ /**
38
+ * Common validation rules
39
+ */
40
+ declare const CommonRules: {
41
+ /**
42
+ * ID field (required, non-empty string)
43
+ */
44
+ id: () => ValidationRule;
45
+ /**
46
+ * Title field (required, 1-500 characters)
47
+ */
48
+ title: (options?: {
49
+ minLength?: number;
50
+ maxLength?: number;
51
+ }) => ValidationRule;
52
+ /**
53
+ * Email field
54
+ */
55
+ email: (required?: boolean) => ValidationRule;
56
+ /**
57
+ * URL field
58
+ */
59
+ url: (required?: boolean) => ValidationRule;
60
+ /**
61
+ * Enum field (value must be in allowed list)
62
+ */
63
+ enum: (field: string, allowedValues: unknown[], required?: boolean) => ValidationRule;
64
+ /**
65
+ * Array field with optional element validation
66
+ */
67
+ array: (field: string, options?: {
68
+ required?: boolean;
69
+ minLength?: number;
70
+ maxLength?: number;
71
+ elementType?: "string" | "number" | "boolean" | "object";
72
+ }) => ValidationRule;
73
+ /**
74
+ * Number field with optional range validation
75
+ */
76
+ number: (field: string, options?: {
77
+ required?: boolean;
78
+ min?: number;
79
+ max?: number;
80
+ integer?: boolean;
81
+ }) => ValidationRule;
82
+ /**
83
+ * Boolean field
84
+ */
85
+ boolean: (field: string, required?: boolean) => ValidationRule;
86
+ };
87
+
88
+ export { CommonRules, isNonEmptyString, requireFields, validateFields };
@@ -0,0 +1,172 @@
1
+ // validation.ts
2
+ function validateField(value, rule) {
3
+ if (rule.required && (value === void 0 || value === null || value === "")) {
4
+ return `${rule.field} is required`;
5
+ }
6
+ if (value === void 0 || value === null) {
7
+ return null;
8
+ }
9
+ if (rule.type) {
10
+ const actualType = Array.isArray(value) ? "array" : typeof value;
11
+ if (actualType !== rule.type) {
12
+ return `${rule.field} must be of type ${rule.type}`;
13
+ }
14
+ }
15
+ if (typeof value === "string") {
16
+ if (rule.minLength !== void 0 && value.length < rule.minLength) {
17
+ return `${rule.field} must be at least ${rule.minLength} characters`;
18
+ }
19
+ if (rule.maxLength !== void 0 && value.length > rule.maxLength) {
20
+ return `${rule.field} must be at most ${rule.maxLength} characters`;
21
+ }
22
+ if (rule.pattern && !rule.pattern.test(value)) {
23
+ return `${rule.field} format is invalid`;
24
+ }
25
+ }
26
+ if (rule.custom) {
27
+ const result = rule.custom(value);
28
+ if (result !== true && typeof result === "string") {
29
+ return result;
30
+ }
31
+ if (result === false) {
32
+ return `${rule.field} is invalid`;
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ function validateFields(data, rules) {
38
+ const errors = [];
39
+ for (const rule of rules) {
40
+ const value = data[rule.field];
41
+ const error = validateField(value, rule);
42
+ if (error) {
43
+ errors.push({ field: rule.field, message: error });
44
+ }
45
+ }
46
+ return {
47
+ valid: errors.length === 0,
48
+ errors
49
+ };
50
+ }
51
+ function requireFields(data, fields) {
52
+ for (const field of fields) {
53
+ const value = data[field];
54
+ if (value === void 0 || value === null || typeof value === "string" && value.trim() === "") {
55
+ return `Missing required field: ${field}`;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ function isNonEmptyString(value) {
61
+ return typeof value === "string" && value.trim() !== "";
62
+ }
63
+ var CommonRules = {
64
+ /**
65
+ * ID field (required, non-empty string)
66
+ */
67
+ id: () => ({
68
+ field: "id",
69
+ required: true,
70
+ type: "string",
71
+ custom: (val) => typeof val === "string" && val.trim() !== "" || "ID cannot be empty"
72
+ }),
73
+ /**
74
+ * Title field (required, 1-500 characters)
75
+ */
76
+ title: (options = {}) => ({
77
+ field: "title",
78
+ required: true,
79
+ type: "string",
80
+ minLength: options.minLength ?? 1,
81
+ maxLength: options.maxLength ?? 500,
82
+ custom: (val) => typeof val === "string" && val.trim() !== "" || "Title cannot be empty"
83
+ }),
84
+ /**
85
+ * Email field
86
+ */
87
+ email: (required = false) => ({
88
+ field: "email",
89
+ required,
90
+ type: "string",
91
+ pattern: /^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/
92
+ }),
93
+ /**
94
+ * URL field
95
+ */
96
+ url: (required = false) => ({
97
+ field: "url",
98
+ required,
99
+ type: "string",
100
+ pattern: /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)$/
101
+ }),
102
+ /**
103
+ * Enum field (value must be in allowed list)
104
+ */
105
+ enum: (field, allowedValues, required = false) => ({
106
+ field,
107
+ required,
108
+ custom: (val) => allowedValues.includes(val) || `${field} must be one of: ${allowedValues.join(", ")}`
109
+ }),
110
+ /**
111
+ * Array field with optional element validation
112
+ */
113
+ array: (field, options = {}) => ({
114
+ field,
115
+ required: options.required,
116
+ type: "array",
117
+ custom: (val) => {
118
+ if (!Array.isArray(val)) return false;
119
+ if (options.minLength !== void 0 && val.length < options.minLength) {
120
+ return `${field} must have at least ${options.minLength} items`;
121
+ }
122
+ if (options.maxLength !== void 0 && val.length > options.maxLength) {
123
+ return `${field} must have at most ${options.maxLength} items`;
124
+ }
125
+ if (options.elementType) {
126
+ const invalidItem = val.find((item) => {
127
+ const actualType = Array.isArray(item) ? "array" : typeof item;
128
+ return actualType !== options.elementType;
129
+ });
130
+ if (invalidItem !== void 0) {
131
+ return `All ${field} items must be of type ${options.elementType}`;
132
+ }
133
+ }
134
+ return true;
135
+ }
136
+ }),
137
+ /**
138
+ * Number field with optional range validation
139
+ */
140
+ number: (field, options = {}) => ({
141
+ field,
142
+ required: options.required,
143
+ type: "number",
144
+ custom: (val) => {
145
+ if (typeof val !== "number") return false;
146
+ if (options.integer && !Number.isInteger(val)) {
147
+ return `${field} must be an integer`;
148
+ }
149
+ if (options.min !== void 0 && val < options.min) {
150
+ return `${field} must be at least ${options.min}`;
151
+ }
152
+ if (options.max !== void 0 && val > options.max) {
153
+ return `${field} must be at most ${options.max}`;
154
+ }
155
+ return true;
156
+ }
157
+ }),
158
+ /**
159
+ * Boolean field
160
+ */
161
+ boolean: (field, required = false) => ({
162
+ field,
163
+ required,
164
+ type: "boolean"
165
+ })
166
+ };
167
+ export {
168
+ CommonRules,
169
+ isNonEmptyString,
170
+ requireFields,
171
+ validateFields
172
+ };
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "@hadoku/worker-utils",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.6.20",
4
+ "license": "ISC",
5
+ "description": "Shared utilities for Cloudflare Workers",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/WolffM/hadoku_site.git"
9
+ },
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "exports": {
13
+ ".": "./dist/index.js",
14
+ "./auth": "./dist/auth.js",
15
+ "./authPage": "./dist/authPage.js",
16
+ "./context": "./dist/context.js",
17
+ "./validation": "./dist/validation.js",
18
+ "./cors": "./dist/cors.js",
19
+ "./logging": "./dist/cloudflareWorkerLogger.js",
20
+ "./responses": "./dist/responses.js",
21
+ "./types": "./dist/types.js",
22
+ "./edge-routes": "./dist/edge-routes.js",
23
+ "./content-level": "./dist/content-level.js",
24
+ "./identity": "./dist/identity.js"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "dependencies": {
30
+ "@hono/zod-openapi": "^1.1.5",
31
+ "@wolffm/logger": "npm:@hadoku/logger@^1.4.0"
32
+ },
33
+ "peerDependencies": {
34
+ "hono": "^4.11.4"
35
+ }
36
+ }