@ubean/server 0.1.13 → 0.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/dist/analytics-entry.d.ts +2 -0
- package/dist/analytics-entry.js +2 -0
- package/dist/cache-C84ix1Vq.js +173 -0
- package/dist/cache-b-MZlyv0.d.ts +48 -0
- package/dist/cache-directive-C1Nekkza.js +304 -0
- package/dist/cache-directive-CAxJAQyE.d.ts +175 -0
- package/dist/cache-directive.d.ts +2 -0
- package/dist/cache-directive.js +2 -0
- package/dist/cache-entry.d.ts +3 -0
- package/dist/cache-entry.js +3 -0
- package/dist/cron-entry.d.ts +2 -0
- package/dist/cron-entry.js +2 -0
- package/dist/cron-scheduler-BF33PPn4.d.ts +77 -0
- package/dist/cron-scheduler-BVuXv7nn.js +258 -0
- package/dist/database-CfpFznl-.d.ts +67 -0
- package/dist/database-DNrY44SQ.js +352 -0
- package/dist/database.d.ts +2 -0
- package/dist/database.js +2 -0
- package/dist/email-BjfRiR9b.js +354 -0
- package/dist/email-BvpEuNn_.d.ts +226 -0
- package/dist/email.d.ts +2 -0
- package/dist/email.js +2 -0
- package/dist/feature-flags-CdLwsMD2.js +657 -0
- package/dist/feature-flags-DWkS6p0D.d.ts +386 -0
- package/dist/fetch-memo-rbkxxnW4.js +338 -0
- package/dist/index.d.ts +183 -488
- package/dist/index.js +352 -2023
- package/dist/middleware.d.ts +2 -0
- package/dist/middleware.js +3 -0
- package/dist/observability-Cio6Qq1H.js +339 -0
- package/dist/observability-DUNUEjj3.d.ts +70 -0
- package/dist/observability.d.ts +2 -0
- package/dist/observability.js +2 -0
- package/dist/queue-Bwzi3mhK.js +210 -0
- package/dist/queue-GOfTAWlz.d.ts +55 -0
- package/dist/queue.d.ts +2 -0
- package/dist/queue.js +2 -0
- package/dist/realtime.d.ts +2 -0
- package/dist/realtime.js +2 -0
- package/dist/security.d.ts +2 -0
- package/dist/security.js +2 -0
- package/dist/sessions-BLqFFQTL.d.ts +217 -0
- package/dist/sessions-BsBsyFAG.js +450 -0
- package/dist/single-flight-BJyhDLdU.d.ts +422 -0
- package/dist/single-flight-mJ4ZKbx1.js +715 -0
- package/dist/sse-Ct72zhic.d.ts +95 -0
- package/dist/sse-a6Ky9Vcl.js +310 -0
- package/dist/static-DPHaovQe.js +90 -0
- package/dist/static-K2dRvjpS.d.ts +11 -0
- package/dist/static.d.ts +2 -0
- package/dist/static.js +2 -0
- package/dist/storage-BZLMaqHr.js +162 -0
- package/dist/storage-QdlPtPtR.d.ts +48 -0
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +2 -0
- package/package.json +68 -6
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { o as useStorage } from "./storage-BZLMaqHr.js";
|
|
2
|
+
//#region src/csrf.ts
|
|
3
|
+
const SAFE_METHODS = /* @__PURE__ */ new Set([
|
|
4
|
+
"GET",
|
|
5
|
+
"HEAD",
|
|
6
|
+
"OPTIONS",
|
|
7
|
+
"TRACE"
|
|
8
|
+
]);
|
|
9
|
+
/**
|
|
10
|
+
* 生成密码学安全的随机 token
|
|
11
|
+
*/
|
|
12
|
+
function defaultGenerateToken(length = 32) {
|
|
13
|
+
const bytes = new Uint8Array(length);
|
|
14
|
+
if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.getRandomValues) globalThis.crypto.getRandomValues(bytes);
|
|
15
|
+
else for (let i = 0; i < length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
16
|
+
return Buffer.from(bytes).toString("base64url");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 从 cookie header 中解析指定 cookie 值
|
|
20
|
+
*/
|
|
21
|
+
function parseCookie$1(cookieHeader, name) {
|
|
22
|
+
if (!cookieHeader) return void 0;
|
|
23
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
|
|
24
|
+
return match ? match[1] : void 0;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 校验 Origin/Referer 是否与目标 host 匹配
|
|
28
|
+
*/
|
|
29
|
+
function isOriginAllowed(c) {
|
|
30
|
+
const origin = c.req.header("origin");
|
|
31
|
+
const referer = c.req.header("referer");
|
|
32
|
+
const host = c.req.header("host");
|
|
33
|
+
if (!host) return false;
|
|
34
|
+
if (origin) try {
|
|
35
|
+
return new URL(origin).host === host;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (referer) try {
|
|
40
|
+
return new URL(referer).host === host;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
function isExcluded$1(path, exclude) {
|
|
47
|
+
if (!exclude || exclude.length === 0) return false;
|
|
48
|
+
return exclude.some((pattern) => {
|
|
49
|
+
if (pattern.endsWith("/**")) return path.startsWith(pattern.slice(0, -3));
|
|
50
|
+
if (pattern.endsWith("/*")) {
|
|
51
|
+
const prefix = pattern.slice(0, -2);
|
|
52
|
+
return path.startsWith(prefix) && !path.slice(prefix.length).includes("/");
|
|
53
|
+
}
|
|
54
|
+
return path === pattern;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function defaultErrorHandler(c) {
|
|
58
|
+
return c.json({
|
|
59
|
+
error: "CSRF Token Invalid",
|
|
60
|
+
message: "The CSRF token is missing or invalid."
|
|
61
|
+
}, 403);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 创建 CSRF 保护中间件
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* // 使用默认配置(double-submit cookie 模式)
|
|
69
|
+
* app.use('*', createCsrfMiddleware());
|
|
70
|
+
*
|
|
71
|
+
* // 仅校验 Origin
|
|
72
|
+
* app.use('*', createCsrfMiddleware({ mode: 'origin' }));
|
|
73
|
+
*
|
|
74
|
+
* // 排除 API 路径
|
|
75
|
+
* app.use('*', createCsrfMiddleware({ exclude: ['/api/webhook/**'] }));
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
function createCsrfMiddleware(options = {}) {
|
|
79
|
+
const { mode = "token", cookieName = "ubean_csrf", headerName = "x-csrf-token", fieldName = "csrfToken", tokenLength = 32, cookie: cookieOpts = {}, generateToken = () => defaultGenerateToken(tokenLength), exclude = [], handler = defaultErrorHandler } = options;
|
|
80
|
+
const cookiePath = cookieOpts.path || "/";
|
|
81
|
+
const cookieSecure = cookieOpts.secure ?? false;
|
|
82
|
+
const cookieSameSite = cookieOpts.sameSite || "lax";
|
|
83
|
+
const cookieDomain = cookieOpts.domain;
|
|
84
|
+
return async function csrfMiddleware(c, next) {
|
|
85
|
+
if (isExcluded$1(c.req.path, exclude)) {
|
|
86
|
+
await next();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const method = c.req.method.toUpperCase();
|
|
90
|
+
if (SAFE_METHODS.has(method)) {
|
|
91
|
+
if (!parseCookie$1(c.req.header("cookie"), cookieName)) {
|
|
92
|
+
const token = generateToken();
|
|
93
|
+
const parts = [`${cookieName}=${token}`, `Path=${cookiePath}`];
|
|
94
|
+
if (cookieSecure) parts.push("Secure");
|
|
95
|
+
parts.push(`SameSite=${cookieSameSite}`);
|
|
96
|
+
if (cookieDomain) parts.push(`Domain=${cookieDomain}`);
|
|
97
|
+
c.header("Set-Cookie", parts.join("; "));
|
|
98
|
+
}
|
|
99
|
+
await next();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const cookieToken = parseCookie$1(c.req.header("cookie"), cookieName);
|
|
103
|
+
let originValid = true;
|
|
104
|
+
if (mode === "origin" || mode === "both") originValid = isOriginAllowed(c);
|
|
105
|
+
let tokenValid = true;
|
|
106
|
+
if (mode === "token" || mode === "both") {
|
|
107
|
+
const headerToken = c.req.header(headerName);
|
|
108
|
+
let bodyToken;
|
|
109
|
+
if (!headerToken) try {
|
|
110
|
+
const contentType = c.req.header("content-type") || "";
|
|
111
|
+
if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) bodyToken = (await c.req.formData()).get(fieldName);
|
|
112
|
+
} catch {}
|
|
113
|
+
const requestToken = headerToken || bodyToken;
|
|
114
|
+
if (!cookieToken || !requestToken) tokenValid = false;
|
|
115
|
+
else if (cookieToken !== requestToken) tokenValid = false;
|
|
116
|
+
}
|
|
117
|
+
if (!originValid || !tokenValid) return handler(c);
|
|
118
|
+
await next();
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 生成 CSRF token(供前端使用)
|
|
123
|
+
*/
|
|
124
|
+
function generateCsrfToken(length = 32) {
|
|
125
|
+
return defaultGenerateToken(length);
|
|
126
|
+
}
|
|
127
|
+
function defineCsrf(options) {
|
|
128
|
+
return createCsrfMiddleware(options);
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/security-headers.ts
|
|
132
|
+
/**
|
|
133
|
+
* 将 CSP 指令对象序列化为 header 值
|
|
134
|
+
*/
|
|
135
|
+
function serializeCsp(directives) {
|
|
136
|
+
const parts = [];
|
|
137
|
+
for (const [key, value] of Object.entries(directives)) {
|
|
138
|
+
if (value === void 0 || value === null) continue;
|
|
139
|
+
if (value === true) parts.push(key);
|
|
140
|
+
else if (Array.isArray(value)) {
|
|
141
|
+
if (value.length > 0) parts.push(`${key} ${value.join(" ")}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return parts.join("; ");
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 将 Permissions-Policy 对象序列化为 header 值
|
|
148
|
+
*/
|
|
149
|
+
function serializePermissionsPolicy(policy) {
|
|
150
|
+
return Object.entries(policy).map(([feature, values]) => {
|
|
151
|
+
if (values.length === 0) return `${feature}=()`;
|
|
152
|
+
return `${feature}=(${values.join(" ")})`;
|
|
153
|
+
}).join(", ");
|
|
154
|
+
}
|
|
155
|
+
const DEFAULT_CSP = {
|
|
156
|
+
"default-src": ["'self'"],
|
|
157
|
+
"script-src": [
|
|
158
|
+
"'self'",
|
|
159
|
+
"'unsafe-inline'",
|
|
160
|
+
"'unsafe-eval'"
|
|
161
|
+
],
|
|
162
|
+
"style-src": ["'self'", "'unsafe-inline'"],
|
|
163
|
+
"img-src": [
|
|
164
|
+
"'self'",
|
|
165
|
+
"data:",
|
|
166
|
+
"blob:"
|
|
167
|
+
],
|
|
168
|
+
"font-src": ["'self'", "data:"],
|
|
169
|
+
"connect-src": ["'self'"],
|
|
170
|
+
"object-src": ["'none'"],
|
|
171
|
+
"base-uri": ["'self'"],
|
|
172
|
+
"form-action": ["'self'"],
|
|
173
|
+
"frame-ancestors": ["'self'"],
|
|
174
|
+
"upgrade-insecure-requests": true
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* 创建安全头中间件
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* ```typescript
|
|
181
|
+
* // 使用默认安全头
|
|
182
|
+
* app.use('*', createSecurityHeadersMiddleware());
|
|
183
|
+
*
|
|
184
|
+
* // 自定义 CSP
|
|
185
|
+
* app.use('*', createSecurityHeadersMiddleware({
|
|
186
|
+
* contentSecurityPolicy: {
|
|
187
|
+
* 'default-src': ["'self'"],
|
|
188
|
+
* 'script-src': ["'self'", 'https://cdn.example.com'],
|
|
189
|
+
* 'style-src': ["'self'", "'unsafe-inline'"],
|
|
190
|
+
* 'img-src': ["'self'", 'data:', 'https:']
|
|
191
|
+
* }
|
|
192
|
+
* }));
|
|
193
|
+
*
|
|
194
|
+
* // 禁用某些头
|
|
195
|
+
* app.use('*', createSecurityHeadersMiddleware({
|
|
196
|
+
* xFrameOptions: false,
|
|
197
|
+
* contentSecurityPolicy: false
|
|
198
|
+
* }));
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
function createSecurityHeadersMiddleware(options = {}) {
|
|
202
|
+
const { contentSecurityPolicy = DEFAULT_CSP, contentSecurityPolicyReportOnly = false, strictTransportSecurity = {
|
|
203
|
+
maxAge: 15552e3,
|
|
204
|
+
includeSubDomains: true
|
|
205
|
+
}, xFrameOptions = "SAMEORIGIN", xContentTypeOptions = "nosniff", referrerPolicy = "strict-origin-when-cross-origin", permissionsPolicy = false, crossOriginOpenerPolicy = "same-origin", crossOriginEmbedderPolicy = false, crossOriginResourcePolicy = "same-origin", exclude = [], extraHeaders = {} } = options;
|
|
206
|
+
const headers = {};
|
|
207
|
+
if (contentSecurityPolicy !== false) {
|
|
208
|
+
const cspValue = serializeCsp(contentSecurityPolicy);
|
|
209
|
+
if (cspValue) {
|
|
210
|
+
const headerName = contentSecurityPolicyReportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy";
|
|
211
|
+
headers[headerName] = cspValue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (strictTransportSecurity !== false) {
|
|
215
|
+
const sts = strictTransportSecurity;
|
|
216
|
+
let stsValue = `max-age=${sts.maxAge ?? 15552e3}`;
|
|
217
|
+
if (sts.includeSubDomains) stsValue += "; includeSubDomains";
|
|
218
|
+
if (sts.preload) stsValue += "; preload";
|
|
219
|
+
headers["Strict-Transport-Security"] = stsValue;
|
|
220
|
+
}
|
|
221
|
+
if (xFrameOptions !== false) headers["X-Frame-Options"] = xFrameOptions;
|
|
222
|
+
if (xContentTypeOptions !== false) headers["X-Content-Type-Options"] = xContentTypeOptions;
|
|
223
|
+
if (referrerPolicy !== false) headers["Referrer-Policy"] = referrerPolicy;
|
|
224
|
+
if (permissionsPolicy !== false) {
|
|
225
|
+
const ppValue = serializePermissionsPolicy(permissionsPolicy);
|
|
226
|
+
if (ppValue) headers["Permissions-Policy"] = ppValue;
|
|
227
|
+
}
|
|
228
|
+
if (crossOriginOpenerPolicy !== false) headers["Cross-Origin-Opener-Policy"] = crossOriginOpenerPolicy;
|
|
229
|
+
if (crossOriginEmbedderPolicy !== false) headers["Cross-Origin-Embedder-Policy"] = crossOriginEmbedderPolicy;
|
|
230
|
+
if (crossOriginResourcePolicy !== false) headers["Cross-Origin-Resource-Policy"] = crossOriginResourcePolicy;
|
|
231
|
+
Object.assign(headers, extraHeaders);
|
|
232
|
+
return async function securityHeadersMiddleware(c, next) {
|
|
233
|
+
if (exclude.length > 0) {
|
|
234
|
+
const path = c.req.path;
|
|
235
|
+
if (exclude.some((pattern) => path.startsWith(pattern))) {
|
|
236
|
+
await next();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
for (const [name, value] of Object.entries(headers)) c.header(name, value);
|
|
241
|
+
await next();
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function defineSecurityHeaders(options) {
|
|
245
|
+
return createSecurityHeadersMiddleware(options);
|
|
246
|
+
}
|
|
247
|
+
//#endregion
|
|
248
|
+
//#region src/sessions.ts
|
|
249
|
+
var SessionImpl = class {
|
|
250
|
+
id;
|
|
251
|
+
_store;
|
|
252
|
+
_ttl;
|
|
253
|
+
_onSave;
|
|
254
|
+
_onDestroy;
|
|
255
|
+
_data;
|
|
256
|
+
_dirty = false;
|
|
257
|
+
_destroyed = false;
|
|
258
|
+
constructor(id, initialData, _store, _ttl, _onSave, _onDestroy) {
|
|
259
|
+
this.id = id;
|
|
260
|
+
this._store = _store;
|
|
261
|
+
this._ttl = _ttl;
|
|
262
|
+
this._onSave = _onSave;
|
|
263
|
+
this._onDestroy = _onDestroy;
|
|
264
|
+
this._data = { ...initialData };
|
|
265
|
+
}
|
|
266
|
+
get(key) {
|
|
267
|
+
if (this._destroyed) return void 0;
|
|
268
|
+
return this._data[key];
|
|
269
|
+
}
|
|
270
|
+
set(key, value) {
|
|
271
|
+
if (this._destroyed) return;
|
|
272
|
+
this._data[key] = value;
|
|
273
|
+
this._dirty = true;
|
|
274
|
+
}
|
|
275
|
+
delete(key) {
|
|
276
|
+
if (this._destroyed) return;
|
|
277
|
+
if (key in this._data) {
|
|
278
|
+
delete this._data[key];
|
|
279
|
+
this._dirty = true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
has(key) {
|
|
283
|
+
if (this._destroyed) return false;
|
|
284
|
+
return key in this._data;
|
|
285
|
+
}
|
|
286
|
+
all() {
|
|
287
|
+
return { ...this._data };
|
|
288
|
+
}
|
|
289
|
+
get isDirty() {
|
|
290
|
+
return this._dirty;
|
|
291
|
+
}
|
|
292
|
+
get isDestroyed() {
|
|
293
|
+
return this._destroyed;
|
|
294
|
+
}
|
|
295
|
+
async save() {
|
|
296
|
+
if (this._destroyed) return;
|
|
297
|
+
if (this._onSave) await this._onSave(this.id, this._data);
|
|
298
|
+
else if (this._store) await this._store.set(this.id, this._data, this._ttl);
|
|
299
|
+
this._dirty = false;
|
|
300
|
+
}
|
|
301
|
+
async destroy() {
|
|
302
|
+
if (this._destroyed) return;
|
|
303
|
+
if (this._onDestroy) await this._onDestroy(this.id);
|
|
304
|
+
else if (this._store) await this._store.delete(this.id);
|
|
305
|
+
this._data = {};
|
|
306
|
+
this._destroyed = true;
|
|
307
|
+
this._dirty = false;
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
function defaultGenerateId() {
|
|
311
|
+
const bytes = /* @__PURE__ */ new Uint8Array(32);
|
|
312
|
+
if (typeof globalThis.crypto !== "undefined" && globalThis.crypto.getRandomValues) globalThis.crypto.getRandomValues(bytes);
|
|
313
|
+
else for (let i = 0; i < 32; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
314
|
+
return Buffer.from(bytes).toString("hex");
|
|
315
|
+
}
|
|
316
|
+
function parseCookie(cookieHeader, name) {
|
|
317
|
+
if (!cookieHeader) return void 0;
|
|
318
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
|
|
319
|
+
return match ? match[1] : void 0;
|
|
320
|
+
}
|
|
321
|
+
function serializeCookie(name, value, opts) {
|
|
322
|
+
const parts = [`${name}=${value}`];
|
|
323
|
+
if (opts.path) parts.push(`Path=${opts.path}`);
|
|
324
|
+
if (opts.domain) parts.push(`Domain=${opts.domain}`);
|
|
325
|
+
if (opts.secure) parts.push("Secure");
|
|
326
|
+
if (opts.httpOnly) parts.push("HttpOnly");
|
|
327
|
+
if (opts.sameSite) parts.push(`SameSite=${opts.sameSite}`);
|
|
328
|
+
return parts.join("; ");
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* 简单签名/验证(cookie 模式用,非加密级别安全)
|
|
332
|
+
* 生产环境建议使用 storage 模式或提供更强的 secret
|
|
333
|
+
*/
|
|
334
|
+
function sign(value, secret) {
|
|
335
|
+
let hash = 0;
|
|
336
|
+
const str = secret + value;
|
|
337
|
+
for (let i = 0; i < str.length; i++) {
|
|
338
|
+
const char = str.charCodeAt(i);
|
|
339
|
+
hash = (hash << 5) - hash + char;
|
|
340
|
+
hash |= 0;
|
|
341
|
+
}
|
|
342
|
+
return `${value}.${Math.abs(hash).toString(36)}`;
|
|
343
|
+
}
|
|
344
|
+
function verify(signed, secret) {
|
|
345
|
+
const idx = signed.lastIndexOf(".");
|
|
346
|
+
if (idx === -1) return null;
|
|
347
|
+
const value = signed.slice(0, idx);
|
|
348
|
+
return signed === sign(value, secret) ? value : null;
|
|
349
|
+
}
|
|
350
|
+
function isExcluded(path, exclude) {
|
|
351
|
+
if (!exclude || exclude.length === 0) return false;
|
|
352
|
+
return exclude.some((p) => path.startsWith(p));
|
|
353
|
+
}
|
|
354
|
+
function createStorageSessionStore(storage, prefix = "session:") {
|
|
355
|
+
const store = storage || useStorage();
|
|
356
|
+
return {
|
|
357
|
+
async get(id) {
|
|
358
|
+
return await store.get(`${prefix}${id}`);
|
|
359
|
+
},
|
|
360
|
+
async set(id, data, ttl) {
|
|
361
|
+
await store.set(`${prefix}${id}`, data, ttl);
|
|
362
|
+
},
|
|
363
|
+
async delete(id) {
|
|
364
|
+
await store.remove(`${prefix}${id}`);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const SESSION_CONTEXT_KEY = "__ubean_session__";
|
|
369
|
+
/**
|
|
370
|
+
* 创建 session 中间件
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```typescript
|
|
374
|
+
* // cookie 模式(默认,数据存 signed cookie)
|
|
375
|
+
* app.use('*', createSessionMiddleware({ secret: 'my-secret' }));
|
|
376
|
+
*
|
|
377
|
+
* // storage 模式(数据存服务端 KV/storage)
|
|
378
|
+
* app.use('*', createSessionMiddleware({
|
|
379
|
+
* store: createStorageSessionStore()
|
|
380
|
+
* }));
|
|
381
|
+
* ```
|
|
382
|
+
*/
|
|
383
|
+
function createSessionMiddleware(options = {}) {
|
|
384
|
+
const { cookieName = "ubean_session", ttl = 604800, cookie: cookieOpts = {}, store = null, generateId = defaultGenerateId, secret, exclude = [] } = options;
|
|
385
|
+
const cookieDefaults = {
|
|
386
|
+
path: "/",
|
|
387
|
+
httpOnly: true,
|
|
388
|
+
sameSite: "lax",
|
|
389
|
+
secure: false,
|
|
390
|
+
...cookieOpts
|
|
391
|
+
};
|
|
392
|
+
return async function sessionMiddleware(c, next) {
|
|
393
|
+
if (isExcluded(c.req.path, exclude)) {
|
|
394
|
+
await next();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
let sessionId = parseCookie(c.req.header("cookie"), cookieName);
|
|
398
|
+
let cookieData = {};
|
|
399
|
+
if (sessionId && !store && secret) {
|
|
400
|
+
const verified = verify(sessionId, secret);
|
|
401
|
+
if (verified) try {
|
|
402
|
+
cookieData = JSON.parse(Buffer.from(verified, "base64").toString("utf-8"));
|
|
403
|
+
sessionId = void 0;
|
|
404
|
+
} catch {
|
|
405
|
+
sessionId = void 0;
|
|
406
|
+
}
|
|
407
|
+
else sessionId = void 0;
|
|
408
|
+
}
|
|
409
|
+
let session;
|
|
410
|
+
if (store) {
|
|
411
|
+
if (!sessionId) {
|
|
412
|
+
sessionId = generateId();
|
|
413
|
+
session = new SessionImpl(sessionId, {}, store, ttl);
|
|
414
|
+
} else {
|
|
415
|
+
const data = await store.get(sessionId);
|
|
416
|
+
session = new SessionImpl(sessionId, data || {}, store, ttl);
|
|
417
|
+
}
|
|
418
|
+
} else session = new SessionImpl(sessionId || generateId(), cookieData, null, ttl);
|
|
419
|
+
c[SESSION_CONTEXT_KEY] = session;
|
|
420
|
+
await next();
|
|
421
|
+
if (session.isDestroyed) c.header("Set-Cookie", `${serializeCookie(cookieName, "", {
|
|
422
|
+
...cookieDefaults,
|
|
423
|
+
path: cookieDefaults.path
|
|
424
|
+
})}; Max-Age=0`);
|
|
425
|
+
else if (session.isDirty) {
|
|
426
|
+
if (store) {
|
|
427
|
+
await session.save();
|
|
428
|
+
c.header("Set-Cookie", serializeCookie(cookieName, session.id, cookieDefaults));
|
|
429
|
+
} else if (secret) {
|
|
430
|
+
const json = JSON.stringify(session.all());
|
|
431
|
+
const signed = sign(Buffer.from(json, "utf-8").toString("base64"), secret);
|
|
432
|
+
c.header("Set-Cookie", serializeCookie(cookieName, signed, cookieDefaults));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* 从 Hono Context 获取 session
|
|
439
|
+
*/
|
|
440
|
+
function useSession(c) {
|
|
441
|
+
return c[SESSION_CONTEXT_KEY] || null;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* 定义 session store 别名
|
|
445
|
+
*/
|
|
446
|
+
function defineSessionStore(store) {
|
|
447
|
+
return store;
|
|
448
|
+
}
|
|
449
|
+
//#endregion
|
|
450
|
+
export { createSecurityHeadersMiddleware as a, createCsrfMiddleware as c, useSession as i, defineCsrf as l, createStorageSessionStore as n, defineSecurityHeaders as o, defineSessionStore as r, serializeCsp as s, createSessionMiddleware as t, generateCsrfToken as u };
|