@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.
- package/LICENSE +15 -0
- package/README.md +334 -2
- package/dist/auth.d.ts +153 -0
- package/dist/auth.js +39 -0
- package/dist/authPage.d.ts +113 -0
- package/dist/authPage.js +833 -0
- package/dist/cloudflareWorkerLogger.d.ts +71 -0
- package/dist/cloudflareWorkerLogger.js +94 -0
- package/dist/content-level.d.ts +52 -0
- package/dist/content-level.js +28 -0
- package/dist/context.d.ts +110 -0
- package/dist/context.js +75 -0
- package/dist/cors.d.ts +123 -0
- package/dist/cors.js +113 -0
- package/dist/edge-routes.d.ts +47 -0
- package/dist/edge-routes.js +34 -0
- package/dist/identity.d.ts +477 -0
- package/dist/identity.js +239 -0
- package/dist/index.d.ts +108 -0
- package/dist/index.js +976 -0
- package/dist/masking.d.ts +108 -0
- package/dist/masking.js +90 -0
- package/dist/openapi-config.d.ts +252 -0
- package/dist/openapi-config.js +131 -0
- package/dist/openapi-schemas.d.ts +157 -0
- package/dist/openapi-schemas.js +79 -0
- package/dist/rate-limit.d.ts +39 -0
- package/dist/rate-limit.js +59 -0
- package/dist/responses.d.ts +159 -0
- package/dist/responses.js +80 -0
- package/dist/types.d.ts +166 -0
- package/dist/types.js +0 -0
- package/dist/validation.d.ts +88 -0
- package/dist/validation.js +172 -0
- package/package.json +34 -4
|
@@ -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 };
|
package/dist/context.js
ADDED
|
@@ -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
|
+
};
|