@orieljs/session 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,801 @@
1
+ // Store.ts
2
+ import { Arr, Str } from "@orieljs/support";
3
+ var unique = (values) => [...new Set(values)];
4
+ var Store = class {
5
+ #id = "";
6
+ #name;
7
+ #attributes = {};
8
+ #started = false;
9
+ #handler;
10
+ #encrypter;
11
+ constructor(name, handler, id, encrypter) {
12
+ this.#name = name;
13
+ this.#handler = handler;
14
+ this.#encrypter = encrypter;
15
+ this.setId(id);
16
+ }
17
+ /** Loads the session from the handler and makes sure it has a CSRF token. */
18
+ async start() {
19
+ this.#attributes = { ...this.#attributes, ...await this.#readFromHandler() };
20
+ if (!this.has("_token")) this.regenerateToken();
21
+ this.#started = true;
22
+ }
23
+ /** Ages the flash data and writes the session through the handler. */
24
+ async save() {
25
+ this.ageFlashData();
26
+ const payload = JSON.stringify(this.#attributes);
27
+ await this.#handler.write(this.getId(), this.#encrypter === void 0 ? payload : this.#encrypter.encrypt(payload, false));
28
+ this.#started = false;
29
+ }
30
+ /** Forgets last request's flash data and marks this request's flash data as old. */
31
+ ageFlashData() {
32
+ this.forget(...this.get("_flash.old", []));
33
+ this.put("_flash.old", this.get("_flash.new", []));
34
+ this.put("_flash.new", []);
35
+ }
36
+ all() {
37
+ return { ...this.#attributes };
38
+ }
39
+ only(keys) {
40
+ return Arr.only(this.#attributes, keys);
41
+ }
42
+ except(keys) {
43
+ return Arr.except(this.#attributes, keys);
44
+ }
45
+ /** Whether the key is present, even with a null value. */
46
+ exists(...keys) {
47
+ return keys.every((key) => Arr.has(this.#attributes, key));
48
+ }
49
+ missing(...keys) {
50
+ return !this.exists(...keys);
51
+ }
52
+ /** Whether the key is present and not null/undefined. */
53
+ has(key) {
54
+ const value = this.get(key);
55
+ return value !== void 0 && value !== null;
56
+ }
57
+ get(key, defaultValue) {
58
+ return Arr.get(this.#attributes, key, defaultValue);
59
+ }
60
+ pull(key, defaultValue) {
61
+ const value = this.get(key, defaultValue);
62
+ this.forget(key);
63
+ return value;
64
+ }
65
+ put(key, value) {
66
+ const values = typeof key === "string" ? { [key]: value } : key;
67
+ for (const [name, item] of Object.entries(values)) Arr.set(this.#attributes, name, item);
68
+ }
69
+ push(key, value) {
70
+ const list = this.get(key, []);
71
+ this.put(key, [...Array.isArray(list) ? list : [list], value]);
72
+ }
73
+ increment(key, amount = 1) {
74
+ const value = Number(this.get(key, 0)) + amount;
75
+ this.put(key, value);
76
+ return value;
77
+ }
78
+ decrement(key, amount = 1) {
79
+ return this.increment(key, -amount);
80
+ }
81
+ forget(...keys) {
82
+ for (const key of keys) Arr.forget(this.#attributes, key);
83
+ }
84
+ /** Removes a key and returns its value. */
85
+ remove(key) {
86
+ return this.pull(key);
87
+ }
88
+ flush() {
89
+ this.#attributes = {};
90
+ }
91
+ /** Available on this request and the next one. */
92
+ flash(key, value = true) {
93
+ this.put(key, value);
94
+ this.push("_flash.new", key);
95
+ this.#removeFromOldFlashData([key]);
96
+ }
97
+ /** Available on this request only. */
98
+ now(key, value) {
99
+ this.put(key, value);
100
+ this.push("_flash.old", key);
101
+ }
102
+ /** Keeps all of last request's flash data for one more request. */
103
+ reflash() {
104
+ this.#mergeNewFlashes(this.get("_flash.old", []));
105
+ this.put("_flash.old", []);
106
+ }
107
+ /** Keeps some of last request's flash data for one more request. */
108
+ keep(...keys) {
109
+ this.#mergeNewFlashes(keys);
110
+ this.#removeFromOldFlashData(keys);
111
+ }
112
+ flashInput(input) {
113
+ this.flash("_old_input", input);
114
+ }
115
+ hasOldInput(key) {
116
+ const old2 = this.getOldInput(key);
117
+ return key === void 0 ? typeof old2 === "object" && old2 !== null && Object.keys(old2).length > 0 : old2 !== void 0 && old2 !== null;
118
+ }
119
+ getOldInput(key, defaultValue) {
120
+ return Arr.get(this.get("_old_input", {}), key, defaultValue);
121
+ }
122
+ token() {
123
+ return this.get("_token");
124
+ }
125
+ regenerateToken() {
126
+ this.put("_token", Str.random(40));
127
+ }
128
+ /** New session id and CSRF token. With destroy, the old record is deleted. */
129
+ async regenerate(destroy = false) {
130
+ await this.migrate(destroy);
131
+ this.regenerateToken();
132
+ }
133
+ /** New session id, keeping the data (and the CSRF token). With destroy, the old record is deleted. */
134
+ async migrate(destroy = false) {
135
+ if (destroy) await this.#handler.destroy(this.getId());
136
+ this.#handler.setExists?.(false);
137
+ this.setId(this.#generateSessionId());
138
+ }
139
+ /** Flushes the data and migrates to a new id, destroying the old record. */
140
+ async invalidate() {
141
+ this.flush();
142
+ await this.migrate(true);
143
+ }
144
+ previousUrl() {
145
+ return this.get("_previous.url");
146
+ }
147
+ setPreviousUrl(url) {
148
+ this.put("_previous.url", url);
149
+ }
150
+ isStarted() {
151
+ return this.#started;
152
+ }
153
+ getName() {
154
+ return this.#name;
155
+ }
156
+ setName(name) {
157
+ this.#name = name;
158
+ }
159
+ getId() {
160
+ return this.#id;
161
+ }
162
+ /** Invalid ids (anything but 40 alphanumerics) are replaced by a fresh one, so ids from cookies can't be forged. */
163
+ setId(id) {
164
+ this.#id = this.isValidId(id) ? id : this.#generateSessionId();
165
+ }
166
+ isValidId(id) {
167
+ return typeof id === "string" && /^[a-zA-Z0-9]{40}$/.test(id);
168
+ }
169
+ getHandler() {
170
+ return this.#handler;
171
+ }
172
+ #generateSessionId() {
173
+ return Str.random(40);
174
+ }
175
+ async #readFromHandler() {
176
+ let data = await this.#handler.read(this.getId());
177
+ if (data === "") return {};
178
+ if (this.#encrypter !== void 0) {
179
+ try {
180
+ data = this.#encrypter.decrypt(data, false);
181
+ } catch {
182
+ return {};
183
+ }
184
+ }
185
+ try {
186
+ const parsed = JSON.parse(data);
187
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
188
+ } catch {
189
+ return {};
190
+ }
191
+ }
192
+ #mergeNewFlashes(keys) {
193
+ this.put("_flash.new", unique([...this.get("_flash.new", []), ...keys]));
194
+ }
195
+ #removeFromOldFlashData(keys) {
196
+ this.put("_flash.old", this.get("_flash.old", []).filter((key) => !keys.includes(key)));
197
+ }
198
+ };
199
+
200
+ // handlers/ArraySessionHandler.ts
201
+ var ArraySessionHandler = class {
202
+ constructor(minutes, clock = Date.now) {
203
+ this.minutes = minutes;
204
+ this.clock = clock;
205
+ }
206
+ minutes;
207
+ clock;
208
+ #storage = /* @__PURE__ */ new Map();
209
+ async read(id) {
210
+ const session2 = this.#storage.get(id);
211
+ if (session2 === void 0) return "";
212
+ return session2.time >= this.clock() - this.minutes * 6e4 ? session2.data : "";
213
+ }
214
+ async write(id, data) {
215
+ this.#storage.set(id, { data, time: this.clock() });
216
+ }
217
+ async destroy(id) {
218
+ this.#storage.delete(id);
219
+ }
220
+ async gc(maxLifetimeSeconds) {
221
+ const expiration = this.clock() - maxLifetimeSeconds * 1e3;
222
+ for (const [id, session2] of this.#storage) if (session2.time <= expiration) this.#storage.delete(id);
223
+ }
224
+ /** The raw stored payload (for tests and debugging). */
225
+ raw(id) {
226
+ return this.#storage.get(id)?.data;
227
+ }
228
+ };
229
+
230
+ // handlers/FileSessionHandler.ts
231
+ import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
232
+ import { join } from "path";
233
+ import { Str as Str2 } from "@orieljs/support";
234
+ var VALID_ID = /^[a-zA-Z0-9]{40}$/;
235
+ var FileSessionHandler = class {
236
+ constructor(path, minutes, clock = Date.now) {
237
+ this.path = path;
238
+ this.minutes = minutes;
239
+ this.clock = clock;
240
+ }
241
+ path;
242
+ minutes;
243
+ clock;
244
+ async read(id) {
245
+ if (!VALID_ID.test(id)) return "";
246
+ const file = join(this.path, id);
247
+ try {
248
+ const { mtimeMs } = await stat(file);
249
+ if (mtimeMs < this.clock() - this.minutes * 6e4) return "";
250
+ return await readFile(file, "utf8");
251
+ } catch {
252
+ return "";
253
+ }
254
+ }
255
+ async write(id, data) {
256
+ if (!VALID_ID.test(id)) return;
257
+ await mkdir(this.path, { recursive: true });
258
+ const temporary = join(this.path, `.${id}.${Str2.random(8)}.tmp`);
259
+ await writeFile(temporary, data, "utf8");
260
+ await rename(temporary, join(this.path, id));
261
+ }
262
+ async destroy(id) {
263
+ if (VALID_ID.test(id)) await rm(join(this.path, id), { force: true });
264
+ }
265
+ async gc(maxLifetimeSeconds) {
266
+ const expiration = this.clock() - maxLifetimeSeconds * 1e3;
267
+ const files = await readdir(this.path).catch(() => []);
268
+ for (const file of files) {
269
+ if (!VALID_ID.test(file)) continue;
270
+ const { mtimeMs } = await stat(join(this.path, file)).catch(() => ({ mtimeMs: Number.POSITIVE_INFINITY }));
271
+ if (mtimeMs <= expiration) await rm(join(this.path, file), { force: true });
272
+ }
273
+ }
274
+ };
275
+
276
+ // handlers/DatabaseSessionHandler.ts
277
+ import { AuthFactory, ExceptionHandler, HttpRequest } from "@orieljs/contracts";
278
+ var DatabaseSessionHandler = class {
279
+ constructor(connection, table, minutes, container = () => void 0, clock = Date.now) {
280
+ this.connection = connection;
281
+ this.table = table;
282
+ this.minutes = minutes;
283
+ this.container = container;
284
+ this.clock = clock;
285
+ }
286
+ connection;
287
+ table;
288
+ minutes;
289
+ container;
290
+ clock;
291
+ #exists = false;
292
+ async read(id) {
293
+ const session2 = await this.#query().where("id", id).first();
294
+ if (session2 === void 0) return "";
295
+ this.#exists = true;
296
+ if (Number(session2.last_activity) < this.#now() - this.minutes * 60) return "";
297
+ return typeof session2.payload === "string" ? Buffer.from(session2.payload, "base64").toString("utf8") : "";
298
+ }
299
+ async write(id, data) {
300
+ const payload = await this.#payload(data);
301
+ if (this.#exists) {
302
+ await this.#query().where("id", id).update(payload);
303
+ return;
304
+ }
305
+ try {
306
+ await this.#query().insert({ id, ...payload });
307
+ } catch {
308
+ await this.#query().where("id", id).update(payload);
309
+ }
310
+ this.#exists = true;
311
+ }
312
+ async destroy(id) {
313
+ await this.#query().where("id", id).delete();
314
+ }
315
+ async gc(maxLifetimeSeconds) {
316
+ await this.#query().where("last_activity", "<=", this.#now() - maxLifetimeSeconds).delete();
317
+ }
318
+ /** Called by Store.migrate(): the next write inserts a new row. */
319
+ setExists(exists) {
320
+ this.#exists = exists;
321
+ }
322
+ #query() {
323
+ return this.connection().table(this.table);
324
+ }
325
+ #now() {
326
+ return Math.floor(this.clock() / 1e3);
327
+ }
328
+ async #payload(data) {
329
+ const payload = { payload: Buffer.from(data, "utf8").toString("base64"), last_activity: this.#now() };
330
+ const container = this.container();
331
+ if (container === void 0) return payload;
332
+ if (container.bound(HttpRequest)) {
333
+ const request = container.make(HttpRequest);
334
+ payload.ip_address = request.ip() ?? null;
335
+ payload.user_agent = (request.header("user-agent") ?? "").slice(0, 500);
336
+ }
337
+ if (container.bound(AuthFactory)) {
338
+ const guard = await this.#guard(container);
339
+ payload.user_id = guard?.hasUser() ? await guard.id() ?? null : null;
340
+ }
341
+ return payload;
342
+ }
343
+ /** The default guard, or undefined when it can't be built (the error is reported, not thrown). */
344
+ async #guard(container) {
345
+ try {
346
+ return container.make(AuthFactory).guard();
347
+ } catch (error) {
348
+ try {
349
+ if (container.bound(ExceptionHandler)) await container.make(ExceptionHandler).report(error);
350
+ } catch {
351
+ }
352
+ return void 0;
353
+ }
354
+ }
355
+ };
356
+
357
+ // handlers/CookieSessionHandler.ts
358
+ var CookieSessionHandler = class {
359
+ constructor(jar, request, minutes, expireOnClose = false, clock = Date.now) {
360
+ this.jar = jar;
361
+ this.request = request;
362
+ this.minutes = minutes;
363
+ this.expireOnClose = expireOnClose;
364
+ this.clock = clock;
365
+ }
366
+ jar;
367
+ request;
368
+ minutes;
369
+ expireOnClose;
370
+ clock;
371
+ async read(id) {
372
+ const value = this.request()?.cookie(id);
373
+ if (value === void 0 || value === "") return "";
374
+ try {
375
+ const decoded = JSON.parse(value);
376
+ return typeof decoded.data === "string" && typeof decoded.expires === "number" && decoded.expires >= this.#now() ? decoded.data : "";
377
+ } catch {
378
+ return "";
379
+ }
380
+ }
381
+ async write(id, data) {
382
+ const jar = this.jar();
383
+ const value = JSON.stringify({ data, expires: this.#now() + this.minutes * 60 });
384
+ jar.queue(jar.make(id, value, this.expireOnClose ? 0 : this.minutes));
385
+ }
386
+ async destroy(id) {
387
+ const jar = this.jar();
388
+ jar.queue(jar.forget(id));
389
+ }
390
+ async gc() {
391
+ }
392
+ #now() {
393
+ return Math.floor(this.clock() / 1e3);
394
+ }
395
+ };
396
+
397
+ // handlers/RedisSessionHandler.ts
398
+ var RedisSessionHandler = class {
399
+ constructor(client, minutes, prefix = "oriel_session:") {
400
+ this.client = client;
401
+ this.minutes = minutes;
402
+ this.prefix = prefix;
403
+ }
404
+ client;
405
+ minutes;
406
+ prefix;
407
+ async read(id) {
408
+ return await this.client.get(this.prefix + id) ?? "";
409
+ }
410
+ async write(id, data) {
411
+ await this.client.set(this.prefix + id, data, "EX", Math.max(1, this.minutes * 60));
412
+ }
413
+ async destroy(id) {
414
+ await this.client.del(this.prefix + id);
415
+ }
416
+ /** Redis expires keys itself. */
417
+ async gc() {
418
+ }
419
+ };
420
+
421
+ // SessionManager.ts
422
+ import { Redis } from "ioredis";
423
+ import {
424
+ ConfigRepository,
425
+ CookieJar,
426
+ DatabaseManager,
427
+ Encrypter,
428
+ HttpRequest as HttpRequest2
429
+ } from "@orieljs/contracts";
430
+ function makeRedisClient(config = {}) {
431
+ const options = {};
432
+ for (const key of ["host", "port", "password", "username", "db", "lazyConnect"]) if (config[key] !== void 0) options[key] = config[key];
433
+ return typeof config.url === "string" && config.url !== "" ? new Redis(config.url, options) : new Redis(options);
434
+ }
435
+ var SessionManager = class {
436
+ constructor(app) {
437
+ this.app = app;
438
+ }
439
+ app;
440
+ #custom = /* @__PURE__ */ new Map();
441
+ #shared = /* @__PURE__ */ new Map();
442
+ #redis;
443
+ getSessionConfig() {
444
+ return this.app.make(ConfigRepository).get("session", {});
445
+ }
446
+ getDefaultDriver() {
447
+ return this.getSessionConfig().driver ?? void 0;
448
+ }
449
+ /** Register a custom driver: Session.extend('mongo', (container, config) => new MongoHandler(...)). */
450
+ extend(driver, factory) {
451
+ this.#custom.set(driver, factory);
452
+ return this;
453
+ }
454
+ /** A new Store for `container` (the request or job scope). */
455
+ build(container, driver) {
456
+ const config = this.getSessionConfig();
457
+ const handler = this.handler(driver ?? config.driver ?? "array", container);
458
+ const encrypter = config.encrypt === true ? container.make(Encrypter) : void 0;
459
+ return new Store(config.cookie ?? "oriel_session", handler, void 0, encrypter);
460
+ }
461
+ handler(driver, container) {
462
+ const config = this.getSessionConfig();
463
+ const lifetime = config.lifetime ?? 120;
464
+ const custom = this.#custom.get(driver);
465
+ if (custom !== void 0) return custom(container, config);
466
+ switch (driver) {
467
+ case "array":
468
+ return this.#share("array", () => new ArraySessionHandler(lifetime));
469
+ case "file":
470
+ return this.#share("file", () => new FileSessionHandler(config.files ?? "storage/framework/sessions", lifetime));
471
+ case "redis":
472
+ return this.#share("redis", () => new RedisSessionHandler(this.redis(), lifetime));
473
+ case "database":
474
+ return new DatabaseSessionHandler(
475
+ () => container.make(DatabaseManager).connection(config.connection ?? void 0),
476
+ config.table ?? "sessions",
477
+ lifetime,
478
+ () => container
479
+ );
480
+ case "cookie":
481
+ return new CookieSessionHandler(
482
+ () => container.make(CookieJar),
483
+ () => container.bound(HttpRequest2) ? container.make(HttpRequest2) : void 0,
484
+ lifetime,
485
+ config.expire_on_close === true
486
+ );
487
+ default:
488
+ throw new Error(`Driver [${driver}] not supported.`);
489
+ }
490
+ }
491
+ /** The redis client for config('session.connection') (an entry of config('database.redis'), default 'default'). */
492
+ redis() {
493
+ if (this.#redis === void 0) {
494
+ const name = this.getSessionConfig().connection ?? "default";
495
+ this.#redis = makeRedisClient(this.app.make(ConfigRepository).get(`database.redis.${name}`, {}));
496
+ }
497
+ return this.#redis;
498
+ }
499
+ /** Closes the redis connection, if one was opened. */
500
+ async disconnect() {
501
+ await this.#redis?.quit();
502
+ this.#redis = void 0;
503
+ }
504
+ #share(driver, make) {
505
+ let handler = this.#shared.get(driver);
506
+ if (handler === void 0) {
507
+ handler = make();
508
+ this.#shared.set(driver, handler);
509
+ }
510
+ return handler;
511
+ }
512
+ };
513
+
514
+ // errors.ts
515
+ var TokenMismatchError = class extends Error {
516
+ kind = "TokenMismatch";
517
+ status = 419;
518
+ name = "TokenMismatchError";
519
+ constructor(message = "CSRF token mismatch.") {
520
+ super(message);
521
+ }
522
+ };
523
+
524
+ // middleware/StartSession.ts
525
+ import { randomInt } from "crypto";
526
+ import { ConfigRepository as ConfigRepository2, Container, Session } from "@orieljs/contracts";
527
+ import { inject } from "@orieljs/container";
528
+
529
+ // middleware/cookies.ts
530
+ function serialize(cookie) {
531
+ const parts = [`${cookie.name}=${encodeURIComponent(cookie.value)}`];
532
+ if (cookie.minutes > 0) {
533
+ parts.push(`Expires=${new Date(Date.now() + cookie.minutes * 6e4).toUTCString()}`, `Max-Age=${cookie.minutes * 60}`);
534
+ }
535
+ parts.push(`Path=${cookie.path}`);
536
+ if (cookie.domain !== void 0 && cookie.domain !== "") parts.push(`Domain=${cookie.domain}`);
537
+ if (cookie.secure) parts.push("Secure");
538
+ if (cookie.httpOnly) parts.push("HttpOnly");
539
+ parts.push(`SameSite=${cookie.sameSite.charAt(0).toUpperCase()}${cookie.sameSite.slice(1)}`);
540
+ return parts.join("; ");
541
+ }
542
+ function attachCookie(response, cookie) {
543
+ const headers = new Headers(response.headers);
544
+ headers.append("Set-Cookie", serialize(cookie));
545
+ return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
546
+ }
547
+ function resolveSecure(secure, request) {
548
+ return secure ?? (request !== void 0 && request.url().startsWith("https:"));
549
+ }
550
+ function sessionCookie(config, options, request) {
551
+ return {
552
+ name: options.name,
553
+ value: options.value,
554
+ minutes: config.expire_on_close === true ? 0 : config.lifetime ?? 120,
555
+ path: config.path ?? "/",
556
+ ...config.domain ? { domain: config.domain } : {},
557
+ secure: resolveSecure(config.secure, request),
558
+ httpOnly: options.httpOnly,
559
+ sameSite: config.same_site ?? "lax"
560
+ };
561
+ }
562
+
563
+ // middleware/StartSession.ts
564
+ var StartSession = class {
565
+ #container = inject(Container);
566
+ #config = inject(ConfigRepository2);
567
+ async handle(request, next) {
568
+ const config = this.#config.get("session", {});
569
+ if (config.driver === void 0 || config.driver === null) return next(request);
570
+ const session2 = this.#container.make(Session);
571
+ session2.setId?.(request.cookie(session2.getName?.() ?? config.cookie ?? "oriel_session"));
572
+ await session2.start();
573
+ await this.#collectGarbage(session2, config);
574
+ let response = await next(request);
575
+ this.#storeCurrentUrl(request, session2);
576
+ response = attachCookie(
577
+ response,
578
+ sessionCookie(
579
+ config,
580
+ {
581
+ name: session2.getName?.() ?? config.cookie ?? "oriel_session",
582
+ value: session2.getId(),
583
+ httpOnly: config.http_only ?? true
584
+ },
585
+ request
586
+ )
587
+ );
588
+ await session2.save();
589
+ return response;
590
+ }
591
+ /** Laravel's lottery: on average lottery[0] requests out of lottery[1] clean up expired sessions. */
592
+ async #collectGarbage(session2, config) {
593
+ const [chances, outOf] = config.lottery ?? [2, 100];
594
+ if (session2.getHandler !== void 0 && randomInt(1, outOf + 1) <= chances) {
595
+ await session2.getHandler().gc((config.lifetime ?? 120) * 60);
596
+ }
597
+ }
598
+ #storeCurrentUrl(request, session2) {
599
+ const prefetch = request.header("purpose") === "prefetch" || request.header("sec-purpose") === "prefetch";
600
+ if (request.isMethod("GET") && !request.ajax() && !prefetch) session2.setPreviousUrl(request.fullUrl());
601
+ }
602
+ };
603
+
604
+ // middleware/VerifyCsrfToken.ts
605
+ import {
606
+ Application,
607
+ ConfigRepository as ConfigRepository3,
608
+ Container as Container2,
609
+ Encrypter as Encrypter2,
610
+ Session as Session2
611
+ } from "@orieljs/contracts";
612
+ import { inject as inject2 } from "@orieljs/container";
613
+ import { Str as Str3 } from "@orieljs/support";
614
+
615
+ // equals.ts
616
+ import { timingSafeEqual } from "crypto";
617
+ function equals(known, user) {
618
+ const a = Buffer.from(known);
619
+ const b = Buffer.from(user);
620
+ return a.length === b.length && timingSafeEqual(a, b);
621
+ }
622
+
623
+ // middleware/VerifyCsrfToken.ts
624
+ var VerifyCsrfToken = class {
625
+ /** URIs (wildcards allowed) excluded from the check, in addition to config('http.csrf_except'). */
626
+ except = [];
627
+ /** Whether to send the XSRF-TOKEN cookie. */
628
+ addHttpCookie = true;
629
+ #container = inject2(Container2);
630
+ #config = inject2(ConfigRepository3);
631
+ async handle(request, next) {
632
+ if (this.isReading(request) || this.runningUnitTests() || this.inExceptArray(request) || this.tokensMatch(request)) {
633
+ const response = await next(request);
634
+ return this.addHttpCookie ? this.addCookieToResponse(response, request) : response;
635
+ }
636
+ throw new TokenMismatchError();
637
+ }
638
+ isReading(request) {
639
+ return ["HEAD", "GET", "OPTIONS"].includes(request.method().toUpperCase());
640
+ }
641
+ runningUnitTests() {
642
+ if (!this.#container.bound(Application)) return false;
643
+ const app = this.#container.make(Application);
644
+ return app.runningInConsole() && app.runningUnitTests();
645
+ }
646
+ inExceptArray(request) {
647
+ const configured = this.#config.get("http.csrf_except", []);
648
+ const patterns = [...this.except, ...Array.isArray(configured) ? configured.map(String) : []];
649
+ return patterns.some((pattern) => {
650
+ const trimmed = pattern === "/" ? pattern : pattern.replace(/^\/+|\/+$/g, "");
651
+ return Str3.is(trimmed, request.fullUrl()) || request.is(trimmed);
652
+ });
653
+ }
654
+ tokensMatch(request) {
655
+ const sessionToken = this.#container.make(Session2).token();
656
+ const token = this.getTokenFromRequest(request);
657
+ return typeof sessionToken === "string" && typeof token === "string" && token !== "" && equals(sessionToken, token);
658
+ }
659
+ getTokenFromRequest(request) {
660
+ const input = request.input("_token");
661
+ const token = (typeof input === "string" && input !== "" ? input : void 0) ?? request.header("X-CSRF-TOKEN");
662
+ if (token !== void 0 && token !== "") return token;
663
+ const header = request.header("X-XSRF-TOKEN");
664
+ if (header === void 0 || header === "" || !this.#container.bound(Encrypter2)) return void 0;
665
+ try {
666
+ return this.#container.make(Encrypter2).decrypt(header, false).slice(41);
667
+ } catch {
668
+ return void 0;
669
+ }
670
+ }
671
+ addCookieToResponse(response, request) {
672
+ const token = this.#container.make(Session2).token();
673
+ if (!token) return response;
674
+ const config = this.#config.get("session", {});
675
+ return attachCookie(
676
+ response,
677
+ sessionCookie(config, { name: "XSRF-TOKEN", value: token, httpOnly: false }, request)
678
+ );
679
+ }
680
+ };
681
+
682
+ // ViewErrorBag.ts
683
+ var SessionMessageBag = class {
684
+ #messages;
685
+ constructor(messages = {}) {
686
+ this.#messages = messages;
687
+ }
688
+ has(key) {
689
+ return this.get(key).length > 0;
690
+ }
691
+ first(key) {
692
+ return key === void 0 ? this.all()[0] : this.get(key)[0];
693
+ }
694
+ get(key) {
695
+ const messages = Object.hasOwn(this.#messages, key) ? this.#messages[key] : void 0;
696
+ return Array.isArray(messages) ? messages.map(String) : [];
697
+ }
698
+ all() {
699
+ return Object.values(this.#messages).flatMap((messages) => Array.isArray(messages) ? messages.map(String) : []);
700
+ }
701
+ isEmpty() {
702
+ return this.count() === 0;
703
+ }
704
+ count() {
705
+ return this.all().length;
706
+ }
707
+ toObject() {
708
+ return Object.fromEntries(Object.keys(this.#messages).map((key) => [key, this.get(key)]));
709
+ }
710
+ };
711
+ function errorBagFrom(errors2, bag = "default") {
712
+ if (typeof errors2 !== "object" || errors2 === null) return new SessionMessageBag();
713
+ const messages = errors2[bag];
714
+ return new SessionMessageBag(typeof messages === "object" && messages !== null ? messages : {});
715
+ }
716
+
717
+ // facade.ts
718
+ import { Application as Application2, Session as Session3 } from "@orieljs/contracts";
719
+ import { currentContainer, inject as inject3 } from "@orieljs/container";
720
+ import { facade } from "@orieljs/support";
721
+ function scopeSession() {
722
+ const container = currentContainer();
723
+ if (container === void 0 || !container.bound(Session3)) return void 0;
724
+ if (container.bound(Application2) && container.make(Application2) === container) return void 0;
725
+ return container.make(Session3);
726
+ }
727
+ var SessionFacade = facade(Session3, {
728
+ extend: (driver, factory) => inject3(SessionManager).extend(driver, factory)
729
+ });
730
+ function session(key, defaultValue) {
731
+ const store = inject3(Session3);
732
+ if (key === void 0) return store;
733
+ if (typeof key === "string") return store.get(key, defaultValue);
734
+ store.put(key);
735
+ return void 0;
736
+ }
737
+ function old(key, defaultValue) {
738
+ const store = scopeSession();
739
+ return store === void 0 ? defaultValue : store.getOldInput(key, defaultValue);
740
+ }
741
+ function csrfToken() {
742
+ return inject3(Session3).token();
743
+ }
744
+ function errors(bag = "default") {
745
+ return errorBagFrom(scopeSession()?.get("errors"), bag);
746
+ }
747
+
748
+ // SessionServiceProvider.ts
749
+ import { Router, Session as Session4 } from "@orieljs/contracts";
750
+ import { ServiceProvider } from "@orieljs/support";
751
+ var SessionServiceProvider = class extends ServiceProvider {
752
+ register() {
753
+ this.mergeConfigFrom(
754
+ {
755
+ driver: "database",
756
+ lifetime: 120,
757
+ expire_on_close: false,
758
+ encrypt: false,
759
+ files: this.app.storagePath("framework/sessions"),
760
+ connection: null,
761
+ table: "sessions",
762
+ lottery: [2, 100],
763
+ cookie: "oriel_session",
764
+ path: "/",
765
+ domain: null,
766
+ secure: null,
767
+ http_only: true,
768
+ same_site: "lax"
769
+ },
770
+ "session"
771
+ );
772
+ this.app.singleton(SessionManager, (container) => new SessionManager(container));
773
+ this.app.scoped(Session4, (container) => container.make(SessionManager).build(container));
774
+ }
775
+ boot() {
776
+ if (!this.app.bound(Router)) return;
777
+ this.app.make(Router).pushMiddlewareToGroup("web", StartSession).pushMiddlewareToGroup("web", VerifyCsrfToken);
778
+ }
779
+ };
780
+ export {
781
+ ArraySessionHandler,
782
+ CookieSessionHandler,
783
+ DatabaseSessionHandler,
784
+ FileSessionHandler,
785
+ RedisSessionHandler,
786
+ SessionFacade as Session,
787
+ SessionManager,
788
+ SessionMessageBag,
789
+ SessionServiceProvider,
790
+ StartSession,
791
+ Store,
792
+ TokenMismatchError,
793
+ VerifyCsrfToken,
794
+ csrfToken,
795
+ errorBagFrom,
796
+ errors,
797
+ makeRedisClient,
798
+ old,
799
+ session
800
+ };
801
+ //# sourceMappingURL=index.js.map