@guren/server 1.0.0-rc.9 → 1.0.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.
Files changed (68) hide show
  1. package/dist/{Application-DtWDHXr1.d.ts → Application-BnsyCKXY.d.ts} +79 -8
  2. package/dist/AuthManager-SfhCNkAU.d.ts +79 -0
  3. package/dist/{BroadcastManager-AkIWUGJo.d.ts → BroadcastManager-CGWl9rUO.d.ts} +5 -0
  4. package/dist/{ConsoleKernel-CqCVrdZs.d.ts → ConsoleKernel-BDtBETjm.d.ts} +1 -1
  5. package/dist/{Gate-CNkBYf8m.d.ts → Gate-CynjZCtS.d.ts} +5 -0
  6. package/dist/{I18nManager-Dtgzsf5n.d.ts → I18nManager-BiSoczfV.d.ts} +6 -1
  7. package/dist/McpServiceProvider-JW6PDVMD.js +7 -0
  8. package/dist/api-token-BSSCLlFW.d.ts +541 -0
  9. package/dist/auth/index.d.ts +9 -327
  10. package/dist/auth/index.js +59 -6684
  11. package/dist/authorization/index.d.ts +2 -2
  12. package/dist/authorization/index.js +19 -604
  13. package/dist/broadcasting/index.d.ts +2 -2
  14. package/dist/broadcasting/index.js +12 -895
  15. package/dist/cache/index.js +8 -809
  16. package/dist/chunk-2T6JN4VR.js +1563 -0
  17. package/dist/chunk-44F7JQ7I.js +950 -0
  18. package/dist/chunk-74HTZG3V.js +331 -0
  19. package/dist/chunk-A3ISJVEV.js +598 -0
  20. package/dist/chunk-CSDKWLFD.js +652 -0
  21. package/dist/chunk-CSRQTEQA.js +839 -0
  22. package/dist/chunk-DAQKYKLH.js +182 -0
  23. package/dist/chunk-EDRGAM6G.js +647 -0
  24. package/dist/chunk-EGU5KB7V.js +818 -0
  25. package/dist/chunk-H32L2NE3.js +372 -0
  26. package/dist/chunk-HKQSAFSN.js +837 -0
  27. package/dist/chunk-IOTWFHZU.js +558 -0
  28. package/dist/chunk-ONSDE37A.js +125 -0
  29. package/dist/chunk-QQKTH5KX.js +114 -0
  30. package/dist/chunk-R2TCP7D7.js +409 -0
  31. package/dist/chunk-SIP34GBE.js +380 -0
  32. package/dist/chunk-THSX7OOR.js +454 -0
  33. package/dist/chunk-UY3AZSYL.js +14 -0
  34. package/dist/chunk-VT5KRDPH.js +134 -0
  35. package/dist/chunk-VXXZIXAP.js +255 -0
  36. package/dist/chunk-WJJ5CTNI.js +907 -0
  37. package/dist/chunk-WVY45EIW.js +359 -0
  38. package/dist/chunk-ZRBLZY3M.js +462 -0
  39. package/dist/client-CKXJLsTe.d.ts +232 -0
  40. package/dist/email-verification-CAeArjui.d.ts +327 -0
  41. package/dist/encryption/index.js +48 -556
  42. package/dist/errors-JOOPDDQ6.js +34 -0
  43. package/dist/events/index.js +14 -316
  44. package/dist/health/index.js +12 -367
  45. package/dist/i18n/index.d.ts +2 -2
  46. package/dist/i18n/index.js +14 -583
  47. package/dist/index.d.ts +37 -239
  48. package/dist/index.js +2873 -19166
  49. package/dist/lambda/index.d.ts +9 -7
  50. package/dist/lambda/index.js +4 -9
  51. package/dist/logging/index.js +12 -545
  52. package/dist/mail/index.d.ts +29 -1
  53. package/dist/mail/index.js +15 -684
  54. package/dist/mcp/index.d.ts +7 -5
  55. package/dist/mcp/index.js +5 -378
  56. package/dist/notifications/index.d.ts +8 -6
  57. package/dist/notifications/index.js +13 -730
  58. package/dist/queue/index.d.ts +37 -7
  59. package/dist/queue/index.js +22 -940
  60. package/dist/redis/index.d.ts +366 -0
  61. package/dist/redis/index.js +597 -0
  62. package/dist/runtime/index.d.ts +8 -6
  63. package/dist/runtime/index.js +26 -244
  64. package/dist/scheduling/index.js +14 -822
  65. package/dist/storage/index.d.ts +1 -0
  66. package/dist/storage/index.js +6 -824
  67. package/package.json +15 -7
  68. package/dist/api-token-JOif2CtG.d.ts +0 -1792
