@h3ravel/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,1391 @@
1
+ import { ServiceProvider, safeDot, setNested } from "@h3ravel/support";
2
+ import { DB } from "@h3ravel/database";
3
+ import crypto$1, { createHash, createHmac, randomBytes } from "crypto";
4
+ import { ConfigException } from "@h3ravel/foundation";
5
+ import { IHttpContext, ISessionDriver, ISessionManager } from "@h3ravel/contracts";
6
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
7
+ import path from "path";
8
+ import { Command } from "@h3ravel/musket";
9
+
10
+ //#region src/Encryption.ts
11
+ var Encryption = class {
12
+ key;
13
+ constructor() {
14
+ const appKey = process.env.APP_KEY;
15
+ if (!appKey) throw new ConfigException("APP_KEY not set in env");
16
+ this.key = createHash("sha256").update(Buffer.from(appKey, "base64")).digest();
17
+ }
18
+ /**
19
+ * Encrypt session data using AES-256-CBC and the APP_KEY.
20
+ */
21
+ encrypt(value) {
22
+ value = typeof value === "string" ? value : JSON.stringify(value);
23
+ const iv = crypto$1.randomBytes(16);
24
+ const cipher = crypto$1.createCipheriv("aes-256-cbc", this.key, iv);
25
+ const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
26
+ return iv.toString("hex") + ":" + encrypted.toString("hex");
27
+ }
28
+ /**
29
+ * Decrypt session data.
30
+ */
31
+ decrypt(value) {
32
+ const [ivHex, encryptedHex] = value.split(":");
33
+ const iv = Buffer.from(ivHex, "hex");
34
+ const encrypted = Buffer.from(encryptedHex, "hex");
35
+ const decipher = crypto$1.createDecipheriv("aes-256-cbc", this.key, iv);
36
+ const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
37
+ try {
38
+ return JSON.parse(decrypted);
39
+ } catch {
40
+ return decrypted;
41
+ }
42
+ }
43
+ };
44
+
45
+ //#endregion
46
+ //#region src/FlashBag.ts
47
+ /**
48
+ * FlashBag
49
+ *
50
+ * Manages flash data for session management, handling temporary data
51
+ * that persists for one request cycle.
52
+ */
53
+ var FlashBag = class {
54
+ /**
55
+ * Storage for flash data
56
+ *
57
+ * Structure:
58
+ * {
59
+ * new: { key1: value1, key2: value2 },
60
+ * old: { key3: value3, key4: value4 }
61
+ * }
62
+ */
63
+ flashData = {
64
+ new: {},
65
+ old: {}
66
+ };
67
+ /**
68
+ * Flash a value for the next request
69
+ *
70
+ * @param key Key to store in flash
71
+ * @param value Value to be flashed
72
+ */
73
+ flash(key, value) {
74
+ this.flashData.new[key] = value;
75
+ }
76
+ /**
77
+ * Store a temporary value for the current request only
78
+ *
79
+ * @param key Key to store
80
+ * @param value Value to store
81
+ */
82
+ now(key, value) {
83
+ this.flashData.new[key] = value;
84
+ }
85
+ /**
86
+ * Reflash all current flash data for another request cycle
87
+ */
88
+ reflash() {
89
+ this.flashData.old = { ...this.flashData.new };
90
+ }
91
+ /**
92
+ * Keep only specific flash keys for the next request
93
+ *
94
+ * @param keys Keys to keep
95
+ */
96
+ keep(keys) {
97
+ const keptNew = {};
98
+ const keptOld = {};
99
+ keys.forEach((key) => {
100
+ if (this.flashData.new[key] !== void 0) keptNew[key] = this.flashData.new[key];
101
+ if (this.flashData.old[key] !== void 0) keptOld[key] = this.flashData.old[key];
102
+ });
103
+ this.flashData.new = keptNew;
104
+ this.flashData.old = keptOld;
105
+ }
106
+ /**
107
+ * Age flash data at the end of the request
108
+ *
109
+ * - Removes old flash data
110
+ * - Moves new flash data to old
111
+ * - Clears new flash data
112
+ */
113
+ ageFlashData() {
114
+ this.flashData.old = {};
115
+ this.flashData.old = { ...this.flashData.new };
116
+ this.flashData.new = {};
117
+ }
118
+ /**
119
+ * Get a flash value
120
+ *
121
+ * @param key Key to retrieve
122
+ * @param defaultValue Default value if key doesn't exist
123
+ * @returns Flash value or default
124
+ */
125
+ get(key, defaultValue) {
126
+ return this.flashData.new[key] ?? this.flashData.old[key] ?? defaultValue;
127
+ }
128
+ /**
129
+ * Check if a flash key exists
130
+ *
131
+ * @param key Key to check
132
+ * @returns Boolean indicating existence
133
+ */
134
+ has(key) {
135
+ return key in this.flashData.new || key in this.flashData.old;
136
+ }
137
+ /**
138
+ * Get all flash data
139
+ *
140
+ * @returns Combined flash data
141
+ */
142
+ all() {
143
+ return {
144
+ ...this.flashData.old,
145
+ ...this.flashData.new
146
+ };
147
+ }
148
+ /**
149
+ * Get all flash data keys
150
+ *
151
+ * @returns Combined flash data
152
+ */
153
+ keys() {
154
+ return Object.keys({
155
+ ...this.flashData.old,
156
+ ...this.flashData.new
157
+ });
158
+ }
159
+ /**
160
+ * Get the raww flash data
161
+ *
162
+ * @returns raw flash data
163
+ */
164
+ raw() {
165
+ return this.flashData;
166
+ }
167
+ /**
168
+ * Clear all flash data
169
+ */
170
+ clear() {
171
+ this.flashData.new = {};
172
+ this.flashData.old = {};
173
+ }
174
+ };
175
+
176
+ //#endregion
177
+ //#region src/drivers/Driver.ts
178
+ /**
179
+ * Driver
180
+ *
181
+ * Base Session driver.
182
+ */
183
+ var Driver = class extends ISessionDriver {
184
+ encryptor = new Encryption();
185
+ sessionId;
186
+ flashBag = new FlashBag();
187
+ /**
188
+ * Save the raw session payload (session + flash)
189
+ */
190
+ saveRawPayload() {
191
+ this.savePayload(Object.assign({}, this.fetchPayload(), { _flash: this.flashBag.raw() }));
192
+ }
193
+ /**
194
+ * Retrieve all data from the session including flash
195
+ *
196
+ * @returns
197
+ */
198
+ getAll() {
199
+ const payload = this.fetchPayload();
200
+ const flash = payload._flash ?? {
201
+ old: {},
202
+ new: {}
203
+ };
204
+ return {
205
+ ...payload,
206
+ ...flash.old,
207
+ ...flash.new
208
+ };
209
+ }
210
+ /**
211
+ * Retrieve a value from the session
212
+ *
213
+ * @param key
214
+ * @param defaultValue
215
+ * @returns
216
+ */
217
+ get(key, defaultValue) {
218
+ return safeDot(this.getAll(), key) || defaultValue;
219
+ }
220
+ /**
221
+ * Store a value in the session
222
+ *
223
+ * @param key
224
+ * @param value
225
+ */
226
+ set(value) {
227
+ const payload = this.fetchPayload();
228
+ Object.assign(payload, value);
229
+ return this.savePayload(payload);
230
+ }
231
+ /**
232
+ * Store multiple key/value pairs
233
+ *
234
+ * @param values
235
+ */
236
+ put(key, value) {
237
+ const payload = this.fetchPayload();
238
+ setNested(payload, key, value);
239
+ return this.savePayload(payload);
240
+ }
241
+ /**
242
+ * Append a value to an array key
243
+ *
244
+ * @param key
245
+ * @param value
246
+ */
247
+ push(key, value) {
248
+ const payload = this.fetchPayload();
249
+ if (!Array.isArray(payload[key])) payload[key] = [];
250
+ payload[key].push(value);
251
+ return this.savePayload(payload);
252
+ }
253
+ /**
254
+ * Remove a key from the session
255
+ *
256
+ * @param key
257
+ */
258
+ forget(key) {
259
+ const payload = this.fetchPayload();
260
+ delete payload[key];
261
+ return this.savePayload(payload);
262
+ }
263
+ /**
264
+ * Retrieve all session data
265
+ *
266
+ * @returns
267
+ */
268
+ all() {
269
+ return this.fetchPayload();
270
+ }
271
+ /**
272
+ * Determine if a key exists (even if null).
273
+ *
274
+ * @param key
275
+ * @returns
276
+ */
277
+ exists(key) {
278
+ const data = this.getAll();
279
+ return Object.prototype.hasOwnProperty.call(data, key);
280
+ }
281
+ /**
282
+ * Determine if a key has a non-null value.
283
+ *
284
+ * @param key
285
+ * @returns
286
+ */
287
+ has(key) {
288
+ const data = this.getAll();
289
+ return data[key] !== void 0 && data[key] !== null;
290
+ }
291
+ /**
292
+ * Get only specific keys.
293
+ *
294
+ * @param keys
295
+ * @returns
296
+ */
297
+ only(keys) {
298
+ const data = this.fetchPayload();
299
+ const result = {};
300
+ keys.forEach((k) => {
301
+ if (k in data) result[k] = data[k];
302
+ });
303
+ return result;
304
+ }
305
+ /**
306
+ * Return all keys except the specified ones.
307
+ *
308
+ * @param keys
309
+ * @returns
310
+ */
311
+ except(keys) {
312
+ const data = this.fetchPayload();
313
+ keys.forEach((k) => delete data[k]);
314
+ return data;
315
+ }
316
+ /**
317
+ * Return and delete a key from the session.
318
+ *
319
+ * @param key
320
+ * @param defaultValue
321
+ * @returns
322
+ */
323
+ pull(key, defaultValue = null) {
324
+ const data = this.fetchPayload();
325
+ const value = data[key] ?? defaultValue;
326
+ delete data[key];
327
+ this.savePayload(data);
328
+ return value;
329
+ }
330
+ /**
331
+ * Increment a numeric value by amount (default 1).
332
+ *
333
+ * @param key
334
+ * @param amount
335
+ * @returns
336
+ */
337
+ increment(key, amount = 1) {
338
+ const data = this.fetchPayload();
339
+ const newVal = (parseFloat(data[key]) || 0) + amount;
340
+ data[key] = newVal;
341
+ this.savePayload(data);
342
+ return newVal;
343
+ }
344
+ /**
345
+ * Decrement a numeric value by amount (default 1).
346
+ *
347
+ * @param key
348
+ * @param amount
349
+ * @returns
350
+ */
351
+ decrement(key, amount = 1) {
352
+ return this.increment(key, -amount);
353
+ }
354
+ /**
355
+ * Flash a value for next request only.
356
+ *
357
+ * @param key
358
+ * @param value
359
+ */
360
+ flash(key, value) {
361
+ this.flashBag.flash(key, value);
362
+ this.saveRawPayload();
363
+ }
364
+ /**
365
+ * Reflash all flash data for one more cycle.
366
+ *
367
+ * @returns
368
+ */
369
+ reflash() {
370
+ this.flashBag.reflash();
371
+ this.saveRawPayload();
372
+ }
373
+ /**
374
+ * Keep only selected flash data.
375
+ *
376
+ * @param keys
377
+ * @returns
378
+ */
379
+ keep(keys) {
380
+ this.flashBag.keep(keys);
381
+ this.saveRawPayload();
382
+ }
383
+ /**
384
+ * Store a temporary value (flash) for this request only (not persisted)
385
+ *
386
+ * @param key
387
+ * @param value
388
+ */
389
+ now(key, value) {
390
+ this.flashBag.now(key, value);
391
+ this.saveRawPayload();
392
+ }
393
+ /**
394
+ * Regenerate session ID and persist data under new ID.
395
+ */
396
+ regenerate() {
397
+ const oldData = this.fetchPayload();
398
+ this.sessionId = crypto.randomUUID();
399
+ this.savePayload(oldData);
400
+ }
401
+ /**
402
+ * Age flash data at the end of the request lifecycle.
403
+ */
404
+ ageFlashData() {
405
+ const data = this.flashBag.ageFlashData();
406
+ this.saveRawPayload();
407
+ return data;
408
+ }
409
+ /**
410
+ * Determine if an item is not present in the session.
411
+ *
412
+ * @param key
413
+ * @returns
414
+ */
415
+ missing(key) {
416
+ return !this.exists(key);
417
+ }
418
+ /**
419
+ * Flush all session data
420
+ */
421
+ flush() {
422
+ return this.savePayload({});
423
+ }
424
+ };
425
+
426
+ //#endregion
427
+ //#region src/drivers/DatabaseDriver.ts
428
+ /**
429
+ * DatabaseDriver
430
+ *
431
+ * Stores sessions in a database table. Each session ID maps to a row.
432
+ * The `payload` column contains all session key/value pairs as JSON.
433
+ */
434
+ var DatabaseDriver = class extends Driver {
435
+ /**
436
+ *
437
+ * @param sessionId The current session ID
438
+ * @param table
439
+ */
440
+ constructor(sessionId, table = "sessions") {
441
+ super();
442
+ this.sessionId = sessionId;
443
+ this.table = table;
444
+ }
445
+ /**
446
+ * Get the query builder for this table
447
+ */
448
+ query() {
449
+ return DB.table(this.table).where("id", this.sessionId);
450
+ }
451
+ /**
452
+ * Fetch the session payload
453
+ */
454
+ async fetchPayload() {
455
+ const row = await this.query().first();
456
+ if (!row) return {};
457
+ try {
458
+ const decrypted = this.encryptor.decrypt(row.payload);
459
+ return typeof decrypted === "string" ? JSON.parse(decrypted) : decrypted;
460
+ } catch {
461
+ return {};
462
+ }
463
+ }
464
+ /**
465
+ * Save the session payload back to DB
466
+ */
467
+ async savePayload(payload) {
468
+ const now = Math.floor(Date.now() / 1e3);
469
+ const exists = await this.query().exists();
470
+ const encrypted = this.encryptor.encrypt(JSON.stringify(payload));
471
+ if (exists) await this.query().update({
472
+ payload: encrypted,
473
+ last_activity: now
474
+ });
475
+ else await DB.table(this.table).insert({
476
+ id: this.sessionId,
477
+ payload: encrypted,
478
+ last_activity: now
479
+ });
480
+ }
481
+ /**
482
+ * Retrieve all data from the session including flash
483
+ */
484
+ async getAll() {
485
+ const payload = await this.fetchPayload();
486
+ const flash = payload._flash ?? {
487
+ old: {},
488
+ new: {}
489
+ };
490
+ return {
491
+ ...payload,
492
+ ...flash.old,
493
+ ...flash.new
494
+ };
495
+ }
496
+ /**
497
+ * Get a value from the session
498
+ */
499
+ async get(key, defaultValue) {
500
+ return safeDot(await this.getAll(), key) || defaultValue;
501
+ }
502
+ /**
503
+ * Set one or multiple session values
504
+ */
505
+ async set(values) {
506
+ const payload = await this.fetchPayload();
507
+ Object.assign(payload, values);
508
+ await this.savePayload(payload);
509
+ }
510
+ /**
511
+ * Store a single key/value pair
512
+ */
513
+ async put(key, value) {
514
+ const payload = await this.fetchPayload();
515
+ setNested(payload, key, value);
516
+ await this.savePayload(payload);
517
+ }
518
+ /**
519
+ * Append a value to an array key
520
+ */
521
+ async push(key, value) {
522
+ const payload = await this.fetchPayload();
523
+ if (!Array.isArray(payload[key])) payload[key] = [];
524
+ payload[key].push(value);
525
+ await this.savePayload(payload);
526
+ }
527
+ /**
528
+ * Forget a session key
529
+ */
530
+ async forget(key) {
531
+ const payload = await this.fetchPayload();
532
+ delete payload[key];
533
+ await this.savePayload(payload);
534
+ }
535
+ /**
536
+ * Retrieve all session data (excluding flash)
537
+ */
538
+ async all() {
539
+ return this.fetchPayload();
540
+ }
541
+ /**
542
+ * Determine if a key exists (even if null)
543
+ */
544
+ async exists(key) {
545
+ const data = await this.getAll();
546
+ return Object.prototype.hasOwnProperty.call(data, key);
547
+ }
548
+ /**
549
+ * Determine if a key has a non-null value
550
+ */
551
+ async has(key) {
552
+ const data = await this.getAll();
553
+ return data[key] !== void 0 && data[key] !== null;
554
+ }
555
+ /**
556
+ * Get only specific keys
557
+ */
558
+ async only(keys) {
559
+ const data = await this.fetchPayload();
560
+ const result = {};
561
+ keys.forEach((k) => {
562
+ if (k in data) result[k] = data[k];
563
+ });
564
+ return result;
565
+ }
566
+ /**
567
+ * Return all except specific keys
568
+ */
569
+ async except(keys) {
570
+ const data = await this.fetchPayload();
571
+ keys.forEach((k) => delete data[k]);
572
+ return data;
573
+ }
574
+ /**
575
+ * Retrieve and delete a value
576
+ */
577
+ async pull(key, defaultValue = null) {
578
+ const data = await this.fetchPayload();
579
+ const value = data[key] ?? defaultValue;
580
+ delete data[key];
581
+ await this.savePayload(data);
582
+ return value;
583
+ }
584
+ /**
585
+ * Increment a numeric value
586
+ */
587
+ async increment(key, amount = 1) {
588
+ const data = await this.fetchPayload();
589
+ const newVal = (parseFloat(data[key]) || 0) + amount;
590
+ data[key] = newVal;
591
+ await this.savePayload(data);
592
+ return newVal;
593
+ }
594
+ /**
595
+ * Decrement a numeric value
596
+ */
597
+ async decrement(key, amount = 1) {
598
+ return this.increment(key, -amount);
599
+ }
600
+ /**
601
+ * Flash a value for next request only
602
+ */
603
+ async flash(key, value) {
604
+ this.flashBag.flash(key, value);
605
+ }
606
+ /**
607
+ * Reflash all flash data for one more cycle
608
+ */
609
+ async reflash() {
610
+ this.flashBag.reflash();
611
+ }
612
+ /**
613
+ * Keep only specific flash keys
614
+ */
615
+ async keep(keys) {
616
+ this.flashBag.keep(keys);
617
+ }
618
+ /**
619
+ * Store a temporary value (flash) for this request only (not persisted)
620
+ */
621
+ async now(key, value) {
622
+ this.flashBag.now(key, value);
623
+ }
624
+ /**
625
+ * Regenerate session ID with same data
626
+ */
627
+ async regenerate() {
628
+ const oldData = await this.fetchPayload();
629
+ this.sessionId = crypto.randomUUID();
630
+ await this.savePayload(oldData);
631
+ }
632
+ /**
633
+ * Check if a key is missing
634
+ */
635
+ async missing(key) {
636
+ return !await this.exists(key);
637
+ }
638
+ /**
639
+ * Flush all session data
640
+ */
641
+ async flush() {
642
+ await this.savePayload({});
643
+ }
644
+ /**
645
+ * Invalidate the session and regenerate
646
+ */
647
+ async invalidate() {
648
+ await DB.table(this.table).where("id", this.sessionId).delete();
649
+ this.sessionId = crypto.randomUUID();
650
+ this.flashBag = new FlashBag();
651
+ await this.savePayload({});
652
+ }
653
+ /**
654
+ * Age flash data at the end of the request lifecycle.
655
+ */
656
+ async ageFlashData() {
657
+ this.flashBag.ageFlashData();
658
+ }
659
+ };
660
+
661
+ //#endregion
662
+ //#region src/drivers/FileDriver.ts
663
+ /**
664
+ * FileDriver
665
+ *
666
+ * Stores session data as encrypted JSON files.
667
+ * Each session is stored in its own file named after the session ID.
668
+ * Ideal for local development or low-scale deployments.
669
+ */
670
+ var FileDriver = class extends Driver {
671
+ constructor(sessionId, sessionDir = path.resolve(".sessions"), cwd = process.cwd()) {
672
+ super();
673
+ this.sessionId = sessionId;
674
+ this.sessionDir = sessionDir;
675
+ this.cwd = cwd;
676
+ this.sessionDir = path.join(this.cwd, sessionDir);
677
+ this.sessionId = sessionId;
678
+ if (!existsSync(this.sessionDir)) mkdirSync(this.sessionDir, { recursive: true });
679
+ this.ensureSessionFile();
680
+ }
681
+ /**
682
+ * Ensures the session file exists and is initialized.
683
+ */
684
+ ensureSessionFile() {
685
+ if (!existsSync(this.sessionFilePath())) this.savePayload({});
686
+ }
687
+ /**
688
+ * Get the absolute path for the current session file.
689
+ */
690
+ sessionFilePath() {
691
+ return path.join(this.sessionDir, this.sessionId);
692
+ }
693
+ /**
694
+ * Read raw decrypted payload (including _flash).
695
+ */
696
+ readRawPayload() {
697
+ const file = this.sessionFilePath();
698
+ if (!existsSync(file)) return {};
699
+ const content = readFileSync(file, "utf8");
700
+ try {
701
+ return this.encryptor.decrypt(content);
702
+ } catch {
703
+ return {};
704
+ }
705
+ }
706
+ /**
707
+ * Fetch decrypted payload and strip out flash metadata.
708
+ */
709
+ fetchPayload() {
710
+ return this.readRawPayload();
711
+ }
712
+ /**
713
+ * Write and encrypt session data to file.
714
+ * Always persists flash state.
715
+ *
716
+ * @param data
717
+ */
718
+ savePayload(payload) {
719
+ writeFileSync(this.sessionFilePath(), this.encryptor.encrypt(payload), "utf8");
720
+ }
721
+ /**
722
+ * Completely invalidate the current session and regenerate a new one.
723
+ */
724
+ invalidate() {
725
+ const file = this.sessionFilePath();
726
+ if (existsSync(file)) rmSync(file, { recursive: true });
727
+ this.sessionId = crypto.randomUUID();
728
+ this.flashBag = new FlashBag();
729
+ this.savePayload({});
730
+ }
731
+ };
732
+
733
+ //#endregion
734
+ //#region src/drivers/MemoryDriver.ts
735
+ /**
736
+ * MemoryDriver
737
+ *
738
+ * Lightweight, ephemeral session storage.
739
+ * Intended for tests, local development, or short-lived apps.
740
+ */
741
+ var MemoryDriver = class MemoryDriver extends Driver {
742
+ static store = {};
743
+ constructor(sessionId) {
744
+ super();
745
+ this.sessionId = sessionId;
746
+ this.sessionId = sessionId;
747
+ if (!MemoryDriver.store[this.sessionId]) MemoryDriver.store[this.sessionId] = {};
748
+ }
749
+ /**
750
+ * Fetch and return session payload.
751
+ *
752
+ * @returns Decrypted and usable payload
753
+ */
754
+ fetchPayload() {
755
+ return { ...MemoryDriver.store[this.sessionId] };
756
+ }
757
+ /**
758
+ * Persist session payload and flash bag state.
759
+ *
760
+ * @param data
761
+ */
762
+ savePayload(payload) {
763
+ MemoryDriver.store[this.sessionId] = { ...payload };
764
+ }
765
+ /**
766
+ * Invalidate current session and regenerate new session ID.
767
+ */
768
+ invalidate() {
769
+ delete MemoryDriver.store[this.sessionId];
770
+ this.sessionId = crypto$1.randomUUID();
771
+ this.flashBag = new FlashBag();
772
+ this.savePayload({});
773
+ }
774
+ };
775
+
776
+ //#endregion
777
+ //#region src/drivers/RedisDriver.ts
778
+ /**
779
+ * RedisDriver (placeholder)
780
+ */
781
+ var RedisDriver = class extends Driver {
782
+ static store = {};
783
+ constructor(sessionId, redisClient, prefix) {
784
+ super();
785
+ this.sessionId = sessionId;
786
+ this.redisClient = redisClient;
787
+ this.prefix = prefix;
788
+ }
789
+ /**
790
+ * Fetch and return session payload.
791
+ *
792
+ * @returns Decrypted and usable payload
793
+ */
794
+ fetchPayload() {
795
+ return {};
796
+ }
797
+ /**
798
+ * Persist session payload and flash bag state.
799
+ *
800
+ * @param data
801
+ */
802
+ savePayload(_payload) {}
803
+ /**
804
+ * Invalidate current session and regenerate new session ID.
805
+ */
806
+ invalidate() {
807
+ this.flashBag = new FlashBag();
808
+ this.savePayload({});
809
+ }
810
+ };
811
+
812
+ //#endregion
813
+ //#region src/adapters.ts
814
+ /**
815
+ * FileDriver builder
816
+ * constructor(sessionId: string, sessionDir?: string, cwd?: string)
817
+ */
818
+ const fileBuilder = (sessionId, options = {}) => {
819
+ return new FileDriver(sessionId, options.sessionDir ?? options.dir ?? "./storage/sessions", options.cwd ?? process.cwd());
820
+ };
821
+ /**
822
+ * DatabaseDriver builder
823
+ * constructor(sessionId: string, table?: string)
824
+ */
825
+ const dbBuilder = (sessionId, options = {}) => {
826
+ const table = options.table ?? "sessions";
827
+ return new DatabaseDriver(options.sessionId ?? sessionId, table);
828
+ };
829
+ /**
830
+ * MemoryDriver builder
831
+ * constructor(sessionId: string)
832
+ */
833
+ const memoryBuilder = (sessionId) => {
834
+ return new MemoryDriver(sessionId);
835
+ };
836
+ /**
837
+ * RedisDriver builder
838
+ * constructor(sessionId: string, redisClient?: RedisClient, prefix?: string)
839
+ */
840
+ const redisBuilder = (sessionId, options = {}) => {
841
+ const client = options.client;
842
+ return new RedisDriver(sessionId, client, options.prefix ?? "h3ravel:sessions:");
843
+ };
844
+
845
+ //#endregion
846
+ //#region src/Commands/MakeSessionTableCommand.ts
847
+ var MakeSessionTableCommand = class extends Command {
848
+ /**
849
+ * The name and signature of the console command.
850
+ *
851
+ * @var string
852
+ */
853
+ signature = "make:session-table";
854
+ /**
855
+ * The console command description.
856
+ *
857
+ * @var string
858
+ */
859
+ description = "Create a migration for the session database table";
860
+ async handle() {
861
+ await DB.instance().schema.hasTable("sessions").then(async function(exists) {
862
+ if (!exists) return DB.instance().schema.createTable("sessions", (table) => {
863
+ table.string("id", 255).primary();
864
+ table.bigInteger("user_id").nullable().index();
865
+ table.string("ip_address", 45).nullable();
866
+ table.text("user_agent").nullable();
867
+ table.text("payload", "longtext").nullable();
868
+ table.integer("last_activity").index();
869
+ });
870
+ });
871
+ this.info("INFO: session table created successfully.");
872
+ }
873
+ };
874
+
875
+ //#endregion
876
+ //#region ../../node_modules/.pnpm/h3@2.0.1-rc.5/node_modules/h3/dist/h3.mjs
877
+ const kEventNS = "h3.internal.event.";
878
+ const kEventRes = /* @__PURE__ */ Symbol.for(`${kEventNS}res`);
879
+ const kEventResHeaders = /* @__PURE__ */ Symbol.for(`${kEventNS}res.headers`);
880
+ function parse(str, options) {
881
+ if (typeof str !== "string") throw new TypeError("argument str must be a string");
882
+ const obj = {};
883
+ const opt = options || {};
884
+ const dec = opt.decode || decode;
885
+ let index = 0;
886
+ while (index < str.length) {
887
+ const eqIdx = str.indexOf("=", index);
888
+ if (eqIdx === -1) break;
889
+ let endIdx = str.indexOf(";", index);
890
+ if (endIdx === -1) endIdx = str.length;
891
+ else if (endIdx < eqIdx) {
892
+ index = str.lastIndexOf(";", eqIdx - 1) + 1;
893
+ continue;
894
+ }
895
+ const key = str.slice(index, eqIdx).trim();
896
+ if (opt?.filter && !opt?.filter(key)) {
897
+ index = endIdx + 1;
898
+ continue;
899
+ }
900
+ if (void 0 === obj[key]) {
901
+ let val = str.slice(eqIdx + 1, endIdx).trim();
902
+ if (val.codePointAt(0) === 34) val = val.slice(1, -1);
903
+ obj[key] = tryDecode(val, dec);
904
+ }
905
+ index = endIdx + 1;
906
+ }
907
+ return obj;
908
+ }
909
+ function decode(str) {
910
+ return str.includes("%") ? decodeURIComponent(str) : str;
911
+ }
912
+ function tryDecode(str, decode2) {
913
+ try {
914
+ return decode2(str);
915
+ } catch {
916
+ return str;
917
+ }
918
+ }
919
+ const fieldContentRegExp = /^[\u0009\u0020-\u007E\u0080-\u00FF]+$/;
920
+ function serialize(name, value, options) {
921
+ const opt = options || {};
922
+ const enc = opt.encode || encodeURIComponent;
923
+ if (typeof enc !== "function") throw new TypeError("option encode is invalid");
924
+ if (!fieldContentRegExp.test(name)) throw new TypeError("argument name is invalid");
925
+ const encodedValue = enc(value);
926
+ if (encodedValue && !fieldContentRegExp.test(encodedValue)) throw new TypeError("argument val is invalid");
927
+ let str = name + "=" + encodedValue;
928
+ if (void 0 !== opt.maxAge && opt.maxAge !== null) {
929
+ const maxAge = opt.maxAge - 0;
930
+ if (Number.isNaN(maxAge) || !Number.isFinite(maxAge)) throw new TypeError("option maxAge is invalid");
931
+ str += "; Max-Age=" + Math.floor(maxAge);
932
+ }
933
+ if (opt.domain) {
934
+ if (!fieldContentRegExp.test(opt.domain)) throw new TypeError("option domain is invalid");
935
+ str += "; Domain=" + opt.domain;
936
+ }
937
+ if (opt.path) {
938
+ if (!fieldContentRegExp.test(opt.path)) throw new TypeError("option path is invalid");
939
+ str += "; Path=" + opt.path;
940
+ }
941
+ if (opt.expires) {
942
+ if (!isDate(opt.expires) || Number.isNaN(opt.expires.valueOf())) throw new TypeError("option expires is invalid");
943
+ str += "; Expires=" + opt.expires.toUTCString();
944
+ }
945
+ if (opt.httpOnly) str += "; HttpOnly";
946
+ if (opt.secure) str += "; Secure";
947
+ if (opt.priority) switch (typeof opt.priority === "string" ? opt.priority.toLowerCase() : opt.priority) {
948
+ case "low":
949
+ str += "; Priority=Low";
950
+ break;
951
+ case "medium":
952
+ str += "; Priority=Medium";
953
+ break;
954
+ case "high":
955
+ str += "; Priority=High";
956
+ break;
957
+ default: throw new TypeError("option priority is invalid");
958
+ }
959
+ if (opt.sameSite) switch (typeof opt.sameSite === "string" ? opt.sameSite.toLowerCase() : opt.sameSite) {
960
+ case true:
961
+ str += "; SameSite=Strict";
962
+ break;
963
+ case "lax":
964
+ str += "; SameSite=Lax";
965
+ break;
966
+ case "strict":
967
+ str += "; SameSite=Strict";
968
+ break;
969
+ case "none":
970
+ str += "; SameSite=None";
971
+ break;
972
+ default: throw new TypeError("option sameSite is invalid");
973
+ }
974
+ if (opt.partitioned) str += "; Partitioned";
975
+ return str;
976
+ }
977
+ function isDate(val) {
978
+ return Object.prototype.toString.call(val) === "[object Date]" || val instanceof Date;
979
+ }
980
+ function parseSetCookie(setCookieValue, options) {
981
+ const parts = (setCookieValue || "").split(";").filter((str) => typeof str === "string" && !!str.trim());
982
+ const parsed = _parseNameValuePair(parts.shift() || "");
983
+ const name = parsed.name;
984
+ let value = parsed.value;
985
+ try {
986
+ value = options?.decode === false ? value : (options?.decode || decodeURIComponent)(value);
987
+ } catch {}
988
+ const cookie = {
989
+ name,
990
+ value
991
+ };
992
+ for (const part of parts) {
993
+ const sides = part.split("=");
994
+ const partKey = (sides.shift() || "").trimStart().toLowerCase();
995
+ const partValue = sides.join("=");
996
+ switch (partKey) {
997
+ case "expires":
998
+ cookie.expires = new Date(partValue);
999
+ break;
1000
+ case "max-age":
1001
+ cookie.maxAge = Number.parseInt(partValue, 10);
1002
+ break;
1003
+ case "secure":
1004
+ cookie.secure = true;
1005
+ break;
1006
+ case "httponly":
1007
+ cookie.httpOnly = true;
1008
+ break;
1009
+ case "samesite":
1010
+ cookie.sameSite = partValue;
1011
+ break;
1012
+ default: cookie[partKey] = partValue;
1013
+ }
1014
+ }
1015
+ return cookie;
1016
+ }
1017
+ function _parseNameValuePair(nameValuePairStr) {
1018
+ let name = "";
1019
+ let value = "";
1020
+ const nameValueArr = nameValuePairStr.split("=");
1021
+ if (nameValueArr.length > 1) {
1022
+ name = nameValueArr.shift();
1023
+ value = nameValueArr.join("=");
1024
+ } else value = nameValuePairStr;
1025
+ return {
1026
+ name,
1027
+ value
1028
+ };
1029
+ }
1030
+ function parseCookies(event) {
1031
+ return parse(event.req.headers.get("cookie") || "");
1032
+ }
1033
+ function getCookie(event, name) {
1034
+ return parseCookies(event)[name];
1035
+ }
1036
+ function setCookie(event, name, value, options) {
1037
+ const newCookie = serialize(name, value, {
1038
+ path: "/",
1039
+ ...options
1040
+ });
1041
+ const currentCookies = event.res.headers.getSetCookie();
1042
+ if (currentCookies.length === 0) {
1043
+ event.res.headers.set("set-cookie", newCookie);
1044
+ return;
1045
+ }
1046
+ const newCookieKey = _getDistinctCookieKey(name, options || {});
1047
+ event.res.headers.delete("set-cookie");
1048
+ for (const cookie of currentCookies) {
1049
+ if (_getDistinctCookieKey(cookie.split("=")?.[0], parseSetCookie(cookie)) === newCookieKey) continue;
1050
+ event.res.headers.append("set-cookie", cookie);
1051
+ }
1052
+ event.res.headers.append("set-cookie", newCookie);
1053
+ }
1054
+ function _getDistinctCookieKey(name, options) {
1055
+ return [
1056
+ name,
1057
+ options.domain || "",
1058
+ options.path || "/"
1059
+ ].join(";");
1060
+ }
1061
+
1062
+ //#endregion
1063
+ //#region src/SessionStore.ts
1064
+ /**
1065
+ * SessionStore (Driver registry)
1066
+ *
1067
+ * Register driver builders under a name and then create instances using:
1068
+ * SessionStore.make('file', sessionId, options)
1069
+ */
1070
+ var SessionStore = class {
1071
+ static registry = /* @__PURE__ */ new Map();
1072
+ /**
1073
+ * Register a driver builder under a key (e.g. 'file', 'database', 'memory').
1074
+ */
1075
+ static register(name, builder) {
1076
+ this.registry.set(name, builder);
1077
+ }
1078
+ /**
1079
+ * Create a driver instance for the given sessionId using the named builder.
1080
+ *
1081
+ * If driver not found, throws. Options is a simple key/value bag passed to the builder.
1082
+ */
1083
+ static make(name, sessionId, options = {}) {
1084
+ const builder = this.registry.get(name);
1085
+ if (!builder) throw new Error(`Session driver "${name}" is not registered`);
1086
+ return builder(sessionId, options);
1087
+ }
1088
+ };
1089
+
1090
+ //#endregion
1091
+ //#region src/SessionManager.ts
1092
+ /**
1093
+ * SessionManager
1094
+ *
1095
+ * Handles session initialization, ID generation, and encryption.
1096
+ * Each request gets a unique session namespace tied to its ID.
1097
+ */
1098
+ var SessionManager = class SessionManager extends ISessionManager {
1099
+ app;
1100
+ ctx;
1101
+ driver;
1102
+ appKey;
1103
+ sessionId;
1104
+ request;
1105
+ flashBag;
1106
+ constructor(app, driverName = "file", driverOptions = {}) {
1107
+ super();
1108
+ this.appKey = process.env.APP_KEY;
1109
+ if (app instanceof IHttpContext) {
1110
+ this.request = app.request;
1111
+ this.ctx = app;
1112
+ this.app = app.app;
1113
+ } else {
1114
+ this.app = app;
1115
+ this.ctx = app.make("http.context");
1116
+ this.request = this.ctx.request;
1117
+ }
1118
+ this.sessionId = this.resolveSessionId();
1119
+ this.driver = SessionStore.make(driverName, driverOptions.sessionId ?? this.sessionId, driverOptions);
1120
+ this.flashBag = this.driver.flashBag;
1121
+ }
1122
+ /**
1123
+ * Initialize the Session Manager
1124
+ *
1125
+ * @param ctx
1126
+ * @returns
1127
+ */
1128
+ static init(app) {
1129
+ return new SessionManager(app, config("session.driver", "file"), {
1130
+ cwd: config("session.files"),
1131
+ sessionDir: "/",
1132
+ dir: "/",
1133
+ table: config("session.table"),
1134
+ prefix: config("database.connections.redis.options.prefix"),
1135
+ client: config(`database.connections.${config("session.driver", "file")}.client`)
1136
+ });
1137
+ }
1138
+ /**
1139
+ * Generate a secure session ID unique to the user device.
1140
+ */
1141
+ generateSessionId() {
1142
+ const userAgent = this.request.getHeader("user-agent") || "";
1143
+ const ip = this.request.getHeader("x-forwarded-for") || this.request.ip() || "";
1144
+ const random = randomBytes(32).toString("hex");
1145
+ const fingerprint = createHash("sha256").update(`${userAgent}-${ip}`).digest("hex");
1146
+ return createHmac("sha256", this.appKey).update(`${fingerprint}-${random}`).digest("hex");
1147
+ }
1148
+ /**
1149
+ * Resolve the session ID from cookie, header, or create a new one.
1150
+ */
1151
+ resolveSessionId() {
1152
+ const cookieSession = getCookie(this.ctx.event, "h3ravel_session");
1153
+ if (cookieSession) return cookieSession;
1154
+ const newId = this.generateSessionId();
1155
+ setCookie(this.ctx.event, "h3ravel_session", newId, {
1156
+ httpOnly: true,
1157
+ secure: true,
1158
+ sameSite: "lax",
1159
+ maxAge: 3600 * 24 * 7
1160
+ });
1161
+ return newId;
1162
+ }
1163
+ /**
1164
+ * Access the current session ID.
1165
+ */
1166
+ id() {
1167
+ return this.sessionId;
1168
+ }
1169
+ /**
1170
+ * Get the current session driver
1171
+ */
1172
+ getDriver() {
1173
+ return this.driver;
1174
+ }
1175
+ /**
1176
+ * Retrieve a value from the session
1177
+ *
1178
+ * @param key
1179
+ * @param defaultValue
1180
+ * @returns
1181
+ */
1182
+ get(key, defaultValue) {
1183
+ return this.driver.get(key, defaultValue);
1184
+ }
1185
+ /**
1186
+ * Store a value in the session
1187
+ *
1188
+ * @param key
1189
+ * @param value
1190
+ */
1191
+ set(value) {
1192
+ return this.driver.set(value);
1193
+ }
1194
+ /**
1195
+ * Store multiple key/value pairs
1196
+ *
1197
+ * @param values
1198
+ */
1199
+ put(key, value) {
1200
+ return this.driver.put(key, value);
1201
+ }
1202
+ /**
1203
+ * Append a value to an array key
1204
+ *
1205
+ * @param key
1206
+ * @param value
1207
+ */
1208
+ push(key, value) {
1209
+ return this.driver.push(key, value);
1210
+ }
1211
+ /**
1212
+ * Remove a key from the session
1213
+ *
1214
+ * @param key
1215
+ */
1216
+ forget(key) {
1217
+ return this.driver.forget(key);
1218
+ }
1219
+ /**
1220
+ * Retrieve all session data
1221
+ *
1222
+ * @returns
1223
+ */
1224
+ all() {
1225
+ return this.driver.all();
1226
+ }
1227
+ /**
1228
+ * Determine if a key exists (even if null).
1229
+ *
1230
+ * @param key
1231
+ * @returns
1232
+ */
1233
+ exists(key) {
1234
+ return this.driver.exists(key);
1235
+ }
1236
+ /**
1237
+ * Determine if a key has a non-null value.
1238
+ *
1239
+ * @param key
1240
+ * @returns
1241
+ */
1242
+ has(key) {
1243
+ return this.driver.has(key);
1244
+ }
1245
+ /**
1246
+ * Get only specific keys.
1247
+ *
1248
+ * @param keys
1249
+ * @returns
1250
+ */
1251
+ only(keys) {
1252
+ return this.driver.only(keys);
1253
+ }
1254
+ /**
1255
+ * Return all keys except the specified ones.
1256
+ *
1257
+ * @param keys
1258
+ * @returns
1259
+ */
1260
+ except(keys) {
1261
+ return this.driver.except(keys);
1262
+ }
1263
+ /**
1264
+ * Return and delete a key from the session.
1265
+ *
1266
+ * @param key
1267
+ * @param defaultValue
1268
+ * @returns
1269
+ */
1270
+ pull(key, defaultValue = null) {
1271
+ return this.driver.pull(key, defaultValue);
1272
+ }
1273
+ /**
1274
+ * Increment a numeric value by amount (default 1).
1275
+ *
1276
+ * @param key
1277
+ * @param amount
1278
+ * @returns
1279
+ */
1280
+ increment(key, amount = 1) {
1281
+ return this.driver.increment(key, amount);
1282
+ }
1283
+ /**
1284
+ * Decrement a numeric value by amount (default 1).
1285
+ *
1286
+ * @param key
1287
+ * @param amount
1288
+ * @returns
1289
+ */
1290
+ decrement(key, amount = 1) {
1291
+ return this.driver.decrement(key, amount);
1292
+ }
1293
+ /**
1294
+ * Flash a value for next request only.
1295
+ *
1296
+ * @param key
1297
+ * @param value
1298
+ */
1299
+ flash(key, value) {
1300
+ return this.driver.flash(key, value);
1301
+ }
1302
+ /**
1303
+ * Reflash all flash data for one more cycle.
1304
+ *
1305
+ * @returns
1306
+ */
1307
+ reflash() {
1308
+ return this.driver.reflash();
1309
+ }
1310
+ /**
1311
+ * Keep only selected flash data.
1312
+ *
1313
+ * @param keys
1314
+ * @returns
1315
+ */
1316
+ keep(keys) {
1317
+ return this.driver.keep(keys);
1318
+ }
1319
+ /**
1320
+ * Store data only for current request cycle (not persisted).
1321
+ *
1322
+ * @param key
1323
+ * @param value
1324
+ */
1325
+ now(key, value) {
1326
+ return this.driver.now(key, value);
1327
+ }
1328
+ /**
1329
+ * Regenerate session ID and persist data under new ID.
1330
+ */
1331
+ regenerate() {
1332
+ return this.driver.regenerate();
1333
+ }
1334
+ /**
1335
+ * Determine if an item is not present in the session.
1336
+ *
1337
+ * @param key
1338
+ * @returns
1339
+ */
1340
+ missing(key) {
1341
+ return this.driver.missing(key);
1342
+ }
1343
+ /**
1344
+ * Flush all session data
1345
+ */
1346
+ flush() {
1347
+ return this.driver.flush();
1348
+ }
1349
+ /**
1350
+ * Invalidate the session completely and regenerate ID.
1351
+ *
1352
+ * @returns
1353
+ */
1354
+ invalidate() {
1355
+ return this.driver.invalidate();
1356
+ }
1357
+ /**
1358
+ * Age flash data at the end of the request lifecycle.
1359
+ *
1360
+ * @returns
1361
+ */
1362
+ ageFlashData() {
1363
+ return this.driver.ageFlashData();
1364
+ }
1365
+ };
1366
+
1367
+ //#endregion
1368
+ //#region src/Providers/SessionServiceProvider.ts
1369
+ var SessionServiceProvider = class extends ServiceProvider {
1370
+ static priority = 895;
1371
+ static order = "before:HttpServiceProvider";
1372
+ register() {
1373
+ /**
1374
+ * Register default drivers.
1375
+ */
1376
+ SessionStore.register("file", fileBuilder);
1377
+ SessionStore.register("database", dbBuilder);
1378
+ SessionStore.register("memory", memoryBuilder);
1379
+ SessionStore.register("redis", redisBuilder);
1380
+ this.app.singleton("session", (app) => {
1381
+ return SessionManager.init(app);
1382
+ });
1383
+ this.app.singleton("session.store", (app) => {
1384
+ return app.make("session").getDriver();
1385
+ });
1386
+ this.registerCommands([MakeSessionTableCommand]);
1387
+ }
1388
+ };
1389
+
1390
+ //#endregion
1391
+ export { DatabaseDriver, Driver, Encryption, FileDriver, FlashBag, MakeSessionTableCommand, MemoryDriver, RedisDriver, SessionManager, SessionServiceProvider, SessionStore, dbBuilder, fileBuilder, memoryBuilder, redisBuilder };