@h-ai/kit 0.1.0-alpha5
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 +202 -0
- package/README.md +121 -0
- package/dist/adapter/index.d.ts +44 -0
- package/dist/adapter/index.js +28 -0
- package/dist/adapter/index.js.map +1 -0
- package/dist/browser.d.ts +38 -0
- package/dist/browser.js +26 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunk-475WECWK.js +110 -0
- package/dist/chunk-475WECWK.js.map +1 -0
- package/dist/chunk-6TMSXG7J.js +169 -0
- package/dist/chunk-6TMSXG7J.js.map +1 -0
- package/dist/chunk-AD6S473Y.js +137 -0
- package/dist/chunk-AD6S473Y.js.map +1 -0
- package/dist/chunk-FRYJMRTP.js +71 -0
- package/dist/chunk-FRYJMRTP.js.map +1 -0
- package/dist/chunk-PHS7ZD5L.js +274 -0
- package/dist/chunk-PHS7ZD5L.js.map +1 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +4 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index-BotXLTck.d.ts +203 -0
- package/dist/index.d.ts +963 -0
- package/dist/index.js +886 -0
- package/dist/index.js.map +1 -0
- package/dist/kit-crypto-types-C-gEvx04.d.ts +253 -0
- package/dist/kit-i18n-CgpNTh9G.d.ts +206 -0
- package/dist/kit-types-BHlDNNpd.d.ts +618 -0
- package/dist/modules/a2a/index.d.ts +150 -0
- package/dist/modules/a2a/index.js +57 -0
- package/dist/modules/a2a/index.js.map +1 -0
- package/dist/modules/crypto/index.d.ts +180 -0
- package/dist/modules/crypto/index.js +151 -0
- package/dist/modules/crypto/index.js.map +1 -0
- package/package.json +81 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
import { defineCrud } from './chunk-FRYJMRTP.js';
|
|
2
|
+
export { defineCrud, resolveOptions, resolveText } from './chunk-FRYJMRTP.js';
|
|
3
|
+
import { createHandleFetch, createTokenStore, clearBrowserToken, setBrowserToken, logout, registerAndLogin, loginWithApiKey, loginWithLdap, loginWithOtp, login, createKitClient, configureAuth, getAccessToken } from './chunk-PHS7ZD5L.js';
|
|
4
|
+
import { resolveA2AConfig, handleA2ARequest } from './chunk-475WECWK.js';
|
|
5
|
+
import { transportEncryptionMiddleware } from './chunk-AD6S473Y.js';
|
|
6
|
+
import { kitM, setAllModulesLocale } from './chunk-6TMSXG7J.js';
|
|
7
|
+
import { core } from '@h-ai/core';
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
// src/guards/kit-permission.ts
|
|
11
|
+
function matchPermission(required, userPermissions) {
|
|
12
|
+
for (const userPerm of userPermissions) {
|
|
13
|
+
if (userPerm === required) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (userPerm === "*") {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
if (userPerm.endsWith(":*")) {
|
|
20
|
+
const prefix = userPerm.slice(0, -1);
|
|
21
|
+
if (required.startsWith(prefix)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
function hasPermission(session, permission) {
|
|
29
|
+
if (!session)
|
|
30
|
+
return false;
|
|
31
|
+
return matchPermission(permission, session.permissions ?? []);
|
|
32
|
+
}
|
|
33
|
+
function assertPermission(session, permission) {
|
|
34
|
+
if (!session) {
|
|
35
|
+
return new Response(
|
|
36
|
+
JSON.stringify({ success: false, error: kitM("kit_unauthorized") }),
|
|
37
|
+
{ status: 401, headers: { "Content-Type": "application/json" } }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
if (!matchPermission(permission, session.permissions ?? [])) {
|
|
41
|
+
return new Response(
|
|
42
|
+
JSON.stringify({ success: false, error: kitM("kit_forbidden", { params: { permission } }) }),
|
|
43
|
+
{ status: 403, headers: { "Content-Type": "application/json" } }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
function requirePermission(session, permission) {
|
|
49
|
+
const denied = assertPermission(session, permission);
|
|
50
|
+
if (denied) {
|
|
51
|
+
throw denied;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/kit-response.ts
|
|
56
|
+
function ok(data, requestId) {
|
|
57
|
+
const response = {
|
|
58
|
+
success: true,
|
|
59
|
+
data,
|
|
60
|
+
requestId
|
|
61
|
+
};
|
|
62
|
+
return new Response(JSON.stringify(response), {
|
|
63
|
+
status: 200,
|
|
64
|
+
headers: { "Content-Type": "application/json" }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function created(data, requestId) {
|
|
68
|
+
const response = {
|
|
69
|
+
success: true,
|
|
70
|
+
data,
|
|
71
|
+
requestId
|
|
72
|
+
};
|
|
73
|
+
return new Response(JSON.stringify(response), {
|
|
74
|
+
status: 201,
|
|
75
|
+
headers: { "Content-Type": "application/json" }
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function noContent() {
|
|
79
|
+
return new Response(null, { status: 204 });
|
|
80
|
+
}
|
|
81
|
+
function error(code, message, status = 400, requestId, details) {
|
|
82
|
+
const response = {
|
|
83
|
+
success: false,
|
|
84
|
+
error: {
|
|
85
|
+
code,
|
|
86
|
+
message,
|
|
87
|
+
details
|
|
88
|
+
},
|
|
89
|
+
requestId
|
|
90
|
+
};
|
|
91
|
+
return new Response(JSON.stringify(response), {
|
|
92
|
+
status,
|
|
93
|
+
headers: { "Content-Type": "application/json" }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function badRequest(message, requestId, details) {
|
|
97
|
+
return error("BAD_REQUEST", message, 400, requestId, details);
|
|
98
|
+
}
|
|
99
|
+
function unauthorized(message, requestId) {
|
|
100
|
+
return error("UNAUTHORIZED", message ?? kitM("kit_authRequired"), 401, requestId);
|
|
101
|
+
}
|
|
102
|
+
function forbidden(message, requestId) {
|
|
103
|
+
return error("FORBIDDEN", message ?? kitM("kit_accessDenied"), 403, requestId);
|
|
104
|
+
}
|
|
105
|
+
function notFound(message, requestId) {
|
|
106
|
+
return error("NOT_FOUND", message ?? kitM("kit_resourceNotFound"), 404, requestId);
|
|
107
|
+
}
|
|
108
|
+
function conflict(message, requestId) {
|
|
109
|
+
return error("CONFLICT", message, 409, requestId);
|
|
110
|
+
}
|
|
111
|
+
function validationError(errors, requestId) {
|
|
112
|
+
return error("VALIDATION_ERROR", kitM("kit_validationFailed"), 422, requestId, { errors });
|
|
113
|
+
}
|
|
114
|
+
function internalError(message, requestId) {
|
|
115
|
+
return error("INTERNAL_ERROR", message ?? kitM("kit_internalError"), 500, requestId);
|
|
116
|
+
}
|
|
117
|
+
function redirect(url, status = 302) {
|
|
118
|
+
return new Response(null, {
|
|
119
|
+
status,
|
|
120
|
+
headers: { Location: url }
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function fromResult(result, httpStatusMap, requestId) {
|
|
124
|
+
if (result.success) {
|
|
125
|
+
return ok(result.data, requestId);
|
|
126
|
+
}
|
|
127
|
+
const { code, message } = result.error;
|
|
128
|
+
const status = httpStatusMap?.[code] ?? 400;
|
|
129
|
+
return error(String(code), message, status, requestId);
|
|
130
|
+
}
|
|
131
|
+
function fromError(haiError, requestId) {
|
|
132
|
+
return error(String(haiError.code), haiError.message, haiError.httpStatus ?? 400, requestId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/kit-utils.ts
|
|
136
|
+
function isResponseLike(value) {
|
|
137
|
+
if (value instanceof Response)
|
|
138
|
+
return true;
|
|
139
|
+
if (typeof value === "object" && value !== null && "status" in value && "headers" in value && typeof value.text === "function" && typeof value.json === "function") {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
function isSvelteKitControlFlow(value) {
|
|
145
|
+
if (isResponseLike(value))
|
|
146
|
+
return false;
|
|
147
|
+
if (typeof value === "object" && value !== null && "status" in value) {
|
|
148
|
+
const status = value.status;
|
|
149
|
+
return typeof status === "number";
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
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
|
+
function loggingMiddleware(config = {}) {
|
|
211
|
+
const { logBody = false, logResponse = false, redactFields = ["password", "token", "secret", "authorization", "apikey", "api_key", "creditcard", "credit_card"] } = config;
|
|
212
|
+
return async (context, next) => {
|
|
213
|
+
const { event, requestId } = context;
|
|
214
|
+
const startTime = Date.now();
|
|
215
|
+
let clientIp = "unknown";
|
|
216
|
+
try {
|
|
217
|
+
clientIp = event.getClientAddress?.() ?? "unknown";
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
const logData = {
|
|
221
|
+
requestId,
|
|
222
|
+
method: event.request.method,
|
|
223
|
+
path: event.url.pathname,
|
|
224
|
+
query: Object.fromEntries(event.url.searchParams),
|
|
225
|
+
userAgent: event.request.headers.get("user-agent"),
|
|
226
|
+
ip: clientIp
|
|
227
|
+
};
|
|
228
|
+
if (logBody && event.request.method !== "GET") {
|
|
229
|
+
try {
|
|
230
|
+
const clonedRequest = event.request.clone();
|
|
231
|
+
const body = await clonedRequest.json();
|
|
232
|
+
logData.body = redactObject(body, redactFields);
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
core.logger.trace("Incoming request", { ...logData });
|
|
237
|
+
const response = await next();
|
|
238
|
+
const duration = Date.now() - startTime;
|
|
239
|
+
const responseLogData = {
|
|
240
|
+
requestId,
|
|
241
|
+
status: response.status,
|
|
242
|
+
duration
|
|
243
|
+
};
|
|
244
|
+
if (logResponse) {
|
|
245
|
+
responseLogData.headers = Object.fromEntries(response.headers);
|
|
246
|
+
}
|
|
247
|
+
core.logger.trace("Request completed", { ...responseLogData });
|
|
248
|
+
return response;
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function redactObject(obj, fields) {
|
|
252
|
+
if (typeof obj !== "object" || obj === null) {
|
|
253
|
+
return obj;
|
|
254
|
+
}
|
|
255
|
+
if (Array.isArray(obj)) {
|
|
256
|
+
return obj.map((item) => redactObject(item, fields));
|
|
257
|
+
}
|
|
258
|
+
if (obj instanceof Date || obj instanceof RegExp) {
|
|
259
|
+
return obj;
|
|
260
|
+
}
|
|
261
|
+
const result = {};
|
|
262
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
263
|
+
if (fields.includes(key.toLowerCase())) {
|
|
264
|
+
result[key] = "[REDACTED]";
|
|
265
|
+
} else if (typeof value === "object" && value !== null) {
|
|
266
|
+
result[key] = redactObject(value, fields);
|
|
267
|
+
} else {
|
|
268
|
+
result[key] = value;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
var MemoryRateLimitStore = class {
|
|
274
|
+
store = /* @__PURE__ */ new Map();
|
|
275
|
+
cleanupTimer = null;
|
|
276
|
+
/**
|
|
277
|
+
* 启动定期清理过期条目
|
|
278
|
+
*
|
|
279
|
+
* @param intervalMs - 清理间隔(毫秒)
|
|
280
|
+
*/
|
|
281
|
+
startCleanup(intervalMs) {
|
|
282
|
+
if (this.cleanupTimer)
|
|
283
|
+
return;
|
|
284
|
+
this.cleanupTimer = setInterval(() => {
|
|
285
|
+
const now = Date.now();
|
|
286
|
+
for (const [key, entry] of this.store) {
|
|
287
|
+
if (entry.resetAt < now) {
|
|
288
|
+
this.store.delete(key);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}, intervalMs);
|
|
292
|
+
if (this.cleanupTimer && typeof this.cleanupTimer === "object" && "unref" in this.cleanupTimer) {
|
|
293
|
+
this.cleanupTimer.unref();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
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
|
+
/**
|
|
306
|
+
* 原子自增(内存实现为同步操作,天然原子)
|
|
307
|
+
*
|
|
308
|
+
* @param key - 限流键
|
|
309
|
+
* @param windowMs - 窗口时长
|
|
310
|
+
* @returns 自增后的条目
|
|
311
|
+
*/
|
|
312
|
+
increment(key, windowMs) {
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
let entry = this.store.get(key);
|
|
315
|
+
if (!entry || entry.resetAt < now) {
|
|
316
|
+
entry = { count: 1, resetAt: now + windowMs };
|
|
317
|
+
} else {
|
|
318
|
+
entry.count++;
|
|
319
|
+
}
|
|
320
|
+
this.store.set(key, entry);
|
|
321
|
+
return entry;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
function rateLimitMiddleware(config) {
|
|
325
|
+
const {
|
|
326
|
+
windowMs,
|
|
327
|
+
maxRequests,
|
|
328
|
+
keyGenerator = (event) => {
|
|
329
|
+
try {
|
|
330
|
+
return event.getClientAddress?.() ?? "unknown";
|
|
331
|
+
} catch {
|
|
332
|
+
return "unknown";
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
onLimitReached
|
|
336
|
+
} = config;
|
|
337
|
+
const store = config.store ?? (() => {
|
|
338
|
+
const memStore = new MemoryRateLimitStore();
|
|
339
|
+
memStore.startCleanup(windowMs);
|
|
340
|
+
return memStore;
|
|
341
|
+
})();
|
|
342
|
+
return async (context, next) => {
|
|
343
|
+
const { event, requestId } = context;
|
|
344
|
+
const key = keyGenerator(event);
|
|
345
|
+
const now = Date.now();
|
|
346
|
+
let entry;
|
|
347
|
+
if (store.increment) {
|
|
348
|
+
entry = await store.increment(key, windowMs);
|
|
349
|
+
} else {
|
|
350
|
+
const existing = await store.get(key);
|
|
351
|
+
if (!existing || existing.resetAt < now) {
|
|
352
|
+
entry = { count: 1, resetAt: now + windowMs };
|
|
353
|
+
} else {
|
|
354
|
+
entry = { ...existing, count: existing.count + 1 };
|
|
355
|
+
}
|
|
356
|
+
await store.set(key, entry);
|
|
357
|
+
}
|
|
358
|
+
const remaining = Math.max(0, maxRequests - entry.count);
|
|
359
|
+
const resetTime = Math.ceil(entry.resetAt / 1e3);
|
|
360
|
+
if (entry.count > maxRequests) {
|
|
361
|
+
core.logger.warn("Rate limit exceeded", { key, requestId });
|
|
362
|
+
if (onLimitReached) {
|
|
363
|
+
return onLimitReached(event);
|
|
364
|
+
}
|
|
365
|
+
return new Response(
|
|
366
|
+
JSON.stringify({
|
|
367
|
+
success: false,
|
|
368
|
+
error: {
|
|
369
|
+
code: "RATE_LIMIT_EXCEEDED",
|
|
370
|
+
message: kitM("kit_rateLimitExceeded")
|
|
371
|
+
},
|
|
372
|
+
requestId
|
|
373
|
+
}),
|
|
374
|
+
{
|
|
375
|
+
status: 429,
|
|
376
|
+
headers: {
|
|
377
|
+
"Content-Type": "application/json",
|
|
378
|
+
"X-RateLimit-Limit": String(maxRequests),
|
|
379
|
+
"X-RateLimit-Remaining": "0",
|
|
380
|
+
"X-RateLimit-Reset": String(resetTime),
|
|
381
|
+
"Retry-After": String(Math.ceil((entry.resetAt - now) / 1e3))
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
const response = await next();
|
|
387
|
+
response.headers.set("X-RateLimit-Limit", String(maxRequests));
|
|
388
|
+
response.headers.set("X-RateLimit-Remaining", String(remaining));
|
|
389
|
+
response.headers.set("X-RateLimit-Reset", String(resetTime));
|
|
390
|
+
return response;
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
var ENCRYPTED_PREFIX = "enc:";
|
|
394
|
+
function createEncryptedCookieProxy(cookies, config) {
|
|
395
|
+
const { names, symmetric, encryptionKey } = config;
|
|
396
|
+
const logger = core.logger.child({ module: "kit", scope: "cookie-proxy" });
|
|
397
|
+
return new Proxy(cookies, {
|
|
398
|
+
get(target, prop, receiver) {
|
|
399
|
+
if (prop === "get") {
|
|
400
|
+
return (name, opts) => {
|
|
401
|
+
const raw = target.get(name, opts);
|
|
402
|
+
if (!raw || !names.has(name))
|
|
403
|
+
return raw;
|
|
404
|
+
if (!raw.startsWith(ENCRYPTED_PREFIX))
|
|
405
|
+
return raw;
|
|
406
|
+
try {
|
|
407
|
+
const ciphertext = raw.slice(ENCRYPTED_PREFIX.length);
|
|
408
|
+
const separatorIndex = ciphertext.indexOf(":");
|
|
409
|
+
if (separatorIndex === -1)
|
|
410
|
+
return raw;
|
|
411
|
+
const iv = ciphertext.slice(0, separatorIndex);
|
|
412
|
+
const encrypted = ciphertext.slice(separatorIndex + 1);
|
|
413
|
+
const result = symmetric.decryptWithIV(encrypted, encryptionKey, iv);
|
|
414
|
+
if (!result.success || typeof result.data !== "string") {
|
|
415
|
+
logger.warn("Cookie decryption failed, returning raw value", { name });
|
|
416
|
+
return raw;
|
|
417
|
+
}
|
|
418
|
+
return result.data;
|
|
419
|
+
} catch {
|
|
420
|
+
logger.warn("Cookie decryption error, returning raw value", { name });
|
|
421
|
+
return raw;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (prop === "set") {
|
|
426
|
+
return (name, value, opts) => {
|
|
427
|
+
if (names.has(name)) {
|
|
428
|
+
try {
|
|
429
|
+
const result = symmetric.encryptWithIV(value, encryptionKey);
|
|
430
|
+
if (result.success && result.data) {
|
|
431
|
+
value = `${ENCRYPTED_PREFIX}${result.data.iv}:${result.data.ciphertext}`;
|
|
432
|
+
} else {
|
|
433
|
+
logger.warn("Cookie encryption failed, storing plaintext", { name });
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
logger.warn("Cookie encryption error, storing plaintext", { name });
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return target.set(name, value, opts);
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
return Reflect.get(target, prop, receiver);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/hooks/kit-handle.ts
|
|
448
|
+
function generateId(prefix) {
|
|
449
|
+
const timestamp = Date.now().toString(36);
|
|
450
|
+
const random = crypto.randomUUID().replace(/-/g, "").substring(0, 8);
|
|
451
|
+
return `${prefix}_${timestamp}${random}`;
|
|
452
|
+
}
|
|
453
|
+
function createHandle(config = {}) {
|
|
454
|
+
const {
|
|
455
|
+
auth: authConfig,
|
|
456
|
+
rateLimit: rateLimitConfig,
|
|
457
|
+
logging: loggingConfig = true,
|
|
458
|
+
crypto: cryptoConfig,
|
|
459
|
+
onError,
|
|
460
|
+
guards: customGuards = [],
|
|
461
|
+
middleware: customMiddleware = [],
|
|
462
|
+
a2a: a2aInput
|
|
463
|
+
} = config;
|
|
464
|
+
const a2aResolved = resolveA2AConfig(a2aInput);
|
|
465
|
+
if (authConfig?.cookieName || authConfig?.operations) {
|
|
466
|
+
configureAuth({ cookieName: authConfig.cookieName, operations: authConfig.operations });
|
|
467
|
+
}
|
|
468
|
+
const guards = [];
|
|
469
|
+
if (authConfig?.protectedPaths?.length) {
|
|
470
|
+
guards.push({
|
|
471
|
+
guard: buildAuthGuard(authConfig.verifyToken, authConfig.loginUrl),
|
|
472
|
+
paths: authConfig.protectedPaths,
|
|
473
|
+
exclude: authConfig.publicPaths
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
guards.push(...customGuards);
|
|
477
|
+
const builtinMiddleware = [];
|
|
478
|
+
if (loggingConfig) {
|
|
479
|
+
const logOpts = typeof loggingConfig === "object" ? loggingConfig : { logBody: false };
|
|
480
|
+
builtinMiddleware.push(loggingMiddleware(logOpts));
|
|
481
|
+
}
|
|
482
|
+
if (rateLimitConfig) {
|
|
483
|
+
builtinMiddleware.push(rateLimitMiddleware({
|
|
484
|
+
windowMs: rateLimitConfig.windowMs ?? 6e4,
|
|
485
|
+
maxRequests: rateLimitConfig.maxRequests ?? 100
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
const allMiddleware = [...builtinMiddleware, ...customMiddleware];
|
|
489
|
+
const finalMiddleware = buildTransportMiddleware(allMiddleware, cryptoConfig);
|
|
490
|
+
const cookieProxyConfig = buildCookieProxyConfig(cryptoConfig);
|
|
491
|
+
return async ({ event, resolve }) => {
|
|
492
|
+
const requestId = generateId("req");
|
|
493
|
+
const locals = event.locals;
|
|
494
|
+
locals.requestId = requestId;
|
|
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
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
if (a2aResolved) {
|
|
505
|
+
const a2aResponse = await handleA2ARequest(event, requestId, a2aResolved);
|
|
506
|
+
if (a2aResponse)
|
|
507
|
+
return a2aResponse;
|
|
508
|
+
}
|
|
509
|
+
let session;
|
|
510
|
+
if (authConfig) {
|
|
511
|
+
const token = getAccessToken(event.request, event.cookies);
|
|
512
|
+
if (token) {
|
|
513
|
+
session = await authConfig.verifyToken(token) ?? void 0;
|
|
514
|
+
locals.session = session;
|
|
515
|
+
if (session) {
|
|
516
|
+
locals.accessToken = token;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
for (const guardConfig of guards) {
|
|
521
|
+
const guardResult = await executeGuard(guardConfig, event, session);
|
|
522
|
+
if (!guardResult.allowed) {
|
|
523
|
+
if (guardResult.redirect) {
|
|
524
|
+
return new Response(null, {
|
|
525
|
+
status: 302,
|
|
526
|
+
headers: { Location: guardResult.redirect }
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return new Response(
|
|
530
|
+
JSON.stringify({
|
|
531
|
+
success: false,
|
|
532
|
+
error: {
|
|
533
|
+
code: "FORBIDDEN",
|
|
534
|
+
message: guardResult.message ?? kitM("kit_accessDenied")
|
|
535
|
+
},
|
|
536
|
+
requestId
|
|
537
|
+
}),
|
|
538
|
+
{
|
|
539
|
+
status: guardResult.status ?? 403,
|
|
540
|
+
headers: { "Content-Type": "application/json" }
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const context = {
|
|
546
|
+
event,
|
|
547
|
+
session,
|
|
548
|
+
requestId
|
|
549
|
+
};
|
|
550
|
+
const response = await executeMiddlewareChain(
|
|
551
|
+
finalMiddleware,
|
|
552
|
+
context,
|
|
553
|
+
() => resolve(event)
|
|
554
|
+
);
|
|
555
|
+
response.headers.set("X-Request-Id", requestId);
|
|
556
|
+
return response;
|
|
557
|
+
} catch (error2) {
|
|
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
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function buildAuthGuard(verifyToken, loginUrl) {
|
|
583
|
+
return (event, session) => {
|
|
584
|
+
if (session) {
|
|
585
|
+
return { allowed: true };
|
|
586
|
+
}
|
|
587
|
+
const isApiRoute = event.url.pathname.startsWith("/api/");
|
|
588
|
+
if (isApiRoute) {
|
|
589
|
+
return {
|
|
590
|
+
allowed: false,
|
|
591
|
+
message: kitM("kit_authRequired"),
|
|
592
|
+
status: 401
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
if (loginUrl) {
|
|
596
|
+
const returnUrl = encodeURIComponent(event.url.pathname + event.url.search);
|
|
597
|
+
return {
|
|
598
|
+
allowed: false,
|
|
599
|
+
redirect: `${loginUrl}?returnUrl=${returnUrl}`
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
allowed: false,
|
|
604
|
+
message: kitM("kit_authRequired"),
|
|
605
|
+
status: 401
|
|
606
|
+
};
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
async function executeGuard(config, event, session) {
|
|
610
|
+
const { guard, paths, exclude } = config;
|
|
611
|
+
const pathname = event.url.pathname;
|
|
612
|
+
if (exclude?.some((pattern) => matchPath(pathname, pattern))) {
|
|
613
|
+
return { allowed: true };
|
|
614
|
+
}
|
|
615
|
+
if (paths && !paths.some((pattern) => matchPath(pathname, pattern))) {
|
|
616
|
+
return { allowed: true };
|
|
617
|
+
}
|
|
618
|
+
return guard(event, session);
|
|
619
|
+
}
|
|
620
|
+
async function executeMiddlewareChain(middleware, context, final) {
|
|
621
|
+
if (middleware.length === 0) {
|
|
622
|
+
return final();
|
|
623
|
+
}
|
|
624
|
+
const [current, ...rest] = middleware;
|
|
625
|
+
return current(context, () => executeMiddlewareChain(rest, context, final));
|
|
626
|
+
}
|
|
627
|
+
function matchPath(pathname, pattern) {
|
|
628
|
+
if (pattern.endsWith("/*")) {
|
|
629
|
+
const base = pattern.slice(0, -2);
|
|
630
|
+
return pathname === base || pathname.startsWith(`${base}/`);
|
|
631
|
+
}
|
|
632
|
+
if (pattern.endsWith("/**")) {
|
|
633
|
+
const base = pattern.slice(0, -3);
|
|
634
|
+
return pathname === base || pathname.startsWith(`${base}/`);
|
|
635
|
+
}
|
|
636
|
+
return pathname === pattern;
|
|
637
|
+
}
|
|
638
|
+
function buildTransportMiddleware(userMiddleware, cryptoConfig) {
|
|
639
|
+
if (!cryptoConfig?.transport)
|
|
640
|
+
return userMiddleware;
|
|
641
|
+
const transportOpts = typeof cryptoConfig.transport === "object" ? cryptoConfig.transport : {};
|
|
642
|
+
const transportMw = transportEncryptionMiddleware({
|
|
643
|
+
enabled: true,
|
|
644
|
+
crypto: cryptoConfig.crypto,
|
|
645
|
+
keyExchangePath: transportOpts.keyExchangePath,
|
|
646
|
+
excludePaths: transportOpts.excludePaths,
|
|
647
|
+
encryptResponse: transportOpts.encryptResponse,
|
|
648
|
+
requireEncryption: transportOpts.requireEncryption
|
|
649
|
+
});
|
|
650
|
+
return [transportMw, ...userMiddleware];
|
|
651
|
+
}
|
|
652
|
+
function buildCookieProxyConfig(cryptoConfig) {
|
|
653
|
+
if (!cryptoConfig?.encryptedCookies?.length)
|
|
654
|
+
return null;
|
|
655
|
+
const key = cryptoConfig.cookieEncryptionKey ?? (typeof process !== "undefined" ? process.env?.HAI_KIT_COOKIE_KEY : void 0);
|
|
656
|
+
if (!key) {
|
|
657
|
+
core.logger.warn("Cookie encryption configured but no key provided (set crypto.cookieEncryptionKey or HAI_KIT_COOKIE_KEY env)");
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
names: new Set(cryptoConfig.encryptedCookies),
|
|
662
|
+
symmetric: cryptoConfig.crypto.symmetric,
|
|
663
|
+
encryptionKey: key
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function sequence(...handles) {
|
|
667
|
+
const filtered = handles.filter(Boolean);
|
|
668
|
+
if (filtered.length === 0) {
|
|
669
|
+
return ({ event, resolve }) => resolve(event);
|
|
670
|
+
}
|
|
671
|
+
if (filtered.length === 1) {
|
|
672
|
+
return filtered[0];
|
|
673
|
+
}
|
|
674
|
+
return async ({ event, resolve }) => {
|
|
675
|
+
return filtered.reduceRight(
|
|
676
|
+
(next, handle) => (event2) => handle({ event: event2, resolve: next }),
|
|
677
|
+
resolve
|
|
678
|
+
)(event);
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function extractZodIssues(error2) {
|
|
682
|
+
const zodError = error2;
|
|
683
|
+
return zodError.issues ?? zodError.errors ?? [];
|
|
684
|
+
}
|
|
685
|
+
function zodIssuesToFormErrors(issues) {
|
|
686
|
+
return issues.map((issue) => ({
|
|
687
|
+
field: issue.path.join("."),
|
|
688
|
+
message: issue.message
|
|
689
|
+
}));
|
|
690
|
+
}
|
|
691
|
+
function createValidationResult(error2) {
|
|
692
|
+
return {
|
|
693
|
+
valid: false,
|
|
694
|
+
errors: zodIssuesToFormErrors(extractZodIssues(error2))
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
async function validateForm(request, schema) {
|
|
698
|
+
try {
|
|
699
|
+
const contentType = request.headers.get("content-type") ?? "";
|
|
700
|
+
let data;
|
|
701
|
+
if (contentType.includes("application/json")) {
|
|
702
|
+
data = await request.json();
|
|
703
|
+
} else if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
|
|
704
|
+
const formData = await request.formData();
|
|
705
|
+
data = Object.fromEntries(formData);
|
|
706
|
+
} else {
|
|
707
|
+
return {
|
|
708
|
+
valid: false,
|
|
709
|
+
errors: [{ field: "_", message: kitM("kit_unsupportedContentType") }]
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
const result = schema.safeParse(data);
|
|
713
|
+
if (result.success) {
|
|
714
|
+
return { valid: true, data: result.data, errors: [] };
|
|
715
|
+
}
|
|
716
|
+
return createValidationResult(result.error);
|
|
717
|
+
} catch {
|
|
718
|
+
return {
|
|
719
|
+
valid: false,
|
|
720
|
+
errors: [{ field: "_", message: kitM("kit_parseBodyFailed") }]
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
function validateQuery(url, schema) {
|
|
725
|
+
const data = Object.fromEntries(url.searchParams);
|
|
726
|
+
const result = schema.safeParse(data);
|
|
727
|
+
if (result.success) {
|
|
728
|
+
return { valid: true, data: result.data, errors: [] };
|
|
729
|
+
}
|
|
730
|
+
return createValidationResult(result.error);
|
|
731
|
+
}
|
|
732
|
+
function validateParams(params, schema) {
|
|
733
|
+
const result = schema.safeParse(params);
|
|
734
|
+
if (result.success) {
|
|
735
|
+
return { valid: true, data: result.data, errors: [] };
|
|
736
|
+
}
|
|
737
|
+
return createValidationResult(result.error);
|
|
738
|
+
}
|
|
739
|
+
async function validateFormOrFail(request, schema) {
|
|
740
|
+
const result = await validateForm(request, schema);
|
|
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;
|
|
749
|
+
}
|
|
750
|
+
function validateQueryOrFail(url, schema) {
|
|
751
|
+
const result = validateQuery(url, schema);
|
|
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;
|
|
760
|
+
}
|
|
761
|
+
function validateParamsOrFail(params, schema) {
|
|
762
|
+
const result = validateParams(params, schema);
|
|
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;
|
|
771
|
+
}
|
|
772
|
+
var IdParamSchema = z.object({
|
|
773
|
+
id: z.string().min(1, kitM("kit_idRequired"))
|
|
774
|
+
});
|
|
775
|
+
var MAX_PAGE_SIZE = 100;
|
|
776
|
+
var PaginationQuerySchema = z.object({
|
|
777
|
+
page: z.coerce.number().int().min(1).default(1),
|
|
778
|
+
pageSize: z.coerce.number().int().min(1).max(MAX_PAGE_SIZE).default(20),
|
|
779
|
+
search: z.string().optional()
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// src/kit-main.ts
|
|
783
|
+
var kit = {
|
|
784
|
+
// ─── Handle Hook ───
|
|
785
|
+
/** 创建 SvelteKit Handle Hook(含 auth / logging / rateLimit 内置配置) */
|
|
786
|
+
createHandle,
|
|
787
|
+
/** 组合多个 Handle */
|
|
788
|
+
sequence,
|
|
789
|
+
/** API Handler 包装器(自动错误边界) */
|
|
790
|
+
handler,
|
|
791
|
+
/** 基于 API 契约创建类型安全的路由 handler */
|
|
792
|
+
fromContract,
|
|
793
|
+
// ─── 路由守卫 ───
|
|
794
|
+
guard: {
|
|
795
|
+
/** 要求权限,不满足时 throw Response(SvelteKit 控制流) */
|
|
796
|
+
require: requirePermission,
|
|
797
|
+
/** 检查会话是否具有指定权限(布尔) */
|
|
798
|
+
check: hasPermission
|
|
799
|
+
},
|
|
800
|
+
// ─── API 响应 ───
|
|
801
|
+
response: {
|
|
802
|
+
/** 200 成功 */
|
|
803
|
+
ok,
|
|
804
|
+
/** 201 创建成功 */
|
|
805
|
+
created,
|
|
806
|
+
/** 204 无内容 */
|
|
807
|
+
noContent,
|
|
808
|
+
/** 自定义错误响应 */
|
|
809
|
+
error,
|
|
810
|
+
/** 400 BadRequest */
|
|
811
|
+
badRequest,
|
|
812
|
+
/** 401 Unauthorized */
|
|
813
|
+
unauthorized,
|
|
814
|
+
/** 403 Forbidden */
|
|
815
|
+
forbidden,
|
|
816
|
+
/** 404 NotFound */
|
|
817
|
+
notFound,
|
|
818
|
+
/** 409 Conflict */
|
|
819
|
+
conflict,
|
|
820
|
+
/** 422 验证错误 */
|
|
821
|
+
validationError,
|
|
822
|
+
/** 500 InternalError */
|
|
823
|
+
internalError,
|
|
824
|
+
/** 重定向 */
|
|
825
|
+
redirect,
|
|
826
|
+
/** 将 HaiResult<T> 转为标准 API Response(支持 httpStatusMap) */
|
|
827
|
+
fromResult,
|
|
828
|
+
/** 将模块错误码映射为标准 HTTP Response */
|
|
829
|
+
fromError
|
|
830
|
+
},
|
|
831
|
+
// ─── 验证 ───
|
|
832
|
+
validate: {
|
|
833
|
+
/** 验证请求体(JSON/表单),失败 throw Response */
|
|
834
|
+
body: validateFormOrFail,
|
|
835
|
+
/** 验证查询参数,失败 throw Response */
|
|
836
|
+
query: validateQueryOrFail,
|
|
837
|
+
/** 验证路径参数,失败 throw Response */
|
|
838
|
+
params: validateParamsOrFail,
|
|
839
|
+
/** 路径参数 id Schema({id: string}) */
|
|
840
|
+
IdParamSchema,
|
|
841
|
+
/** 通用分页查询 Schema(page / pageSize / search) */
|
|
842
|
+
PaginationQuerySchema
|
|
843
|
+
},
|
|
844
|
+
// ─── 客户端工具 ───
|
|
845
|
+
client: {
|
|
846
|
+
/** 创建统一客户端(CSRF + 传输加密透明合并) */
|
|
847
|
+
create: createKitClient
|
|
848
|
+
},
|
|
849
|
+
// ─── 认证工具 ───
|
|
850
|
+
auth: {
|
|
851
|
+
/** 服务端登录(密码):内部调用 iam.auth.login + 自动写入 Token Cookie */
|
|
852
|
+
login,
|
|
853
|
+
/** 服务端登录(OTP 验证码):内部调用 iam.auth.loginWithOtp + 自动写入 Token Cookie */
|
|
854
|
+
loginWithOtp,
|
|
855
|
+
/** 服务端登录(LDAP):内部调用 iam.auth.loginWithLdap + 自动写入 Token Cookie */
|
|
856
|
+
loginWithLdap,
|
|
857
|
+
/** 服务端登录(API Key):内部调用 iam.auth.loginWithApiKey + 自动写入 Token Cookie */
|
|
858
|
+
loginWithApiKey,
|
|
859
|
+
/** 服务端注册并登录:内部调用 iam.auth.registerAndLogin + 自动写入 Token Cookie */
|
|
860
|
+
registerAndLogin,
|
|
861
|
+
/** 服务端登出:内部调用 iam.auth.logout + 清除 Token Cookie */
|
|
862
|
+
logout,
|
|
863
|
+
/** 写入浏览器端 Access Token(客户端 login/register 用) */
|
|
864
|
+
setBrowserToken,
|
|
865
|
+
/** 清除浏览器端 Access Token(客户端 logout 用) */
|
|
866
|
+
clearBrowserToken,
|
|
867
|
+
/** 创建浏览器端 Token 存储器(自定义 key 时使用) */
|
|
868
|
+
createTokenStore,
|
|
869
|
+
/** 创建浏览器端同源请求自动附加 Authorization 的 HandleFetch */
|
|
870
|
+
createHandleFetch
|
|
871
|
+
},
|
|
872
|
+
// ─── CRUD ───
|
|
873
|
+
crud: {
|
|
874
|
+
/** 定义 CRUD 资源(声明式配置 → 操作对象) */
|
|
875
|
+
define: defineCrud
|
|
876
|
+
},
|
|
877
|
+
// ─── i18n ───
|
|
878
|
+
i18n: {
|
|
879
|
+
/** 统一设置所有 hai 模块的默认语言 */
|
|
880
|
+
setLocale: setAllModulesLocale
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
export { IdParamSchema, PaginationQuerySchema, defineEndpoint, kit, matchPermission };
|
|
885
|
+
//# sourceMappingURL=index.js.map
|
|
886
|
+
//# sourceMappingURL=index.js.map
|