@@ -0,0 +1,1563 @@
1
+ import {
2
+ AuthenticationException
3
+ } from "./chunk-DAQKYKLH.js";
4
+ import {
5
+ MessageSigner,
6
+ deriveAppKeyring,
7
+ getAppKeyringFromEnv
8
+ } from "./chunk-ONSDE37A.js";
9
+ import {
10
+ hashPassword,
11
+ needsRehash,
12
+ verifyPassword
13
+ } from "./chunk-QQKTH5KX.js";
14
+
15
+ // src/http/middleware/session.ts
16
+ import { getCookie, setCookie, deleteCookie } from "hono/cookie";
17
+ import { createHmac, timingSafeEqual } from "crypto";
18
+ var MemorySessionStore = class {
19
+ constructor(now = () => Date.now()) {
20
+ this.now = now;
21
+ }
22
+ store = /* @__PURE__ */ new Map();
23
+ async read(id) {
24
+ const entry = this.store.get(id);
25
+ if (!entry) {
26
+ return void 0;
27
+ }
28
+ if (entry.expiresAt <= this.now()) {
29
+ this.store.delete(id);
30
+ return void 0;
31
+ }
32
+ return { ...entry.data };
33
+ }
34
+ async write(id, data, ttlSeconds) {
35
+ const expiresAt = this.now() + ttlSeconds * 1e3;
36
+ this.store.set(id, { data: { ...data }, expiresAt });
37
+ }
38
+ async destroy(id) {
39
+ this.store.delete(id);
40
+ }
41
+ };
42
+ var DEFAULT_COOKIE_NAME = "guren.session";
43
+ var DEFAULT_TTL_SECONDS = 60 * 60 * 2;
44
+ var DEFAULT_COOKIE_SECURE = typeof process !== "undefined" ? process.env.NODE_ENV === "production" : true;
45
+ var SESSION_CONTEXT_KEY = "guren:session";
46
+ var SessionImpl = class {
47
+ constructor(id, initialData, isNew) {
48
+ this.isNew = isNew;
49
+ this.currentId = id;
50
+ this.originalId = id;
51
+ const { _flash, ...rest } = initialData;
52
+ this.data = { ...rest };
53
+ if (_flash && typeof _flash === "object") {
54
+ const bag = _flash;
55
+ this._flash = {
56
+ new: bag.new && typeof bag.new === "object" ? { ...bag.new } : {},
57
+ old: bag.old && typeof bag.old === "object" ? { ...bag.old } : {}
58
+ };
59
+ }
60
+ }
61
+ currentId;
62
+ originalId;
63
+ data;
64
+ dirty = false;
65
+ destroyed = false;
66
+ regenerated = false;
67
+ _flash = { new: {}, old: {} };
68
+ get id() {
69
+ return this.currentId;
70
+ }
71
+ get(key) {
72
+ return this.data[key];
73
+ }
74
+ set(key, value) {
75
+ this.data[key] = value;
76
+ this.dirty = true;
77
+ }
78
+ has(key) {
79
+ return Object.prototype.hasOwnProperty.call(this.data, key);
80
+ }
81
+ forget(key) {
82
+ if (this.has(key)) {
83
+ delete this.data[key];
84
+ this.dirty = true;
85
+ }
86
+ }
87
+ flush() {
88
+ if (Object.keys(this.data).length > 0) {
89
+ this.data = {};
90
+ this.dirty = true;
91
+ }
92
+ }
93
+ all() {
94
+ return { ...this.data };
95
+ }
96
+ regenerate() {
97
+ this.currentId = globalThis.crypto.randomUUID();
98
+ this.regenerated = true;
99
+ this.dirty = true;
100
+ }
101
+ invalidate() {
102
+ this.flush();
103
+ this.destroyed = true;
104
+ }
105
+ flash(key, value) {
106
+ this._flash.new[key] = value;
107
+ this.dirty = true;
108
+ }
109
+ getFlash(key) {
110
+ return this._flash.old[key];
111
+ }
112
+ reflash() {
113
+ for (const [key, value] of Object.entries(this._flash.old)) {
114
+ this._flash.new[key] = value;
115
+ }
116
+ this.dirty = true;
117
+ }
118
+ keep(...keys) {
119
+ for (const key of keys) {
120
+ if (key in this._flash.old) {
121
+ this._flash.new[key] = this._flash.old[key];
122
+ }
123
+ }
124
+ this.dirty = true;
125
+ }
126
+ /**
127
+ * Age flash data: move `new` → `old`, clear previous `old`.
128
+ * Called at the start of each request by the session middleware.
129
+ */
130
+ ageFlashData() {
131
+ this._flash.old = { ...this._flash.new };
132
+ this._flash.new = {};
133
+ this.dirty = true;
134
+ }
135
+ markTouched() {
136
+ this.dirty = true;
137
+ }
138
+ wasDestroyed() {
139
+ return this.destroyed;
140
+ }
141
+ wasRegenerated() {
142
+ return this.regenerated;
143
+ }
144
+ shouldPersist() {
145
+ return this.dirty || this.isNew;
146
+ }
147
+ snapshot() {
148
+ const hasFlash = Object.keys(this._flash.new).length > 0 || Object.keys(this._flash.old).length > 0;
149
+ return {
150
+ ...this.data,
151
+ ...hasFlash ? { _flash: { new: { ...this._flash.new }, old: { ...this._flash.old } } } : {}
152
+ };
153
+ }
154
+ originalSessionId() {
155
+ return this.originalId;
156
+ }
157
+ };
158
+ function encodeBase64Url(value) {
159
+ return Buffer.from(value, "utf8").toString("base64url");
160
+ }
161
+ function decodeBase64Url(value) {
162
+ return Buffer.from(value, "base64url").toString("utf8");
163
+ }
164
+ function createCookieSigner(cookieName, cookiePath) {
165
+ const keyring = deriveAppKeyring(getAppKeyringFromEnv(), "cookie-signing");
166
+ const keys = [keyring.current, ...keyring.previous];
167
+ const canonicalize = (encodedId) => `${cookieName}|${cookiePath}|${encodedId}`;
168
+ const sign = (sessionId) => {
169
+ const encodedId = encodeBase64Url(sessionId);
170
+ const signature = createHmac("sha256", keyring.current).update(canonicalize(encodedId)).digest("base64url");
171
+ return `${encodedId}.${signature}`;
172
+ };
173
+ const verify = (cookieValue) => {
174
+ if (!cookieValue) {
175
+ return null;
176
+ }
177
+ const [encodedId, signature, extra] = cookieValue.split(".");
178
+ if (!encodedId || !signature || extra) {
179
+ return null;
180
+ }
181
+ const canonical = canonicalize(encodedId);
182
+ const matches = keys.some((key) => {
183
+ const expected = createHmac("sha256", key).update(canonical).digest("base64url");
184
+ const actualBuffer = Buffer.from(signature, "utf8");
185
+ const expectedBuffer = Buffer.from(expected, "utf8");
186
+ if (actualBuffer.length !== expectedBuffer.length) {
187
+ return false;
188
+ }
189
+ return timingSafeEqual(actualBuffer, expectedBuffer);
190
+ });
191
+ if (!matches) {
192
+ return null;
193
+ }
194
+ try {
195
+ return decodeBase64Url(encodedId);
196
+ } catch {
197
+ return null;
198
+ }
199
+ };
200
+ return { sign, verify };
201
+ }
202
+ function createSessionMiddleware(options = {}) {
203
+ const {
204
+ cookieName = DEFAULT_COOKIE_NAME,
205
+ cookiePath = "/",
206
+ cookieDomain,
207
+ cookieSecure = DEFAULT_COOKIE_SECURE,
208
+ cookieSameSite = "Lax",
209
+ cookieHttpOnly = true,
210
+ cookieMaxAgeSeconds,
211
+ ttlSeconds = DEFAULT_TTL_SECONDS,
212
+ store = new MemorySessionStore()
213
+ } = options;
214
+ const signer = createCookieSigner(cookieName, cookiePath);
215
+ return async (ctx, next) => {
216
+ const existingId = signer.verify(getCookie(ctx, cookieName));
217
+ const sessionId = existingId ?? globalThis.crypto.randomUUID();
218
+ const isNew = !existingId;
219
+ const initialData = existingId ? await store.read(existingId) ?? {} : {};
220
+ const session = new SessionImpl(sessionId, initialData, isNew);
221
+ session.ageFlashData();
222
+ ctx.set(SESSION_CONTEXT_KEY, session);
223
+ try {
224
+ await next();
225
+ } finally {
226
+ if (session.wasDestroyed()) {
227
+ await store.destroy(session.originalSessionId());
228
+ deleteCookie(ctx, cookieName, {
229
+ path: cookiePath,
230
+ domain: cookieDomain,
231
+ secure: cookieSecure,
232
+ sameSite: cookieSameSite,
233
+ httpOnly: cookieHttpOnly
234
+ });
235
+ return;
236
+ }
237
+ if (!session.shouldPersist()) {
238
+ if (existingId) {
239
+ await store.write(existingId, session.snapshot(), ttlSeconds);
240
+ setCookie(ctx, cookieName, signer.sign(existingId), {
241
+ path: cookiePath,
242
+ domain: cookieDomain,
243
+ secure: cookieSecure,
244
+ sameSite: cookieSameSite,
245
+ httpOnly: cookieHttpOnly,
246
+ maxAge: cookieMaxAgeSeconds ?? ttlSeconds
247
+ });
248
+ }
249
+ return;
250
+ }
251
+ const nextId = session.id;
252
+ await store.write(nextId, session.snapshot(), ttlSeconds);
253
+ if (session.wasRegenerated() && session.originalSessionId() !== nextId) {
254
+ await store.destroy(session.originalSessionId());
255
+ }
256
+ setCookie(ctx, cookieName, signer.sign(nextId), {
257
+ path: cookiePath,
258
+ domain: cookieDomain,
259
+ secure: cookieSecure,
260
+ sameSite: cookieSameSite,
261
+ httpOnly: cookieHttpOnly,
262
+ maxAge: cookieMaxAgeSeconds ?? ttlSeconds
263
+ });
264
+ }
265
+ };
266
+ }
267
+ function getSessionFromContext(ctx) {
268
+ return ctx.get(SESSION_CONTEXT_KEY);
269
+ }
270
+
271
+ // src/auth/RequestAuthContext.ts
272
+ var RequestAuthContext = class {
273
+ constructor(manager, ctx, resolveSession, resolveGuard) {
274
+ this.manager = manager;
275
+ this.ctx = ctx;
276
+ this.resolveSession = resolveSession;
277
+ this.resolveGuard = resolveGuard;
278
+ }
279
+ guardCache = /* @__PURE__ */ new Map();
280
+ guard(name) {
281
+ const key = name ?? this.manager.getDefaultGuard();
282
+ if (!this.guardCache.has(key)) {
283
+ const guard = this.resolveGuard(name);
284
+ this.guardCache.set(key, guard);
285
+ }
286
+ return this.guardCache.get(key);
287
+ }
288
+ session() {
289
+ return this.resolveSession();
290
+ }
291
+ async check() {
292
+ return this.guard().check();
293
+ }
294
+ async guest() {
295
+ return this.guard().guest();
296
+ }
297
+ async user() {
298
+ return this.guard().user();
299
+ }
300
+ async userOrFail() {
301
+ const u = await this.user();
302
+ if (!u) throw new AuthenticationException();
303
+ return u;
304
+ }
305
+ async id() {
306
+ return this.guard().id();
307
+ }
308
+ async login(user, remember) {
309
+ await this.guard().login(user, remember);
310
+ }
311
+ async attempt(credentials, remember) {
312
+ return this.guard().attempt(credentials, remember);
313
+ }
314
+ async logout() {
315
+ await this.guard().logout();
316
+ }
317
+ };
318
+
319
+ // src/auth/password/ScryptHasher.ts
320
+ var ScryptHasher = class {
321
+ algorithm;
322
+ memoryCost;
323
+ timeCost;
324
+ cost;
325
+ constructor(options = {}) {
326
+ this.algorithm = options.algorithm ?? "argon2id";
327
+ this.memoryCost = options.memoryCost;
328
+ this.timeCost = options.timeCost;
329
+ this.cost = options.cost;
330
+ }
331
+ async hash(plain) {
332
+ if (this.algorithm === "bcrypt") {
333
+ return Bun.password.hash(plain, {
334
+ algorithm: "bcrypt",
335
+ cost: this.cost
336
+ });
337
+ }
338
+ return Bun.password.hash(plain, {
339
+ algorithm: this.algorithm,
340
+ memoryCost: this.memoryCost,
341
+ timeCost: this.timeCost
342
+ });
343
+ }
344
+ async verify(hashed, plain) {
345
+ return Bun.password.verify(plain, hashed);
346
+ }
347
+ needsRehash(hashed) {
348
+ if (this.algorithm === "bcrypt") {
349
+ if (!hashed.startsWith("$2")) {
350
+ return true;
351
+ }
352
+ if (this.cost != null) {
353
+ const costSegment = hashed.slice(4, 6);
354
+ const parsedCost = Number.parseInt(costSegment, 10);
355
+ if (!Number.isNaN(parsedCost) && parsedCost !== this.cost) {
356
+ return true;
357
+ }
358
+ }
359
+ return false;
360
+ }
361
+ if (!hashed.startsWith(`$${this.algorithm}$`)) {
362
+ return true;
363
+ }
364
+ const [, , , parameterSegment] = hashed.split("$");
365
+ if (!parameterSegment) {
366
+ return false;
367
+ }
368
+ const params = Object.fromEntries(
369
+ parameterSegment.split(",").map((pair) => pair.split("=")).filter((parts) => parts.length === 2)
370
+ );
371
+ if (this.memoryCost != null) {
372
+ const memory = Number(params.m);
373
+ if (!Number.isNaN(memory) && memory !== this.memoryCost) {
374
+ return true;
375
+ }
376
+ }
377
+ if (this.timeCost != null) {
378
+ const time = Number(params.t);
379
+ if (!Number.isNaN(time) && time !== this.timeCost) {
380
+ return true;
381
+ }
382
+ }
383
+ return false;
384
+ }
385
+ };
386
+
387
+ // src/auth/providers/UserProvider.ts
388
+ var BaseUserProvider = class {
389
+ async setRememberToken(user, token) {
390
+ if (typeof user.setRememberToken === "function") {
391
+ await user.setRememberToken(token);
392
+ }
393
+ }
394
+ async getRememberToken(user) {
395
+ if (typeof user.getRememberToken === "function") {
396
+ return await user.getRememberToken() ?? null;
397
+ }
398
+ return null;
399
+ }
400
+ };
401
+
402
+ // src/auth/providers/ModelUserProvider.ts
403
+ var ModelUserProvider = class extends BaseUserProvider {
404
+ constructor(model, options = {}) {
405
+ super();
406
+ this.model = model;
407
+ this.idColumn = options.idColumn ?? "id";
408
+ this.usernameColumn = options.usernameColumn ?? "email";
409
+ this.passwordColumn = options.passwordColumn ?? "password";
410
+ this.rememberTokenColumn = options.rememberTokenColumn ?? "remember_token";
411
+ this.hasher = options.hasher ?? new ScryptHasher();
412
+ this.credentialsPasswordField = options.credentialsPasswordField ?? "password";
413
+ }
414
+ idColumn;
415
+ usernameColumn;
416
+ passwordColumn;
417
+ rememberTokenColumn;
418
+ hasher;
419
+ credentialsPasswordField;
420
+ cast(record) {
421
+ if (record && typeof record === "object") {
422
+ return record;
423
+ }
424
+ return null;
425
+ }
426
+ async retrieveById(identifier) {
427
+ const record = await this.model.find(identifier, this.idColumn);
428
+ return this.cast(record);
429
+ }
430
+ async retrieveByCredentials(credentials) {
431
+ const rememberToken = credentials["rememberToken"] ?? credentials["remember_token"];
432
+ if (rememberToken != null) {
433
+ const records2 = await this.model.where({ [this.rememberTokenColumn]: rememberToken });
434
+ return this.cast(records2[0] ?? null);
435
+ }
436
+ const username = credentials[this.usernameColumn];
437
+ if (username == null) {
438
+ return null;
439
+ }
440
+ const records = await this.model.where({ [this.usernameColumn]: username });
441
+ return this.cast(records[0] ?? null);
442
+ }
443
+ async validateCredentials(user, credentials) {
444
+ const plain = credentials[this.credentialsPasswordField];
445
+ if (typeof plain !== "string") {
446
+ return false;
447
+ }
448
+ const hashed = user[this.passwordColumn];
449
+ if (typeof hashed !== "string") {
450
+ await this.hasher.hash("dummy-timing-equalization");
451
+ return false;
452
+ }
453
+ return this.hasher.verify(hashed, plain);
454
+ }
455
+ getId(user) {
456
+ return user[this.idColumn];
457
+ }
458
+ /**
459
+ * Strip the password hash, remember token, and the model's `hidden`
460
+ * fields before the record leaves the auth layer. Credential
461
+ * validation happens on the raw record before sanitizing, so this
462
+ * never affects login — only what `auth.user()` exposes.
463
+ */
464
+ sanitize(user) {
465
+ const blocked = /* @__PURE__ */ new Set([
466
+ this.passwordColumn,
467
+ this.rememberTokenColumn,
468
+ ...this.model.hidden ?? []
469
+ ]);
470
+ const clean = {};
471
+ for (const [key, value] of Object.entries(user)) {
472
+ if (!blocked.has(key)) {
473
+ clean[key] = value;
474
+ }
475
+ }
476
+ return clean;
477
+ }
478
+ async setRememberToken(user, token) {
479
+ if (typeof user[this.rememberTokenColumn] !== "undefined") {
480
+ ;
481
+ user[this.rememberTokenColumn] = token;
482
+ await this.model.forceUpdate({ [this.idColumn]: this.getId(user) }, { [this.rememberTokenColumn]: token });
483
+ }
484
+ }
485
+ async getRememberToken(user) {
486
+ const token = user[this.rememberTokenColumn];
487
+ if (token == null) {
488
+ return null;
489
+ }
490
+ return String(token);
491
+ }
492
+ };
493
+
494
+ // src/auth/utils.ts
495
+ import { randomBytes, createHash, timingSafeEqual as timingSafeEqual2 } from "crypto";
496
+ function hashToken(token, algorithm = "sha256") {
497
+ return createHash(algorithm).update(token).digest("hex");
498
+ }
499
+ function generateToken(length = 32) {
500
+ return randomBytes(length).toString("hex");
501
+ }
502
+ function generateId() {
503
+ return randomBytes(16).toString("hex");
504
+ }
505
+ function secureCompare(a, b) {
506
+ if (a.length !== b.length) return false;
507
+ try {
508
+ return timingSafeEqual2(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
509
+ } catch {
510
+ return false;
511
+ }
512
+ }
513
+ function secureStringCompare(a, b) {
514
+ const bufA = Buffer.from(a, "utf8");
515
+ const bufB = Buffer.from(b, "utf8");
516
+ if (bufA.length !== bufB.length) return false;
517
+ return timingSafeEqual2(bufA, bufB);
518
+ }
519
+ function buildTokenUrl(baseUrl, token, email) {
520
+ const url = new URL(baseUrl);
521
+ url.searchParams.set("token", token);
522
+ if (email) {
523
+ url.searchParams.set("email", email);
524
+ }
525
+ return url.toString();
526
+ }
527
+ function parseTokenUrl(url) {
528
+ try {
529
+ const parsed = new URL(url);
530
+ return {
531
+ token: parsed.searchParams.get("token"),
532
+ email: parsed.searchParams.get("email")
533
+ };
534
+ } catch {
535
+ return { token: null, email: null };
536
+ }
537
+ }
538
+
539
+ // src/auth/SessionGuard.ts
540
+ var DEFAULT_SESSION_KEY = "auth:user_id";
541
+ var DEFAULT_REMEMBER_KEY = "auth:remember_token";
542
+ var SessionGuard = class {
543
+ constructor(options) {
544
+ this.options = options;
545
+ }
546
+ cachedUser;
547
+ get currentSession() {
548
+ return this.options.session;
549
+ }
550
+ get provider() {
551
+ return this.options.provider;
552
+ }
553
+ sessionKey() {
554
+ return this.options.sessionKey ?? DEFAULT_SESSION_KEY;
555
+ }
556
+ rememberSessionKey() {
557
+ return this.options.rememberSessionKey ?? DEFAULT_REMEMBER_KEY;
558
+ }
559
+ async loadRememberedUser() {
560
+ if (!this.currentSession || !this.provider.getRememberToken) {
561
+ return null;
562
+ }
563
+ const rememberToken = this.currentSession.get(this.rememberSessionKey());
564
+ if (!rememberToken) {
565
+ return null;
566
+ }
567
+ const user = await this.provider.retrieveByCredentials({ rememberToken });
568
+ if (!user) {
569
+ this.currentSession.forget(this.rememberSessionKey());
570
+ return null;
571
+ }
572
+ const providerToken = await this.provider.getRememberToken?.(user);
573
+ if (!providerToken || !secureStringCompare(providerToken, rememberToken)) {
574
+ this.currentSession.forget(this.rememberSessionKey());
575
+ return null;
576
+ }
577
+ await this.provider.setRememberToken?.(user, rememberToken);
578
+ const sanitized = this.sanitizeUser(user);
579
+ this.cachedUser = sanitized;
580
+ return sanitized;
581
+ }
582
+ /** Strip auth-internal fields before a record is cached or exposed. */
583
+ sanitizeUser(user) {
584
+ return this.provider.sanitize ? this.provider.sanitize(user) : user;
585
+ }
586
+ async resolveUser() {
587
+ if (this.cachedUser !== void 0) {
588
+ return this.cachedUser;
589
+ }
590
+ const session = this.currentSession;
591
+ if (!session) {
592
+ this.cachedUser = null;
593
+ return null;
594
+ }
595
+ const identifier = session.get(this.sessionKey());
596
+ if (identifier == null) {
597
+ const remembered = await this.loadRememberedUser();
598
+ this.cachedUser = remembered;
599
+ return remembered;
600
+ }
601
+ const user = await this.provider.retrieveById(identifier);
602
+ if (!user) {
603
+ session.forget(this.sessionKey());
604
+ this.cachedUser = null;
605
+ return null;
606
+ }
607
+ const sanitized = this.sanitizeUser(user);
608
+ this.cachedUser = sanitized;
609
+ return sanitized;
610
+ }
611
+ async check() {
612
+ return await this.resolveUser() !== null;
613
+ }
614
+ async guest() {
615
+ return !await this.check();
616
+ }
617
+ async user() {
618
+ const user = await this.resolveUser();
619
+ return user ?? null;
620
+ }
621
+ async id() {
622
+ const session = this.currentSession;
623
+ if (!session) {
624
+ return null;
625
+ }
626
+ return session.get(this.sessionKey()) ?? null;
627
+ }
628
+ async remember(user) {
629
+ if (!this.currentSession || !this.provider.setRememberToken) {
630
+ return;
631
+ }
632
+ const token = generateToken(32);
633
+ await this.provider.setRememberToken?.(user, token);
634
+ this.currentSession.set(this.rememberSessionKey(), token);
635
+ }
636
+ async login(user, remember = false) {
637
+ const castUser = user;
638
+ const session = this.currentSession;
639
+ if (!session) {
640
+ throw new Error("SessionGuard: session middleware is required to use the session guard.");
641
+ }
642
+ session.regenerate();
643
+ const identifier = this.provider.getId(castUser);
644
+ session.set(this.sessionKey(), identifier);
645
+ this.cachedUser = this.sanitizeUser(castUser);
646
+ if (remember) {
647
+ await this.remember(castUser);
648
+ } else {
649
+ session.forget(this.rememberSessionKey());
650
+ }
651
+ }
652
+ async logout() {
653
+ const session = this.currentSession;
654
+ if (!session) {
655
+ return;
656
+ }
657
+ session.forget(this.sessionKey());
658
+ session.forget(this.rememberSessionKey());
659
+ this.cachedUser = null;
660
+ }
661
+ async attempt(credentials, remember = false) {
662
+ const user = await this.validate(credentials);
663
+ if (!user) {
664
+ return false;
665
+ }
666
+ await this.login(user, remember);
667
+ return true;
668
+ }
669
+ async validate(credentials) {
670
+ const user = await this.provider.retrieveByCredentials(credentials);
671
+ if (!user) {
672
+ await new Promise((resolve) => setTimeout(resolve, 100));
673
+ return null;
674
+ }
675
+ const valid = await this.provider.validateCredentials(user, credentials);
676
+ if (!valid) {
677
+ return null;
678
+ }
679
+ return user;
680
+ }
681
+ session() {
682
+ return this.currentSession;
683
+ }
684
+ };
685
+
686
+ // src/auth/AuthManager.ts
687
+ var DEFAULT_GUARD = "web";
688
+ var AuthManager = class {
689
+ guards = /* @__PURE__ */ new Map();
690
+ providers = /* @__PURE__ */ new Map();
691
+ defaultGuard;
692
+ constructor(options = {}) {
693
+ this.defaultGuard = options.defaultGuard ?? DEFAULT_GUARD;
694
+ }
695
+ registerGuard(name, factory) {
696
+ this.guards.set(name, { factory });
697
+ }
698
+ registerProvider(name, factory) {
699
+ this.providers.set(name, { factory });
700
+ }
701
+ getProvider(name) {
702
+ const entry = this.providers.get(name);
703
+ if (!entry) {
704
+ throw new Error(`AuthManager: provider "${name}" has not been registered.`);
705
+ }
706
+ if (!entry.instance) {
707
+ const instance = entry.factory(this);
708
+ entry.instance = instance;
709
+ this.providers.set(name, entry);
710
+ }
711
+ return entry.instance;
712
+ }
713
+ createGuard(name, context) {
714
+ const entry = this.guards.get(name);
715
+ if (!entry) {
716
+ throw new Error(`AuthManager: guard "${name}" has not been registered.`);
717
+ }
718
+ return entry.factory(context);
719
+ }
720
+ guardNames() {
721
+ return Array.from(this.guards.keys());
722
+ }
723
+ setDefaultGuard(name) {
724
+ if (!this.guards.has(name)) {
725
+ throw new Error(`AuthManager: cannot set default guard to unregistered guard "${name}".`);
726
+ }
727
+ this.defaultGuard = name;
728
+ }
729
+ getDefaultGuard() {
730
+ return this.defaultGuard;
731
+ }
732
+ createAuthContext(ctx, options = {}) {
733
+ const guardName = options.guard ?? this.defaultGuard;
734
+ const resolveSession = () => getSessionFromContext(ctx);
735
+ const guardFactory = (name) => {
736
+ const targetName = name ?? guardName;
737
+ return this.createGuard(targetName, {
738
+ ctx,
739
+ session: resolveSession(),
740
+ manager: this
741
+ });
742
+ };
743
+ return new RequestAuthContext(this, ctx, resolveSession, guardFactory);
744
+ }
745
+ async attempt(name, ctx, credentials, remember) {
746
+ const guard = this.createAuthContext(ctx, { guard: name }).guard(name);
747
+ return guard.attempt(credentials, remember);
748
+ }
749
+ /**
750
+ * Shorthand method to register a model-based authentication provider and session guard.
751
+ * This simplifies the common case of authenticating users via a database model.
752
+ *
753
+ * @param model - The model class to use for user authentication
754
+ * @param options - Options for the ModelUserProvider (partial, with defaults)
755
+ * @param providerName - Name for the provider (defaults to 'users')
756
+ * @param guardName - Name for the guard (defaults to 'web')
757
+ */
758
+ useModel(model, options = {}, providerName = "users", guardName = "web") {
759
+ const defaultOptions = {
760
+ usernameColumn: "email",
761
+ passwordColumn: "passwordHash",
762
+ rememberTokenColumn: "rememberToken",
763
+ credentialsPasswordField: "password",
764
+ hasher: new ScryptHasher(),
765
+ ...options
766
+ };
767
+ this.registerProvider(providerName, () => new ModelUserProvider(model, defaultOptions));
768
+ this.registerGuard(guardName, ({ session, manager }) => {
769
+ const provider = manager.getProvider(providerName);
770
+ return new SessionGuard({ provider, session });
771
+ });
772
+ this.setDefaultGuard(guardName);
773
+ }
774
+ };
775
+
776
+ // src/auth/password/NodeHasher.ts
777
+ var NodeHasher = class {
778
+ options;
779
+ constructor(options = {}) {
780
+ this.options = {
781
+ cost: options.cost,
782
+ memory: options.memory,
783
+ saltLength: options.saltLength,
784
+ keyLength: options.keyLength
785
+ };
786
+ }
787
+ async hash(plain) {
788
+ return hashPassword(plain, this.options);
789
+ }
790
+ async verify(hashed, plain) {
791
+ return verifyPassword(plain, hashed);
792
+ }
793
+ needsRehash(hashed) {
794
+ return needsRehash(hashed, this.options);
795
+ }
796
+ };
797
+
798
+ // src/auth/AuthenticatableModel.ts
799
+ import { Model } from "@guren/orm";
800
+ var AuthenticatableModel = class extends Model {
801
+ static createType = void 0;
802
+ static passwordField = "password";
803
+ static passwordHashField = "passwordHash";
804
+ static passwordHasher = null;
805
+ static resolvePasswordField() {
806
+ return this.passwordField ?? "password";
807
+ }
808
+ static resolvePasswordHashField() {
809
+ return this.passwordHashField ?? "passwordHash";
810
+ }
811
+ static resolvePasswordHasher() {
812
+ if (this.passwordHasher) {
813
+ return this.passwordHasher;
814
+ }
815
+ const hasher = new ScryptHasher();
816
+ this.passwordHasher = hasher;
817
+ return hasher;
818
+ }
819
+ static async preparePersistencePayload(data) {
820
+ const basePayload = await super.preparePersistencePayload(data);
821
+ const passwordField = this.resolvePasswordField();
822
+ if (!(passwordField in basePayload)) {
823
+ return basePayload;
824
+ }
825
+ const payload = { ...basePayload };
826
+ const plainPassword = payload[passwordField];
827
+ const hashField = this.resolvePasswordHashField();
828
+ if (typeof plainPassword === "string" && plainPassword.length > 0) {
829
+ const hasher = this.resolvePasswordHasher();
830
+ payload[hashField] = await hasher.hash(plainPassword);
831
+ }
832
+ if (passwordField !== hashField) {
833
+ delete payload[passwordField];
834
+ }
835
+ return payload;
836
+ }
837
+ };
838
+
839
+ // src/auth/password-reset.ts
840
+ var MemoryPasswordResetStore = class {
841
+ tokens = /* @__PURE__ */ new Map();
842
+ async store(tokenId, email, expiresAt) {
843
+ this.tokens.set(tokenId, { email, expiresAt });
844
+ }
845
+ async find(tokenId) {
846
+ const record = this.tokens.get(tokenId);
847
+ if (!record) return null;
848
+ if (record.expiresAt < /* @__PURE__ */ new Date()) {
849
+ this.tokens.delete(tokenId);
850
+ return null;
851
+ }
852
+ return record;
853
+ }
854
+ async delete(tokenId) {
855
+ this.tokens.delete(tokenId);
856
+ }
857
+ async deleteForEmail(email) {
858
+ for (const [hash, record] of this.tokens) {
859
+ if (record.email === email) {
860
+ this.tokens.delete(hash);
861
+ }
862
+ }
863
+ }
864
+ /** Clear all tokens (useful for testing) */
865
+ clear() {
866
+ this.tokens.clear();
867
+ }
868
+ };
869
+ var DEFAULT_EXPIRES_IN = 60 * 60 * 1e3;
870
+ var DEFAULT_TOKEN_LENGTH = 32;
871
+ var PASSWORD_RESET_PURPOSE = "password-reset";
872
+ function createPasswordResetSigner() {
873
+ return new MessageSigner(deriveAppKeyring(getAppKeyringFromEnv(), "password-reset-signing"));
874
+ }
875
+ async function createPasswordResetToken(email, store, config = {}) {
876
+ const expiresIn = config.expiresIn ?? DEFAULT_EXPIRES_IN;
877
+ const tokenLength = config.tokenLength ?? DEFAULT_TOKEN_LENGTH;
878
+ await store.deleteForEmail(email);
879
+ const tokenId = generateId();
880
+ const expiresAt = new Date(Date.now() + expiresIn);
881
+ const signer = createPasswordResetSigner();
882
+ const token = signer.sign(
883
+ {
884
+ id: tokenId,
885
+ email: email.toLowerCase(),
886
+ bytes: tokenLength
887
+ },
888
+ {
889
+ purpose: PASSWORD_RESET_PURPOSE,
890
+ expiresIn
891
+ }
892
+ );
893
+ await store.store(tokenId, email.toLowerCase(), expiresAt);
894
+ return { token, tokenId, expiresAt };
895
+ }
896
+ async function verifyPasswordResetToken(token, store, _config = {}) {
897
+ const signer = createPasswordResetSigner();
898
+ const payload = signer.verify(token, {
899
+ purpose: PASSWORD_RESET_PURPOSE,
900
+ allowExpired: true
901
+ });
902
+ if (!payload?.id || !payload.email) {
903
+ return null;
904
+ }
905
+ if (typeof payload.exp === "number" && payload.exp < Math.floor(Date.now() / 1e3)) {
906
+ await store.delete(payload.id);
907
+ return null;
908
+ }
909
+ const record = await store.find(payload.id);
910
+ if (!record) return null;
911
+ return record.email.toLowerCase() === payload.email.toLowerCase() ? record.email : null;
912
+ }
913
+ async function completePasswordReset(token, newPassword, store, provider, updatePassword, _config = {}) {
914
+ const signer = createPasswordResetSigner();
915
+ const payload = signer.verify(token, {
916
+ purpose: PASSWORD_RESET_PURPOSE,
917
+ allowExpired: true
918
+ });
919
+ if (!payload?.id || !payload.email) {
920
+ return null;
921
+ }
922
+ if (typeof payload.exp === "number" && payload.exp < Math.floor(Date.now() / 1e3)) {
923
+ await store.delete(payload.id);
924
+ return null;
925
+ }
926
+ const record = await store.find(payload.id);
927
+ if (!record) return null;
928
+ if (record.email.toLowerCase() !== payload.email.toLowerCase()) {
929
+ return null;
930
+ }
931
+ const user = await provider.retrieveByCredentials({ email: record.email });
932
+ if (!user) {
933
+ await store.delete(payload.id);
934
+ return null;
935
+ }
936
+ await updatePassword(user, newPassword);
937
+ await store.delete(payload.id);
938
+ return user;
939
+ }
940
+ var buildPasswordResetUrl = buildTokenUrl;
941
+ var parsePasswordResetUrl = parseTokenUrl;
942
+
943
+ // src/auth/email-verification.ts
944
+ var MemoryEmailVerificationStore = class {
945
+ tokens = /* @__PURE__ */ new Map();
946
+ async store(token) {
947
+ this.tokens.set(token.tokenId, token);
948
+ }
949
+ async findByTokenId(tokenId) {
950
+ return this.tokens.get(tokenId) ?? null;
951
+ }
952
+ async delete(tokenId) {
953
+ this.tokens.delete(tokenId);
954
+ }
955
+ async deleteForEmail(email) {
956
+ const normalizedEmail = email.toLowerCase();
957
+ for (const [key, token] of this.tokens.entries()) {
958
+ if (token.email.toLowerCase() === normalizedEmail) {
959
+ this.tokens.delete(key);
960
+ }
961
+ }
962
+ }
963
+ /**
964
+ * Clear all tokens (useful for testing).
965
+ */
966
+ clear() {
967
+ this.tokens.clear();
968
+ }
969
+ /**
970
+ * Get the count of stored tokens (useful for testing).
971
+ */
972
+ get size() {
973
+ return this.tokens.size;
974
+ }
975
+ };
976
+ var DEFAULT_CONFIG = {
977
+ expiresIn: 24 * 60 * 60 * 1e3,
978
+ // 24 hours
979
+ tokenLength: 32
980
+ };
981
+ var EMAIL_VERIFICATION_PURPOSE = "email-verification";
982
+ function createEmailVerificationSigner() {
983
+ return new MessageSigner(deriveAppKeyring(getAppKeyringFromEnv(), "email-verification-signing"));
984
+ }
985
+ async function createEmailVerificationToken(email, store, config = {}) {
986
+ const { expiresIn, tokenLength } = { ...DEFAULT_CONFIG, ...config };
987
+ await store.deleteForEmail(email);
988
+ const tokenId = generateId();
989
+ const now = /* @__PURE__ */ new Date();
990
+ const expiresAt = new Date(now.getTime() + expiresIn);
991
+ const signer = createEmailVerificationSigner();
992
+ const token = signer.sign(
993
+ {
994
+ id: tokenId,
995
+ email: email.toLowerCase(),
996
+ bytes: tokenLength
997
+ },
998
+ {
999
+ purpose: EMAIL_VERIFICATION_PURPOSE,
1000
+ expiresIn
1001
+ }
1002
+ );
1003
+ await store.store({
1004
+ email: email.toLowerCase(),
1005
+ tokenId,
1006
+ expiresAt,
1007
+ createdAt: now
1008
+ });
1009
+ return { token, expiresAt };
1010
+ }
1011
+ async function verifyEmailToken(token, store, _config = {}) {
1012
+ const signer = createEmailVerificationSigner();
1013
+ const payload = signer.verify(token, {
1014
+ purpose: EMAIL_VERIFICATION_PURPOSE,
1015
+ allowExpired: true
1016
+ });
1017
+ if (!payload?.id || !payload.email) {
1018
+ return null;
1019
+ }
1020
+ if (typeof payload.exp === "number" && payload.exp < Math.floor(Date.now() / 1e3)) {
1021
+ await store.delete(payload.id);
1022
+ return null;
1023
+ }
1024
+ const storedToken = await store.findByTokenId(payload.id);
1025
+ if (!storedToken) {
1026
+ return null;
1027
+ }
1028
+ if (/* @__PURE__ */ new Date() > storedToken.expiresAt) {
1029
+ await store.delete(payload.id);
1030
+ return null;
1031
+ }
1032
+ return storedToken.email.toLowerCase() === payload.email.toLowerCase() ? storedToken.email : null;
1033
+ }
1034
+ async function completeEmailVerification(token, store, markVerified) {
1035
+ const signer = createEmailVerificationSigner();
1036
+ const payload = signer.verify(token, {
1037
+ purpose: EMAIL_VERIFICATION_PURPOSE,
1038
+ allowExpired: true
1039
+ });
1040
+ if (!payload?.id || !payload.email) {
1041
+ return null;
1042
+ }
1043
+ if (typeof payload.exp === "number" && payload.exp < Math.floor(Date.now() / 1e3)) {
1044
+ await store.delete(payload.id);
1045
+ return null;
1046
+ }
1047
+ const storedToken = await store.findByTokenId(payload.id);
1048
+ if (!storedToken) {
1049
+ return null;
1050
+ }
1051
+ if (/* @__PURE__ */ new Date() > storedToken.expiresAt) {
1052
+ await store.delete(payload.id);
1053
+ return null;
1054
+ }
1055
+ if (storedToken.email.toLowerCase() !== payload.email.toLowerCase()) {
1056
+ return null;
1057
+ }
1058
+ const result = await markVerified(storedToken.email);
1059
+ await store.delete(payload.id);
1060
+ return result;
1061
+ }
1062
+ var buildVerificationUrl = buildTokenUrl;
1063
+ var parseVerificationUrl = parseTokenUrl;
1064
+ function isEmailVerified(user) {
1065
+ return user?.emailVerifiedAt != null;
1066
+ }
1067
+ function requireVerifiedEmail(options = {}) {
1068
+ const { redirectTo = "/verify-email" } = options;
1069
+ return async (ctx, next) => {
1070
+ const getUser = options.getUser ?? (async (c) => {
1071
+ const auth = c.get("guren:auth");
1072
+ return auth?.user?.() ?? null;
1073
+ });
1074
+ const user = await getUser(ctx);
1075
+ if (!isEmailVerified(user)) {
1076
+ return ctx.redirect(redirectTo);
1077
+ }
1078
+ await next();
1079
+ };
1080
+ }
1081
+
1082
+ // src/auth/api-token.ts
1083
+ var MemoryApiTokenStore = class {
1084
+ tokens = /* @__PURE__ */ new Map();
1085
+ byHash = /* @__PURE__ */ new Map();
1086
+ // hash -> id
1087
+ async store(token) {
1088
+ this.tokens.set(token.id, token);
1089
+ this.byHash.set(token.hashedToken, token.id);
1090
+ }
1091
+ async findByHashedToken(hashedToken) {
1092
+ const id = this.byHash.get(hashedToken);
1093
+ if (!id) return null;
1094
+ return this.tokens.get(id) ?? null;
1095
+ }
1096
+ async findByUserId(userId) {
1097
+ const userIdStr = String(userId);
1098
+ return Array.from(this.tokens.values()).filter(
1099
+ (t) => String(t.userId) === userIdStr
1100
+ );
1101
+ }
1102
+ async delete(id) {
1103
+ const token = this.tokens.get(id);
1104
+ if (token) {
1105
+ this.byHash.delete(token.hashedToken);
1106
+ this.tokens.delete(id);
1107
+ }
1108
+ }
1109
+ async deleteForUser(userId) {
1110
+ const userIdStr = String(userId);
1111
+ for (const [id, token] of this.tokens.entries()) {
1112
+ if (String(token.userId) === userIdStr) {
1113
+ this.byHash.delete(token.hashedToken);
1114
+ this.tokens.delete(id);
1115
+ }
1116
+ }
1117
+ }
1118
+ async updateLastUsed(id, timestamp) {
1119
+ const token = this.tokens.get(id);
1120
+ if (token) {
1121
+ token.lastUsedAt = timestamp;
1122
+ }
1123
+ }
1124
+ /**
1125
+ * Clear all tokens (useful for testing).
1126
+ */
1127
+ clear() {
1128
+ this.tokens.clear();
1129
+ this.byHash.clear();
1130
+ }
1131
+ /**
1132
+ * Get the number of stored tokens.
1133
+ */
1134
+ get size() {
1135
+ return this.tokens.size;
1136
+ }
1137
+ };
1138
+ async function createApiToken(store, options) {
1139
+ const {
1140
+ name,
1141
+ userId,
1142
+ abilities = ["*"],
1143
+ expiresIn = null,
1144
+ tokenLength = 32
1145
+ } = options;
1146
+ const id = generateId();
1147
+ const plainToken = generateToken(tokenLength);
1148
+ const hashedToken = hashToken(plainToken);
1149
+ const now = /* @__PURE__ */ new Date();
1150
+ const token = {
1151
+ id,
1152
+ name,
1153
+ hashedToken,
1154
+ userId,
1155
+ abilities,
1156
+ lastUsedAt: null,
1157
+ expiresAt: expiresIn == null ? null : new Date(now.getTime() + expiresIn),
1158
+ createdAt: now
1159
+ };
1160
+ await store.store(token);
1161
+ return {
1162
+ plainTextToken: `${id}|${plainToken}`,
1163
+ token
1164
+ };
1165
+ }
1166
+ function parseApiToken(plainTextToken) {
1167
+ const parts = plainTextToken.split("|");
1168
+ if (parts.length !== 2) return null;
1169
+ const [id, token] = parts;
1170
+ if (!id || !token) return null;
1171
+ return { id, token };
1172
+ }
1173
+ async function verifyApiToken(plainTextToken, store, options = {}) {
1174
+ const { updateLastUsed = true } = options;
1175
+ const parsed = parseApiToken(plainTextToken);
1176
+ if (!parsed) return null;
1177
+ const hashedToken = hashToken(parsed.token);
1178
+ const token = await store.findByHashedToken(hashedToken);
1179
+ if (!token) return null;
1180
+ if (token.id !== parsed.id) return null;
1181
+ if (token.expiresAt && /* @__PURE__ */ new Date() > token.expiresAt) {
1182
+ return null;
1183
+ }
1184
+ if (!secureCompare(token.hashedToken, hashedToken)) {
1185
+ return null;
1186
+ }
1187
+ if (updateLastUsed) {
1188
+ await store.updateLastUsed(token.id, /* @__PURE__ */ new Date());
1189
+ }
1190
+ return {
1191
+ token,
1192
+ userId: token.userId,
1193
+ abilities: token.abilities
1194
+ };
1195
+ }
1196
+ function tokenCan(token, ability) {
1197
+ if (token.abilities.includes("*")) return true;
1198
+ return token.abilities.includes(ability);
1199
+ }
1200
+ function tokenCanAll(token, abilities) {
1201
+ if (token.abilities.includes("*")) return true;
1202
+ return abilities.every((ability) => token.abilities.includes(ability));
1203
+ }
1204
+ function tokenCanAny(token, abilities) {
1205
+ if (token.abilities.includes("*")) return true;
1206
+ return abilities.some((ability) => token.abilities.includes(ability));
1207
+ }
1208
+ async function revokeApiToken(id, store) {
1209
+ await store.delete(id);
1210
+ }
1211
+ async function revokeAllApiTokens(userId, store) {
1212
+ await store.deleteForUser(userId);
1213
+ }
1214
+ async function getUserApiTokens(userId, store) {
1215
+ return store.findByUserId(userId);
1216
+ }
1217
+ var API_TOKEN_KEY = "guren:api-token";
1218
+ function createBearerTokenMiddleware(options) {
1219
+ const {
1220
+ store,
1221
+ loadUser,
1222
+ abilities,
1223
+ onUnauthorized,
1224
+ onForbidden,
1225
+ headerName = "Authorization",
1226
+ updateLastUsed = true
1227
+ } = options;
1228
+ return async (ctx, next) => {
1229
+ const authHeader = ctx.req.header(headerName);
1230
+ if (!authHeader) {
1231
+ if (onUnauthorized) return onUnauthorized(ctx);
1232
+ return ctx.json({ error: "Authentication required" }, 401);
1233
+ }
1234
+ const match = authHeader.match(/^Bearer\s+(.+)$/i);
1235
+ if (!match) {
1236
+ if (onUnauthorized) return onUnauthorized(ctx);
1237
+ return ctx.json({ error: "Invalid authorization format" }, 401);
1238
+ }
1239
+ const plainTextToken = match[1];
1240
+ const result = await verifyApiToken(plainTextToken, store, { updateLastUsed });
1241
+ if (!result) {
1242
+ if (onUnauthorized) return onUnauthorized(ctx);
1243
+ return ctx.json({ error: "Invalid or expired token" }, 401);
1244
+ }
1245
+ if (abilities && abilities.length > 0) {
1246
+ if (!tokenCanAll(result.token, abilities)) {
1247
+ if (onForbidden) return onForbidden(ctx, abilities);
1248
+ return ctx.json(
1249
+ { error: "Token lacks required abilities", required: abilities },
1250
+ 403
1251
+ );
1252
+ }
1253
+ }
1254
+ ctx.set(API_TOKEN_KEY, result);
1255
+ if (loadUser) {
1256
+ const user = await loadUser(result.userId);
1257
+ ctx.set("guren:user", user);
1258
+ }
1259
+ return next();
1260
+ };
1261
+ }
1262
+ function getApiToken(ctx) {
1263
+ return ctx.get(API_TOKEN_KEY);
1264
+ }
1265
+ function getApiTokenOrFail(ctx) {
1266
+ const result = getApiToken(ctx);
1267
+ if (!result) {
1268
+ throw new AuthenticationException("Unauthenticated.");
1269
+ }
1270
+ return result;
1271
+ }
1272
+
1273
+ // src/auth/oauth/index.ts
1274
+ var DEFAULT_STATE_EXPIRES_IN = 10 * 60 * 1e3;
1275
+ var DEFAULT_STATE_LENGTH = 24;
1276
+ var DEFAULT_STATE_HASH_ALGORITHM = "sha256";
1277
+ var MemoryOAuthStateStore = class {
1278
+ states = /* @__PURE__ */ new Map();
1279
+ maxEntries;
1280
+ constructor(options = {}) {
1281
+ this.maxEntries = options.maxEntries ?? 1e4;
1282
+ }
1283
+ async store(stateHash, payload) {
1284
+ if (this.states.size >= this.maxEntries) {
1285
+ this.sweepExpired();
1286
+ }
1287
+ while (this.states.size >= this.maxEntries) {
1288
+ const oldest = this.states.keys().next().value;
1289
+ if (oldest === void 0) break;
1290
+ this.states.delete(oldest);
1291
+ }
1292
+ this.states.set(stateHash, payload);
1293
+ }
1294
+ sweepExpired() {
1295
+ const now = Date.now();
1296
+ for (const [hash, payload] of this.states) {
1297
+ if (payload.expiresAt.getTime() <= now) {
1298
+ this.states.delete(hash);
1299
+ }
1300
+ }
1301
+ }
1302
+ async find(stateHash) {
1303
+ const payload = this.states.get(stateHash);
1304
+ if (!payload) return null;
1305
+ if (payload.expiresAt.getTime() <= Date.now()) {
1306
+ this.states.delete(stateHash);
1307
+ return null;
1308
+ }
1309
+ return payload;
1310
+ }
1311
+ async delete(stateHash) {
1312
+ this.states.delete(stateHash);
1313
+ }
1314
+ clear() {
1315
+ this.states.clear();
1316
+ }
1317
+ };
1318
+ var OAuthManager = class {
1319
+ providers = /* @__PURE__ */ new Map();
1320
+ stateStore;
1321
+ stateConfig;
1322
+ constructor(options = {}) {
1323
+ this.stateStore = options.stateStore ?? new MemoryOAuthStateStore();
1324
+ this.stateConfig = {
1325
+ expiresIn: options.stateConfig?.expiresIn ?? DEFAULT_STATE_EXPIRES_IN,
1326
+ stateLength: options.stateConfig?.stateLength ?? DEFAULT_STATE_LENGTH,
1327
+ hashAlgorithm: options.stateConfig?.hashAlgorithm ?? DEFAULT_STATE_HASH_ALGORITHM
1328
+ };
1329
+ }
1330
+ registerProvider(name, config) {
1331
+ this.providers.set(name, config);
1332
+ }
1333
+ providerNames() {
1334
+ return Array.from(this.providers.keys()).sort((a, b) => a.localeCompare(b));
1335
+ }
1336
+ getProvider(name) {
1337
+ const provider = this.providers.get(name);
1338
+ if (!provider) {
1339
+ throw new AuthenticationException(`OAuth provider "${name}" is not configured.`);
1340
+ }
1341
+ return provider;
1342
+ }
1343
+ async authorize(providerName, options = {}) {
1344
+ const provider = this.getProvider(providerName);
1345
+ const { state, expiresAt } = await createOAuthState(
1346
+ providerName,
1347
+ this.stateStore,
1348
+ this.stateConfig,
1349
+ options.redirectTo,
1350
+ options.state
1351
+ );
1352
+ const scope = options.scope ?? provider.scopes;
1353
+ const url = buildOAuthAuthorizeUrl(provider, state, { scope, extraParams: options.extraParams });
1354
+ return { url, state, expiresAt };
1355
+ }
1356
+ async user(providerName, payload) {
1357
+ const provider = this.getProvider(providerName);
1358
+ const verified = await verifyOAuthState(payload.state, providerName, this.stateStore, this.stateConfig);
1359
+ if (!verified) {
1360
+ throw new AuthenticationException("Invalid or expired OAuth state.");
1361
+ }
1362
+ const token = await exchangeOAuthCode(provider, payload.code);
1363
+ return fetchOAuthUserProfile(provider, token);
1364
+ }
1365
+ };
1366
+ function createOAuthManager(options = {}) {
1367
+ return new OAuthManager(options);
1368
+ }
1369
+ async function createOAuthState(provider, store, config = {}, redirectTo, fixedState) {
1370
+ const state = fixedState ?? generateToken(config.stateLength ?? DEFAULT_STATE_LENGTH);
1371
+ const hashAlgorithm = config.hashAlgorithm ?? DEFAULT_STATE_HASH_ALGORITHM;
1372
+ const expiresAt = new Date(Date.now() + (config.expiresIn ?? DEFAULT_STATE_EXPIRES_IN));
1373
+ const stateHash = hashToken(state, hashAlgorithm);
1374
+ await store.store(stateHash, { provider, redirectTo, expiresAt });
1375
+ return { state, expiresAt };
1376
+ }
1377
+ async function verifyOAuthState(state, provider, store, config = {}) {
1378
+ const hashAlgorithm = config.hashAlgorithm ?? DEFAULT_STATE_HASH_ALGORITHM;
1379
+ const stateHash = hashToken(state, hashAlgorithm);
1380
+ const payload = await store.find(stateHash);
1381
+ if (!payload) return null;
1382
+ await store.delete(stateHash);
1383
+ if (payload.provider !== provider) return null;
1384
+ return payload;
1385
+ }
1386
+ function buildOAuthAuthorizeUrl(provider, state, options = {}) {
1387
+ const url = new URL(provider.authorizeUrl);
1388
+ url.searchParams.set("response_type", "code");
1389
+ url.searchParams.set("client_id", provider.clientId);
1390
+ url.searchParams.set("redirect_uri", provider.redirectUri);
1391
+ url.searchParams.set("state", state);
1392
+ const scopes = options.scope ?? provider.scopes;
1393
+ if (scopes && scopes.length > 0) {
1394
+ url.searchParams.set("scope", scopes.join(" "));
1395
+ }
1396
+ for (const [key, value] of Object.entries(options.extraParams ?? {})) {
1397
+ url.searchParams.set(key, value);
1398
+ }
1399
+ return url.toString();
1400
+ }
1401
+ async function exchangeOAuthCode(provider, code) {
1402
+ const params = new URLSearchParams({
1403
+ grant_type: "authorization_code",
1404
+ code,
1405
+ redirect_uri: provider.redirectUri,
1406
+ client_id: provider.clientId
1407
+ });
1408
+ const headers = {
1409
+ Accept: "application/json",
1410
+ "Content-Type": "application/x-www-form-urlencoded"
1411
+ };
1412
+ if (provider.tokenAuthMethod === "client_secret_basic") {
1413
+ const credentials = Buffer.from(`${provider.clientId}:${provider.clientSecret}`).toString("base64");
1414
+ headers.Authorization = `Basic ${credentials}`;
1415
+ } else {
1416
+ params.set("client_secret", provider.clientSecret);
1417
+ }
1418
+ const response = await fetch(provider.tokenUrl, {
1419
+ method: "POST",
1420
+ headers,
1421
+ body: params.toString()
1422
+ });
1423
+ if (!response.ok) {
1424
+ throw new AuthenticationException(`OAuth token exchange failed (${response.status}).`);
1425
+ }
1426
+ const raw = await response.json();
1427
+ const accessToken = typeof raw.access_token === "string" ? raw.access_token : "";
1428
+ if (!accessToken) {
1429
+ throw new AuthenticationException("OAuth token exchange did not return an access token.");
1430
+ }
1431
+ return {
1432
+ accessToken,
1433
+ tokenType: typeof raw.token_type === "string" ? raw.token_type : void 0,
1434
+ refreshToken: typeof raw.refresh_token === "string" ? raw.refresh_token : void 0,
1435
+ expiresIn: typeof raw.expires_in === "number" ? raw.expires_in : void 0,
1436
+ scope: typeof raw.scope === "string" ? raw.scope : void 0,
1437
+ raw
1438
+ };
1439
+ }
1440
+ async function fetchOAuthUserProfile(provider, token) {
1441
+ const response = await fetch(provider.userInfoUrl, {
1442
+ method: provider.userInfoMethod ?? "GET",
1443
+ headers: {
1444
+ Accept: "application/json",
1445
+ Authorization: `Bearer ${token.accessToken}`,
1446
+ "User-Agent": "guren-oauth"
1447
+ }
1448
+ });
1449
+ if (!response.ok) {
1450
+ throw new AuthenticationException(`OAuth user fetch failed (${response.status}).`);
1451
+ }
1452
+ const raw = await response.json();
1453
+ if (provider.mapProfile) {
1454
+ return provider.mapProfile(raw, token);
1455
+ }
1456
+ const idCandidate = raw.id ?? raw.sub;
1457
+ const id = typeof idCandidate === "string" || typeof idCandidate === "number" ? String(idCandidate) : "";
1458
+ if (!id) {
1459
+ throw new AuthenticationException("OAuth user profile is missing an identifier.");
1460
+ }
1461
+ return {
1462
+ id,
1463
+ email: typeof raw.email === "string" ? raw.email : void 0,
1464
+ name: typeof raw.name === "string" ? raw.name : void 0,
1465
+ avatar: typeof raw.avatar_url === "string" ? raw.avatar_url : typeof raw.picture === "string" ? raw.picture : typeof raw.avatar === "string" ? raw.avatar : void 0,
1466
+ token,
1467
+ raw
1468
+ };
1469
+ }
1470
+ function createGitHubOAuthProviderConfig(input) {
1471
+ return {
1472
+ ...input,
1473
+ authorizeUrl: "https://github.com/login/oauth/authorize",
1474
+ tokenUrl: "https://github.com/login/oauth/access_token",
1475
+ userInfoUrl: "https://api.github.com/user",
1476
+ scopes: input.scopes ?? ["read:user", "user:email"]
1477
+ };
1478
+ }
1479
+ function createGoogleOAuthProviderConfig(input) {
1480
+ return {
1481
+ ...input,
1482
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1483
+ tokenUrl: "https://oauth2.googleapis.com/token",
1484
+ userInfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
1485
+ scopes: input.scopes ?? ["openid", "profile", "email"]
1486
+ };
1487
+ }
1488
+ function createDiscordOAuthProviderConfig(input) {
1489
+ return {
1490
+ ...input,
1491
+ authorizeUrl: "https://discord.com/api/oauth2/authorize",
1492
+ tokenUrl: "https://discord.com/api/oauth2/token",
1493
+ userInfoUrl: "https://discord.com/api/users/@me",
1494
+ scopes: input.scopes ?? ["identify", "email"]
1495
+ };
1496
+ }
1497
+ function buildOAuthRedirectUrl(baseUrl, token, email) {
1498
+ return buildTokenUrl(baseUrl, token, email);
1499
+ }
1500
+ function parseOAuthRedirectUrl(url) {
1501
+ return parseTokenUrl(url);
1502
+ }
1503
+
1504
+ export {
1505
+ MemorySessionStore,
1506
+ createSessionMiddleware,
1507
+ getSessionFromContext,
1508
+ ScryptHasher,
1509
+ BaseUserProvider,
1510
+ ModelUserProvider,
1511
+ hashToken,
1512
+ generateToken,
1513
+ generateId,
1514
+ secureCompare,
1515
+ secureStringCompare,
1516
+ buildTokenUrl,
1517
+ parseTokenUrl,
1518
+ SessionGuard,
1519
+ AuthManager,
1520
+ MemoryApiTokenStore,
1521
+ createApiToken,
1522
+ parseApiToken,
1523
+ verifyApiToken,
1524
+ tokenCan,
1525
+ tokenCanAll,
1526
+ tokenCanAny,
1527
+ revokeApiToken,
1528
+ revokeAllApiTokens,
1529
+ getUserApiTokens,
1530
+ API_TOKEN_KEY,
1531
+ createBearerTokenMiddleware,
1532
+ getApiToken,
1533
+ getApiTokenOrFail,
1534
+ MemoryOAuthStateStore,
1535
+ OAuthManager,
1536
+ createOAuthManager,
1537
+ createOAuthState,
1538
+ verifyOAuthState,
1539
+ buildOAuthAuthorizeUrl,
1540
+ exchangeOAuthCode,
1541
+ fetchOAuthUserProfile,
1542
+ createGitHubOAuthProviderConfig,
1543
+ createGoogleOAuthProviderConfig,
1544
+ createDiscordOAuthProviderConfig,
1545
+ buildOAuthRedirectUrl,
1546
+ parseOAuthRedirectUrl,
1547
+ NodeHasher,
1548
+ AuthenticatableModel,
1549
+ MemoryPasswordResetStore,
1550
+ createPasswordResetToken,
1551
+ verifyPasswordResetToken,
1552
+ completePasswordReset,
1553
+ buildPasswordResetUrl,
1554
+ parsePasswordResetUrl,
1555
+ MemoryEmailVerificationStore,
1556
+ createEmailVerificationToken,
1557
+ verifyEmailToken,
1558
+ completeEmailVerification,
1559
+ buildVerificationUrl,
1560
+ parseVerificationUrl,
1561
+ isEmailVerified,
1562
+ requireVerifiedEmail
1563
+ };