@hoox-sh/hoox-shared 1.2.0
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/README.md +44 -0
- package/dist/agent-config-fields.d.ts +32 -0
- package/dist/agent-config-fields.d.ts.map +1 -0
- package/dist/agent-config-fields.js +220 -0
- package/dist/analytics.d.ts +47 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +45 -0
- package/dist/api-client.d.ts +48 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +191 -0
- package/dist/colors.d.ts +76 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +82 -0
- package/dist/config.d.ts +61 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +146 -0
- package/dist/cron-handler.d.ts +38 -0
- package/dist/cron-handler.d.ts.map +1 -0
- package/dist/cron-handler.js +48 -0
- package/dist/d1/index.d.ts +9 -0
- package/dist/d1/index.d.ts.map +1 -0
- package/dist/d1/index.js +360 -0
- package/dist/d1/repository.d.ts +86 -0
- package/dist/d1/repository.d.ts.map +1 -0
- package/dist/d1/repository.js +297 -0
- package/dist/d1/schemas.d.ts +89 -0
- package/dist/d1/schemas.d.ts.map +1 -0
- package/dist/d1/schemas.js +81 -0
- package/dist/dashboard-manifest.d.ts +135 -0
- package/dist/dashboard-manifest.d.ts.map +1 -0
- package/dist/dashboard-manifest.js +401 -0
- package/dist/errors.d.ts +44 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +149 -0
- package/dist/exchange-client.d.ts +98 -0
- package/dist/exchange-client.d.ts.map +1 -0
- package/dist/exchange-client.js +107 -0
- package/dist/exchanges/base-exchange-client.d.ts +81 -0
- package/dist/exchanges/base-exchange-client.d.ts.map +1 -0
- package/dist/exchanges/base-exchange-client.js +123 -0
- package/dist/exchanges/index.d.ts +7 -0
- package/dist/exchanges/index.d.ts.map +1 -0
- package/dist/exchanges/index.js +123 -0
- package/dist/exchanges/types.d.ts +23 -0
- package/dist/exchanges/types.d.ts.map +1 -0
- package/dist/exchanges/types.js +1 -0
- package/dist/format-time.d.ts +28 -0
- package/dist/format-time.d.ts.map +1 -0
- package/dist/format-time.js +57 -0
- package/dist/formatters.d.ts +36 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +155 -0
- package/dist/health.d.ts +19 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +160 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4198 -0
- package/dist/kvKeys.d.ts +76 -0
- package/dist/kvKeys.d.ts.map +1 -0
- package/dist/kvKeys.js +107 -0
- package/dist/kvUtils.d.ts +30 -0
- package/dist/kvUtils.d.ts.map +1 -0
- package/dist/kvUtils.js +170 -0
- package/dist/legal.d.ts +15 -0
- package/dist/legal.d.ts.map +1 -0
- package/dist/legal.js +40 -0
- package/dist/middleware/auth.d.ts +102 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +151 -0
- package/dist/middleware/cors.d.ts +58 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +77 -0
- package/dist/middleware/index.d.ts +14 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +635 -0
- package/dist/middleware/logger.d.ts +17 -0
- package/dist/middleware/logger.d.ts.map +1 -0
- package/dist/middleware/logger.js +202 -0
- package/dist/middleware/rate-limit.d.ts +30 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +181 -0
- package/dist/middleware/security-headers.d.ts +43 -0
- package/dist/middleware/security-headers.d.ts.map +1 -0
- package/dist/middleware/security-headers.js +58 -0
- package/dist/middleware/validate.d.ts +25 -0
- package/dist/middleware/validate.d.ts.map +1 -0
- package/dist/middleware/validate.js +56 -0
- package/dist/operator-transport.d.ts +65 -0
- package/dist/operator-transport.d.ts.map +1 -0
- package/dist/operator-transport.js +87 -0
- package/dist/path-utils.d.ts +190 -0
- package/dist/path-utils.d.ts.map +1 -0
- package/dist/path-utils.js +188 -0
- package/dist/queue-handler.d.ts +47 -0
- package/dist/queue-handler.d.ts.map +1 -0
- package/dist/queue-handler.js +46 -0
- package/dist/router.d.ts +15 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +130 -0
- package/dist/schemas/index.d.ts +8 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +705 -0
- package/dist/schemas/registry.d.ts +12 -0
- package/dist/schemas/registry.d.ts.map +1 -0
- package/dist/schemas/registry.js +433 -0
- package/dist/schemas/types.d.ts +83 -0
- package/dist/schemas/types.d.ts.map +1 -0
- package/dist/schemas/types.js +1 -0
- package/dist/schemas/validators.d.ts +39 -0
- package/dist/schemas/validators.d.ts.map +1 -0
- package/dist/schemas/validators.js +290 -0
- package/dist/service-bindings.d.ts +78 -0
- package/dist/service-bindings.d.ts.map +1 -0
- package/dist/service-bindings.js +248 -0
- package/dist/session.d.ts +39 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +261 -0
- package/dist/sse.d.ts +39 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +154 -0
- package/dist/stores/config-store.d.ts +91 -0
- package/dist/stores/config-store.d.ts.map +1 -0
- package/dist/stores/config-store.js +255 -0
- package/dist/stores/service-store.d.ts +266 -0
- package/dist/stores/service-store.d.ts.map +1 -0
- package/dist/stores/service-store.js +516 -0
- package/dist/stores/ui-store.d.ts +67 -0
- package/dist/stores/ui-store.d.ts.map +1 -0
- package/dist/stores/ui-store.js +62 -0
- package/dist/test-utils.d.ts +136 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +128 -0
- package/dist/types/errors.d.ts +20 -0
- package/dist/types/errors.d.ts.map +1 -0
- package/dist/types/errors.js +1 -0
- package/dist/types/router.d.ts +36 -0
- package/dist/types/router.d.ts.map +1 -0
- package/dist/types/router.js +1 -0
- package/dist/types.d.ts +442 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +83 -0
- package/dist/wizard/engine.d.ts +46 -0
- package/dist/wizard/engine.d.ts.map +1 -0
- package/dist/wizard/engine.js +525 -0
- package/dist/wizard/index.d.ts +11 -0
- package/dist/wizard/index.d.ts.map +1 -0
- package/dist/wizard/index.js +544 -0
- package/dist/wizard/persistence.d.ts +18 -0
- package/dist/wizard/persistence.d.ts.map +1 -0
- package/dist/wizard/persistence.js +31 -0
- package/dist/wizard/presets.d.ts +38 -0
- package/dist/wizard/presets.d.ts.map +1 -0
- package/dist/wizard/presets.js +223 -0
- package/dist/wizard/provisioner.d.ts +15 -0
- package/dist/wizard/provisioner.d.ts.map +1 -0
- package/dist/wizard/provisioner.js +1 -0
- package/dist/wizard/types.d.ts +76 -0
- package/dist/wizard/types.d.ts.map +1 -0
- package/dist/wizard/types.js +1 -0
- package/package.json +117 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
|
|
18
|
+
// src/errors.ts
|
|
19
|
+
function toError(err, fallback = "Unknown error") {
|
|
20
|
+
if (err instanceof Error)
|
|
21
|
+
return err.message;
|
|
22
|
+
if (typeof err === "string")
|
|
23
|
+
return err;
|
|
24
|
+
if (err && typeof err === "object" && "message" in err && typeof err.message === "string") {
|
|
25
|
+
return err.message;
|
|
26
|
+
}
|
|
27
|
+
if (err === null || err === undefined)
|
|
28
|
+
return fallback;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.stringify(err) || fallback;
|
|
31
|
+
} catch {
|
|
32
|
+
return fallback;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
var SENSITIVE_FIELDS = new Set([
|
|
36
|
+
"password",
|
|
37
|
+
"token",
|
|
38
|
+
"secret",
|
|
39
|
+
"api_key",
|
|
40
|
+
"apiKey",
|
|
41
|
+
"authorization"
|
|
42
|
+
]);
|
|
43
|
+
function hasSensitiveFields(obj) {
|
|
44
|
+
for (const key of Object.keys(obj)) {
|
|
45
|
+
if (SENSITIVE_FIELDS.has(key))
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
function hasErrorValues(obj) {
|
|
51
|
+
for (const v of Object.values(obj)) {
|
|
52
|
+
if (v instanceof Error)
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
function sanitizeOutput(data) {
|
|
58
|
+
if (data === null || data === undefined)
|
|
59
|
+
return data;
|
|
60
|
+
const t = typeof data;
|
|
61
|
+
if (t === "string" || t === "number" || t === "boolean")
|
|
62
|
+
return data;
|
|
63
|
+
if (data instanceof Error) {
|
|
64
|
+
return { name: data.name, message: data.message };
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(data)) {
|
|
67
|
+
return data.map(sanitizeOutput);
|
|
68
|
+
}
|
|
69
|
+
if (t === "object") {
|
|
70
|
+
const obj = data;
|
|
71
|
+
const keys = Object.keys(obj);
|
|
72
|
+
if (keys.length < 10) {
|
|
73
|
+
const sanitized2 = {};
|
|
74
|
+
for (const k of keys) {
|
|
75
|
+
if (k === "stack" || k === "cause")
|
|
76
|
+
continue;
|
|
77
|
+
const v = obj[k];
|
|
78
|
+
sanitized2[k] = v instanceof Error ? { name: v.name, message: v.message } : sanitizeOutput(v);
|
|
79
|
+
}
|
|
80
|
+
return sanitized2;
|
|
81
|
+
}
|
|
82
|
+
if (!hasSensitiveFields(obj) && !hasErrorValues(obj)) {
|
|
83
|
+
return obj;
|
|
84
|
+
}
|
|
85
|
+
const sanitized = {};
|
|
86
|
+
for (const k of keys) {
|
|
87
|
+
if (k === "stack" || k === "cause")
|
|
88
|
+
continue;
|
|
89
|
+
const v = obj[k];
|
|
90
|
+
sanitized[k] = v instanceof Error ? { name: v.name, message: v.message } : sanitizeOutput(v);
|
|
91
|
+
}
|
|
92
|
+
return sanitized;
|
|
93
|
+
}
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
96
|
+
function createJsonResponse(data, status = 200) {
|
|
97
|
+
return new Response(JSON.stringify(sanitizeOutput(data)), {
|
|
98
|
+
status,
|
|
99
|
+
headers: { "Content-Type": "application/json" }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function createSuccessResponse(result) {
|
|
103
|
+
return createJsonResponse({ success: true, result }, 200);
|
|
104
|
+
}
|
|
105
|
+
function createErrorResponse(error, status) {
|
|
106
|
+
const message = typeof error === "string" ? error : error.message;
|
|
107
|
+
const statusCode = typeof error === "string" ? status ?? 500 : error.status ?? 500;
|
|
108
|
+
const body = { success: false, error: message };
|
|
109
|
+
if (typeof error !== "string" && error.code)
|
|
110
|
+
body.code = error.code;
|
|
111
|
+
if (typeof error !== "string" && error.details !== undefined)
|
|
112
|
+
body.details = error.details;
|
|
113
|
+
return new Response(JSON.stringify(body), {
|
|
114
|
+
status: statusCode,
|
|
115
|
+
headers: { "Content-Type": "application/json" }
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
var Errors = {
|
|
119
|
+
badRequest: (message) => createErrorResponse({ message, status: 400, code: "BAD_REQUEST" }),
|
|
120
|
+
unauthorized: (message = "Unauthorized") => createErrorResponse({ message, status: 401, code: "UNAUTHORIZED" }),
|
|
121
|
+
forbidden: (message = "Forbidden") => createErrorResponse({ message, status: 403, code: "FORBIDDEN" }),
|
|
122
|
+
notFound: (message = "Not found") => createErrorResponse({ message, status: 404, code: "NOT_FOUND" }),
|
|
123
|
+
methodNotAllowed: (message = "Method not allowed") => createErrorResponse({ message, status: 405, code: "METHOD_NOT_ALLOWED" }),
|
|
124
|
+
rateLimited: (retryAfter) => {
|
|
125
|
+
const res = createErrorResponse({
|
|
126
|
+
message: "Rate limit exceeded",
|
|
127
|
+
status: 429,
|
|
128
|
+
code: "RATE_LIMITED"
|
|
129
|
+
});
|
|
130
|
+
if (retryAfter)
|
|
131
|
+
res.headers.set("Retry-After", String(retryAfter));
|
|
132
|
+
return res;
|
|
133
|
+
},
|
|
134
|
+
internal: (err) => {
|
|
135
|
+
const message = err ? toError(err, "Internal server error") : "Internal server error";
|
|
136
|
+
return createErrorResponse({
|
|
137
|
+
message,
|
|
138
|
+
status: 500,
|
|
139
|
+
code: "INTERNAL_ERROR"
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/middleware/auth.ts
|
|
145
|
+
function timingSafeEqual(a, b) {
|
|
146
|
+
const encoder = new TextEncoder;
|
|
147
|
+
const aBuf = encoder.encode(a);
|
|
148
|
+
const bBuf = encoder.encode(b);
|
|
149
|
+
const len = Math.max(aBuf.length, bBuf.length);
|
|
150
|
+
let result = aBuf.length === bBuf.length ? 0 : 1;
|
|
151
|
+
for (let i = 0;i < len; i++) {
|
|
152
|
+
const av = i < aBuf.length ? aBuf[i] : 0;
|
|
153
|
+
const bv = i < bBuf.length ? bBuf[i] : 0;
|
|
154
|
+
result |= av ^ bv;
|
|
155
|
+
}
|
|
156
|
+
return result === 0;
|
|
157
|
+
}
|
|
158
|
+
function resolveOperatorApiKey(env) {
|
|
159
|
+
const preferred = env.OPERATOR_API_KEY;
|
|
160
|
+
if (typeof preferred === "string" && preferred.length > 0) {
|
|
161
|
+
return preferred;
|
|
162
|
+
}
|
|
163
|
+
const legacy = env.INTERNAL_API_KEY;
|
|
164
|
+
if (typeof legacy === "string" && legacy.length > 0) {
|
|
165
|
+
return legacy;
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
async function requireAuth(request, env) {
|
|
170
|
+
return requireOperatorAuth(request, env);
|
|
171
|
+
}
|
|
172
|
+
async function requireOperatorAuth(request, env) {
|
|
173
|
+
const apiKey = resolveOperatorApiKey(env);
|
|
174
|
+
if (!apiKey) {
|
|
175
|
+
return new Response(JSON.stringify({
|
|
176
|
+
error: "Operator API key not configured",
|
|
177
|
+
hint: "Set OPERATOR_API_KEY (preferred) or INTERNAL_API_KEY on the Worker"
|
|
178
|
+
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
179
|
+
}
|
|
180
|
+
const authHeader = request.headers.get("Authorization");
|
|
181
|
+
const expectedHeader = `Bearer ${apiKey}`;
|
|
182
|
+
if (!authHeader || !timingSafeEqual(authHeader, expectedHeader)) {
|
|
183
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
184
|
+
status: 401,
|
|
185
|
+
headers: { "Content-Type": "application/json" }
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
function createOperatorAuthMiddleware() {
|
|
191
|
+
return async (request, env, _ctx) => {
|
|
192
|
+
const denied = await requireOperatorAuth(request, env);
|
|
193
|
+
if (denied)
|
|
194
|
+
return denied;
|
|
195
|
+
return;
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function normalizeKeyNames(keyName) {
|
|
199
|
+
if (typeof keyName === "string") {
|
|
200
|
+
return [keyName];
|
|
201
|
+
}
|
|
202
|
+
return [...keyName];
|
|
203
|
+
}
|
|
204
|
+
function collectInternalAuthKeys(env, keyNames) {
|
|
205
|
+
const keys = [];
|
|
206
|
+
for (const name of normalizeKeyNames(keyNames)) {
|
|
207
|
+
const value = env[name];
|
|
208
|
+
if (typeof value === "string" && value.length > 0) {
|
|
209
|
+
keys.push(value);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return keys;
|
|
213
|
+
}
|
|
214
|
+
function matchesAnyInternalAuthKey(providedKey, expectedKeys) {
|
|
215
|
+
for (const expected of expectedKeys) {
|
|
216
|
+
if (timingSafeEqual(providedKey, expected)) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
function internalAuthNotConfiguredResponse(keyNames) {
|
|
223
|
+
const label = normalizeKeyNames(keyNames).join(" | ");
|
|
224
|
+
return new Response(JSON.stringify({
|
|
225
|
+
success: false,
|
|
226
|
+
error: `Internal auth key(s) not configured: ${label}`
|
|
227
|
+
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
228
|
+
}
|
|
229
|
+
function requireInternalAuth(request, env, keyName = "INTERNAL_KEY_BINDING") {
|
|
230
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
231
|
+
if (expectedKeys.length === 0) {
|
|
232
|
+
return internalAuthNotConfiguredResponse(keyName);
|
|
233
|
+
}
|
|
234
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
235
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
236
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
function createInternalAuthMiddleware(keyName = "INTERNAL_KEY_BINDING") {
|
|
241
|
+
return async (request, env, _ctx) => {
|
|
242
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
243
|
+
if (expectedKeys.length === 0) {
|
|
244
|
+
return internalAuthNotConfiguredResponse(keyName);
|
|
245
|
+
}
|
|
246
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
247
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
248
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function checkInternalAuth(request, env, keyName = "INTERNAL_KEY_BINDING") {
|
|
254
|
+
const expectedKeys = collectInternalAuthKeys(env, keyName);
|
|
255
|
+
if (expectedKeys.length === 0) {
|
|
256
|
+
return {
|
|
257
|
+
authorized: false,
|
|
258
|
+
error: `${normalizeKeyNames(keyName).join(" | ")} not configured`
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const providedKey = request.headers.get("X-Internal-Auth-Key");
|
|
262
|
+
if (!providedKey || !matchesAnyInternalAuthKey(providedKey, expectedKeys)) {
|
|
263
|
+
return { authorized: false, error: "Unauthorized" };
|
|
264
|
+
}
|
|
265
|
+
return { authorized: true };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// src/middleware/cors.ts
|
|
269
|
+
var DEFAULT_OPTIONS = {
|
|
270
|
+
allowOrigin: "",
|
|
271
|
+
allowMethods: "GET, POST, OPTIONS, PUT, DELETE",
|
|
272
|
+
allowHeaders: "Content-Type, Authorization, X-Request-ID",
|
|
273
|
+
allowCredentials: false,
|
|
274
|
+
maxAge: 86400
|
|
275
|
+
};
|
|
276
|
+
function corsHeaders(options) {
|
|
277
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
278
|
+
const headers = {
|
|
279
|
+
"Access-Control-Allow-Methods": opts.allowMethods,
|
|
280
|
+
"Access-Control-Allow-Headers": opts.allowHeaders,
|
|
281
|
+
"Access-Control-Max-Age": String(opts.maxAge)
|
|
282
|
+
};
|
|
283
|
+
if (opts.allowOrigin) {
|
|
284
|
+
headers["Access-Control-Allow-Origin"] = opts.allowOrigin;
|
|
285
|
+
}
|
|
286
|
+
if (opts.allowCredentials) {
|
|
287
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
288
|
+
}
|
|
289
|
+
return headers;
|
|
290
|
+
}
|
|
291
|
+
function publicCorsHeaders(origin = "*", options) {
|
|
292
|
+
return corsHeaders({ ...options, allowOrigin: origin });
|
|
293
|
+
}
|
|
294
|
+
function internalCorsHeaders() {
|
|
295
|
+
return {};
|
|
296
|
+
}
|
|
297
|
+
function resolveCorsOptions(request, env) {
|
|
298
|
+
const raw = env && typeof env === "object" && "CORS_ALLOW_ORIGIN" in env && typeof env.CORS_ALLOW_ORIGIN === "string" ? env.CORS_ALLOW_ORIGIN : undefined;
|
|
299
|
+
const configured = raw?.trim();
|
|
300
|
+
if (!configured) {
|
|
301
|
+
return {};
|
|
302
|
+
}
|
|
303
|
+
const allowed = configured.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
304
|
+
const origin = request.headers.get("Origin");
|
|
305
|
+
if (origin && allowed.includes(origin)) {
|
|
306
|
+
return { allowOrigin: origin, allowCredentials: true };
|
|
307
|
+
}
|
|
308
|
+
if (!origin && allowed.length === 1) {
|
|
309
|
+
return { allowOrigin: allowed[0] };
|
|
310
|
+
}
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
313
|
+
function handleCorsPreflightRequest(request, options) {
|
|
314
|
+
if (request.method !== "OPTIONS")
|
|
315
|
+
return null;
|
|
316
|
+
return new Response(null, {
|
|
317
|
+
status: 204,
|
|
318
|
+
headers: corsHeaders(options)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/middleware/logger.ts
|
|
323
|
+
function createLogger(ctx) {
|
|
324
|
+
const base = { service: ctx.service, module: ctx.module };
|
|
325
|
+
function emit(level, message, context) {
|
|
326
|
+
const entry = {
|
|
327
|
+
level,
|
|
328
|
+
timestamp: new Date().toISOString(),
|
|
329
|
+
...base,
|
|
330
|
+
message,
|
|
331
|
+
...context && { context }
|
|
332
|
+
};
|
|
333
|
+
const line = JSON.stringify(entry);
|
|
334
|
+
if (level === "error")
|
|
335
|
+
console.error(line);
|
|
336
|
+
else if (level === "warn")
|
|
337
|
+
console.warn(line);
|
|
338
|
+
else if (level === "debug")
|
|
339
|
+
console.debug(line);
|
|
340
|
+
else
|
|
341
|
+
console.info(line);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
debug: (msg, ctx2) => emit("debug", msg, ctx2),
|
|
345
|
+
info: (msg, ctx2) => emit("info", msg, ctx2),
|
|
346
|
+
warn: (msg, ctx2) => emit("warn", msg, ctx2),
|
|
347
|
+
error: (msg, ctx2) => emit("error", msg, ctx2)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function withRequestLog(handler, logCtx) {
|
|
351
|
+
return async (request, env, ctx) => {
|
|
352
|
+
const start = Date.now();
|
|
353
|
+
const logger = createLogger(logCtx);
|
|
354
|
+
try {
|
|
355
|
+
const response = await handler(request, env, ctx);
|
|
356
|
+
const duration = Date.now() - start;
|
|
357
|
+
const url = new URL(request.url);
|
|
358
|
+
logger.info(`${request.method} ${url.pathname}`, {
|
|
359
|
+
method: request.method,
|
|
360
|
+
path: url.pathname,
|
|
361
|
+
status: response.status,
|
|
362
|
+
durationMs: duration
|
|
363
|
+
});
|
|
364
|
+
return response;
|
|
365
|
+
} catch (error) {
|
|
366
|
+
const duration = Date.now() - start;
|
|
367
|
+
logger.error("Request failed", {
|
|
368
|
+
method: request.method,
|
|
369
|
+
path: new URL(request.url).pathname,
|
|
370
|
+
durationMs: duration,
|
|
371
|
+
error: toError(error)
|
|
372
|
+
});
|
|
373
|
+
throw error;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/middleware/rate-limit.ts
|
|
379
|
+
function createStorage(kv) {
|
|
380
|
+
if (!kv) {
|
|
381
|
+
const memory = new Map;
|
|
382
|
+
return {
|
|
383
|
+
async get(key) {
|
|
384
|
+
const entry = memory.get(key);
|
|
385
|
+
if (!entry)
|
|
386
|
+
return null;
|
|
387
|
+
if (Date.now() > entry.expiresAt) {
|
|
388
|
+
memory.delete(key);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
return entry.value;
|
|
392
|
+
},
|
|
393
|
+
async put(key, value, opts) {
|
|
394
|
+
const ttl = opts?.expirationTtl ?? 0;
|
|
395
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl * 1000 : Infinity;
|
|
396
|
+
memory.set(key, { value, expiresAt });
|
|
397
|
+
},
|
|
398
|
+
async incr(key, opts) {
|
|
399
|
+
let entry = memory.get(key);
|
|
400
|
+
let newValue;
|
|
401
|
+
let attempts = 0;
|
|
402
|
+
const maxAttempts = 100;
|
|
403
|
+
while (true) {
|
|
404
|
+
const count = entry ? parseInt(entry.value, 10) : 0;
|
|
405
|
+
newValue = count + 1;
|
|
406
|
+
if (!entry) {
|
|
407
|
+
const ttl = opts?.expirationTtl ?? 0;
|
|
408
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl * 1000 : Infinity;
|
|
409
|
+
if (memory.get(key) === undefined) {
|
|
410
|
+
memory.set(key, { value: String(newValue), expiresAt });
|
|
411
|
+
return newValue;
|
|
412
|
+
}
|
|
413
|
+
entry = memory.get(key);
|
|
414
|
+
} else {
|
|
415
|
+
if (Date.now() > entry.expiresAt) {
|
|
416
|
+
memory.delete(key);
|
|
417
|
+
entry = undefined;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const currentEntry = memory.get(key);
|
|
421
|
+
if (currentEntry === entry) {
|
|
422
|
+
const ttl = opts?.expirationTtl ?? 0;
|
|
423
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl * 1000 : Infinity;
|
|
424
|
+
memory.set(key, { value: String(newValue), expiresAt });
|
|
425
|
+
return newValue;
|
|
426
|
+
}
|
|
427
|
+
entry = currentEntry;
|
|
428
|
+
}
|
|
429
|
+
attempts++;
|
|
430
|
+
if (attempts >= maxAttempts) {
|
|
431
|
+
const current = await this.get(key);
|
|
432
|
+
const count2 = current ? parseInt(current, 10) : 0;
|
|
433
|
+
newValue = count2 + 1;
|
|
434
|
+
const ttl = opts?.expirationTtl ?? 0;
|
|
435
|
+
const expiresAt = ttl > 0 ? Date.now() + ttl * 1000 : Infinity;
|
|
436
|
+
memory.set(key, { value: String(newValue), expiresAt });
|
|
437
|
+
return newValue;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
async get(key) {
|
|
445
|
+
return kv.get(key);
|
|
446
|
+
},
|
|
447
|
+
async put(key, value, opts) {
|
|
448
|
+
await kv.put(key, value, opts);
|
|
449
|
+
},
|
|
450
|
+
async incr(key, opts) {
|
|
451
|
+
const current = await kv.get(key);
|
|
452
|
+
const count = current ? parseInt(current, 10) : 0;
|
|
453
|
+
const next = count + 1;
|
|
454
|
+
await kv.put(key, String(next), opts);
|
|
455
|
+
return next;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
function createRateLimiter(kv, config) {
|
|
460
|
+
const prefix = config.keyPrefix ?? "rate-limit";
|
|
461
|
+
const storage = createStorage(kv);
|
|
462
|
+
function getClientIp(request) {
|
|
463
|
+
return request.headers.get("CF-Connecting-IP") ?? "unknown";
|
|
464
|
+
}
|
|
465
|
+
function getWindowKey(ip) {
|
|
466
|
+
const windowStart = Math.floor(Date.now() / (config.windowSeconds * 1000));
|
|
467
|
+
return `${prefix}:${ip}:${windowStart}`;
|
|
468
|
+
}
|
|
469
|
+
async function checkWithKey(fullKey) {
|
|
470
|
+
const current = await storage.get(fullKey);
|
|
471
|
+
const count = current ? parseInt(current, 10) : 0;
|
|
472
|
+
if (count >= config.maxRequests) {
|
|
473
|
+
const retryAfter = Math.ceil(config.windowSeconds - Date.now() % (config.windowSeconds * 1000) / 1000);
|
|
474
|
+
return {
|
|
475
|
+
allowed: false,
|
|
476
|
+
remaining: 0,
|
|
477
|
+
retryAfter
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const newCount = await storage.incr(fullKey, {
|
|
481
|
+
expirationTtl: config.windowSeconds
|
|
482
|
+
});
|
|
483
|
+
if (newCount > config.maxRequests) {
|
|
484
|
+
const retryAfter = Math.ceil(config.windowSeconds - Date.now() % (config.windowSeconds * 1000) / 1000);
|
|
485
|
+
return {
|
|
486
|
+
allowed: false,
|
|
487
|
+
remaining: 0,
|
|
488
|
+
retryAfter
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return {
|
|
492
|
+
allowed: true,
|
|
493
|
+
remaining: config.maxRequests - newCount
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
async function check(request) {
|
|
497
|
+
const ip = getClientIp(request);
|
|
498
|
+
const key = getWindowKey(ip);
|
|
499
|
+
return checkWithKey(key);
|
|
500
|
+
}
|
|
501
|
+
async function enforce(request) {
|
|
502
|
+
const result = await check(request);
|
|
503
|
+
if (!result.allowed) {
|
|
504
|
+
return new Response(JSON.stringify({
|
|
505
|
+
error: "Rate limit exceeded",
|
|
506
|
+
retryAfter: result.retryAfter
|
|
507
|
+
}), {
|
|
508
|
+
status: 429,
|
|
509
|
+
headers: {
|
|
510
|
+
"Content-Type": "application/json",
|
|
511
|
+
"Retry-After": String(result.retryAfter ?? config.windowSeconds)
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
async function checkKey(key) {
|
|
518
|
+
const fullKey = `${prefix}:${key}`;
|
|
519
|
+
return checkWithKey(fullKey);
|
|
520
|
+
}
|
|
521
|
+
async function enforceKey(key) {
|
|
522
|
+
const result = await checkKey(key);
|
|
523
|
+
if (!result.allowed) {
|
|
524
|
+
return new Response(JSON.stringify({
|
|
525
|
+
error: "Rate limit exceeded",
|
|
526
|
+
retryAfter: result.retryAfter
|
|
527
|
+
}), {
|
|
528
|
+
status: 429,
|
|
529
|
+
headers: {
|
|
530
|
+
"Content-Type": "application/json",
|
|
531
|
+
"Retry-After": String(result.retryAfter ?? config.windowSeconds)
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
return { check, enforce, checkKey, enforceKey };
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// src/middleware/validate.ts
|
|
541
|
+
function validateJson(schema, data) {
|
|
542
|
+
const result = schema.safeParse(data);
|
|
543
|
+
if (!result.success) {
|
|
544
|
+
return {
|
|
545
|
+
ok: false,
|
|
546
|
+
error: result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
return { ok: true, value: result.data };
|
|
550
|
+
}
|
|
551
|
+
async function validateJsonLegacy(request) {
|
|
552
|
+
try {
|
|
553
|
+
const body = await request.json();
|
|
554
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
555
|
+
return { ok: false, error: "Request body must be a JSON object" };
|
|
556
|
+
}
|
|
557
|
+
return { ok: true, value: body };
|
|
558
|
+
} catch {
|
|
559
|
+
return { ok: false, error: "Invalid JSON in request body" };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function requireField(body, field) {
|
|
563
|
+
if (!(field in body)) {
|
|
564
|
+
return { ok: false, error: `Missing required field: ${field}` };
|
|
565
|
+
}
|
|
566
|
+
return { ok: true, value: body[field] };
|
|
567
|
+
}
|
|
568
|
+
function optionalField(body, field, defaultValue) {
|
|
569
|
+
if (!(field in body))
|
|
570
|
+
return defaultValue;
|
|
571
|
+
return body[field];
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/middleware/security-headers.ts
|
|
575
|
+
var SECURITY_HEADERS_DEFAULTS = {
|
|
576
|
+
xContentTypeOptions: "nosniff",
|
|
577
|
+
xFrameOptions: "DENY",
|
|
578
|
+
xXssProtection: "1; mode=block",
|
|
579
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
580
|
+
permissionsPolicy: "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()",
|
|
581
|
+
strictTransportSecurity: "max-age=31536000; includeSubDomains",
|
|
582
|
+
contentSecurityPolicy: "default-src 'self'"
|
|
583
|
+
};
|
|
584
|
+
function secureHeaders(options) {
|
|
585
|
+
const opts = { ...SECURITY_HEADERS_DEFAULTS, ...options };
|
|
586
|
+
const headers = {
|
|
587
|
+
"X-Content-Type-Options": opts.xContentTypeOptions,
|
|
588
|
+
"X-Frame-Options": opts.xFrameOptions,
|
|
589
|
+
"X-XSS-Protection": opts.xXssProtection,
|
|
590
|
+
"Referrer-Policy": opts.referrerPolicy,
|
|
591
|
+
"Permissions-Policy": opts.permissionsPolicy,
|
|
592
|
+
"Strict-Transport-Security": opts.strictTransportSecurity
|
|
593
|
+
};
|
|
594
|
+
if (opts.contentSecurityPolicy) {
|
|
595
|
+
headers["Content-Security-Policy"] = opts.contentSecurityPolicy;
|
|
596
|
+
}
|
|
597
|
+
return headers;
|
|
598
|
+
}
|
|
599
|
+
function wrapWithSecurityHeaders(response, options) {
|
|
600
|
+
const headers = new Headers(response.headers);
|
|
601
|
+
for (const [key, value] of Object.entries(secureHeaders(options))) {
|
|
602
|
+
headers.set(key, value);
|
|
603
|
+
}
|
|
604
|
+
return new Response(response.body, {
|
|
605
|
+
status: response.status,
|
|
606
|
+
statusText: response.statusText,
|
|
607
|
+
headers
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
export {
|
|
611
|
+
wrapWithSecurityHeaders,
|
|
612
|
+
withRequestLog,
|
|
613
|
+
validateJsonLegacy,
|
|
614
|
+
validateJson,
|
|
615
|
+
timingSafeEqual,
|
|
616
|
+
secureHeaders,
|
|
617
|
+
resolveOperatorApiKey,
|
|
618
|
+
resolveCorsOptions,
|
|
619
|
+
requireOperatorAuth,
|
|
620
|
+
requireInternalAuth,
|
|
621
|
+
requireField,
|
|
622
|
+
requireAuth,
|
|
623
|
+
publicCorsHeaders,
|
|
624
|
+
optionalField,
|
|
625
|
+
internalCorsHeaders,
|
|
626
|
+
handleCorsPreflightRequest,
|
|
627
|
+
createRateLimiter,
|
|
628
|
+
createOperatorAuthMiddleware,
|
|
629
|
+
createLogger,
|
|
630
|
+
createInternalAuthMiddleware,
|
|
631
|
+
corsHeaders,
|
|
632
|
+
collectInternalAuthKeys,
|
|
633
|
+
checkInternalAuth,
|
|
634
|
+
SECURITY_HEADERS_DEFAULTS
|
|
635
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2026 HOOX · HOOX · jango-blockchained (hoox-sh)
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
export interface LogContext {
|
|
6
|
+
service: string;
|
|
7
|
+
module?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface Logger {
|
|
10
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
11
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
12
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
13
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function createLogger(ctx: LogContext): Logger;
|
|
16
|
+
export declare function withRequestLog<E>(handler: (request: Request, env: E, ctx: ExecutionContext) => Promise<Response>, logCtx: LogContext): (request: Request, env: E, ctx: ExecutionContext) => Promise<Response>;
|
|
17
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/middleware/logger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAChE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACjE;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,CA4BpD;AAED,wBAAgB,cAAc,CAAC,CAAC,EAC9B,OAAO,EAAE,CACP,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,CAAC,EACN,GAAG,EAAE,gBAAgB,KAClB,OAAO,CAAC,QAAQ,CAAC,EACtB,MAAM,EAAE,UAAU,GACjB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,QAAQ,CAAC,CA2BxE"}
|