@guren/server 0.1.1-alpha.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/chunk-GDCUIM6V.js +121 -0
- package/dist/index.d.ts +493 -0
- package/dist/index.js +1392 -0
- package/dist/vite/index.d.ts +31 -0
- package/dist/vite/index.js +7 -0
- package/package.json +49 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1392 @@
|
|
|
1
|
+
import {
|
|
2
|
+
gurenVitePlugin
|
|
3
|
+
} from "./chunk-GDCUIM6V.js";
|
|
4
|
+
|
|
5
|
+
// src/http/Application.ts
|
|
6
|
+
import { Hono } from "hono";
|
|
7
|
+
|
|
8
|
+
// src/mvc/Route.ts
|
|
9
|
+
var Route = class {
|
|
10
|
+
static registry = [];
|
|
11
|
+
static prefixStack = [];
|
|
12
|
+
static add(method, path, handler, middlewares = []) {
|
|
13
|
+
const fullPath = joinPaths(this.prefixStack, path);
|
|
14
|
+
this.registry.push({ method, path: fullPath, handler, middlewares });
|
|
15
|
+
return this;
|
|
16
|
+
}
|
|
17
|
+
static on(method, path, handler, ...middlewares) {
|
|
18
|
+
return this.add(method.toUpperCase(), path, handler, middlewares);
|
|
19
|
+
}
|
|
20
|
+
static get(path, handler, ...middlewares) {
|
|
21
|
+
return this.add("GET", path, handler, middlewares);
|
|
22
|
+
}
|
|
23
|
+
static post(path, handler, ...middlewares) {
|
|
24
|
+
return this.add("POST", path, handler, middlewares);
|
|
25
|
+
}
|
|
26
|
+
static put(path, handler, ...middlewares) {
|
|
27
|
+
return this.add("PUT", path, handler, middlewares);
|
|
28
|
+
}
|
|
29
|
+
static patch(path, handler, ...middlewares) {
|
|
30
|
+
return this.add("PATCH", path, handler, middlewares);
|
|
31
|
+
}
|
|
32
|
+
static delete(path, handler, ...middlewares) {
|
|
33
|
+
return this.add("DELETE", path, handler, middlewares);
|
|
34
|
+
}
|
|
35
|
+
static group(prefix, callback) {
|
|
36
|
+
this.prefixStack.push(prefix);
|
|
37
|
+
callback();
|
|
38
|
+
this.prefixStack.pop();
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
static mount(app) {
|
|
42
|
+
for (const route of this.registry) {
|
|
43
|
+
const handler = resolveHandler(route.handler);
|
|
44
|
+
app.on(route.method, route.path, ...route.middlewares, handler);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
static clear() {
|
|
48
|
+
this.registry.splice(0, this.registry.length);
|
|
49
|
+
}
|
|
50
|
+
static definitions() {
|
|
51
|
+
return this.registry.map(({ method, path, name }) => ({ method, path, name }));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function resolveHandler(action) {
|
|
55
|
+
if (isControllerAction(action)) {
|
|
56
|
+
const [ControllerClass, methodName] = action;
|
|
57
|
+
return async (c) => {
|
|
58
|
+
const controller = new ControllerClass();
|
|
59
|
+
controller.setContext(c);
|
|
60
|
+
const method = controller[methodName];
|
|
61
|
+
if (typeof method !== "function") {
|
|
62
|
+
throw new Error(`Controller method ${String(methodName)} is not defined on ${ControllerClass.name}.`);
|
|
63
|
+
}
|
|
64
|
+
const result = await method.call(controller, c);
|
|
65
|
+
return ensureResponse(result);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return async (c) => {
|
|
69
|
+
const result = await action(c);
|
|
70
|
+
return ensureResponse(result);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function ensureResponse(result) {
|
|
74
|
+
if (result instanceof Response) {
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
if (result === void 0 || result === null) {
|
|
78
|
+
return new Response(null, { status: 204 });
|
|
79
|
+
}
|
|
80
|
+
if (typeof result === "string") {
|
|
81
|
+
return new Response(result, {
|
|
82
|
+
headers: { "Content-Type": "text/html; charset=utf-8" }
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (typeof result === "object") {
|
|
86
|
+
return new Response(JSON.stringify(result), {
|
|
87
|
+
headers: { "Content-Type": "application/json" }
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return new Response(String(result));
|
|
91
|
+
}
|
|
92
|
+
function isControllerAction(action) {
|
|
93
|
+
return Array.isArray(action);
|
|
94
|
+
}
|
|
95
|
+
function joinPaths(prefixStack, path) {
|
|
96
|
+
const segments = [...prefixStack, path].filter(Boolean).map((segment) => segment.replace(/\/*$/u, "").replace(/^\/*/u, "")).filter(Boolean);
|
|
97
|
+
if (segments.length === 0) {
|
|
98
|
+
return "/";
|
|
99
|
+
}
|
|
100
|
+
const combined = segments.join("/");
|
|
101
|
+
return "/" + combined.replace(/\/+/gu, "/");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/mvc/ViewEngine.ts
|
|
105
|
+
var ViewEngine = class {
|
|
106
|
+
static engines = /* @__PURE__ */ new Map();
|
|
107
|
+
static register(name, renderer) {
|
|
108
|
+
this.engines.set(name, renderer);
|
|
109
|
+
}
|
|
110
|
+
static has(name) {
|
|
111
|
+
return this.engines.has(name);
|
|
112
|
+
}
|
|
113
|
+
static render(name, template, props) {
|
|
114
|
+
const engine = this.engines.get(name);
|
|
115
|
+
if (!engine) {
|
|
116
|
+
throw new Error(`View engine "${name}" has not been registered.`);
|
|
117
|
+
}
|
|
118
|
+
return engine(template, props);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/plugins/ApplicationContext.ts
|
|
123
|
+
var ApplicationContext = class {
|
|
124
|
+
constructor(application, authManager) {
|
|
125
|
+
this.application = application;
|
|
126
|
+
this.authManager = authManager;
|
|
127
|
+
}
|
|
128
|
+
get app() {
|
|
129
|
+
return this.application;
|
|
130
|
+
}
|
|
131
|
+
get hono() {
|
|
132
|
+
return this.application.hono;
|
|
133
|
+
}
|
|
134
|
+
get routes() {
|
|
135
|
+
return Route;
|
|
136
|
+
}
|
|
137
|
+
get views() {
|
|
138
|
+
return ViewEngine;
|
|
139
|
+
}
|
|
140
|
+
get auth() {
|
|
141
|
+
return this.authManager;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
// src/plugins/PluginManager.ts
|
|
146
|
+
var PluginManager = class {
|
|
147
|
+
constructor(contextFactory) {
|
|
148
|
+
this.contextFactory = contextFactory;
|
|
149
|
+
}
|
|
150
|
+
entries = [];
|
|
151
|
+
bootCompleted = false;
|
|
152
|
+
add(provider) {
|
|
153
|
+
if (this.bootCompleted) {
|
|
154
|
+
throw new Error("Cannot register providers after application has booted.");
|
|
155
|
+
}
|
|
156
|
+
const instance = this.instantiate(provider);
|
|
157
|
+
this.entries.push({ instance, registered: false, booted: false });
|
|
158
|
+
return instance;
|
|
159
|
+
}
|
|
160
|
+
addMany(providers) {
|
|
161
|
+
for (const provider of providers) {
|
|
162
|
+
this.add(provider);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async registerAll() {
|
|
166
|
+
const context = this.contextFactory();
|
|
167
|
+
for (const entry of this.entries) {
|
|
168
|
+
if (!entry.registered) {
|
|
169
|
+
if (typeof entry.instance.register === "function") {
|
|
170
|
+
await entry.instance.register(context);
|
|
171
|
+
}
|
|
172
|
+
entry.registered = true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async bootAll() {
|
|
177
|
+
const context = this.contextFactory();
|
|
178
|
+
for (const entry of this.entries) {
|
|
179
|
+
if (!entry.booted) {
|
|
180
|
+
if (typeof entry.instance.boot === "function") {
|
|
181
|
+
await entry.instance.boot(context);
|
|
182
|
+
}
|
|
183
|
+
entry.booted = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
this.bootCompleted = true;
|
|
187
|
+
}
|
|
188
|
+
instantiate(provider) {
|
|
189
|
+
if (typeof provider === "function") {
|
|
190
|
+
return new provider();
|
|
191
|
+
}
|
|
192
|
+
return provider;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// src/mvc/inertia/InertiaEngine.ts
|
|
197
|
+
var DEFAULT_TITLE = "Guren";
|
|
198
|
+
var DEFAULT_IMPORT_MAP = {
|
|
199
|
+
react: "https://esm.sh/react@19.0.0?dev",
|
|
200
|
+
"react/jsx-runtime": "https://esm.sh/react@19.0.0/jsx-runtime?dev",
|
|
201
|
+
"react/jsx-dev-runtime": "https://esm.sh/react@19.0.0/jsx-dev-runtime?dev",
|
|
202
|
+
"react-dom/client": "https://esm.sh/react-dom@19.0.0/client?dev",
|
|
203
|
+
"@inertiajs/react": "https://esm.sh/@inertiajs/react@2.2.15?dev&external=react,react-dom/client",
|
|
204
|
+
"@guren/inertia-client": "/vendor/inertia-client.tsx"
|
|
205
|
+
};
|
|
206
|
+
function inertia(component, props, options = {}) {
|
|
207
|
+
const page = {
|
|
208
|
+
component,
|
|
209
|
+
props,
|
|
210
|
+
url: options.url ?? "",
|
|
211
|
+
version: options.version
|
|
212
|
+
};
|
|
213
|
+
const request = options.request;
|
|
214
|
+
const isInertiaVisit = Boolean(request?.headers.get("X-Inertia"));
|
|
215
|
+
const prefersJson = request ? acceptsJson(request) : false;
|
|
216
|
+
if (isInertiaVisit || prefersJson) {
|
|
217
|
+
return new Response(serializePage(page), {
|
|
218
|
+
status: options.status ?? 200,
|
|
219
|
+
headers: {
|
|
220
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
221
|
+
"X-Inertia": "true",
|
|
222
|
+
"Vary": "Accept",
|
|
223
|
+
...options.version ? { "X-Inertia-Version": options.version } : {},
|
|
224
|
+
...options.headers
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const html = renderDocument(page, options);
|
|
229
|
+
return new Response(html, {
|
|
230
|
+
status: options.status ?? 200,
|
|
231
|
+
headers: {
|
|
232
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
233
|
+
"X-Inertia": "true",
|
|
234
|
+
"Vary": "Accept",
|
|
235
|
+
...options.version ? { "X-Inertia-Version": options.version } : {},
|
|
236
|
+
...options.headers
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
function renderDocument(page, options) {
|
|
241
|
+
const defaultEntry = process.env.GUREN_INERTIA_ENTRY ?? "/resources/js/app.tsx";
|
|
242
|
+
const entry = options.entry ?? defaultEntry;
|
|
243
|
+
const title = escapeHtml(options.title ?? DEFAULT_TITLE);
|
|
244
|
+
const styles = options.styles ?? parseStylesEnv(process.env.GUREN_INERTIA_STYLES);
|
|
245
|
+
const importMap = JSON.stringify(
|
|
246
|
+
{
|
|
247
|
+
imports: {
|
|
248
|
+
...DEFAULT_IMPORT_MAP,
|
|
249
|
+
...options.importMap ?? {}
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
null,
|
|
253
|
+
2
|
|
254
|
+
);
|
|
255
|
+
const serializedPage = serializePage(page);
|
|
256
|
+
const stylesheetLinks = renderStyles(styles);
|
|
257
|
+
return `<!DOCTYPE html>
|
|
258
|
+
<html lang="en">
|
|
259
|
+
<head>
|
|
260
|
+
<meta charset="utf-8" />
|
|
261
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
262
|
+
<title>${title}</title>
|
|
263
|
+
${stylesheetLinks}
|
|
264
|
+
<script type="importmap">${importMap}</script>
|
|
265
|
+
<script>window.__INERTIA_PAGE__ = ${serializedPage};</script>
|
|
266
|
+
</head>
|
|
267
|
+
<body>
|
|
268
|
+
<div id="app" data-page="${escapeAttribute(serializedPage)}"></div>
|
|
269
|
+
<script type="module" src="${entry}"></script>
|
|
270
|
+
</body>
|
|
271
|
+
</html>`;
|
|
272
|
+
}
|
|
273
|
+
function renderStyles(styles) {
|
|
274
|
+
if (!styles.length) {
|
|
275
|
+
return "";
|
|
276
|
+
}
|
|
277
|
+
return styles.map((href) => `<link rel="stylesheet" href="${escapeAttribute(href)}" />`).join("\n ");
|
|
278
|
+
}
|
|
279
|
+
function serializePage(page) {
|
|
280
|
+
return JSON.stringify(page).replace(/[<\u2028\u2029]/gu, (char) => {
|
|
281
|
+
switch (char) {
|
|
282
|
+
case "<":
|
|
283
|
+
return "\\u003c";
|
|
284
|
+
case "\u2028":
|
|
285
|
+
return "\\u2028";
|
|
286
|
+
case "\u2029":
|
|
287
|
+
return "\\u2029";
|
|
288
|
+
default:
|
|
289
|
+
return char;
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function acceptsJson(request) {
|
|
294
|
+
const accept = request.headers.get("accept")?.toLowerCase() ?? "";
|
|
295
|
+
if (!accept || accept === "*/*") {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
if (accept.includes("text/html")) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
return accept.includes("application/json") || accept.includes("json");
|
|
302
|
+
}
|
|
303
|
+
function escapeHtml(value) {
|
|
304
|
+
return value.replace(/[&<>"']/gu, (char) => {
|
|
305
|
+
switch (char) {
|
|
306
|
+
case "&":
|
|
307
|
+
return "&";
|
|
308
|
+
case "<":
|
|
309
|
+
return "<";
|
|
310
|
+
case ">":
|
|
311
|
+
return ">";
|
|
312
|
+
case '"':
|
|
313
|
+
return """;
|
|
314
|
+
case "'":
|
|
315
|
+
return "'";
|
|
316
|
+
default:
|
|
317
|
+
return char;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
function escapeAttribute(value) {
|
|
322
|
+
return value.replace(/[&"]/gu, (char) => {
|
|
323
|
+
if (char === "&") {
|
|
324
|
+
return "&";
|
|
325
|
+
}
|
|
326
|
+
return """;
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function parseStylesEnv(value) {
|
|
330
|
+
if (!value) {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
return value.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/plugins/providers/InertiaViewProvider.ts
|
|
337
|
+
var InertiaViewProvider = class {
|
|
338
|
+
register(_context) {
|
|
339
|
+
if (!ViewEngine.has("inertia")) {
|
|
340
|
+
ViewEngine.register("inertia", inertia);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/http/middleware/auth.ts
|
|
346
|
+
var AUTH_CONTEXT_KEY = "guren:auth";
|
|
347
|
+
function attachAuthContext(contextFactory) {
|
|
348
|
+
return async (ctx, next) => {
|
|
349
|
+
ctx.set(AUTH_CONTEXT_KEY, contextFactory(ctx));
|
|
350
|
+
await next();
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function resolveAuth(ctx) {
|
|
354
|
+
return ctx.get(AUTH_CONTEXT_KEY);
|
|
355
|
+
}
|
|
356
|
+
function requireAuthenticated(options = {}) {
|
|
357
|
+
const { redirectTo, status = 401, responseFactory } = options;
|
|
358
|
+
return async (ctx, next) => {
|
|
359
|
+
const auth = resolveAuth(ctx);
|
|
360
|
+
if (!auth) {
|
|
361
|
+
throw new Error("Auth context has not been attached. Did you register the auth middleware?");
|
|
362
|
+
}
|
|
363
|
+
if (!await auth.check()) {
|
|
364
|
+
if (redirectTo) {
|
|
365
|
+
return ctx.redirect(redirectTo);
|
|
366
|
+
}
|
|
367
|
+
if (responseFactory) {
|
|
368
|
+
return responseFactory();
|
|
369
|
+
}
|
|
370
|
+
return new Response(JSON.stringify({ message: "Unauthorized" }), {
|
|
371
|
+
status,
|
|
372
|
+
headers: { "Content-Type": "application/json; charset=utf-8" }
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
await next();
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function requireGuest(options = {}) {
|
|
379
|
+
const { redirectTo, status = 403, responseFactory } = options;
|
|
380
|
+
return async (ctx, next) => {
|
|
381
|
+
const auth = resolveAuth(ctx);
|
|
382
|
+
if (!auth) {
|
|
383
|
+
throw new Error("Auth context has not been attached. Did you register the auth middleware?");
|
|
384
|
+
}
|
|
385
|
+
if (!await auth.guest()) {
|
|
386
|
+
if (redirectTo) {
|
|
387
|
+
return ctx.redirect(redirectTo);
|
|
388
|
+
}
|
|
389
|
+
if (responseFactory) {
|
|
390
|
+
return responseFactory();
|
|
391
|
+
}
|
|
392
|
+
return new Response(JSON.stringify({ message: "Already authenticated" }), {
|
|
393
|
+
status,
|
|
394
|
+
headers: { "Content-Type": "application/json; charset=utf-8" }
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
await next();
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/auth/SessionGuard.ts
|
|
402
|
+
var DEFAULT_SESSION_KEY = "auth:user_id";
|
|
403
|
+
var DEFAULT_REMEMBER_KEY = "auth:remember_token";
|
|
404
|
+
var SessionGuard = class {
|
|
405
|
+
constructor(options) {
|
|
406
|
+
this.options = options;
|
|
407
|
+
}
|
|
408
|
+
cachedUser;
|
|
409
|
+
get currentSession() {
|
|
410
|
+
return this.options.session;
|
|
411
|
+
}
|
|
412
|
+
get provider() {
|
|
413
|
+
return this.options.provider;
|
|
414
|
+
}
|
|
415
|
+
sessionKey() {
|
|
416
|
+
return this.options.sessionKey ?? DEFAULT_SESSION_KEY;
|
|
417
|
+
}
|
|
418
|
+
rememberSessionKey() {
|
|
419
|
+
return this.options.rememberSessionKey ?? DEFAULT_REMEMBER_KEY;
|
|
420
|
+
}
|
|
421
|
+
async loadRememberedUser() {
|
|
422
|
+
if (!this.currentSession || !this.provider.getRememberToken) {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
const rememberToken = this.currentSession.get(this.rememberSessionKey());
|
|
426
|
+
if (!rememberToken) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
const user = await this.provider.retrieveByCredentials({ rememberToken });
|
|
430
|
+
if (!user) {
|
|
431
|
+
this.currentSession.forget(this.rememberSessionKey());
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
const providerToken = await this.provider.getRememberToken?.(user);
|
|
435
|
+
if (!providerToken || providerToken !== rememberToken) {
|
|
436
|
+
this.currentSession.forget(this.rememberSessionKey());
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
this.cachedUser = user;
|
|
440
|
+
await this.provider.setRememberToken?.(user, rememberToken);
|
|
441
|
+
return user;
|
|
442
|
+
}
|
|
443
|
+
async resolveUser() {
|
|
444
|
+
if (this.cachedUser !== void 0) {
|
|
445
|
+
return this.cachedUser;
|
|
446
|
+
}
|
|
447
|
+
const session = this.currentSession;
|
|
448
|
+
if (!session) {
|
|
449
|
+
this.cachedUser = null;
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
const identifier = session.get(this.sessionKey());
|
|
453
|
+
if (identifier == null) {
|
|
454
|
+
const remembered = await this.loadRememberedUser();
|
|
455
|
+
this.cachedUser = remembered;
|
|
456
|
+
return remembered;
|
|
457
|
+
}
|
|
458
|
+
const user = await this.provider.retrieveById(identifier);
|
|
459
|
+
if (!user) {
|
|
460
|
+
session.forget(this.sessionKey());
|
|
461
|
+
this.cachedUser = null;
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
this.cachedUser = user;
|
|
465
|
+
return user;
|
|
466
|
+
}
|
|
467
|
+
async check() {
|
|
468
|
+
return await this.resolveUser() !== null;
|
|
469
|
+
}
|
|
470
|
+
async guest() {
|
|
471
|
+
return !await this.check();
|
|
472
|
+
}
|
|
473
|
+
async user() {
|
|
474
|
+
const user = await this.resolveUser();
|
|
475
|
+
return user ?? null;
|
|
476
|
+
}
|
|
477
|
+
async id() {
|
|
478
|
+
const session = this.currentSession;
|
|
479
|
+
if (!session) {
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
return session.get(this.sessionKey()) ?? null;
|
|
483
|
+
}
|
|
484
|
+
async remember(user) {
|
|
485
|
+
if (!this.currentSession || !this.provider.setRememberToken) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const token = globalThis.crypto.randomUUID();
|
|
489
|
+
await this.provider.setRememberToken?.(user, token);
|
|
490
|
+
this.currentSession.set(this.rememberSessionKey(), token);
|
|
491
|
+
}
|
|
492
|
+
async login(user, remember = false) {
|
|
493
|
+
const castUser = user;
|
|
494
|
+
const session = this.currentSession;
|
|
495
|
+
if (!session) {
|
|
496
|
+
throw new Error("SessionGuard: session middleware is required to use the session guard.");
|
|
497
|
+
}
|
|
498
|
+
const identifier = this.provider.getId(castUser);
|
|
499
|
+
session.set(this.sessionKey(), identifier);
|
|
500
|
+
this.cachedUser = castUser;
|
|
501
|
+
if (remember) {
|
|
502
|
+
await this.remember(castUser);
|
|
503
|
+
} else {
|
|
504
|
+
session.forget(this.rememberSessionKey());
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async logout() {
|
|
508
|
+
const session = this.currentSession;
|
|
509
|
+
if (!session) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
session.forget(this.sessionKey());
|
|
513
|
+
session.forget(this.rememberSessionKey());
|
|
514
|
+
this.cachedUser = null;
|
|
515
|
+
}
|
|
516
|
+
async attempt(credentials, remember = false) {
|
|
517
|
+
const user = await this.validate(credentials);
|
|
518
|
+
if (!user) {
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
await this.login(user, remember);
|
|
522
|
+
return true;
|
|
523
|
+
}
|
|
524
|
+
async validate(credentials) {
|
|
525
|
+
const user = await this.provider.retrieveByCredentials(credentials);
|
|
526
|
+
if (!user) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
const valid = await this.provider.validateCredentials(user, credentials);
|
|
530
|
+
if (!valid) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
return user;
|
|
534
|
+
}
|
|
535
|
+
session() {
|
|
536
|
+
return this.currentSession;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// src/plugins/providers/AuthServiceProvider.ts
|
|
541
|
+
var DEFAULT_GUARD = "web";
|
|
542
|
+
var DEFAULT_PROVIDER = "users";
|
|
543
|
+
var AuthServiceProvider = class {
|
|
544
|
+
register(context) {
|
|
545
|
+
const auth = context.auth;
|
|
546
|
+
if (!auth.guardNames().length) {
|
|
547
|
+
auth.registerGuard(DEFAULT_GUARD, createDefaultGuardFactory(DEFAULT_PROVIDER));
|
|
548
|
+
auth.setDefaultGuard(DEFAULT_GUARD);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
boot(context) {
|
|
552
|
+
const { app, auth } = context;
|
|
553
|
+
app.use("*", attachAuthContext((ctx) => auth.createAuthContext(ctx)));
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
function createDefaultGuardFactory(providerName) {
|
|
557
|
+
return ({ ctx, session, manager }) => {
|
|
558
|
+
const provider = manager.getProvider(providerName);
|
|
559
|
+
return new SessionGuard({
|
|
560
|
+
provider,
|
|
561
|
+
session
|
|
562
|
+
});
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/http/middleware/session.ts
|
|
567
|
+
import { getCookie, setCookie, deleteCookie } from "hono/cookie";
|
|
568
|
+
var MemorySessionStore = class {
|
|
569
|
+
constructor(now = () => Date.now()) {
|
|
570
|
+
this.now = now;
|
|
571
|
+
}
|
|
572
|
+
store = /* @__PURE__ */ new Map();
|
|
573
|
+
async read(id) {
|
|
574
|
+
const entry = this.store.get(id);
|
|
575
|
+
if (!entry) {
|
|
576
|
+
return void 0;
|
|
577
|
+
}
|
|
578
|
+
if (entry.expiresAt <= this.now()) {
|
|
579
|
+
this.store.delete(id);
|
|
580
|
+
return void 0;
|
|
581
|
+
}
|
|
582
|
+
return { ...entry.data };
|
|
583
|
+
}
|
|
584
|
+
async write(id, data, ttlSeconds) {
|
|
585
|
+
const expiresAt = this.now() + ttlSeconds * 1e3;
|
|
586
|
+
this.store.set(id, { data: { ...data }, expiresAt });
|
|
587
|
+
}
|
|
588
|
+
async destroy(id) {
|
|
589
|
+
this.store.delete(id);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
var DEFAULT_COOKIE_NAME = "guren.session";
|
|
593
|
+
var DEFAULT_TTL_SECONDS = 60 * 60 * 2;
|
|
594
|
+
var SESSION_CONTEXT_KEY = "guren:session";
|
|
595
|
+
var SessionImpl = class {
|
|
596
|
+
constructor(id, initialData, isNew) {
|
|
597
|
+
this.isNew = isNew;
|
|
598
|
+
this.currentId = id;
|
|
599
|
+
this.originalId = id;
|
|
600
|
+
this.data = { ...initialData };
|
|
601
|
+
}
|
|
602
|
+
currentId;
|
|
603
|
+
originalId;
|
|
604
|
+
data;
|
|
605
|
+
dirty = false;
|
|
606
|
+
destroyed = false;
|
|
607
|
+
regenerated = false;
|
|
608
|
+
get id() {
|
|
609
|
+
return this.currentId;
|
|
610
|
+
}
|
|
611
|
+
get(key) {
|
|
612
|
+
return this.data[key];
|
|
613
|
+
}
|
|
614
|
+
set(key, value) {
|
|
615
|
+
this.data[key] = value;
|
|
616
|
+
this.dirty = true;
|
|
617
|
+
}
|
|
618
|
+
has(key) {
|
|
619
|
+
return Object.prototype.hasOwnProperty.call(this.data, key);
|
|
620
|
+
}
|
|
621
|
+
forget(key) {
|
|
622
|
+
if (this.has(key)) {
|
|
623
|
+
delete this.data[key];
|
|
624
|
+
this.dirty = true;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
flush() {
|
|
628
|
+
if (Object.keys(this.data).length > 0) {
|
|
629
|
+
this.data = {};
|
|
630
|
+
this.dirty = true;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
all() {
|
|
634
|
+
return { ...this.data };
|
|
635
|
+
}
|
|
636
|
+
regenerate() {
|
|
637
|
+
this.currentId = globalThis.crypto.randomUUID();
|
|
638
|
+
this.regenerated = true;
|
|
639
|
+
this.dirty = true;
|
|
640
|
+
}
|
|
641
|
+
invalidate() {
|
|
642
|
+
this.flush();
|
|
643
|
+
this.destroyed = true;
|
|
644
|
+
}
|
|
645
|
+
markTouched() {
|
|
646
|
+
this.dirty = true;
|
|
647
|
+
}
|
|
648
|
+
wasDestroyed() {
|
|
649
|
+
return this.destroyed;
|
|
650
|
+
}
|
|
651
|
+
wasRegenerated() {
|
|
652
|
+
return this.regenerated;
|
|
653
|
+
}
|
|
654
|
+
shouldPersist() {
|
|
655
|
+
return this.dirty || this.isNew;
|
|
656
|
+
}
|
|
657
|
+
snapshot() {
|
|
658
|
+
return { ...this.data };
|
|
659
|
+
}
|
|
660
|
+
originalSessionId() {
|
|
661
|
+
return this.originalId;
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
function createSessionMiddleware(options = {}) {
|
|
665
|
+
const {
|
|
666
|
+
cookieName = DEFAULT_COOKIE_NAME,
|
|
667
|
+
cookiePath = "/",
|
|
668
|
+
cookieDomain,
|
|
669
|
+
cookieSecure = true,
|
|
670
|
+
cookieSameSite = "Lax",
|
|
671
|
+
cookieHttpOnly = true,
|
|
672
|
+
cookieMaxAgeSeconds,
|
|
673
|
+
ttlSeconds = DEFAULT_TTL_SECONDS,
|
|
674
|
+
store = new MemorySessionStore()
|
|
675
|
+
} = options;
|
|
676
|
+
return async (ctx, next) => {
|
|
677
|
+
const existingId = getCookie(ctx, cookieName);
|
|
678
|
+
const sessionId = existingId ?? globalThis.crypto.randomUUID();
|
|
679
|
+
const isNew = !existingId;
|
|
680
|
+
const initialData = existingId ? await store.read(existingId) ?? {} : {};
|
|
681
|
+
const session = new SessionImpl(sessionId, initialData, isNew);
|
|
682
|
+
ctx.set(SESSION_CONTEXT_KEY, session);
|
|
683
|
+
try {
|
|
684
|
+
await next();
|
|
685
|
+
} finally {
|
|
686
|
+
if (session.wasDestroyed()) {
|
|
687
|
+
await store.destroy(session.originalSessionId());
|
|
688
|
+
deleteCookie(ctx, cookieName, {
|
|
689
|
+
path: cookiePath,
|
|
690
|
+
domain: cookieDomain,
|
|
691
|
+
secure: cookieSecure,
|
|
692
|
+
sameSite: cookieSameSite,
|
|
693
|
+
httpOnly: cookieHttpOnly
|
|
694
|
+
});
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (!session.shouldPersist()) {
|
|
698
|
+
if (existingId) {
|
|
699
|
+
await store.write(existingId, session.snapshot(), ttlSeconds);
|
|
700
|
+
setCookie(ctx, cookieName, existingId, {
|
|
701
|
+
path: cookiePath,
|
|
702
|
+
domain: cookieDomain,
|
|
703
|
+
secure: cookieSecure,
|
|
704
|
+
sameSite: cookieSameSite,
|
|
705
|
+
httpOnly: cookieHttpOnly,
|
|
706
|
+
maxAge: cookieMaxAgeSeconds ?? ttlSeconds
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const nextId = session.id;
|
|
712
|
+
await store.write(nextId, session.snapshot(), ttlSeconds);
|
|
713
|
+
setCookie(ctx, cookieName, nextId, {
|
|
714
|
+
path: cookiePath,
|
|
715
|
+
domain: cookieDomain,
|
|
716
|
+
secure: cookieSecure,
|
|
717
|
+
sameSite: cookieSameSite,
|
|
718
|
+
httpOnly: cookieHttpOnly,
|
|
719
|
+
maxAge: cookieMaxAgeSeconds ?? ttlSeconds
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function getSessionFromContext(ctx) {
|
|
725
|
+
return ctx.get(SESSION_CONTEXT_KEY);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/auth/RequestAuthContext.ts
|
|
729
|
+
var RequestAuthContext = class {
|
|
730
|
+
constructor(manager, ctx, currentSession, resolveGuard) {
|
|
731
|
+
this.manager = manager;
|
|
732
|
+
this.ctx = ctx;
|
|
733
|
+
this.currentSession = currentSession;
|
|
734
|
+
this.resolveGuard = resolveGuard;
|
|
735
|
+
}
|
|
736
|
+
guardCache = /* @__PURE__ */ new Map();
|
|
737
|
+
guard(name) {
|
|
738
|
+
const key = name ?? this.manager.getDefaultGuard();
|
|
739
|
+
if (!this.guardCache.has(key)) {
|
|
740
|
+
const guard = this.resolveGuard(name);
|
|
741
|
+
this.guardCache.set(key, guard);
|
|
742
|
+
}
|
|
743
|
+
return this.guardCache.get(key);
|
|
744
|
+
}
|
|
745
|
+
session() {
|
|
746
|
+
return this.currentSession;
|
|
747
|
+
}
|
|
748
|
+
async check() {
|
|
749
|
+
return this.guard().check();
|
|
750
|
+
}
|
|
751
|
+
async guest() {
|
|
752
|
+
return this.guard().guest();
|
|
753
|
+
}
|
|
754
|
+
async user() {
|
|
755
|
+
return this.guard().user();
|
|
756
|
+
}
|
|
757
|
+
async id() {
|
|
758
|
+
return this.guard().id();
|
|
759
|
+
}
|
|
760
|
+
async login(user, remember) {
|
|
761
|
+
await this.guard().login(user, remember);
|
|
762
|
+
}
|
|
763
|
+
async attempt(credentials, remember) {
|
|
764
|
+
return this.guard().attempt(credentials, remember);
|
|
765
|
+
}
|
|
766
|
+
async logout() {
|
|
767
|
+
await this.guard().logout();
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// src/auth/AuthManager.ts
|
|
772
|
+
var DEFAULT_GUARD2 = "web";
|
|
773
|
+
var AuthManager = class {
|
|
774
|
+
guards = /* @__PURE__ */ new Map();
|
|
775
|
+
providers = /* @__PURE__ */ new Map();
|
|
776
|
+
defaultGuard;
|
|
777
|
+
constructor(options = {}) {
|
|
778
|
+
this.defaultGuard = options.defaultGuard ?? DEFAULT_GUARD2;
|
|
779
|
+
}
|
|
780
|
+
registerGuard(name, factory) {
|
|
781
|
+
this.guards.set(name, { factory });
|
|
782
|
+
}
|
|
783
|
+
registerProvider(name, factory) {
|
|
784
|
+
this.providers.set(name, { factory });
|
|
785
|
+
}
|
|
786
|
+
getProvider(name) {
|
|
787
|
+
const entry = this.providers.get(name);
|
|
788
|
+
if (!entry) {
|
|
789
|
+
throw new Error(`AuthManager: provider "${name}" has not been registered.`);
|
|
790
|
+
}
|
|
791
|
+
if (!entry.instance) {
|
|
792
|
+
const instance = entry.factory(this);
|
|
793
|
+
entry.instance = instance;
|
|
794
|
+
this.providers.set(name, entry);
|
|
795
|
+
}
|
|
796
|
+
return entry.instance;
|
|
797
|
+
}
|
|
798
|
+
createGuard(name, context) {
|
|
799
|
+
const entry = this.guards.get(name);
|
|
800
|
+
if (!entry) {
|
|
801
|
+
throw new Error(`AuthManager: guard "${name}" has not been registered.`);
|
|
802
|
+
}
|
|
803
|
+
return entry.factory(context);
|
|
804
|
+
}
|
|
805
|
+
guardNames() {
|
|
806
|
+
return Array.from(this.guards.keys());
|
|
807
|
+
}
|
|
808
|
+
setDefaultGuard(name) {
|
|
809
|
+
if (!this.guards.has(name)) {
|
|
810
|
+
throw new Error(`AuthManager: cannot set default guard to unregistered guard "${name}".`);
|
|
811
|
+
}
|
|
812
|
+
this.defaultGuard = name;
|
|
813
|
+
}
|
|
814
|
+
getDefaultGuard() {
|
|
815
|
+
return this.defaultGuard;
|
|
816
|
+
}
|
|
817
|
+
createAuthContext(ctx, options = {}) {
|
|
818
|
+
const guardName = options.guard ?? this.defaultGuard;
|
|
819
|
+
const session = getSessionFromContext(ctx);
|
|
820
|
+
const guardFactory = (name) => {
|
|
821
|
+
const targetName = name ?? guardName;
|
|
822
|
+
return this.createGuard(targetName, {
|
|
823
|
+
ctx,
|
|
824
|
+
session,
|
|
825
|
+
manager: this
|
|
826
|
+
});
|
|
827
|
+
};
|
|
828
|
+
return new RequestAuthContext(this, ctx, session, guardFactory);
|
|
829
|
+
}
|
|
830
|
+
async attempt(name, ctx, credentials, remember) {
|
|
831
|
+
const guard = this.createAuthContext(ctx, { guard: name }).guard(name);
|
|
832
|
+
return guard.attempt(credentials, remember);
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
// src/auth/providers/UserProvider.ts
|
|
837
|
+
var BaseUserProvider = class {
|
|
838
|
+
async setRememberToken(user, token) {
|
|
839
|
+
if (typeof user.setRememberToken === "function") {
|
|
840
|
+
await user.setRememberToken(token);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async getRememberToken(user) {
|
|
844
|
+
if (typeof user.getRememberToken === "function") {
|
|
845
|
+
return await user.getRememberToken() ?? null;
|
|
846
|
+
}
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
// src/auth/password/ScryptHasher.ts
|
|
852
|
+
var ScryptHasher = class {
|
|
853
|
+
algorithm;
|
|
854
|
+
memoryCost;
|
|
855
|
+
timeCost;
|
|
856
|
+
cost;
|
|
857
|
+
constructor(options = {}) {
|
|
858
|
+
this.algorithm = options.algorithm ?? "argon2id";
|
|
859
|
+
this.memoryCost = options.memoryCost;
|
|
860
|
+
this.timeCost = options.timeCost;
|
|
861
|
+
this.cost = options.cost;
|
|
862
|
+
}
|
|
863
|
+
async hash(plain) {
|
|
864
|
+
if (this.algorithm === "bcrypt") {
|
|
865
|
+
return Bun.password.hash(plain, {
|
|
866
|
+
algorithm: "bcrypt",
|
|
867
|
+
cost: this.cost
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
return Bun.password.hash(plain, {
|
|
871
|
+
algorithm: this.algorithm,
|
|
872
|
+
memoryCost: this.memoryCost,
|
|
873
|
+
timeCost: this.timeCost
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
async verify(hashed, plain) {
|
|
877
|
+
return Bun.password.verify(plain, hashed);
|
|
878
|
+
}
|
|
879
|
+
needsRehash(hashed) {
|
|
880
|
+
if (this.algorithm === "bcrypt") {
|
|
881
|
+
if (!hashed.startsWith("$2")) {
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
if (this.cost != null) {
|
|
885
|
+
const costSegment = hashed.slice(4, 6);
|
|
886
|
+
const parsedCost = Number.parseInt(costSegment, 10);
|
|
887
|
+
if (!Number.isNaN(parsedCost) && parsedCost !== this.cost) {
|
|
888
|
+
return true;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
if (!hashed.startsWith(`$${this.algorithm}$`)) {
|
|
894
|
+
return true;
|
|
895
|
+
}
|
|
896
|
+
const [, , , parameterSegment] = hashed.split("$");
|
|
897
|
+
if (!parameterSegment) {
|
|
898
|
+
return false;
|
|
899
|
+
}
|
|
900
|
+
const params = Object.fromEntries(
|
|
901
|
+
parameterSegment.split(",").map((pair) => pair.split("=")).filter((parts) => parts.length === 2)
|
|
902
|
+
);
|
|
903
|
+
if (this.memoryCost != null) {
|
|
904
|
+
const memory = Number(params.m);
|
|
905
|
+
if (!Number.isNaN(memory) && memory !== this.memoryCost) {
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
if (this.timeCost != null) {
|
|
910
|
+
const time = Number(params.t);
|
|
911
|
+
if (!Number.isNaN(time) && time !== this.timeCost) {
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// src/auth/providers/ModelUserProvider.ts
|
|
920
|
+
var ModelUserProvider = class extends BaseUserProvider {
|
|
921
|
+
constructor(model, options = {}) {
|
|
922
|
+
super();
|
|
923
|
+
this.model = model;
|
|
924
|
+
this.idColumn = options.idColumn ?? "id";
|
|
925
|
+
this.usernameColumn = options.usernameColumn ?? "email";
|
|
926
|
+
this.passwordColumn = options.passwordColumn ?? "password";
|
|
927
|
+
this.rememberTokenColumn = options.rememberTokenColumn ?? "remember_token";
|
|
928
|
+
this.hasher = options.hasher ?? new ScryptHasher();
|
|
929
|
+
this.credentialsPasswordField = options.credentialsPasswordField ?? "password";
|
|
930
|
+
}
|
|
931
|
+
idColumn;
|
|
932
|
+
usernameColumn;
|
|
933
|
+
passwordColumn;
|
|
934
|
+
rememberTokenColumn;
|
|
935
|
+
hasher;
|
|
936
|
+
credentialsPasswordField;
|
|
937
|
+
cast(record) {
|
|
938
|
+
if (record && typeof record === "object") {
|
|
939
|
+
return record;
|
|
940
|
+
}
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
async retrieveById(identifier) {
|
|
944
|
+
const record = await this.model.find(identifier, this.idColumn);
|
|
945
|
+
return this.cast(record);
|
|
946
|
+
}
|
|
947
|
+
async retrieveByCredentials(credentials) {
|
|
948
|
+
const rememberToken = credentials["rememberToken"] ?? credentials["remember_token"];
|
|
949
|
+
if (rememberToken != null) {
|
|
950
|
+
const records2 = await this.model.where({ [this.rememberTokenColumn]: rememberToken });
|
|
951
|
+
return this.cast(records2[0] ?? null);
|
|
952
|
+
}
|
|
953
|
+
const username = credentials[this.usernameColumn];
|
|
954
|
+
if (username == null) {
|
|
955
|
+
return null;
|
|
956
|
+
}
|
|
957
|
+
const records = await this.model.where({ [this.usernameColumn]: username });
|
|
958
|
+
return this.cast(records[0] ?? null);
|
|
959
|
+
}
|
|
960
|
+
async validateCredentials(user, credentials) {
|
|
961
|
+
const plain = credentials[this.credentialsPasswordField];
|
|
962
|
+
if (typeof plain !== "string") {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
const hashed = user[this.passwordColumn];
|
|
966
|
+
if (typeof hashed !== "string") {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
return this.hasher.verify(hashed, plain);
|
|
970
|
+
}
|
|
971
|
+
getId(user) {
|
|
972
|
+
return user[this.idColumn];
|
|
973
|
+
}
|
|
974
|
+
async setRememberToken(user, token) {
|
|
975
|
+
if (typeof user[this.rememberTokenColumn] !== "undefined") {
|
|
976
|
+
;
|
|
977
|
+
user[this.rememberTokenColumn] = token;
|
|
978
|
+
await this.model.update({ [this.idColumn]: this.getId(user) }, { [this.rememberTokenColumn]: token });
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
async getRememberToken(user) {
|
|
982
|
+
const token = user[this.rememberTokenColumn];
|
|
983
|
+
if (token == null) {
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
return String(token);
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
// src/http/Application.ts
|
|
991
|
+
var Application = class {
|
|
992
|
+
constructor(options = {}) {
|
|
993
|
+
this.options = options;
|
|
994
|
+
this.hono = new Hono();
|
|
995
|
+
this.authManager = new AuthManager();
|
|
996
|
+
this.plugins = new PluginManager(() => this.resolveContext());
|
|
997
|
+
this.registerDefaultProviders();
|
|
998
|
+
if (Array.isArray(this.options.providers)) {
|
|
999
|
+
this.plugins.addMany(this.options.providers);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
hono;
|
|
1003
|
+
plugins;
|
|
1004
|
+
context;
|
|
1005
|
+
authManager;
|
|
1006
|
+
get auth() {
|
|
1007
|
+
return this.authManager;
|
|
1008
|
+
}
|
|
1009
|
+
/**
|
|
1010
|
+
* Mounts all routes that were defined through the Route DSL.
|
|
1011
|
+
*/
|
|
1012
|
+
mountRoutes() {
|
|
1013
|
+
Route.mount(this.hono);
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Allows registering global middlewares directly on the underlying Hono app.
|
|
1017
|
+
*/
|
|
1018
|
+
use(path, ...middleware) {
|
|
1019
|
+
this.hono.use(path, ...middleware);
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Executes the optional boot callback and mounts the registered routes.
|
|
1023
|
+
*/
|
|
1024
|
+
async boot() {
|
|
1025
|
+
await this.plugins.registerAll();
|
|
1026
|
+
await this.options.boot?.(this.hono);
|
|
1027
|
+
this.mountRoutes();
|
|
1028
|
+
await this.plugins.bootAll();
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* Fetch handler to integrate with Bun.serve or any standard Fetch runtime.
|
|
1032
|
+
*/
|
|
1033
|
+
async fetch(request, env, executionCtx) {
|
|
1034
|
+
return this.hono.fetch(request, env, executionCtx);
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Convenience helper to start a Bun server when available.
|
|
1038
|
+
*/
|
|
1039
|
+
listen(options = {}) {
|
|
1040
|
+
if (!Bun) {
|
|
1041
|
+
throw new Error("Bun runtime is required to call Application.listen");
|
|
1042
|
+
}
|
|
1043
|
+
const { port = 3e3, hostname = "0.0.0.0" } = options;
|
|
1044
|
+
Bun.serve({
|
|
1045
|
+
port,
|
|
1046
|
+
hostname,
|
|
1047
|
+
fetch: (request) => this.fetch(request)
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
register(provider) {
|
|
1051
|
+
this.plugins.add(provider);
|
|
1052
|
+
return this;
|
|
1053
|
+
}
|
|
1054
|
+
registerMany(providers) {
|
|
1055
|
+
this.plugins.addMany(providers);
|
|
1056
|
+
return this;
|
|
1057
|
+
}
|
|
1058
|
+
resolveContext() {
|
|
1059
|
+
if (!this.context) {
|
|
1060
|
+
this.context = new ApplicationContext(this, this.authManager);
|
|
1061
|
+
}
|
|
1062
|
+
return this.context;
|
|
1063
|
+
}
|
|
1064
|
+
registerDefaultProviders() {
|
|
1065
|
+
this.plugins.add(InertiaViewProvider);
|
|
1066
|
+
this.plugins.add(AuthServiceProvider);
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
// src/http/dev-assets.ts
|
|
1071
|
+
import { serveStatic } from "hono/bun";
|
|
1072
|
+
import { dirname, extname, resolve } from "path";
|
|
1073
|
+
import { fileURLToPath } from "url";
|
|
1074
|
+
var DEFAULT_PREFIX = "/resources/js";
|
|
1075
|
+
var DEFAULT_VENDOR_PATH = "/vendor/inertia-client.tsx";
|
|
1076
|
+
var DEFAULT_JSX_RUNTIME = "https://esm.sh/react@19.0.0/jsx-dev-runtime?dev";
|
|
1077
|
+
var gurenInertiaClient = fileURLToPath(new URL("../../../inertia-client/src/app.tsx", import.meta.url));
|
|
1078
|
+
function registerDevAssets(app, options) {
|
|
1079
|
+
if (typeof Bun === "undefined") {
|
|
1080
|
+
throw new Error("Bun runtime is required for dev asset serving.");
|
|
1081
|
+
}
|
|
1082
|
+
const moduleDir = options.importMeta ? dirname(fileURLToPath(options.importMeta.url)) : void 0;
|
|
1083
|
+
const resourcesDir = options.resourcesDir ?? (moduleDir ? resolve(moduleDir, options.resourcesPath ?? "../resources") : void 0);
|
|
1084
|
+
if (!resourcesDir) {
|
|
1085
|
+
throw new Error("registerDevAssets requires either `resourcesDir` or `importMeta`.");
|
|
1086
|
+
}
|
|
1087
|
+
const prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
1088
|
+
const inertiaClientPath = options.inertiaClientPath ?? DEFAULT_VENDOR_PATH;
|
|
1089
|
+
const inertiaClientSource = options.inertiaClientSource ?? gurenInertiaClient;
|
|
1090
|
+
const jsxRuntimeUrl = options.jsxRuntimeUrl ?? DEFAULT_JSX_RUNTIME;
|
|
1091
|
+
const resourcesJsDir = resolve(resourcesDir, "js");
|
|
1092
|
+
const reactImportPattern = /from\s+['"]react['"]/u;
|
|
1093
|
+
const transpilerOptions = {
|
|
1094
|
+
target: "browser",
|
|
1095
|
+
jsx: "transform",
|
|
1096
|
+
jsxFactory: "React.createElement",
|
|
1097
|
+
jsxFragment: "React.Fragment",
|
|
1098
|
+
define: {
|
|
1099
|
+
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "development")
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
const tsxTranspiler = new Bun.Transpiler({ loader: "tsx", ...transpilerOptions });
|
|
1103
|
+
const tsTranspiler = new Bun.Transpiler({ loader: "ts", ...transpilerOptions });
|
|
1104
|
+
app.hono.get(`${prefix}/*`, (ctx) => handleTranspileRequest(ctx, resourcesJsDir, prefix, reactImportPattern, tsxTranspiler, tsTranspiler, jsxRuntimeUrl));
|
|
1105
|
+
if (options.inertiaClient !== false) {
|
|
1106
|
+
app.hono.get(inertiaClientPath, () => transpileFile(inertiaClientSource, reactImportPattern, tsxTranspiler, tsTranspiler, jsxRuntimeUrl));
|
|
1107
|
+
}
|
|
1108
|
+
const publicPathOption = options.publicPath === void 0 ? "../public" : options.publicPath;
|
|
1109
|
+
const publicDir = options.publicDir ?? (moduleDir && publicPathOption ? resolve(moduleDir, publicPathOption) : void 0);
|
|
1110
|
+
const shouldServePublic = publicDir && publicPathOption !== false;
|
|
1111
|
+
if (shouldServePublic) {
|
|
1112
|
+
const publicRoute = options.publicRoute ?? "/public/*";
|
|
1113
|
+
const rewriteRequestPath = createStaticRewrite(publicRoute);
|
|
1114
|
+
app.use(
|
|
1115
|
+
publicRoute,
|
|
1116
|
+
serveStatic({
|
|
1117
|
+
root: publicDir,
|
|
1118
|
+
rewriteRequestPath
|
|
1119
|
+
})
|
|
1120
|
+
);
|
|
1121
|
+
if (options.favicon !== false) {
|
|
1122
|
+
app.hono.get("/favicon.ico", () => new Response(null, { status: 204 }));
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
function createStaticRewrite(route) {
|
|
1127
|
+
const wildcardIndex = route.indexOf("*");
|
|
1128
|
+
const base = wildcardIndex >= 0 ? route.slice(0, wildcardIndex) : route;
|
|
1129
|
+
const normalizedBase = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
1130
|
+
if (!normalizedBase) {
|
|
1131
|
+
return (path) => path || "/";
|
|
1132
|
+
}
|
|
1133
|
+
return (path) => {
|
|
1134
|
+
if (!path.startsWith(normalizedBase)) {
|
|
1135
|
+
return path || "/";
|
|
1136
|
+
}
|
|
1137
|
+
const remainder = path.slice(normalizedBase.length);
|
|
1138
|
+
if (!remainder) {
|
|
1139
|
+
return "/";
|
|
1140
|
+
}
|
|
1141
|
+
return remainder.startsWith("/") ? remainder : `/${remainder}`;
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
async function handleTranspileRequest(ctx, resourcesJsDir, prefix, reactImportPattern, tsxTranspiler, tsTranspiler, jsxRuntimeUrl) {
|
|
1145
|
+
const relative = ctx.req.path.slice(prefix.length + 1);
|
|
1146
|
+
const fsPath = resolve(resourcesJsDir, relative);
|
|
1147
|
+
if (!fsPath.startsWith(resourcesJsDir)) {
|
|
1148
|
+
return ctx.notFound();
|
|
1149
|
+
}
|
|
1150
|
+
return transpileFile(fsPath, reactImportPattern, tsxTranspiler, tsTranspiler, jsxRuntimeUrl);
|
|
1151
|
+
}
|
|
1152
|
+
async function transpileFile(fsPath, reactImportPattern, tsxTranspiler, tsTranspiler, jsxRuntimeUrl) {
|
|
1153
|
+
const candidates = buildCandidatePaths(fsPath);
|
|
1154
|
+
let filePath;
|
|
1155
|
+
let file;
|
|
1156
|
+
for (const candidate of candidates) {
|
|
1157
|
+
const bunFile = Bun.file(candidate);
|
|
1158
|
+
if (await bunFile.exists()) {
|
|
1159
|
+
filePath = candidate;
|
|
1160
|
+
file = bunFile;
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (!file || !filePath) {
|
|
1165
|
+
return new Response("Not Found", { status: 404 });
|
|
1166
|
+
}
|
|
1167
|
+
const ext = extname(filePath);
|
|
1168
|
+
let source = await file.text();
|
|
1169
|
+
if (ext === ".tsx" && !reactImportPattern.test(source)) {
|
|
1170
|
+
source = "import React from 'react'\n" + source;
|
|
1171
|
+
}
|
|
1172
|
+
if (ext === ".tsx" || ext === ".ts") {
|
|
1173
|
+
const transpiled = ext === ".tsx" ? tsxTranspiler.transformSync(source, {
|
|
1174
|
+
loader: "tsx",
|
|
1175
|
+
sourceMap: isDev() ? "inline" : false,
|
|
1176
|
+
filename: filePath
|
|
1177
|
+
}) : tsTranspiler.transformSync(source, {
|
|
1178
|
+
loader: "ts",
|
|
1179
|
+
sourceMap: isDev() ? "inline" : false,
|
|
1180
|
+
filename: filePath
|
|
1181
|
+
});
|
|
1182
|
+
const helpers = collectJsxHelpers(transpiled);
|
|
1183
|
+
const runtimeShim = helpers.size ? createJsxRuntimeShim(helpers, jsxRuntimeUrl) : "";
|
|
1184
|
+
return new Response(runtimeShim + transpiled, {
|
|
1185
|
+
headers: {
|
|
1186
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
1187
|
+
"Cache-Control": isDev() ? "no-cache" : "public, max-age=31536000"
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
const body = await file.arrayBuffer();
|
|
1192
|
+
return new Response(body, {
|
|
1193
|
+
headers: {
|
|
1194
|
+
"Content-Type": file.type || "application/octet-stream",
|
|
1195
|
+
"Cache-Control": isDev() ? "no-cache" : "public, max-age=31536000"
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
function buildCandidatePaths(fsPath) {
|
|
1200
|
+
const ext = extname(fsPath);
|
|
1201
|
+
if (ext) {
|
|
1202
|
+
return [fsPath];
|
|
1203
|
+
}
|
|
1204
|
+
return [
|
|
1205
|
+
`${fsPath}.tsx`,
|
|
1206
|
+
`${fsPath}.ts`,
|
|
1207
|
+
`${fsPath}.jsx`,
|
|
1208
|
+
`${fsPath}.js`
|
|
1209
|
+
];
|
|
1210
|
+
}
|
|
1211
|
+
function collectJsxHelpers(code) {
|
|
1212
|
+
const helpers = /* @__PURE__ */ new Set();
|
|
1213
|
+
const pattern = /(jsxDEV|jsx|jsxs|Fragment)_[0-9a-z]+/gu;
|
|
1214
|
+
for (const match of code.matchAll(pattern)) {
|
|
1215
|
+
helpers.add(match[0]);
|
|
1216
|
+
}
|
|
1217
|
+
return helpers;
|
|
1218
|
+
}
|
|
1219
|
+
function createJsxRuntimeShim(helpers, runtimeUrl) {
|
|
1220
|
+
const assignments = Array.from(helpers).map((helper) => {
|
|
1221
|
+
const base = helper.split("_")[0];
|
|
1222
|
+
return `const ${helper} = __jsxRuntime.${base};`;
|
|
1223
|
+
});
|
|
1224
|
+
return `import * as __jsxRuntime from "${runtimeUrl}";
|
|
1225
|
+
${assignments.join("\n")}
|
|
1226
|
+
`;
|
|
1227
|
+
}
|
|
1228
|
+
function isDev() {
|
|
1229
|
+
return (process.env.NODE_ENV ?? "development") !== "production";
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/http/inertia-assets.ts
|
|
1233
|
+
import { serveStatic as serveStatic2 } from "hono/bun";
|
|
1234
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
1235
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1236
|
+
var DEFAULT_STYLES_ENTRY = "/public/assets/app.css";
|
|
1237
|
+
var DEFAULT_SCRIPT_ENTRY = "/assets/app.js";
|
|
1238
|
+
function configureInertiaAssets(app, options) {
|
|
1239
|
+
const stylesEntry = options.stylesEntry ?? DEFAULT_STYLES_ENTRY;
|
|
1240
|
+
process.env.GUREN_INERTIA_STYLES = process.env.GUREN_INERTIA_STYLES ?? stylesEntry;
|
|
1241
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
1242
|
+
if (!isProduction) {
|
|
1243
|
+
registerDevAssets(app, options);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const scriptEntry = options.scriptEntry ?? DEFAULT_SCRIPT_ENTRY;
|
|
1247
|
+
process.env.GUREN_INERTIA_ENTRY = process.env.GUREN_INERTIA_ENTRY ?? scriptEntry;
|
|
1248
|
+
const moduleDir = options.importMeta ? dirname2(fileURLToPath2(options.importMeta.url)) : void 0;
|
|
1249
|
+
const publicPathOption = options.publicPath === void 0 ? "../public" : options.publicPath;
|
|
1250
|
+
const publicDir = options.publicDir ?? (moduleDir && publicPathOption ? resolve2(moduleDir, publicPathOption) : void 0);
|
|
1251
|
+
if (!publicDir || publicPathOption === false) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const publicRoute = options.publicRoute ?? "/public/*";
|
|
1255
|
+
const rewriteRequestPath = createStaticRewrite(publicRoute);
|
|
1256
|
+
app.use(
|
|
1257
|
+
publicRoute,
|
|
1258
|
+
serveStatic2({
|
|
1259
|
+
root: publicDir,
|
|
1260
|
+
rewriteRequestPath
|
|
1261
|
+
})
|
|
1262
|
+
);
|
|
1263
|
+
if (options.favicon !== false) {
|
|
1264
|
+
app.hono.get("/favicon.ico", () => new Response(null, { status: 204 }));
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/http/request.ts
|
|
1269
|
+
function isPlainObject(value) {
|
|
1270
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1271
|
+
}
|
|
1272
|
+
async function parseRequestPayload(ctx) {
|
|
1273
|
+
const contentType = ctx.req.header("content-type") ?? "";
|
|
1274
|
+
if (contentType.includes("application/json")) {
|
|
1275
|
+
const body = await ctx.req.json().catch(() => ({}));
|
|
1276
|
+
return isPlainObject(body) ? body : {};
|
|
1277
|
+
}
|
|
1278
|
+
if (typeof ctx.req.parseBody === "function") {
|
|
1279
|
+
const form = await ctx.req.parseBody();
|
|
1280
|
+
return Object.fromEntries(
|
|
1281
|
+
Object.entries(form).map(([key, value]) => [key, Array.isArray(value) ? value[0] : value])
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
return {};
|
|
1285
|
+
}
|
|
1286
|
+
function formatValidationErrors(error, fallbackMessage = "The provided data is invalid.") {
|
|
1287
|
+
const errors = {};
|
|
1288
|
+
for (const issue of error.issues ?? []) {
|
|
1289
|
+
const field = issue.path?.[0];
|
|
1290
|
+
if (typeof field === "string" && !errors[field]) {
|
|
1291
|
+
errors[field] = issue.message;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
if (Object.keys(errors).length === 0) {
|
|
1295
|
+
errors.message = fallbackMessage;
|
|
1296
|
+
}
|
|
1297
|
+
return errors;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// src/mvc/Controller.ts
|
|
1301
|
+
var Controller = class {
|
|
1302
|
+
context;
|
|
1303
|
+
setContext(context) {
|
|
1304
|
+
this.context = context;
|
|
1305
|
+
}
|
|
1306
|
+
get ctx() {
|
|
1307
|
+
if (!this.context) {
|
|
1308
|
+
throw new Error("Controller context has not been set.");
|
|
1309
|
+
}
|
|
1310
|
+
return this.context;
|
|
1311
|
+
}
|
|
1312
|
+
get request() {
|
|
1313
|
+
return this.ctx.req;
|
|
1314
|
+
}
|
|
1315
|
+
get auth() {
|
|
1316
|
+
const auth = this.ctx.get(AUTH_CONTEXT_KEY);
|
|
1317
|
+
if (!auth) {
|
|
1318
|
+
throw new Error("Controller auth helper requires the auth middleware. Make sure AuthServiceProvider is registered.");
|
|
1319
|
+
}
|
|
1320
|
+
return auth;
|
|
1321
|
+
}
|
|
1322
|
+
inertia(component, props, options = {}) {
|
|
1323
|
+
const ctx = this.ctx;
|
|
1324
|
+
const { url: overrideUrl, ...rest } = options;
|
|
1325
|
+
const url = overrideUrl ?? ctx.req.path ?? ctx.req.url ?? "";
|
|
1326
|
+
return inertia(component, props, {
|
|
1327
|
+
...rest,
|
|
1328
|
+
url,
|
|
1329
|
+
request: ctx.req.raw
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
json(data, init = {}) {
|
|
1333
|
+
return new Response(JSON.stringify(data), {
|
|
1334
|
+
...init,
|
|
1335
|
+
headers: {
|
|
1336
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1337
|
+
...init.headers
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
text(body, init = {}) {
|
|
1342
|
+
return new Response(body, {
|
|
1343
|
+
...init,
|
|
1344
|
+
headers: {
|
|
1345
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
1346
|
+
...init.headers
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
redirect(url, options = {}) {
|
|
1351
|
+
const { status = 302, headers } = options;
|
|
1352
|
+
return new Response(null, {
|
|
1353
|
+
status,
|
|
1354
|
+
headers: {
|
|
1355
|
+
Location: url,
|
|
1356
|
+
...headers
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
// src/http/middleware/index.ts
|
|
1363
|
+
function defineMiddleware(handler) {
|
|
1364
|
+
return handler;
|
|
1365
|
+
}
|
|
1366
|
+
export {
|
|
1367
|
+
Application,
|
|
1368
|
+
ApplicationContext,
|
|
1369
|
+
AuthManager,
|
|
1370
|
+
AuthServiceProvider,
|
|
1371
|
+
BaseUserProvider,
|
|
1372
|
+
Controller,
|
|
1373
|
+
InertiaViewProvider,
|
|
1374
|
+
MemorySessionStore,
|
|
1375
|
+
ModelUserProvider,
|
|
1376
|
+
Route,
|
|
1377
|
+
ScryptHasher,
|
|
1378
|
+
SessionGuard,
|
|
1379
|
+
ViewEngine,
|
|
1380
|
+
attachAuthContext,
|
|
1381
|
+
configureInertiaAssets,
|
|
1382
|
+
createSessionMiddleware,
|
|
1383
|
+
defineMiddleware,
|
|
1384
|
+
formatValidationErrors,
|
|
1385
|
+
getSessionFromContext,
|
|
1386
|
+
gurenVitePlugin,
|
|
1387
|
+
inertia,
|
|
1388
|
+
parseRequestPayload,
|
|
1389
|
+
registerDevAssets,
|
|
1390
|
+
requireAuthenticated,
|
|
1391
|
+
requireGuest
|
|
1392
|
+
};
|