@h-ai/kit 0.1.0-alpha8 → 1.0.0-beta.1
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.en.md +430 -0
- package/README.ja.md +430 -0
- package/README.md +351 -40
- package/dist/adapter/index.d.ts +17 -2
- package/dist/adapter/index.js.map +1 -1
- package/dist/browser.d.ts +19 -8
- package/dist/browser.js +10 -7
- package/dist/browser.js.map +1 -1
- package/dist/chunk-3OHUSKHI.js +175 -0
- package/dist/chunk-3OHUSKHI.js.map +1 -0
- package/dist/chunk-5VWPH7DO.js +126 -0
- package/dist/chunk-5VWPH7DO.js.map +1 -0
- package/dist/chunk-7EH47MTW.js +141 -0
- package/dist/chunk-7EH47MTW.js.map +1 -0
- package/dist/chunk-JRQPMPQC.js +63 -0
- package/dist/chunk-JRQPMPQC.js.map +1 -0
- package/dist/chunk-USFJN7IO.js +187 -0
- package/dist/chunk-USFJN7IO.js.map +1 -0
- package/dist/chunk-YKJRQCWR.js +363 -0
- package/dist/chunk-YKJRQCWR.js.map +1 -0
- package/dist/client/index.d.ts +51 -2
- package/dist/client/index.js +21 -2
- package/dist/client/index.js.map +1 -1
- package/dist/index.d.ts +303 -409
- package/dist/index.js +598 -310
- package/dist/index.js.map +1 -1
- package/dist/kit-a2a-types-4dapztu2.d.ts +136 -0
- package/dist/kit-client-auth-BnhrkzZF.d.ts +422 -0
- package/dist/kit-crypto-types-CXr888wf.d.ts +200 -0
- package/dist/kit-i18n-EBzZKYN-.d.ts +502 -0
- package/dist/kit-types-BkNdyKj8.d.ts +822 -0
- package/dist/modules/a2a/index.d.ts +15 -113
- package/dist/modules/a2a/index.js +2 -55
- package/dist/modules/a2a/index.js.map +1 -1
- package/dist/modules/crypto/index.d.ts +31 -79
- package/dist/modules/crypto/index.js +3 -3
- package/dist/modules/crypto/index.js.map +1 -1
- package/package.json +36 -22
- package/dist/chunk-475WECWK.js +0 -110
- package/dist/chunk-475WECWK.js.map +0 -1
- package/dist/chunk-6TMSXG7J.js +0 -169
- package/dist/chunk-6TMSXG7J.js.map +0 -1
- package/dist/chunk-AD6S473Y.js +0 -137
- package/dist/chunk-AD6S473Y.js.map +0 -1
- package/dist/chunk-FRYJMRTP.js +0 -71
- package/dist/chunk-FRYJMRTP.js.map +0 -1
- package/dist/chunk-PHS7ZD5L.js +0 -274
- package/dist/chunk-PHS7ZD5L.js.map +0 -1
- package/dist/index-BotXLTck.d.ts +0 -203
- package/dist/kit-crypto-types-C-gEvx04.d.ts +0 -253
- package/dist/kit-i18n-CgpNTh9G.d.ts +0 -206
- package/dist/kit-types-BHlDNNpd.d.ts +0 -618
package/dist/index.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import { defineCrud } from './chunk-
|
|
2
|
-
export { defineCrud, resolveOptions, resolveText } from './chunk-
|
|
3
|
-
import {
|
|
4
|
-
import { resolveA2AConfig, handleA2ARequest } from './chunk-
|
|
5
|
-
import { transportEncryptionMiddleware } from './chunk-
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
1
|
+
import { defineCrud } from './chunk-5VWPH7DO.js';
|
|
2
|
+
export { KitConfigSchema, defineCrud, resolveKitConfig, resolveKitTransportConfig, resolveOptions, resolveText } from './chunk-5VWPH7DO.js';
|
|
3
|
+
import { clearBrowserToken, setBrowserToken, logout, registerAndLogin, loginWithApiKey, loginWithLdap, loginWithOtp, login, installBrowserTransportFetch, installBrowserTransport, createKitAuthActions, createKitClient, getAccessToken, configureAuth } from './chunk-YKJRQCWR.js';
|
|
4
|
+
import { resolveA2AConfig, handleA2ARequest } from './chunk-7EH47MTW.js';
|
|
5
|
+
import { transportEncryptionMiddleware } from './chunk-3OHUSKHI.js';
|
|
6
|
+
import './chunk-JRQPMPQC.js';
|
|
7
|
+
import { kitM, setAllModulesLocale } from './chunk-USFJN7IO.js';
|
|
8
|
+
import { core, HaiCommonError, err } from '@h-ai/core';
|
|
9
|
+
import { audit } from '@h-ai/audit';
|
|
10
|
+
import { TRANSPORT_PROTOCOL } from '@h-ai/crypto';
|
|
11
|
+
import { createHash } from 'crypto';
|
|
12
|
+
import { isIP } from 'net';
|
|
13
|
+
import process2 from 'process';
|
|
14
|
+
import { IamConfirmPasswordResetInputSchema, IamRequestPasswordResetInputSchema, IamSendOtpInputSchema, IamRegisterInputSchema, IamOtpLoginInputSchema, IamLdapLoginInputSchema, IamLoginInputSchema, IamRenewPasswordInputSchema, IamReauthenticateInputSchema, IamCreateCaptchaInputSchema, IamLogoutInputSchema, FeedbackSubmitInputSchema, FeedbackListInputSchema, FeedbackIdInputSchema, FeedbackEvidenceQuerySchema, FeedbackAppendEvidenceInputSchema, FeedbackSetStatusInputSchema, FeedbackRetryInputSchema, FeedbackCompleteInputSchema, FeedbackExportInputSchema, FeedbackUploadInputSchema, FeedbackAttachmentQuerySchema, FeedbackExportQuerySchema } from '@h-ai/api-contract';
|
|
8
15
|
import { z } from 'zod';
|
|
9
16
|
|
|
10
17
|
// src/guards/kit-permission.ts
|
|
@@ -51,8 +58,6 @@ function requirePermission(session, permission) {
|
|
|
51
58
|
throw denied;
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
|
-
|
|
55
|
-
// src/kit-response.ts
|
|
56
61
|
function ok(data, requestId) {
|
|
57
62
|
const response = {
|
|
58
63
|
success: true,
|
|
@@ -79,18 +84,21 @@ function noContent() {
|
|
|
79
84
|
return new Response(null, { status: 204 });
|
|
80
85
|
}
|
|
81
86
|
function error(code, message, status = 400, requestId, details) {
|
|
87
|
+
const safe = core.error.toPublic({ code, message, httpStatus: status }, requestId);
|
|
88
|
+
core.logger.error("API request failed", { traceId: safe.traceId, error: { code, message, httpStatus: status, details } });
|
|
82
89
|
const response = {
|
|
83
90
|
success: false,
|
|
84
91
|
error: {
|
|
85
92
|
code,
|
|
86
|
-
message,
|
|
87
|
-
|
|
93
|
+
message: safe.message,
|
|
94
|
+
traceId: safe.traceId,
|
|
95
|
+
details: status < 500 ? core.sanitize.sanitizeSensitiveFields(details) : void 0
|
|
88
96
|
},
|
|
89
|
-
requestId
|
|
97
|
+
requestId: requestId ?? core.context.get().requestId ?? safe.traceId
|
|
90
98
|
};
|
|
91
99
|
return new Response(JSON.stringify(response), {
|
|
92
100
|
status,
|
|
93
|
-
headers: { "Content-Type": "application/json" }
|
|
101
|
+
headers: { "Content-Type": "application/json", "X-Trace-Id": safe.traceId }
|
|
94
102
|
});
|
|
95
103
|
}
|
|
96
104
|
function badRequest(message, requestId, details) {
|
|
@@ -124,12 +132,23 @@ function fromResult(result, httpStatusMap, requestId) {
|
|
|
124
132
|
if (result.success) {
|
|
125
133
|
return ok(result.data, requestId);
|
|
126
134
|
}
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
return error(String(code), message, status, requestId);
|
|
135
|
+
const status = httpStatusMap?.[result.error.code] ?? (typeof result.error.httpStatus === "number" ? result.error.httpStatus : 400);
|
|
136
|
+
return fromError({ ...result.error, httpStatus: status }, requestId);
|
|
130
137
|
}
|
|
131
138
|
function fromError(haiError, requestId) {
|
|
132
|
-
|
|
139
|
+
const safe = core.error.toPublic(haiError, requestId);
|
|
140
|
+
core.logger.error("API operation failed", { traceId: safe.traceId, error: haiError });
|
|
141
|
+
return error(String(safe.code), safe.message, safe.httpStatus ?? 400, safe.traceId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/guards/kit-sensitive.ts
|
|
145
|
+
async function requireSensitiveOperation(iam, event, operation) {
|
|
146
|
+
const policy = iam.publicConfig?.reauthentication;
|
|
147
|
+
if (!policy?.enabled)
|
|
148
|
+
return;
|
|
149
|
+
const checked = await iam.auth.checkSensitiveOperation(getAccessToken(event.request, event.cookies) ?? "", operation, event.request.headers.get("x-hai-reauthentication") ?? void 0);
|
|
150
|
+
if (!checked.success)
|
|
151
|
+
throw fromError(checked.error);
|
|
133
152
|
}
|
|
134
153
|
|
|
135
154
|
// src/kit-utils.ts
|
|
@@ -150,63 +169,6 @@ function isSvelteKitControlFlow(value) {
|
|
|
150
169
|
}
|
|
151
170
|
return false;
|
|
152
171
|
}
|
|
153
|
-
|
|
154
|
-
// src/kit-handler.ts
|
|
155
|
-
function handler(fn) {
|
|
156
|
-
return async (event) => {
|
|
157
|
-
try {
|
|
158
|
-
return await fn(event);
|
|
159
|
-
} catch (error2) {
|
|
160
|
-
if (isResponseLike(error2)) {
|
|
161
|
-
return error2;
|
|
162
|
-
}
|
|
163
|
-
if (isSvelteKitControlFlow(error2)) {
|
|
164
|
-
throw error2;
|
|
165
|
-
}
|
|
166
|
-
const normalizedError = error2 instanceof Error ? {
|
|
167
|
-
name: error2.name,
|
|
168
|
-
message: error2.message,
|
|
169
|
-
stack: error2.stack,
|
|
170
|
-
cause: error2.cause
|
|
171
|
-
} : error2;
|
|
172
|
-
core.logger.error("Request handler error", {
|
|
173
|
-
error: normalizedError,
|
|
174
|
-
path: event.url.pathname,
|
|
175
|
-
method: event.request.method
|
|
176
|
-
});
|
|
177
|
-
return internalError();
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// src/kit-contract.ts
|
|
183
|
-
function defineEndpoint(def) {
|
|
184
|
-
return def;
|
|
185
|
-
}
|
|
186
|
-
function fromContract(endpoint, fn) {
|
|
187
|
-
return handler(async (event) => {
|
|
188
|
-
let raw;
|
|
189
|
-
if (endpoint.method === "GET") {
|
|
190
|
-
raw = Object.fromEntries(event.url.searchParams);
|
|
191
|
-
} else {
|
|
192
|
-
try {
|
|
193
|
-
raw = await event.request.json();
|
|
194
|
-
} catch {
|
|
195
|
-
raw = {};
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
const parsed = endpoint.input.safeParse(raw);
|
|
199
|
-
if (!parsed.success) {
|
|
200
|
-
const errors = parsed.error.issues.map((issue) => ({
|
|
201
|
-
field: issue.path.join(".") || "_",
|
|
202
|
-
message: issue.message
|
|
203
|
-
}));
|
|
204
|
-
return validationError(errors);
|
|
205
|
-
}
|
|
206
|
-
const result = await fn(parsed.data, event);
|
|
207
|
-
return ok(result);
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
172
|
function loggingMiddleware(config = {}) {
|
|
211
173
|
const { logBody = false, logResponse = false, redactFields = ["password", "token", "secret", "authorization", "apikey", "api_key", "creditcard", "credit_card"] } = config;
|
|
212
174
|
return async (context, next) => {
|
|
@@ -221,7 +183,8 @@ function loggingMiddleware(config = {}) {
|
|
|
221
183
|
requestId,
|
|
222
184
|
method: event.request.method,
|
|
223
185
|
path: event.url.pathname,
|
|
224
|
-
|
|
186
|
+
// SvelteKit 预渲染禁止读取追踪 URL 的查询;日志只读原请求,不建立页面路由依赖。
|
|
187
|
+
query: redactObject(Object.fromEntries(new URL(event.request.url).searchParams), redactFields),
|
|
225
188
|
userAgent: event.request.headers.get("user-agent"),
|
|
226
189
|
ip: clientIp
|
|
227
190
|
};
|
|
@@ -249,76 +212,95 @@ function loggingMiddleware(config = {}) {
|
|
|
249
212
|
};
|
|
250
213
|
}
|
|
251
214
|
function redactObject(obj, fields) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
215
|
+
return core.sanitize.sanitizeSensitiveFields(obj, { matcher: fields });
|
|
216
|
+
}
|
|
217
|
+
function permitsMemory() {
|
|
218
|
+
return !core.module.requiresSharedState(process2.env);
|
|
219
|
+
}
|
|
220
|
+
function createCacheRateLimitStore(backend, options) {
|
|
221
|
+
if (!options.namespace || options.namespace.length > 128)
|
|
222
|
+
throw new TypeError("A rate limit namespace of 1-128 characters is required");
|
|
223
|
+
return {
|
|
224
|
+
scope: "shared",
|
|
225
|
+
async increment(key, windowMs) {
|
|
226
|
+
if (!backend.config || backend.config.type !== "redis" && !(backend.config.type === "memory" && options.allowMemory && permitsMemory()))
|
|
227
|
+
throw new Error("Rate limiting requires a shared Redis cache");
|
|
228
|
+
const digest = createHash("sha256").update(JSON.stringify([options.namespace, windowMs, key])).digest("hex");
|
|
229
|
+
const cacheKey = `hai:kit:ratelimit:{${digest}}`;
|
|
230
|
+
for (let attempt = 0; attempt < 64; attempt++) {
|
|
231
|
+
const read = await backend.kv.get(cacheKey);
|
|
232
|
+
if (!read.success)
|
|
233
|
+
throw new Error("Rate limit cache read failed");
|
|
234
|
+
const old = read.data ?? void 0;
|
|
235
|
+
if (old && (!Number.isSafeInteger(old.count) || old.count <= 0 || old.count >= Number.MAX_SAFE_INTEGER || !Number.isSafeInteger(old.resetAt)))
|
|
236
|
+
throw new Error("Invalid rate limit state");
|
|
237
|
+
const next = old ? { count: old.count + 1, resetAt: old.resetAt } : { count: 1, resetAt: Date.now() + windowMs };
|
|
238
|
+
const written = await backend.kv.compareAndSwap(cacheKey, old, next, old ? { keepTtl: true } : { px: windowMs });
|
|
239
|
+
if (!written.success)
|
|
240
|
+
throw new Error("Rate limit cache commit failed");
|
|
241
|
+
if (written.data)
|
|
242
|
+
return next;
|
|
243
|
+
}
|
|
244
|
+
throw new Error("Rate limit contention limit exceeded");
|
|
269
245
|
}
|
|
270
|
-
}
|
|
271
|
-
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function createMemoryRateLimitStore() {
|
|
249
|
+
if (!permitsMemory())
|
|
250
|
+
throw new Error("Memory rate limiting is restricted to development and tests");
|
|
251
|
+
return new MemoryRateLimitStore();
|
|
252
|
+
}
|
|
253
|
+
function clientKey(address) {
|
|
254
|
+
if (isIP(address) !== 6)
|
|
255
|
+
return address;
|
|
256
|
+
const normalized = new URL(`http://[${address}]`).hostname.slice(1, -1);
|
|
257
|
+
const mapped = /^::ffff:([0-9a-f]+):([0-9a-f]+)$/.exec(normalized);
|
|
258
|
+
if (!mapped)
|
|
259
|
+
return normalized;
|
|
260
|
+
const value = Number.parseInt(mapped[1], 16) * 65536 + Number.parseInt(mapped[2], 16);
|
|
261
|
+
return [value >>> 24, value >>> 16 & 255, value >>> 8 & 255, value & 255].join(".");
|
|
272
262
|
}
|
|
273
263
|
var MemoryRateLimitStore = class {
|
|
264
|
+
scope = "memory";
|
|
274
265
|
store = /* @__PURE__ */ new Map();
|
|
275
|
-
|
|
266
|
+
cleanupCursor;
|
|
276
267
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* @param intervalMs - 清理间隔(毫秒)
|
|
268
|
+
* 有界惰性回收;保留迭代位置,避免每次从头扫描使尾部过期项永久滞留。
|
|
269
|
+
* @param now - 当前请求时间,等于 resetAt 时窗口已经失效。
|
|
280
270
|
*/
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
this.store.delete(key);
|
|
289
|
-
}
|
|
271
|
+
prune(now) {
|
|
272
|
+
this.cleanupCursor ??= this.store.entries();
|
|
273
|
+
for (let index = 0; index < 64; index++) {
|
|
274
|
+
const next = this.cleanupCursor.next();
|
|
275
|
+
if (next.done) {
|
|
276
|
+
this.cleanupCursor = void 0;
|
|
277
|
+
break;
|
|
290
278
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
279
|
+
const [key, entry] = next.value;
|
|
280
|
+
if (entry.resetAt <= now)
|
|
281
|
+
this.store.delete(key);
|
|
294
282
|
}
|
|
295
283
|
}
|
|
296
|
-
get(key) {
|
|
297
|
-
return this.store.get(key);
|
|
298
|
-
}
|
|
299
|
-
set(key, entry) {
|
|
300
|
-
this.store.set(key, entry);
|
|
301
|
-
}
|
|
302
|
-
delete(key) {
|
|
303
|
-
this.store.delete(key);
|
|
304
|
-
}
|
|
305
284
|
/**
|
|
306
285
|
* 原子自增(内存实现为同步操作,天然原子)
|
|
307
286
|
*
|
|
308
287
|
* @param key - 限流键
|
|
309
288
|
* @param windowMs - 窗口时长
|
|
310
|
-
* @returns
|
|
289
|
+
* @returns 自增后的快照,修改返回对象不会改变已保存的计数。
|
|
290
|
+
* @throws 新 key 超出容量或期限非法;中间件将其转换为 503。
|
|
291
|
+
* @example store.increment('client', 60_000)
|
|
311
292
|
*/
|
|
312
293
|
increment(key, windowMs) {
|
|
313
294
|
const now = Date.now();
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
295
|
+
if (!Number.isSafeInteger(windowMs) || windowMs <= 0 || windowMs > 2147483647)
|
|
296
|
+
throw new TypeError(kitM("kit_rateLimitWindowInvalid"));
|
|
297
|
+
this.prune(now);
|
|
298
|
+
const current = this.store.get(key);
|
|
299
|
+
if (!current && this.store.size >= 1e4)
|
|
300
|
+
throw new Error(kitM("kit_rateLimitMemoryCapacity"));
|
|
301
|
+
const entry = !current || current.resetAt <= now ? { count: 1, resetAt: now + windowMs } : { count: current.count + 1, resetAt: current.resetAt };
|
|
320
302
|
this.store.set(key, entry);
|
|
321
|
-
return entry;
|
|
303
|
+
return { ...entry };
|
|
322
304
|
}
|
|
323
305
|
};
|
|
324
306
|
function rateLimitMiddleware(config) {
|
|
@@ -327,38 +309,36 @@ function rateLimitMiddleware(config) {
|
|
|
327
309
|
maxRequests,
|
|
328
310
|
keyGenerator = (event) => {
|
|
329
311
|
try {
|
|
330
|
-
return event.getClientAddress?.() ?? "unknown";
|
|
312
|
+
return clientKey(event.getClientAddress?.() ?? "unknown");
|
|
331
313
|
} catch {
|
|
332
314
|
return "unknown";
|
|
333
315
|
}
|
|
334
316
|
},
|
|
335
317
|
onLimitReached
|
|
336
318
|
} = config;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
319
|
+
if (!Number.isSafeInteger(windowMs) || windowMs <= 0 || windowMs > 2147483647 || !Number.isSafeInteger(maxRequests) || maxRequests <= 0)
|
|
320
|
+
throw new TypeError("Rate limit window/count must be positive integers (window <= 2147483647ms)");
|
|
321
|
+
const store = config.store;
|
|
322
|
+
if (!store || typeof store.increment !== "function")
|
|
323
|
+
throw new TypeError("An atomic rate limit store is required");
|
|
324
|
+
if (store.scope === "memory" && !permitsMemory())
|
|
325
|
+
throw new Error("Memory rate limiting is restricted to development and tests");
|
|
342
326
|
return async (context, next) => {
|
|
343
327
|
const { event, requestId } = context;
|
|
344
|
-
const key = keyGenerator(event);
|
|
345
328
|
const now = Date.now();
|
|
346
329
|
let entry;
|
|
347
|
-
|
|
348
|
-
entry = await store.increment(
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
entry = { ...existing, count: existing.count + 1 };
|
|
355
|
-
}
|
|
356
|
-
await store.set(key, entry);
|
|
330
|
+
try {
|
|
331
|
+
entry = await store.increment(keyGenerator(event), windowMs);
|
|
332
|
+
if (!Number.isSafeInteger(entry.count) || entry.count <= 0 || !Number.isSafeInteger(entry.resetAt))
|
|
333
|
+
throw new TypeError("Invalid atomic rate limit result");
|
|
334
|
+
} catch {
|
|
335
|
+
core.logger.error("Rate limit store unavailable", { requestId });
|
|
336
|
+
return new Response(JSON.stringify({ success: false, error: { code: "RATE_LIMIT_UNAVAILABLE", message: kitM("kit_internalError") }, requestId }), { status: 503, headers: { "Content-Type": "application/json" } });
|
|
357
337
|
}
|
|
358
338
|
const remaining = Math.max(0, maxRequests - entry.count);
|
|
359
339
|
const resetTime = Math.ceil(entry.resetAt / 1e3);
|
|
360
340
|
if (entry.count > maxRequests) {
|
|
361
|
-
core.logger.warn("Rate limit exceeded", {
|
|
341
|
+
core.logger.warn("Rate limit exceeded", { requestId });
|
|
362
342
|
if (onLimitReached) {
|
|
363
343
|
return onLimitReached(event);
|
|
364
344
|
}
|
|
@@ -378,7 +358,7 @@ function rateLimitMiddleware(config) {
|
|
|
378
358
|
"X-RateLimit-Limit": String(maxRequests),
|
|
379
359
|
"X-RateLimit-Remaining": "0",
|
|
380
360
|
"X-RateLimit-Reset": String(resetTime),
|
|
381
|
-
"Retry-After": String(Math.ceil((entry.resetAt - now) / 1e3))
|
|
361
|
+
"Retry-After": String(Math.max(1, Math.ceil((entry.resetAt - now) / 1e3)))
|
|
382
362
|
}
|
|
383
363
|
}
|
|
384
364
|
);
|
|
@@ -401,40 +381,40 @@ function createEncryptedCookieProxy(cookies, config) {
|
|
|
401
381
|
const raw = target.get(name, opts);
|
|
402
382
|
if (!raw || !names.has(name))
|
|
403
383
|
return raw;
|
|
404
|
-
if (!raw.startsWith(ENCRYPTED_PREFIX))
|
|
405
|
-
|
|
384
|
+
if (!raw.startsWith(ENCRYPTED_PREFIX)) {
|
|
385
|
+
logger.warn("Encrypted cookie has invalid format", { name });
|
|
386
|
+
return void 0;
|
|
387
|
+
}
|
|
406
388
|
try {
|
|
407
389
|
const ciphertext = raw.slice(ENCRYPTED_PREFIX.length);
|
|
408
390
|
const separatorIndex = ciphertext.indexOf(":");
|
|
409
|
-
if (separatorIndex === -1)
|
|
410
|
-
|
|
391
|
+
if (separatorIndex === -1) {
|
|
392
|
+
logger.warn("Encrypted cookie has invalid format", { name });
|
|
393
|
+
return void 0;
|
|
394
|
+
}
|
|
411
395
|
const iv = ciphertext.slice(0, separatorIndex);
|
|
412
396
|
const encrypted = ciphertext.slice(separatorIndex + 1);
|
|
413
397
|
const result = symmetric.decryptWithIV(encrypted, encryptionKey, iv);
|
|
414
398
|
if (!result.success || typeof result.data !== "string") {
|
|
415
|
-
logger.warn("Cookie decryption failed
|
|
416
|
-
return
|
|
399
|
+
logger.warn("Cookie decryption failed", { name });
|
|
400
|
+
return void 0;
|
|
417
401
|
}
|
|
418
402
|
return result.data;
|
|
419
403
|
} catch {
|
|
420
|
-
logger.warn("Cookie decryption error
|
|
421
|
-
return
|
|
404
|
+
logger.warn("Cookie decryption error", { name });
|
|
405
|
+
return void 0;
|
|
422
406
|
}
|
|
423
407
|
};
|
|
424
408
|
}
|
|
425
409
|
if (prop === "set") {
|
|
426
410
|
return (name, value, opts) => {
|
|
427
411
|
if (names.has(name)) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
} else {
|
|
433
|
-
logger.warn("Cookie encryption failed, storing plaintext", { name });
|
|
434
|
-
}
|
|
435
|
-
} catch {
|
|
436
|
-
logger.warn("Cookie encryption error, storing plaintext", { name });
|
|
412
|
+
const result = symmetric.encryptWithIV(value, encryptionKey);
|
|
413
|
+
if (!result.success || !result.data) {
|
|
414
|
+
logger.error("Cookie encryption failed", { name });
|
|
415
|
+
throw new Error("Cookie encryption failed");
|
|
437
416
|
}
|
|
417
|
+
value = `${ENCRYPTED_PREFIX}${result.data.iv}:${result.data.ciphertext}`;
|
|
438
418
|
}
|
|
439
419
|
return target.set(name, value, opts);
|
|
440
420
|
};
|
|
@@ -450,7 +430,14 @@ function generateId(prefix) {
|
|
|
450
430
|
const random = crypto.randomUUID().replace(/-/g, "").substring(0, 8);
|
|
451
431
|
return `${prefix}_${timestamp}${random}`;
|
|
452
432
|
}
|
|
433
|
+
function applyDefaultSecurityHeaders(response) {
|
|
434
|
+
response.headers.set("X-Content-Type-Options", "nosniff");
|
|
435
|
+
response.headers.set("X-Frame-Options", "DENY");
|
|
436
|
+
response.headers.set("Referrer-Policy", "no-referrer");
|
|
437
|
+
}
|
|
453
438
|
function createHandle(config = {}) {
|
|
439
|
+
const resolveIp = core.access.createClientIpResolver(config.trustedProxies ?? "auto");
|
|
440
|
+
const checkAccess = config.access && core.access.createRequestCheck(config.access);
|
|
454
441
|
const {
|
|
455
442
|
auth: authConfig,
|
|
456
443
|
rateLimit: rateLimitConfig,
|
|
@@ -461,6 +448,8 @@ function createHandle(config = {}) {
|
|
|
461
448
|
middleware: customMiddleware = [],
|
|
462
449
|
a2a: a2aInput
|
|
463
450
|
} = config;
|
|
451
|
+
if (checkAccess && checkAccess.requiresEncryption && !cryptoConfig?.transport)
|
|
452
|
+
throw new Error("Request access rules require transport encryption, but Kit transport is not configured");
|
|
464
453
|
const a2aResolved = resolveA2AConfig(a2aInput);
|
|
465
454
|
if (authConfig?.cookieName || authConfig?.operations) {
|
|
466
455
|
configureAuth({ cookieName: authConfig.cookieName, operations: authConfig.operations });
|
|
@@ -481,102 +470,172 @@ function createHandle(config = {}) {
|
|
|
481
470
|
}
|
|
482
471
|
if (rateLimitConfig) {
|
|
483
472
|
builtinMiddleware.push(rateLimitMiddleware({
|
|
473
|
+
...rateLimitConfig,
|
|
484
474
|
windowMs: rateLimitConfig.windowMs ?? 6e4,
|
|
485
475
|
maxRequests: rateLimitConfig.maxRequests ?? 100
|
|
486
476
|
}));
|
|
487
477
|
}
|
|
488
478
|
const allMiddleware = [...builtinMiddleware, ...customMiddleware];
|
|
489
|
-
const
|
|
479
|
+
const transportMiddleware = buildTransportMiddleware(cryptoConfig, checkAccess ? checkAccess.encryption : void 0);
|
|
490
480
|
const cookieProxyConfig = buildCookieProxyConfig(cryptoConfig);
|
|
491
481
|
return async ({ event, resolve }) => {
|
|
492
|
-
const
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
if (cookieProxyConfig) {
|
|
496
|
-
const proxiedCookies = createEncryptedCookieProxy(event.cookies, cookieProxyConfig);
|
|
497
|
-
Object.defineProperty(event, "cookies", {
|
|
498
|
-
value: proxiedCookies,
|
|
499
|
-
writable: true,
|
|
500
|
-
configurable: true
|
|
501
|
-
});
|
|
502
|
-
}
|
|
482
|
+
const suppliedRequestId = event.request.headers.get("x-request-id");
|
|
483
|
+
const requestId = suppliedRequestId && /^[\w.:-]{1,128}$/.test(suppliedRequestId) ? suppliedRequestId : generateId("req");
|
|
484
|
+
let clientAddress;
|
|
503
485
|
try {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
486
|
+
clientAddress = event.getClientAddress();
|
|
487
|
+
} catch {
|
|
488
|
+
}
|
|
489
|
+
const resolvedAddress = resolveIp(event.request, clientAddress);
|
|
490
|
+
if (resolvedAddress !== clientAddress) {
|
|
491
|
+
event.getClientAddress = () => {
|
|
492
|
+
if (!resolvedAddress)
|
|
493
|
+
throw new Error("Trusted client address is unavailable");
|
|
494
|
+
return resolvedAddress;
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
clientAddress = resolvedAddress;
|
|
498
|
+
const accessDecision = checkAccess ? checkAccess(event.url.pathname, event.request.method, clientAddress) : void 0;
|
|
499
|
+
return core.context.run({ requestId, userId: void 0, scopeId: void 0, jobId: void 0 }, () => audit.withContext({
|
|
500
|
+
actor: null,
|
|
501
|
+
requestId,
|
|
502
|
+
traceId: requestId,
|
|
503
|
+
scopeId: void 0,
|
|
504
|
+
ipAddress: clientAddress,
|
|
505
|
+
userAgent: event.request.headers.get("user-agent")?.slice(0, 4096)
|
|
506
|
+
}, async () => {
|
|
507
|
+
let actor = null;
|
|
508
|
+
let guardDenied = false;
|
|
509
|
+
let requestFailed = false;
|
|
510
|
+
const captureRequest = async (status) => {
|
|
511
|
+
if (!audit.isInitialized)
|
|
512
|
+
return;
|
|
513
|
+
const resource = event.route.id ?? "unmatched";
|
|
514
|
+
const written = await audit.capture({
|
|
515
|
+
action: "request.execute",
|
|
516
|
+
resource,
|
|
517
|
+
userId: actor?.id ?? null,
|
|
518
|
+
actorSnapshot: actor,
|
|
519
|
+
category: /\/(?:auth|iam|audit)(?:\/|$)/.test(resource) ? "system" : "business",
|
|
520
|
+
outcome: guardDenied || [401, 403, 423, 429].includes(status) ? "denied" : requestFailed || status >= 400 ? "failure" : "success",
|
|
521
|
+
details: { method: event.request.method, status }
|
|
522
|
+
}, { read: ["GET", "HEAD", "OPTIONS"].includes(event.request.method) });
|
|
523
|
+
if (!written.success)
|
|
524
|
+
core.logger.error("Request audit failed", { requestId, resource, error: written.error });
|
|
525
|
+
};
|
|
526
|
+
const context = {
|
|
527
|
+
event,
|
|
528
|
+
requestId
|
|
529
|
+
};
|
|
530
|
+
const locals = event.locals;
|
|
531
|
+
locals.requestId = requestId;
|
|
532
|
+
if (cookieProxyConfig) {
|
|
533
|
+
const proxiedCookies = createEncryptedCookieProxy(event.cookies, cookieProxyConfig);
|
|
534
|
+
Object.defineProperty(event, "cookies", {
|
|
535
|
+
value: proxiedCookies,
|
|
536
|
+
writable: true,
|
|
537
|
+
configurable: true
|
|
538
|
+
});
|
|
508
539
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (session) {
|
|
516
|
-
locals.accessToken = token;
|
|
540
|
+
const runRequest = async () => {
|
|
541
|
+
try {
|
|
542
|
+
if (accessDecision && !accessDecision.allowed) {
|
|
543
|
+
guardDenied = true;
|
|
544
|
+
core.logger.info("Request access policy denied", { ruleId: accessDecision.ruleId, reason: accessDecision.reason, method: event.request.method, ipAddress: clientAddress });
|
|
545
|
+
return Response.json({ success: false, error: { ...HaiCommonError.REQUEST_ACCESS_DENIED, message: core.i18n.coreM("core_requestAccessDenied"), traceId: requestId } }, { status: 403 });
|
|
517
546
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
547
|
+
if (!transportMiddleware && event.request.method !== "OPTIONS" && (event.request.headers.has(TRANSPORT_PROTOCOL.ENCRYPTED_HEADER) || event.request.headers.has(TRANSPORT_PROTOCOL.ENVELOPE_HEADER)) && checkAccess && checkAccess.encryption(event.url.pathname, event.request.method) === "disabled") {
|
|
548
|
+
return Response.json(err({ ...HaiCommonError.VALIDATION_ERROR, message: kitM("kit_transportPlaintextRequired"), traceId: requestId }), { status: 400 });
|
|
549
|
+
}
|
|
550
|
+
if (a2aResolved) {
|
|
551
|
+
const a2aResponse = await handleA2ARequest(event, requestId, a2aResolved);
|
|
552
|
+
if (a2aResponse)
|
|
553
|
+
return a2aResponse;
|
|
554
|
+
}
|
|
555
|
+
let session;
|
|
556
|
+
if (authConfig) {
|
|
557
|
+
const token = getAccessToken(event.request, event.cookies);
|
|
558
|
+
if (token) {
|
|
559
|
+
session = await authConfig.verifyToken(token) ?? void 0;
|
|
560
|
+
locals.session = session;
|
|
561
|
+
if (session) {
|
|
562
|
+
locals.accessToken = token;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
context.session = session;
|
|
567
|
+
actor = session ? { id: session.userId, username: session.username, roles: session.roles } : null;
|
|
568
|
+
return await audit.withContext({ actor }, () => core.context.run({ userId: session?.userId }, async () => {
|
|
569
|
+
for (const guardConfig of guards) {
|
|
570
|
+
const guardResult = await executeGuard(guardConfig, event, session);
|
|
571
|
+
if (!guardResult.allowed) {
|
|
572
|
+
guardDenied = true;
|
|
573
|
+
if (guardResult.redirect) {
|
|
574
|
+
return new Response(null, {
|
|
575
|
+
status: 302,
|
|
576
|
+
headers: { Location: guardResult.redirect }
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
return new Response(
|
|
580
|
+
JSON.stringify({
|
|
581
|
+
success: false,
|
|
582
|
+
error: {
|
|
583
|
+
code: "FORBIDDEN",
|
|
584
|
+
message: guardResult.message ?? kitM("kit_accessDenied")
|
|
585
|
+
},
|
|
586
|
+
requestId
|
|
587
|
+
}),
|
|
588
|
+
{
|
|
589
|
+
status: guardResult.status ?? 403,
|
|
590
|
+
headers: { "Content-Type": "application/json" }
|
|
591
|
+
}
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const response2 = await executeMiddlewareChain(
|
|
596
|
+
allMiddleware,
|
|
597
|
+
context,
|
|
598
|
+
() => resolve(event)
|
|
599
|
+
);
|
|
600
|
+
return response2;
|
|
601
|
+
}));
|
|
602
|
+
} catch (error2) {
|
|
603
|
+
if (isSvelteKitControlFlow(error2)) {
|
|
604
|
+
if (typeof error2 === "object" && error2 !== null && "status" in error2 && typeof error2.status === "number")
|
|
605
|
+
await captureRequest(error2.status);
|
|
606
|
+
throw error2;
|
|
607
|
+
}
|
|
608
|
+
requestFailed = true;
|
|
609
|
+
core.logger.error("Request failed", { requestId, error: error2 });
|
|
610
|
+
if (onError) {
|
|
611
|
+
return onError(error2, event);
|
|
528
612
|
}
|
|
529
613
|
return new Response(
|
|
530
614
|
JSON.stringify({
|
|
531
615
|
success: false,
|
|
532
616
|
error: {
|
|
533
|
-
code: "
|
|
534
|
-
message:
|
|
617
|
+
code: "INTERNAL_ERROR",
|
|
618
|
+
message: core.error.formatMessage({ message: kitM("kit_internalError"), traceId: requestId }),
|
|
619
|
+
traceId: requestId
|
|
535
620
|
},
|
|
536
621
|
requestId
|
|
537
622
|
}),
|
|
538
623
|
{
|
|
539
|
-
status:
|
|
624
|
+
status: 500,
|
|
540
625
|
headers: { "Content-Type": "application/json" }
|
|
541
626
|
}
|
|
542
627
|
);
|
|
543
628
|
}
|
|
544
|
-
}
|
|
545
|
-
const context = {
|
|
546
|
-
event,
|
|
547
|
-
session,
|
|
548
|
-
requestId
|
|
549
629
|
};
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
);
|
|
630
|
+
const keyExchange = cryptoConfig?.transport ? cryptoConfig.transport.keyExchangePath ?? "/api/_hai/key-exchange" : void 0;
|
|
631
|
+
const deniedHandshake = accessDecision && !accessDecision.allowed && event.request.method === "POST" && event.url.pathname === keyExchange;
|
|
632
|
+
const response = deniedHandshake ? await runRequest() : transportMiddleware ? await transportMiddleware(context, runRequest) : await runRequest();
|
|
633
|
+
await captureRequest(response.status);
|
|
634
|
+
applyDefaultSecurityHeaders(response);
|
|
555
635
|
response.headers.set("X-Request-Id", requestId);
|
|
636
|
+
response.headers.set("X-Trace-Id", requestId);
|
|
556
637
|
return response;
|
|
557
|
-
}
|
|
558
|
-
if (isSvelteKitControlFlow(error2)) {
|
|
559
|
-
throw error2;
|
|
560
|
-
}
|
|
561
|
-
core.logger.error("Request failed", { requestId, error: error2 instanceof Error ? error2.message : error2 });
|
|
562
|
-
if (onError) {
|
|
563
|
-
return onError(error2, event);
|
|
564
|
-
}
|
|
565
|
-
return new Response(
|
|
566
|
-
JSON.stringify({
|
|
567
|
-
success: false,
|
|
568
|
-
error: {
|
|
569
|
-
code: "INTERNAL_ERROR",
|
|
570
|
-
message: kitM("kit_internalError")
|
|
571
|
-
},
|
|
572
|
-
requestId
|
|
573
|
-
}),
|
|
574
|
-
{
|
|
575
|
-
status: 500,
|
|
576
|
-
headers: { "Content-Type": "application/json" }
|
|
577
|
-
}
|
|
578
|
-
);
|
|
579
|
-
}
|
|
638
|
+
}));
|
|
580
639
|
};
|
|
581
640
|
}
|
|
582
641
|
function buildAuthGuard(verifyToken, loginUrl) {
|
|
@@ -584,7 +643,8 @@ function buildAuthGuard(verifyToken, loginUrl) {
|
|
|
584
643
|
if (session) {
|
|
585
644
|
return { allowed: true };
|
|
586
645
|
}
|
|
587
|
-
const
|
|
646
|
+
const routePath = event.route.id?.replace(/\/\([^/]*\)/g, "");
|
|
647
|
+
const isApiRoute = event.url.pathname.startsWith("/api/") || routePath?.startsWith("/api/") === true;
|
|
588
648
|
if (isApiRoute) {
|
|
589
649
|
return {
|
|
590
650
|
allowed: false,
|
|
@@ -635,27 +695,28 @@ function matchPath(pathname, pattern) {
|
|
|
635
695
|
}
|
|
636
696
|
return pathname === pattern;
|
|
637
697
|
}
|
|
638
|
-
function buildTransportMiddleware(
|
|
698
|
+
function buildTransportMiddleware(cryptoConfig, encryption) {
|
|
639
699
|
if (!cryptoConfig?.transport)
|
|
640
|
-
return
|
|
641
|
-
const transportOpts =
|
|
642
|
-
|
|
700
|
+
return null;
|
|
701
|
+
const transportOpts = cryptoConfig.transport;
|
|
702
|
+
return transportEncryptionMiddleware({
|
|
643
703
|
enabled: true,
|
|
644
704
|
crypto: cryptoConfig.crypto,
|
|
705
|
+
serverKeyRing: transportOpts.serverKeyRing,
|
|
706
|
+
basePath: transportOpts.basePath,
|
|
645
707
|
keyExchangePath: transportOpts.keyExchangePath,
|
|
646
708
|
excludePaths: transportOpts.excludePaths,
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
709
|
+
keyStore: transportOpts.keyStore,
|
|
710
|
+
maxPlaintextBytes: transportOpts.maxPlaintextBytes,
|
|
711
|
+
routePlaintextBytes: transportOpts.routePlaintextBytes
|
|
712
|
+
}, encryption);
|
|
651
713
|
}
|
|
652
714
|
function buildCookieProxyConfig(cryptoConfig) {
|
|
653
715
|
if (!cryptoConfig?.encryptedCookies?.length)
|
|
654
716
|
return null;
|
|
655
717
|
const key = cryptoConfig.cookieEncryptionKey ?? (typeof process !== "undefined" ? process.env?.HAI_KIT_COOKIE_KEY : void 0);
|
|
656
718
|
if (!key) {
|
|
657
|
-
|
|
658
|
-
return null;
|
|
719
|
+
throw new Error("Cookie encryption requires crypto.cookieEncryptionKey or HAI_KIT_COOKIE_KEY");
|
|
659
720
|
}
|
|
660
721
|
return {
|
|
661
722
|
names: new Set(cryptoConfig.encryptedCookies),
|
|
@@ -672,28 +733,82 @@ function sequence(...handles) {
|
|
|
672
733
|
return filtered[0];
|
|
673
734
|
}
|
|
674
735
|
return async ({ event, resolve }) => {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
736
|
+
function applyHandle(index, currentEvent, options) {
|
|
737
|
+
if (index === filtered.length)
|
|
738
|
+
return resolve(currentEvent, options);
|
|
739
|
+
return filtered[index]({
|
|
740
|
+
event: currentEvent,
|
|
741
|
+
resolve: async (nextEvent, nextOptions) => {
|
|
742
|
+
const merged = {
|
|
743
|
+
...options,
|
|
744
|
+
...nextOptions,
|
|
745
|
+
preload: options?.preload ?? nextOptions?.preload,
|
|
746
|
+
filterSerializedResponseHeaders: options?.filterSerializedResponseHeaders ?? nextOptions?.filterSerializedResponseHeaders
|
|
747
|
+
};
|
|
748
|
+
if (options?.transformPageChunk || nextOptions?.transformPageChunk) {
|
|
749
|
+
merged.transformPageChunk = async ({ html, done }) => {
|
|
750
|
+
if (nextOptions?.transformPageChunk)
|
|
751
|
+
html = await nextOptions.transformPageChunk({ html, done }) ?? "";
|
|
752
|
+
if (options?.transformPageChunk)
|
|
753
|
+
html = await options.transformPageChunk({ html, done }) ?? "";
|
|
754
|
+
return html;
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
return applyHandle(index + 1, nextEvent, merged);
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
return applyHandle(0, event);
|
|
679
762
|
};
|
|
680
763
|
}
|
|
681
|
-
function
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
764
|
+
function handler(fn) {
|
|
765
|
+
return async (event) => {
|
|
766
|
+
try {
|
|
767
|
+
return await fn(event);
|
|
768
|
+
} catch (error2) {
|
|
769
|
+
if (isResponseLike(error2)) {
|
|
770
|
+
return error2;
|
|
771
|
+
}
|
|
772
|
+
if (isSvelteKitControlFlow(error2)) {
|
|
773
|
+
throw error2;
|
|
774
|
+
}
|
|
775
|
+
const normalizedError = error2 instanceof Error ? {
|
|
776
|
+
name: error2.name,
|
|
777
|
+
message: error2.message,
|
|
778
|
+
stack: error2.stack,
|
|
779
|
+
cause: error2.cause
|
|
780
|
+
} : error2;
|
|
781
|
+
core.logger.error("Request handler error", {
|
|
782
|
+
error: normalizedError,
|
|
783
|
+
path: event.url.pathname,
|
|
784
|
+
method: event.request.method
|
|
785
|
+
});
|
|
786
|
+
return internalError();
|
|
787
|
+
}
|
|
788
|
+
};
|
|
690
789
|
}
|
|
691
|
-
|
|
790
|
+
var getKitZodMessage = core.zodValidation.createPrefixedZodMessageGetter(
|
|
791
|
+
"kit",
|
|
792
|
+
(messageKey, params) => kitM(messageKey, { params })
|
|
793
|
+
);
|
|
794
|
+
function validateParsedData(data, schema) {
|
|
795
|
+
const result = schema.safeParse(data);
|
|
796
|
+
if (result.success)
|
|
797
|
+
return { valid: true, data: result.data, errors: [] };
|
|
692
798
|
return {
|
|
693
799
|
valid: false,
|
|
694
|
-
errors:
|
|
800
|
+
errors: core.zodValidation.mapZodErrorToFormErrors(result.error, getKitZodMessage)
|
|
695
801
|
};
|
|
696
802
|
}
|
|
803
|
+
function unwrapValidationResultOrThrow(result) {
|
|
804
|
+
if (result.valid && result.data !== void 0)
|
|
805
|
+
return result.data;
|
|
806
|
+
throw badRequest(
|
|
807
|
+
result.errors[0]?.message ?? kitM("kit_validationFailed"),
|
|
808
|
+
void 0,
|
|
809
|
+
{ errors: result.errors }
|
|
810
|
+
);
|
|
811
|
+
}
|
|
697
812
|
async function validateForm(request, schema) {
|
|
698
813
|
try {
|
|
699
814
|
const contentType = request.headers.get("content-type") ?? "";
|
|
@@ -709,11 +824,7 @@ async function validateForm(request, schema) {
|
|
|
709
824
|
errors: [{ field: "_", message: kitM("kit_unsupportedContentType") }]
|
|
710
825
|
};
|
|
711
826
|
}
|
|
712
|
-
|
|
713
|
-
if (result.success) {
|
|
714
|
-
return { valid: true, data: result.data, errors: [] };
|
|
715
|
-
}
|
|
716
|
-
return createValidationResult(result.error);
|
|
827
|
+
return validateParsedData(data, schema);
|
|
717
828
|
} catch {
|
|
718
829
|
return {
|
|
719
830
|
valid: false,
|
|
@@ -723,51 +834,19 @@ async function validateForm(request, schema) {
|
|
|
723
834
|
}
|
|
724
835
|
function validateQuery(url, schema) {
|
|
725
836
|
const data = Object.fromEntries(url.searchParams);
|
|
726
|
-
|
|
727
|
-
if (result.success) {
|
|
728
|
-
return { valid: true, data: result.data, errors: [] };
|
|
729
|
-
}
|
|
730
|
-
return createValidationResult(result.error);
|
|
837
|
+
return validateParsedData(data, schema);
|
|
731
838
|
}
|
|
732
839
|
function validateParams(params, schema) {
|
|
733
|
-
|
|
734
|
-
if (result.success) {
|
|
735
|
-
return { valid: true, data: result.data, errors: [] };
|
|
736
|
-
}
|
|
737
|
-
return createValidationResult(result.error);
|
|
840
|
+
return validateParsedData(params, schema);
|
|
738
841
|
}
|
|
739
842
|
async function validateFormOrFail(request, schema) {
|
|
740
|
-
|
|
741
|
-
if (!result.valid || !result.data) {
|
|
742
|
-
throw badRequest(
|
|
743
|
-
result.errors[0]?.message ?? kitM("kit_validationFailed"),
|
|
744
|
-
void 0,
|
|
745
|
-
{ errors: result.errors }
|
|
746
|
-
);
|
|
747
|
-
}
|
|
748
|
-
return result.data;
|
|
843
|
+
return unwrapValidationResultOrThrow(await validateForm(request, schema));
|
|
749
844
|
}
|
|
750
845
|
function validateQueryOrFail(url, schema) {
|
|
751
|
-
|
|
752
|
-
if (!result.valid || !result.data) {
|
|
753
|
-
throw badRequest(
|
|
754
|
-
result.errors[0]?.message ?? kitM("kit_validationFailed"),
|
|
755
|
-
void 0,
|
|
756
|
-
{ errors: result.errors }
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
|
-
return result.data;
|
|
846
|
+
return unwrapValidationResultOrThrow(validateQuery(url, schema));
|
|
760
847
|
}
|
|
761
848
|
function validateParamsOrFail(params, schema) {
|
|
762
|
-
|
|
763
|
-
if (!result.valid || !result.data) {
|
|
764
|
-
throw badRequest(
|
|
765
|
-
result.errors[0]?.message ?? kitM("kit_validationFailed"),
|
|
766
|
-
void 0,
|
|
767
|
-
{ errors: result.errors }
|
|
768
|
-
);
|
|
769
|
-
}
|
|
770
|
-
return result.data;
|
|
849
|
+
return unwrapValidationResultOrThrow(validateParams(params, schema));
|
|
771
850
|
}
|
|
772
851
|
var IdParamSchema = z.object({
|
|
773
852
|
id: z.string().min(1, kitM("kit_idRequired"))
|
|
@@ -779,8 +858,205 @@ var PaginationQuerySchema = z.object({
|
|
|
779
858
|
search: z.string().optional()
|
|
780
859
|
});
|
|
781
860
|
|
|
861
|
+
// src/kit-auth-api.ts
|
|
862
|
+
var authApiPublicPaths = ["/api/auth/session/close", "/api/auth/logout", "/api/auth/captcha", "/api/auth/config", "/api/auth/login", "/api/auth/login/ldap", "/api/auth/login/otp", "/api/auth/register", "/api/auth/otp/send", "/api/auth/password-reset/request", "/api/auth/password-reset/confirm", "/api/auth/password/renew"];
|
|
863
|
+
function requestContext(event) {
|
|
864
|
+
let ip;
|
|
865
|
+
try {
|
|
866
|
+
ip = event.getClientAddress();
|
|
867
|
+
} catch {
|
|
868
|
+
}
|
|
869
|
+
const terminal = event.request.headers.get("x-hai-terminal");
|
|
870
|
+
return { ip, userAgent: event.request.headers.get("user-agent") ?? void 0, terminalType: terminal === "app" || terminal === "desktop" || terminal === "api" ? terminal : "web" };
|
|
871
|
+
}
|
|
872
|
+
function createAuthApi(iam) {
|
|
873
|
+
function response(result) {
|
|
874
|
+
return result.success ? ok(result.data) : fromError(result.error);
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
closeSession: handler(async ({ request, cookies }) => {
|
|
878
|
+
const input = await validateFormOrFail(request, IamLogoutInputSchema);
|
|
879
|
+
return response(await iam.auth.logout(input.accessToken ?? getAccessToken(request, cookies) ?? ""));
|
|
880
|
+
}),
|
|
881
|
+
createCaptcha: handler(async ({ request }) => {
|
|
882
|
+
const input = await validateFormOrFail(request, IamCreateCaptchaInputSchema);
|
|
883
|
+
return response(await iam.auth.createCaptcha(input));
|
|
884
|
+
}),
|
|
885
|
+
reauthenticate: handler(async ({ request, cookies }) => {
|
|
886
|
+
const input = await validateFormOrFail(request, IamReauthenticateInputSchema);
|
|
887
|
+
return response(await iam.auth.reauthenticate(getAccessToken(request, cookies) ?? "", input.password, input.operation));
|
|
888
|
+
}),
|
|
889
|
+
publicConfig: handler(async () => response(await iam.getPublicConfig())),
|
|
890
|
+
renewPassword: handler(async ({ request }) => {
|
|
891
|
+
const input = await validateFormOrFail(request, IamRenewPasswordInputSchema);
|
|
892
|
+
return response(await iam.user.renewPassword(input));
|
|
893
|
+
}),
|
|
894
|
+
login: handler(async (event) => {
|
|
895
|
+
const { request, cookies } = event;
|
|
896
|
+
const input = await validateFormOrFail(request, IamLoginInputSchema);
|
|
897
|
+
const result = await login(cookies, input, requestContext(event));
|
|
898
|
+
return result.success ? ok({ user: result.data.user }) : fromError(result.error);
|
|
899
|
+
}),
|
|
900
|
+
loginWithLdap: handler(async (event) => {
|
|
901
|
+
const { request, cookies } = event;
|
|
902
|
+
const input = await validateFormOrFail(request, IamLdapLoginInputSchema);
|
|
903
|
+
const result = await loginWithLdap(cookies, input, requestContext(event));
|
|
904
|
+
return result.success ? ok({ user: result.data.user }) : fromError(result.error);
|
|
905
|
+
}),
|
|
906
|
+
loginWithOtp: handler(async (event) => {
|
|
907
|
+
const { request, cookies } = event;
|
|
908
|
+
const input = await validateFormOrFail(request, IamOtpLoginInputSchema);
|
|
909
|
+
const result = await loginWithOtp(cookies, input, requestContext(event));
|
|
910
|
+
return result.success ? ok({ user: result.data.user }) : fromError(result.error);
|
|
911
|
+
}),
|
|
912
|
+
register: handler(async (event) => {
|
|
913
|
+
const { request, cookies } = event;
|
|
914
|
+
const input = await validateFormOrFail(request, IamRegisterInputSchema);
|
|
915
|
+
const result = await registerAndLogin(cookies, input, requestContext(event));
|
|
916
|
+
return result.success ? ok({ user: result.data.user }) : fromError(result.error);
|
|
917
|
+
}),
|
|
918
|
+
sendOtp: handler(async ({ request }) => {
|
|
919
|
+
const input = await validateFormOrFail(request, IamSendOtpInputSchema);
|
|
920
|
+
return response(await iam.auth.sendOtp(input.identifier, input));
|
|
921
|
+
}),
|
|
922
|
+
requestPasswordReset: handler(async ({ request }) => {
|
|
923
|
+
const input = await validateFormOrFail(request, IamRequestPasswordResetInputSchema);
|
|
924
|
+
return response(await iam.user.requestPasswordReset(input.identifier, input));
|
|
925
|
+
}),
|
|
926
|
+
confirmPasswordReset: handler(async ({ request }) => {
|
|
927
|
+
const input = await validateFormOrFail(request, IamConfirmPasswordResetInputSchema);
|
|
928
|
+
return response(await iam.user.confirmPasswordReset(input.token, input.newPassword));
|
|
929
|
+
})
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function createFeedbackHandler(options) {
|
|
933
|
+
const basePath = (options.basePath ?? "/api/feedback").replace(/\/+$/, "");
|
|
934
|
+
return async (event) => {
|
|
935
|
+
let actor;
|
|
936
|
+
try {
|
|
937
|
+
actor = await options.resolveContext(event);
|
|
938
|
+
} catch {
|
|
939
|
+
return fromError({ ...HaiCommonError.INTERNAL_ERROR, message: kitM("kit_internalError") });
|
|
940
|
+
}
|
|
941
|
+
if (!actor?.scopeId || !actor.userId)
|
|
942
|
+
return fromError({ ...HaiCommonError.UNAUTHORIZED, message: kitM("kit_unauthorized") });
|
|
943
|
+
if (options.feedback.isInitialized === false)
|
|
944
|
+
return fromError({ ...HaiCommonError.SERVICE_UNAVAILABLE, message: kitM("kit_feedbackUnavailable") });
|
|
945
|
+
const suffix = event.url.pathname.startsWith(`${basePath}/`) ? event.url.pathname.slice(basePath.length + 1) : "";
|
|
946
|
+
let parts;
|
|
947
|
+
try {
|
|
948
|
+
parts = suffix.split("/").map(decodeURIComponent);
|
|
949
|
+
} catch {
|
|
950
|
+
return invalid(400);
|
|
951
|
+
}
|
|
952
|
+
if (parts.length === 1 && parts[0] === "policy" && event.request.method === "GET")
|
|
953
|
+
return ok(options.feedback.budget);
|
|
954
|
+
if (parts[0] !== "reports" || parts.length > 4)
|
|
955
|
+
return fromError({ ...HaiCommonError.NOT_FOUND, message: core.i18n.coreM("core_errorNotFound") });
|
|
956
|
+
let body = {};
|
|
957
|
+
if (!["GET", "HEAD"].includes(event.request.method)) {
|
|
958
|
+
if (event.request.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json")
|
|
959
|
+
return invalid(415);
|
|
960
|
+
try {
|
|
961
|
+
body = await readBoundedJson(event.request, options.feedback.budget.maxTransportBytes);
|
|
962
|
+
} catch (error2) {
|
|
963
|
+
return invalid(error2 instanceof RangeError ? 413 : 400);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
967
|
+
return invalid(400);
|
|
968
|
+
const bodyRecord = body;
|
|
969
|
+
if (parts[1] && Object.hasOwn(bodyRecord, "id") && bodyRecord.id !== parts[1])
|
|
970
|
+
return invalid(400);
|
|
971
|
+
const input = { ...Object.fromEntries(event.url.searchParams), ...bodyRecord, ...parts[1] ? { id: parts[1] } : {} };
|
|
972
|
+
const method = event.request.method;
|
|
973
|
+
const service = options.feedback;
|
|
974
|
+
async function execute(schema, operation) {
|
|
975
|
+
const parsed = schema.safeParse(input);
|
|
976
|
+
if (!parsed.success)
|
|
977
|
+
return invalid(400);
|
|
978
|
+
try {
|
|
979
|
+
const result = await core.context.run(actor, () => operation(parsed.data));
|
|
980
|
+
return result.success ? ok(result.data) : fromError(result.error);
|
|
981
|
+
} catch {
|
|
982
|
+
return fromError({ ...HaiCommonError.INTERNAL_ERROR, message: kitM("kit_internalError") });
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
if (parts.length === 1) {
|
|
986
|
+
if (method === "POST")
|
|
987
|
+
return execute(FeedbackSubmitInputSchema, (value) => service.submit(value, actor));
|
|
988
|
+
if (method === "GET")
|
|
989
|
+
return execute(FeedbackListInputSchema, (value) => service.list(value, actor));
|
|
990
|
+
}
|
|
991
|
+
if (parts.length === 2 && method === "GET")
|
|
992
|
+
return execute(FeedbackIdInputSchema, (value) => service.get(value.id, actor));
|
|
993
|
+
if (parts.length === 3) {
|
|
994
|
+
if (parts[2] === "evidence" && method === "GET")
|
|
995
|
+
return execute(FeedbackEvidenceQuerySchema, (value) => service.getEvidence(value, actor));
|
|
996
|
+
if (parts[2] === "evidence" && method === "POST")
|
|
997
|
+
return execute(FeedbackAppendEvidenceInputSchema, (value) => service.appendEvidence(value, actor));
|
|
998
|
+
if (parts[2] === "status" && method === "PATCH")
|
|
999
|
+
return execute(FeedbackSetStatusInputSchema, (value) => service.setStatus(value, actor));
|
|
1000
|
+
if (parts[2] === "retry" && method === "POST")
|
|
1001
|
+
return execute(FeedbackRetryInputSchema, (value) => service.retry(value, actor));
|
|
1002
|
+
if (parts[2] === "complete" && method === "POST")
|
|
1003
|
+
return execute(FeedbackCompleteInputSchema, (value) => service.completeCollection(value, actor));
|
|
1004
|
+
if (parts[2] === "exports" && method === "POST")
|
|
1005
|
+
return execute(FeedbackExportInputSchema, (value) => service.export(value, actor));
|
|
1006
|
+
}
|
|
1007
|
+
if (parts.length === 4 && parts[2] === "attachments") {
|
|
1008
|
+
Object.assign(input, { attachmentId: parts[3] });
|
|
1009
|
+
if (method === "POST")
|
|
1010
|
+
return execute(FeedbackUploadInputSchema, (value) => service.uploadAttachment(value, actor));
|
|
1011
|
+
if (method === "GET")
|
|
1012
|
+
return execute(FeedbackAttachmentQuerySchema, (value) => service.getAttachment(value, actor));
|
|
1013
|
+
}
|
|
1014
|
+
if (parts.length === 4 && parts[2] === "exports" && method === "GET") {
|
|
1015
|
+
Object.assign(input, { jobId: parts[3] });
|
|
1016
|
+
return execute(FeedbackExportQuerySchema, (value) => service.getExport(value, actor));
|
|
1017
|
+
}
|
|
1018
|
+
return fromError({ ...HaiCommonError.NOT_FOUND, message: core.i18n.coreM("core_errorNotFound") });
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
function invalid(status) {
|
|
1022
|
+
return fromError({ ...HaiCommonError.INVALID_REQUEST, httpStatus: status, message: kitM("kit_validationFailed") });
|
|
1023
|
+
}
|
|
1024
|
+
async function readBoundedJson(request, maximum) {
|
|
1025
|
+
const reader = request.body?.getReader();
|
|
1026
|
+
if (!reader)
|
|
1027
|
+
return {};
|
|
1028
|
+
const decoder = new TextDecoder();
|
|
1029
|
+
let length = 0;
|
|
1030
|
+
let text = "";
|
|
1031
|
+
try {
|
|
1032
|
+
while (true) {
|
|
1033
|
+
const chunk = await reader.read();
|
|
1034
|
+
if (chunk.done)
|
|
1035
|
+
break;
|
|
1036
|
+
length += chunk.value.byteLength;
|
|
1037
|
+
if (length > maximum) {
|
|
1038
|
+
await reader.cancel();
|
|
1039
|
+
throw new RangeError("Feedback request is too large");
|
|
1040
|
+
}
|
|
1041
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
1042
|
+
}
|
|
1043
|
+
text += decoder.decode();
|
|
1044
|
+
return text ? JSON.parse(text) : {};
|
|
1045
|
+
} finally {
|
|
1046
|
+
reader.releaseLock();
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
782
1050
|
// src/kit-main.ts
|
|
783
1051
|
var kit = {
|
|
1052
|
+
/** 创建反馈的受控 catch-all HTTP handler,身份范围由宿主解析。 */
|
|
1053
|
+
createFeedbackHandler,
|
|
1054
|
+
/** 原子限流工厂;共享store生命周期由应用管理。 */
|
|
1055
|
+
middleware: {
|
|
1056
|
+
rateLimit: rateLimitMiddleware,
|
|
1057
|
+
createCacheRateLimitStore,
|
|
1058
|
+
createMemoryRateLimitStore
|
|
1059
|
+
},
|
|
784
1060
|
// ─── Handle Hook ───
|
|
785
1061
|
/** 创建 SvelteKit Handle Hook(含 auth / logging / rateLimit 内置配置) */
|
|
786
1062
|
createHandle,
|
|
@@ -788,10 +1064,10 @@ var kit = {
|
|
|
788
1064
|
sequence,
|
|
789
1065
|
/** API Handler 包装器(自动错误边界) */
|
|
790
1066
|
handler,
|
|
791
|
-
/** 基于 API 契约创建类型安全的路由 handler */
|
|
792
|
-
fromContract,
|
|
793
1067
|
// ─── 路由守卫 ───
|
|
794
1068
|
guard: {
|
|
1069
|
+
/** 选定敏感操作要求单次再鉴别;先调用现有权限与资源守卫。 */
|
|
1070
|
+
requireSensitive: requireSensitiveOperation,
|
|
795
1071
|
/** 要求权限,不满足时 throw Response(SvelteKit 控制流) */
|
|
796
1072
|
require: requirePermission,
|
|
797
1073
|
/** 检查会话是否具有指定权限(布尔) */
|
|
@@ -844,10 +1120,23 @@ var kit = {
|
|
|
844
1120
|
// ─── 客户端工具 ───
|
|
845
1121
|
client: {
|
|
846
1122
|
/** 创建统一客户端(CSRF + 传输加密透明合并) */
|
|
847
|
-
create: createKitClient
|
|
1123
|
+
create: createKitClient,
|
|
1124
|
+
/** 标准认证表单动作(Cookie 模式)。 */
|
|
1125
|
+
createAuthActions: createKitAuthActions,
|
|
1126
|
+
/**
|
|
1127
|
+
* 用解析后的 {@link KitConfig} 一键安装浏览器端传输加密
|
|
1128
|
+
* (应用层推荐入口;内部预热 crypto 并调用 installBrowserTransportFetch)
|
|
1129
|
+
*/
|
|
1130
|
+
installBrowserTransport,
|
|
1131
|
+
/** 底层入口:直接传 transport 配置安装浏览器全局 fetch 包装 */
|
|
1132
|
+
installBrowserTransportFetch
|
|
848
1133
|
},
|
|
849
1134
|
// ─── 认证工具 ───
|
|
850
1135
|
auth: {
|
|
1136
|
+
/** 标准匿名认证端点白名单。 */
|
|
1137
|
+
publicPaths: authApiPublicPaths,
|
|
1138
|
+
/** 标准配置/登录/注册/验证码/重置 handler 集合。 */
|
|
1139
|
+
createApi: createAuthApi,
|
|
851
1140
|
/** 服务端登录(密码):内部调用 iam.auth.login + 自动写入 Token Cookie */
|
|
852
1141
|
login,
|
|
853
1142
|
/** 服务端登录(OTP 验证码):内部调用 iam.auth.loginWithOtp + 自动写入 Token Cookie */
|
|
@@ -863,11 +1152,8 @@ var kit = {
|
|
|
863
1152
|
/** 写入浏览器端 Access Token(客户端 login/register 用) */
|
|
864
1153
|
setBrowserToken,
|
|
865
1154
|
/** 清除浏览器端 Access Token(客户端 logout 用) */
|
|
866
|
-
clearBrowserToken
|
|
1155
|
+
clearBrowserToken
|
|
867
1156
|
/** 创建浏览器端 Token 存储器(自定义 key 时使用) */
|
|
868
|
-
createTokenStore,
|
|
869
|
-
/** 创建浏览器端同源请求自动附加 Authorization 的 HandleFetch */
|
|
870
|
-
createHandleFetch
|
|
871
1157
|
},
|
|
872
1158
|
// ─── CRUD ───
|
|
873
1159
|
crud: {
|
|
@@ -877,10 +1163,12 @@ var kit = {
|
|
|
877
1163
|
// ─── i18n ───
|
|
878
1164
|
i18n: {
|
|
879
1165
|
/** 统一设置所有 hai 模块的默认语言 */
|
|
880
|
-
setLocale: setAllModulesLocale
|
|
1166
|
+
setLocale: setAllModulesLocale,
|
|
1167
|
+
/** 请求/SSR 语言隔离;异步处理使用同一请求上下文。 */
|
|
1168
|
+
runWithLocale: core.i18n.runWithLocale
|
|
881
1169
|
}
|
|
882
1170
|
};
|
|
883
1171
|
|
|
884
|
-
export { IdParamSchema, PaginationQuerySchema,
|
|
1172
|
+
export { IdParamSchema, PaginationQuerySchema, kit, matchPermission };
|
|
885
1173
|
//# sourceMappingURL=index.js.map
|
|
886
1174
|
//# sourceMappingURL=index.js.map
|