@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,71 @@
1
+ import { Context } from 'hono';
2
+ import { WorkerLogger } from '@wolffm/logger/worker';
3
+ import { LoggerConfig, LogLevel } from './types.js';
4
+
5
+ /**
6
+ * Worker logging surface — thin adapter that maps the legacy
7
+ * `@wolffm/worker-utils/logging` API onto `@wolffm/logger/worker`.
8
+ *
9
+ * The actual logger implementation (level filtering, stdout/stderr routing,
10
+ * service tag, event() convention, optional telemetry sink) lives in
11
+ * @wolffm/logger. This file preserves the existing call-site surface so the
12
+ * 21 worker import sites in this repo don't all need to change in one PR:
13
+ *
14
+ * createLogger({ prefix: '[task-api]' })
15
+ * logRequest('GET', '/path', ctx)
16
+ * logError('POST', '/path', err)
17
+ * createRequestLogger('POST', '/path', baseCtx)
18
+ * loggerMiddleware({ prefix: '[task-api]' })
19
+ *
20
+ * keep working byte-identically. The only behavior change is that lines
21
+ * now carry a [service] tag from the shared logger (the prefix you pass
22
+ * here is the service name).
23
+ *
24
+ * Stateless utilities below (formatError, startTimer, redactFields,
25
+ * SENSITIVE_FIELDS) are not "logger implementation" — they're convenience
26
+ * formatters that any caller can use independent of the logger. Kept here
27
+ * unchanged.
28
+ */
29
+
30
+ /**
31
+ * Create a worker logger. Adapter signature preserved for the existing
32
+ * `createLogger({prefix, minLevel, ...})` consumers; internally just builds
33
+ * a @wolffm/logger/worker instance.
34
+ */
35
+ declare function createLogger(config?: LoggerConfig): WorkerLogger;
36
+ /**
37
+ * Log HTTP request. Maps to the shared logger; preserved verbatim signature.
38
+ */
39
+ declare function logRequest(method: string, path: string, context?: Record<string, any>, level?: LogLevel): void;
40
+ /**
41
+ * Log error for HTTP request. Same signature; flattens Error.stack into the
42
+ * context object so it survives the structured log payload.
43
+ */
44
+ declare function logError(method: string, path: string, error: string | Error, context?: Record<string, any>): void;
45
+ /**
46
+ * Create request-scoped logger that includes method and path in every line.
47
+ * Wraps a child of the shared logger with the request prefix baked in.
48
+ */
49
+ declare function createRequestLogger(method: string, path: string, baseContext?: Record<string, any>): {
50
+ debug: (message: string, context?: Record<string, any>) => void;
51
+ info: (message: string, context?: Record<string, any>) => void;
52
+ warn: (message: string, context?: Record<string, any>) => void;
53
+ error: (message: string, context?: Record<string, any>) => void;
54
+ };
55
+ /**
56
+ * Hono middleware: stash a logger in the context under the given key.
57
+ */
58
+ declare function loggerMiddleware(config?: LoggerConfig, contextKey?: string): (c: Context, next: () => Promise<void>) => Promise<void>;
59
+ /** Format an Error for safe inclusion in a log context. */
60
+ declare function formatError(error: Error | string): Record<string, any>;
61
+ /** Wall-clock timer for ad-hoc duration logging. */
62
+ declare function startTimer(): {
63
+ end: (message: string, context?: Record<string, any>) => number;
64
+ elapsed: () => number;
65
+ };
66
+ /** Redact sensitive fields from an object before logging it. */
67
+ declare function redactFields(obj: Record<string, any>, fields: string[]): Record<string, any>;
68
+ /** Common field names to pass to `redactFields`. */
69
+ declare const SENSITIVE_FIELDS: string[];
70
+
71
+ export { SENSITIVE_FIELDS, createLogger, createRequestLogger, formatError, logError, logRequest, loggerMiddleware, redactFields, startTimer };
@@ -0,0 +1,94 @@
1
+ // cloudflareWorkerLogger.ts
2
+ import { createWorkerLogger } from "@wolffm/logger/worker";
3
+ function deriveService(prefix) {
4
+ if (!prefix) return "worker";
5
+ const trimmed = prefix.trim();
6
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
7
+ return trimmed.slice(1, -1);
8
+ }
9
+ return trimmed;
10
+ }
11
+ function createLogger(config = {}) {
12
+ return createWorkerLogger({
13
+ service: deriveService(config.prefix),
14
+ minLevel: config.minLevel ?? "info"
15
+ });
16
+ }
17
+ var defaultLogger = createWorkerLogger({ service: "worker" });
18
+ function logRequest(method, path, context, level = "info") {
19
+ defaultLogger[level](`[${method} ${path}]`, context);
20
+ }
21
+ function logError(method, path, error, context) {
22
+ const message = error instanceof Error ? error.message : error;
23
+ const ctx = { ...context ?? {} };
24
+ if (error instanceof Error && error.stack) ctx.stack = error.stack;
25
+ defaultLogger.error(`[${method} ${path}] ${message}`, ctx);
26
+ }
27
+ function createRequestLogger(method, path, baseContext = {}) {
28
+ const scoped = defaultLogger.child(`${method} ${path}`);
29
+ const merge = (extra) => ({ ...baseContext, ...extra ?? {} });
30
+ return {
31
+ debug: (message, context) => scoped.debug(message, merge(context)),
32
+ info: (message, context) => scoped.info(message, merge(context)),
33
+ warn: (message, context) => scoped.warn(message, merge(context)),
34
+ error: (message, context) => scoped.error(message, merge(context))
35
+ };
36
+ }
37
+ function loggerMiddleware(config = {}, contextKey = "logger") {
38
+ return async (c, next) => {
39
+ const logger = createLogger(config);
40
+ c.set(contextKey, logger);
41
+ await next();
42
+ };
43
+ }
44
+ function formatError(error) {
45
+ if (error instanceof Error) {
46
+ return { name: error.name, message: error.message, stack: error.stack };
47
+ }
48
+ return { error: String(error) };
49
+ }
50
+ function startTimer() {
51
+ const start = Date.now();
52
+ return {
53
+ end: (message, context) => {
54
+ const duration = Date.now() - start;
55
+ defaultLogger.info(`${message} (duration: ${duration}ms)`, context);
56
+ return duration;
57
+ },
58
+ elapsed: () => Date.now() - start
59
+ };
60
+ }
61
+ function redactFields(obj, fields) {
62
+ const redacted = { ...obj };
63
+ for (const field of fields) {
64
+ if (field in redacted) redacted[field] = "[REDACTED]";
65
+ }
66
+ return redacted;
67
+ }
68
+ var SENSITIVE_FIELDS = [
69
+ "password",
70
+ "apiKey",
71
+ "api_key",
72
+ "token",
73
+ "accessToken",
74
+ "access_token",
75
+ "refreshToken",
76
+ "refresh_token",
77
+ "secret",
78
+ "privateKey",
79
+ "private_key",
80
+ "authorization",
81
+ "cookie",
82
+ "session"
83
+ ];
84
+ export {
85
+ SENSITIVE_FIELDS,
86
+ createLogger,
87
+ createRequestLogger,
88
+ formatError,
89
+ logError,
90
+ logRequest,
91
+ loggerMiddleware,
92
+ redactFields,
93
+ startTimer
94
+ };
@@ -0,0 +1,52 @@
1
+ export { Tier } from './auth.js';
2
+ import 'hono';
3
+ import './types.js';
4
+
5
+ /**
6
+ * Global content-visibility level — the shared policy table.
7
+ *
8
+ * The LEVEL itself is a property of the person, stored once as prefs-api's D1
9
+ * `users.content_visibility_level` column (global, not app-namespaced, because
10
+ * it follows the user across every content app). This module holds only the
11
+ * policy that derives from tier: the floor, and each tier's ceiling.
12
+ *
13
+ * WHY IT LIVES HERE AND NOT IN EITHER WORKER
14
+ * ------------------------------------------
15
+ * Three places need it and they must agree:
16
+ * - prefs-api — enforces the ceiling on `PUT /api/v1/content-level` (400s
17
+ * anything above it).
18
+ * - edge-router — resolves the level for `X-Hadoku-Content-Level`, and now
19
+ * reports both level and ceiling from `/session/whoami`.
20
+ * - the client — renders one pill segment per ceiling step.
21
+ *
22
+ * A second copy of the ceiling is a live bug, not just duplication: whoami
23
+ * would advertise a ceiling the PUT refuses, so the UI renders a segment that
24
+ * 400s when clicked. `DEFAULT_CONTENT_LEVEL` was already duplicated between
25
+ * the two workers ("matches the prefs-api users column default") — this folds
26
+ * that copy in too.
27
+ */
28
+
29
+ /** Floor, and the fail-safe answer everywhere: the public-safe level.
30
+ * Matches the DEFAULT on prefs-api's `users.content_visibility_level`. */
31
+ declare const DEFAULT_CONTENT_LEVEL = 1;
32
+ /**
33
+ * Ceiling per tier. A tier ABSENT here gets `DEFAULT_CONTENT_LEVEL` — which is
34
+ * the fail-closed reading, and the correct one for `public`: an anonymous
35
+ * caller has no control to render and no level to raise.
36
+ *
37
+ * `wife` is deliberately 3, matching service/friend rather than admin: it is
38
+ * an AUTH elevation, and the level-4 ceiling stays an operator-only decision.
39
+ */
40
+ declare const MAX_CONTENT_LEVEL_BY_TIER: Record<'admin' | 'wife' | 'service' | 'friend' | 'member', number>;
41
+ /**
42
+ * Ceiling for a tier NAME, fail-closed on anything unrecognised.
43
+ *
44
+ * Takes the raw string rather than a narrowed union on purpose: callers get
45
+ * the tier from the edge as a header or a registry row, and an old bundle that
46
+ * predates a newly-added tier must clamp to the floor rather than widen — the
47
+ * same fail-closed property TIER_RANK has. Reading TIER_RANK here keeps the
48
+ * two tables' notion of "is this even a tier" from drifting apart.
49
+ */
50
+ declare function maxContentLevelForTier(tier: string | undefined | null): number;
51
+
52
+ export { DEFAULT_CONTENT_LEVEL, MAX_CONTENT_LEVEL_BY_TIER, maxContentLevelForTier };
@@ -0,0 +1,28 @@
1
+ // auth.ts
2
+ var TIER_RANK = {
3
+ public: 0,
4
+ member: 1,
5
+ friend: 2,
6
+ service: 3,
7
+ wife: 4,
8
+ admin: 5
9
+ };
10
+
11
+ // content-level.ts
12
+ var DEFAULT_CONTENT_LEVEL = 1;
13
+ var MAX_CONTENT_LEVEL_BY_TIER = {
14
+ admin: 4,
15
+ wife: 3,
16
+ service: 3,
17
+ friend: 3,
18
+ member: 3
19
+ };
20
+ function maxContentLevelForTier(tier) {
21
+ if (!tier || TIER_RANK[tier] === void 0) return DEFAULT_CONTENT_LEVEL;
22
+ return MAX_CONTENT_LEVEL_BY_TIER[tier] ?? DEFAULT_CONTENT_LEVEL;
23
+ }
24
+ export {
25
+ DEFAULT_CONTENT_LEVEL,
26
+ MAX_CONTENT_LEVEL_BY_TIER,
27
+ maxContentLevelForTier
28
+ };
@@ -0,0 +1,110 @@
1
+ import { Context } from 'hono';
2
+ import { ContextExtractionConfig } from './types.js';
3
+
4
+ /**
5
+ * Context extraction utilities for Cloudflare Workers
6
+ *
7
+ * Generic helpers to extract common request context (userId, sessionId, etc.)
8
+ * from headers, query parameters, or request body.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // Extract multiple fields at once
13
+ * const context = extractContext(c, {
14
+ * fields: {
15
+ * userId: ['header:X-User-Id', 'query:userId'],
16
+ * sessionId: ['header:X-Session-Id'],
17
+ * boardId: ['query:boardId', 'body:boardId']
18
+ * },
19
+ * defaults: { boardId: 'main', userId: 'anonymous' }
20
+ * });
21
+ * // Result: { userId: '123', sessionId: 'abc', boardId: 'main' }
22
+ *
23
+ * // Extract single field with fallbacks
24
+ * const userId = extractField(c, ['header:X-User-Id', 'query:userId'], 'anonymous');
25
+ * ```
26
+ */
27
+
28
+ /**
29
+ * Extract a single field from multiple sources (in priority order)
30
+ *
31
+ * @param c - Hono context
32
+ * @param sources - Array of sources to check (e.g., ['header:X-User-Id', 'query:userId'])
33
+ * @param defaultValue - Default value if not found in any source
34
+ * @param transform - Optional transform function
35
+ * @returns Extracted value or default
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const userId = extractField(c, ['header:X-User-Id', 'query:userId'], 'public');
40
+ * const boardId = extractField(c, ['query:boardId', 'body:boardId'], 'main');
41
+ * const email = extractField(
42
+ * c,
43
+ * ['header:X-User-Email', 'body:email'],
44
+ * undefined,
45
+ * (val) => val?.toLowerCase()
46
+ * );
47
+ * ```
48
+ */
49
+ declare function extractField<T = string>(c: Context, sources: string[], defaultValue?: T, transform?: (value: string) => T): T | undefined;
50
+ /**
51
+ * Extract multiple fields from request based on configuration
52
+ *
53
+ * @param c - Hono context
54
+ * @param config - Extraction configuration
55
+ * @returns Object with extracted fields
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * const context = extractContext(c, {
60
+ * fields: {
61
+ * userId: ['header:X-User-Id', 'query:userId'],
62
+ * sessionId: ['header:X-Session-Id'],
63
+ * boardId: ['query:boardId', 'body:boardId'],
64
+ * limit: ['query:limit']
65
+ * },
66
+ * defaults: {
67
+ * boardId: 'main',
68
+ * userId: 'public',
69
+ * limit: 100
70
+ * },
71
+ * transforms: {
72
+ * userId: (val) => val?.toLowerCase(),
73
+ * limit: (val) => parseInt(val, 10)
74
+ * }
75
+ * });
76
+ * ```
77
+ */
78
+ declare function extractContext<T extends Record<string, any>>(c: Context, config: ContextExtractionConfig): T;
79
+ /**
80
+ * Parse a request body as JSON, returning a fallback on parse failure.
81
+ *
82
+ * Re-added to worker-utils 1.2.1 after consumers (notably @wolffm/task) kept
83
+ * importing it past the 15c3376 cleanup that removed it. Keeping the original
84
+ * 1.0.0 signature so consumer call sites need no changes.
85
+ */
86
+ declare function parseBody<T = any>(c: Context, fallback?: T): Promise<T>;
87
+ /**
88
+ * Helper to get request metadata (IP, user-agent, etc.)
89
+ *
90
+ * @param c - Hono context
91
+ * @returns Request metadata object
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const metadata = getRequestMetadata(c);
96
+ * // { ip: '1.2.3.4', userAgent: 'Mozilla/5.0...', country: 'US', ... }
97
+ * ```
98
+ */
99
+ declare function getRequestMetadata(c: Context): {
100
+ ip: string | undefined;
101
+ userAgent: string | undefined;
102
+ country: string | undefined;
103
+ city: string | undefined;
104
+ region: string | undefined;
105
+ timezone: string | undefined;
106
+ colo: string | undefined;
107
+ requestId: string;
108
+ };
109
+
110
+ export { extractContext, extractField, getRequestMetadata, parseBody };
@@ -0,0 +1,75 @@
1
+ // context.ts
2
+ function extractField(c, sources, defaultValue, transform) {
3
+ for (const source of sources) {
4
+ const [type, key] = source.split(":");
5
+ let value;
6
+ switch (type.toLowerCase()) {
7
+ case "header":
8
+ value = c.req.header(key);
9
+ break;
10
+ case "query":
11
+ value = c.req.query(key);
12
+ break;
13
+ case "body":
14
+ value = void 0;
15
+ break;
16
+ case "param":
17
+ value = c.req.param(key);
18
+ break;
19
+ case "cookie": {
20
+ const cookieHeader = c.req.header("Cookie");
21
+ if (cookieHeader) {
22
+ const cookies = Object.fromEntries(
23
+ cookieHeader.split(";").map((cookie) => {
24
+ const [k, v] = cookie.trim().split("=");
25
+ return [k, v];
26
+ })
27
+ );
28
+ value = cookies[key];
29
+ }
30
+ break;
31
+ }
32
+ default:
33
+ continue;
34
+ }
35
+ if (value !== void 0 && value !== null && value !== "") {
36
+ return transform ? transform(value) : value;
37
+ }
38
+ }
39
+ return defaultValue;
40
+ }
41
+ function extractContext(c, config) {
42
+ const result = {};
43
+ for (const [fieldName, sources] of Object.entries(config.fields)) {
44
+ const defaultValue = config.defaults?.[fieldName];
45
+ const transform = config.transforms?.[fieldName];
46
+ result[fieldName] = extractField(c, sources, defaultValue, transform);
47
+ }
48
+ return result;
49
+ }
50
+ async function parseBody(c, fallback = {}) {
51
+ try {
52
+ return await c.req.json();
53
+ } catch {
54
+ return fallback;
55
+ }
56
+ }
57
+ function getRequestMetadata(c) {
58
+ const cf = c.req.raw.cf;
59
+ return {
60
+ ip: c.req.header("CF-Connecting-IP") || c.req.header("X-Forwarded-For"),
61
+ userAgent: c.req.header("User-Agent"),
62
+ country: cf?.country,
63
+ city: cf?.city,
64
+ region: cf?.region,
65
+ timezone: cf?.timezone,
66
+ colo: cf?.colo,
67
+ requestId: c.req.header("CF-Ray") || crypto.randomUUID()
68
+ };
69
+ }
70
+ export {
71
+ extractContext,
72
+ extractField,
73
+ getRequestMetadata,
74
+ parseBody
75
+ };
package/dist/cors.d.ts ADDED
@@ -0,0 +1,123 @@
1
+ import * as hono from 'hono';
2
+ import { CORSConfig } from './types.js';
3
+
4
+ /**
5
+ * Default CORS origins for Hadoku services
6
+ */
7
+ declare const DEFAULT_HADOKU_ORIGINS: string[];
8
+ /**
9
+ * Check if origin matches pattern (supports wildcards)
10
+ *
11
+ * @param origin - Origin to check
12
+ * @param pattern - Pattern to match against (supports * wildcards)
13
+ * @returns True if origin matches pattern
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * matchOrigin('http://localhost:3000', 'http://localhost:*') // true
18
+ * matchOrigin('https://api.hadoku.me', 'https://*.hadoku.me') // true
19
+ * matchOrigin('https://evil.com', 'https://hadoku.me') // false
20
+ * ```
21
+ */
22
+ declare function matchOrigin(origin: string, pattern: string): boolean;
23
+ /**
24
+ * Check if origin is allowed based on config
25
+ *
26
+ * @param origin - Origin to check
27
+ * @param allowedOrigins - Array of allowed origin patterns
28
+ * @returns True if origin is allowed
29
+ */
30
+ declare function isOriginAllowed(origin: string, allowedOrigins: string[]): boolean;
31
+ /**
32
+ * Create CORS middleware with configuration
33
+ *
34
+ * @param config - CORS configuration
35
+ * @returns Hono CORS middleware
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * const corsMiddleware = createCorsMiddleware({
40
+ * origins: ['https://hadoku.me', 'http://localhost:*'],
41
+ * methods: ['GET', 'POST', 'PUT', 'DELETE'],
42
+ * credentials: true
43
+ * });
44
+ *
45
+ * app.use('*', corsMiddleware);
46
+ * ```
47
+ */
48
+ declare function createCorsMiddleware(config: CORSConfig): hono.MiddlewareHandler;
49
+ /**
50
+ * Create CORS middleware with Hadoku defaults
51
+ *
52
+ * @param additionalOrigins - Additional origins to allow
53
+ * @returns Hono CORS middleware
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * // Use Hadoku defaults
58
+ * app.use('*', createHadokuCors());
59
+ *
60
+ * // Add custom origins
61
+ * app.use('*', createHadokuCors(['https://custom-domain.com']));
62
+ * ```
63
+ */
64
+ declare function createHadokuCors(additionalOrigins?: string[]): hono.MiddlewareHandler;
65
+ /**
66
+ * Preset CORS configurations for common scenarios
67
+ */
68
+ declare const CORSPresets: {
69
+ /**
70
+ * Development - Allow all localhost origins
71
+ */
72
+ development: {
73
+ origins: string[];
74
+ credentials: boolean;
75
+ };
76
+ /**
77
+ * Production - Strict origin checking
78
+ */
79
+ production: (domains: string[]) => {
80
+ origins: string[];
81
+ credentials: boolean;
82
+ maxAge: number;
83
+ };
84
+ /**
85
+ * Public API - Allow all origins (no credentials)
86
+ */
87
+ publicApi: {
88
+ origins: string[];
89
+ credentials: boolean;
90
+ };
91
+ /**
92
+ * Hadoku standard - Production + development origins
93
+ */
94
+ hadoku: {
95
+ origins: string[];
96
+ credentials: boolean;
97
+ maxAge: number;
98
+ };
99
+ };
100
+ /**
101
+ * Helper to add custom CORS headers manually
102
+ * (use when you need more control than middleware provides)
103
+ *
104
+ * @param origin - Request origin
105
+ * @param allowedOrigins - Allowed origin patterns
106
+ * @returns Headers object
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * app.get('/api/data', (c) => {
111
+ * const origin = c.req.header('Origin');
112
+ * const corsHeaders = getCorsHeaders(origin, ['https://hadoku.me']);
113
+ *
114
+ * return c.json({ data: '...' }, { headers: corsHeaders });
115
+ * });
116
+ * ```
117
+ */
118
+ declare function getCorsHeaders(origin: string | undefined, allowedOrigins: string[], options?: {
119
+ credentials?: boolean;
120
+ exposedHeaders?: string[];
121
+ }): Record<string, string>;
122
+
123
+ export { CORSPresets, DEFAULT_HADOKU_ORIGINS, createCorsMiddleware, createHadokuCors, getCorsHeaders, isOriginAllowed, matchOrigin };
package/dist/cors.js ADDED
@@ -0,0 +1,113 @@
1
+ // cors.ts
2
+ import { cors } from "hono/cors";
3
+
4
+ // edge-routes.ts
5
+ var EDGE_BASE = "https://hadoku.me";
6
+ var MGMT_BASE = `${EDGE_BASE}/mgmt`;
7
+ var MONITORING_BASE = `${EDGE_BASE}/health`;
8
+ var SCRAPER_BASE = `${EDGE_BASE}/scraper`;
9
+ var CONTACT_BASE = `${EDGE_BASE}/contact`;
10
+ var RESUME_BASE = `${EDGE_BASE}/resume`;
11
+ var DISPATCH_BASE = `${EDGE_BASE}/dispatch`;
12
+ var PREFS_BASE = `${EDGE_BASE}/prefs`;
13
+ var JOBPLATFORM_BASE = `${EDGE_BASE}/jobplatform`;
14
+
15
+ // cors.ts
16
+ var DEFAULT_HADOKU_ORIGINS = [
17
+ EDGE_BASE,
18
+ "https://task-api.hadoku.me",
19
+ "http://localhost:*"
20
+ ];
21
+ function matchOrigin(origin, pattern) {
22
+ if (origin === pattern) return true;
23
+ if (pattern.includes("*")) {
24
+ const regexPattern = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
25
+ const regex = new RegExp(`^${regexPattern}$`);
26
+ return regex.test(origin);
27
+ }
28
+ return false;
29
+ }
30
+ function isOriginAllowed(origin, allowedOrigins) {
31
+ return allowedOrigins.some((pattern) => matchOrigin(origin, pattern));
32
+ }
33
+ function createCorsMiddleware(config) {
34
+ const {
35
+ origins,
36
+ methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
37
+ allowedHeaders,
38
+ exposedHeaders,
39
+ credentials = true,
40
+ maxAge = 86400
41
+ } = config;
42
+ return cors({
43
+ origin: (origin) => {
44
+ if (!origin) return origins[0];
45
+ return isOriginAllowed(origin, origins) ? origin : origins[0];
46
+ },
47
+ allowMethods: methods,
48
+ allowHeaders: allowedHeaders,
49
+ exposeHeaders: exposedHeaders,
50
+ credentials,
51
+ maxAge
52
+ });
53
+ }
54
+ function createHadokuCors(additionalOrigins = []) {
55
+ return createCorsMiddleware({
56
+ origins: [...DEFAULT_HADOKU_ORIGINS, ...additionalOrigins],
57
+ credentials: true
58
+ });
59
+ }
60
+ var CORSPresets = {
61
+ /**
62
+ * Development - Allow all localhost origins
63
+ */
64
+ development: {
65
+ origins: ["http://localhost:*", "http://127.0.0.1:*"],
66
+ credentials: true
67
+ },
68
+ /**
69
+ * Production - Strict origin checking
70
+ */
71
+ production: (domains) => ({
72
+ origins: domains,
73
+ credentials: true,
74
+ maxAge: 86400
75
+ }),
76
+ /**
77
+ * Public API - Allow all origins (no credentials)
78
+ */
79
+ publicApi: {
80
+ origins: ["*"],
81
+ credentials: false
82
+ },
83
+ /**
84
+ * Hadoku standard - Production + development origins
85
+ */
86
+ hadoku: {
87
+ origins: DEFAULT_HADOKU_ORIGINS,
88
+ credentials: true,
89
+ maxAge: 86400
90
+ }
91
+ };
92
+ function getCorsHeaders(origin, allowedOrigins, options = {}) {
93
+ const headers = {};
94
+ if (origin && isOriginAllowed(origin, allowedOrigins)) {
95
+ headers["Access-Control-Allow-Origin"] = origin;
96
+ if (options.credentials !== false) {
97
+ headers["Access-Control-Allow-Credentials"] = "true";
98
+ }
99
+ if (options.exposedHeaders && options.exposedHeaders.length > 0) {
100
+ headers["Access-Control-Expose-Headers"] = options.exposedHeaders.join(", ");
101
+ }
102
+ }
103
+ return headers;
104
+ }
105
+ export {
106
+ CORSPresets,
107
+ DEFAULT_HADOKU_ORIGINS,
108
+ createCorsMiddleware,
109
+ createHadokuCors,
110
+ getCorsHeaders,
111
+ isOriginAllowed,
112
+ matchOrigin
113
+ };