@c9up/ream 0.1.33 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Application.d.ts +68 -0
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +142 -6
- package/dist/Application.js.map +1 -1
- package/dist/ConfigLoader.d.ts +4 -0
- package/dist/ConfigLoader.d.ts.map +1 -1
- package/dist/ConfigLoader.js +0 -8
- package/dist/ConfigLoader.js.map +1 -1
- package/dist/HealthCheck.d.ts +14 -7
- package/dist/HealthCheck.d.ts.map +1 -1
- package/dist/HealthCheck.js +21 -12
- package/dist/HealthCheck.js.map +1 -1
- package/dist/HttpKernel.d.ts +12 -0
- package/dist/HttpKernel.d.ts.map +1 -1
- package/dist/HttpKernel.js +15 -4
- package/dist/HttpKernel.js.map +1 -1
- package/dist/Ignitor.d.ts +8 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +11 -0
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts +13 -2
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +50 -30
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +65 -6
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +100 -7
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/console/BaseCommand.d.ts +8 -1
- package/dist/console/BaseCommand.d.ts.map +1 -1
- package/dist/console/BaseCommand.js +1 -1
- package/dist/console/BaseCommand.js.map +1 -1
- package/dist/console/Kernel.d.ts +15 -0
- package/dist/console/Kernel.d.ts.map +1 -1
- package/dist/console/Kernel.js +17 -0
- package/dist/console/Kernel.js.map +1 -1
- package/dist/directories.d.ts +52 -0
- package/dist/directories.d.ts.map +1 -0
- package/dist/directories.js +38 -0
- package/dist/directories.js.map +1 -0
- package/dist/events/Emitter.d.ts +49 -6
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +117 -7
- package/dist/events/Emitter.js.map +1 -1
- package/dist/events/EventsBuffer.d.ts +47 -0
- package/dist/events/EventsBuffer.d.ts.map +1 -0
- package/dist/events/EventsBuffer.js +107 -0
- package/dist/events/EventsBuffer.js.map +1 -0
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +1 -0
- package/dist/events/index.js.map +1 -1
- package/dist/health/BaseCheck.d.ts +19 -0
- package/dist/health/BaseCheck.d.ts.map +1 -0
- package/dist/health/BaseCheck.js +24 -0
- package/dist/health/BaseCheck.js.map +1 -0
- package/dist/health/HealthChecks.d.ts +15 -0
- package/dist/health/HealthChecks.d.ts.map +1 -0
- package/dist/health/HealthChecks.js +69 -0
- package/dist/health/HealthChecks.js.map +1 -0
- package/dist/health/Result.d.ts +27 -0
- package/dist/health/Result.d.ts.map +1 -0
- package/dist/health/Result.js +55 -0
- package/dist/health/Result.js.map +1 -0
- package/dist/health/checks/DiskSpaceCheck.d.ts +29 -0
- package/dist/health/checks/DiskSpaceCheck.d.ts.map +1 -0
- package/dist/health/checks/DiskSpaceCheck.js +58 -0
- package/dist/health/checks/DiskSpaceCheck.js.map +1 -0
- package/dist/health/checks/MemoryHeapCheck.d.ts +21 -0
- package/dist/health/checks/MemoryHeapCheck.d.ts.map +1 -0
- package/dist/health/checks/MemoryHeapCheck.js +55 -0
- package/dist/health/checks/MemoryHeapCheck.js.map +1 -0
- package/dist/health/checks/MemoryRSSCheck.d.ts +21 -0
- package/dist/health/checks/MemoryRSSCheck.d.ts.map +1 -0
- package/dist/health/checks/MemoryRSSCheck.js +55 -0
- package/dist/health/checks/MemoryRSSCheck.js.map +1 -0
- package/dist/health/checks/memory.d.ts +24 -0
- package/dist/health/checks/memory.d.ts.map +1 -0
- package/dist/health/checks/memory.js +88 -0
- package/dist/health/checks/memory.js.map +1 -0
- package/dist/health/index.d.ts +9 -0
- package/dist/health/index.d.ts.map +1 -0
- package/dist/health/index.js +8 -0
- package/dist/health/index.js.map +1 -0
- package/dist/health/tracing.d.ts +12 -0
- package/dist/health/tracing.d.ts.map +1 -0
- package/dist/health/tracing.js +11 -0
- package/dist/health/tracing.js.map +1 -0
- package/dist/health/types.d.ts +51 -0
- package/dist/health/types.d.ts.map +1 -0
- package/dist/health/types.js +5 -0
- package/dist/health/types.js.map +1 -0
- package/dist/helpers/bytes.d.ts +28 -0
- package/dist/helpers/bytes.d.ts.map +1 -0
- package/dist/helpers/bytes.js +63 -0
- package/dist/helpers/bytes.js.map +1 -0
- package/dist/helpers/string.d.ts +4 -0
- package/dist/helpers/string.d.ts.map +1 -1
- package/dist/helpers/string.js +2 -0
- package/dist/helpers/string.js.map +1 -1
- package/dist/http/HttpContext.d.ts +38 -2
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +81 -3
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/Request.d.ts +57 -3
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +51 -38
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +82 -2
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +240 -13
- package/dist/http/Response.js.map +1 -1
- package/dist/http/SseStream.d.ts +10 -0
- package/dist/http/SseStream.d.ts.map +1 -1
- package/dist/http/SseStream.js.map +1 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/migrations/MigrationRegistry.d.ts +28 -0
- package/dist/migrations/MigrationRegistry.d.ts.map +1 -0
- package/dist/migrations/MigrationRegistry.js +41 -0
- package/dist/migrations/MigrationRegistry.js.map +1 -0
- package/dist/migrations/types.d.ts +64 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +15 -0
- package/dist/migrations/types.js.map +1 -0
- package/dist/router/Router.d.ts +112 -9
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +206 -28
- package/dist/router/Router.js.map +1 -1
- package/dist/security/CookieSigner.d.ts +43 -4
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +68 -10
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/session/Session.d.ts +46 -1
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +57 -4
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +19 -9
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/drivers/CookieDriver.d.ts +1 -1
- package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
- package/dist/session/drivers/CookieDriver.js +3 -3
- package/dist/session/drivers/CookieDriver.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +2 -2
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/FileDriver.d.ts +1 -1
- package/dist/session/drivers/FileDriver.d.ts.map +1 -1
- package/dist/session/drivers/FileDriver.js +7 -5
- package/dist/session/drivers/FileDriver.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +1 -1
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts +1 -1
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +11 -4
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +5 -2
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Application.ts +169 -6
- package/src/ConfigLoader.ts +6 -0
- package/src/HealthCheck.ts +27 -14
- package/src/HttpKernel.ts +27 -4
- package/src/Ignitor.ts +17 -0
- package/src/bodyparser/BodyParserMiddleware.ts +70 -36
- package/src/bodyparser/MultipartFile.ts +125 -8
- package/src/console/BaseCommand.ts +8 -1
- package/src/console/Kernel.ts +18 -0
- package/src/directories.ts +81 -0
- package/src/events/Emitter.ts +129 -13
- package/src/events/EventsBuffer.ts +131 -0
- package/src/events/index.ts +7 -0
- package/src/health/BaseCheck.ts +30 -0
- package/src/health/HealthChecks.ts +83 -0
- package/src/health/Result.ts +69 -0
- package/src/health/checks/DiskSpaceCheck.ts +69 -0
- package/src/health/checks/MemoryHeapCheck.ts +68 -0
- package/src/health/checks/MemoryRSSCheck.ts +68 -0
- package/src/health/checks/memory.ts +115 -0
- package/src/health/index.ts +13 -0
- package/src/health/tracing.ts +15 -0
- package/src/health/types.ts +51 -0
- package/src/helpers/bytes.ts +71 -0
- package/src/helpers/string.ts +2 -0
- package/src/http/HttpContext.ts +86 -4
- package/src/http/Request.ts +82 -10
- package/src/http/Response.ts +283 -14
- package/src/http/SseStream.ts +10 -0
- package/src/index.ts +27 -1
- package/src/migrations/MigrationRegistry.ts +49 -0
- package/src/migrations/types.ts +68 -0
- package/src/router/Router.ts +241 -32
- package/src/security/CookieSigner.ts +92 -18
- package/src/session/Session.ts +71 -5
- package/src/session/SessionMiddleware.ts +19 -9
- package/src/session/drivers/CookieDriver.ts +4 -4
- package/src/session/drivers/DatabaseDriver.ts +3 -3
- package/src/session/drivers/FileDriver.ts +8 -6
- package/src/session/drivers/MemoryDriver.ts +2 -2
- package/src/session/drivers/RedisDriver.ts +12 -5
- package/tsconfig.app.json +13 -13
- package/tsconfig.base.json +19 -0
- package/tsconfig.client.json +16 -0
- package/tsconfig.package.json +15 -0
package/src/session/Session.ts
CHANGED
|
@@ -13,7 +13,16 @@
|
|
|
13
13
|
import type { SessionRedisClientSource } from './drivers/RedisDriver.js'
|
|
14
14
|
|
|
15
15
|
export interface SessionDriver {
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* The stored session, or `null` when the store has no entry for this id
|
|
18
|
+
* (AdonisJS `SessionStoreContract.read`).
|
|
19
|
+
*
|
|
20
|
+
* `null` and `{}` mean different things: the first is "no session here",
|
|
21
|
+
* the second "a session that happens to be empty". Without the difference a
|
|
22
|
+
* cookie whose server-side row has expired looked like a live session, and
|
|
23
|
+
* `commit()` touched a row that was not there.
|
|
24
|
+
*/
|
|
25
|
+
read(sessionId: string): Promise<Record<string, unknown> | null>
|
|
17
26
|
write(sessionId: string, data: Record<string, unknown>, ttl: number): Promise<void>
|
|
18
27
|
destroy(sessionId: string): Promise<void>
|
|
19
28
|
touch(sessionId: string, ttl: number): Promise<void>
|
|
@@ -51,6 +60,14 @@ export function supportsTagging(driver: SessionDriver): driver is SessionDriverW
|
|
|
51
60
|
)
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The session key the flash bag travels in.
|
|
65
|
+
*
|
|
66
|
+
* One constant rather than three literals: the read, the delete and the write
|
|
67
|
+
* below all have to agree, and they had to agree by hand.
|
|
68
|
+
*/
|
|
69
|
+
const FLASH_KEY = '__flash'
|
|
70
|
+
|
|
54
71
|
/** Where `flashAll` / `flashOnly` / `flashExcept` put the request input. */
|
|
55
72
|
const FLASH_INPUT_KEY = 'input'
|
|
56
73
|
|
|
@@ -175,9 +192,9 @@ export class Session {
|
|
|
175
192
|
*/
|
|
176
193
|
#hydrate(data: Record<string, unknown>): void {
|
|
177
194
|
this.#data = { ...data }
|
|
178
|
-
const flash = data
|
|
195
|
+
const flash = data[FLASH_KEY]
|
|
179
196
|
this.#previousFlash = isPlainRecord(flash) ? { ...flash } : {}
|
|
180
|
-
delete this.#data
|
|
197
|
+
delete this.#data[FLASH_KEY]
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
/** Current session id. Mutated by `regenerate()`. */
|
|
@@ -437,7 +454,7 @@ export class Session {
|
|
|
437
454
|
toJSON(): Record<string, unknown> {
|
|
438
455
|
const result = { ...this.#data }
|
|
439
456
|
if (Object.keys(this.#flashData).length > 0) {
|
|
440
|
-
result
|
|
457
|
+
result[FLASH_KEY] = this.#flashData
|
|
441
458
|
}
|
|
442
459
|
return result
|
|
443
460
|
}
|
|
@@ -472,6 +489,51 @@ export class Session {
|
|
|
472
489
|
* has no read-only session mode, and reporting `true` would tell a caller
|
|
473
490
|
* its writes were dropped when they were not.
|
|
474
491
|
*/
|
|
492
|
+
/**
|
|
493
|
+
* The options this session was seated with (AdonisJS `config`).
|
|
494
|
+
*
|
|
495
|
+
* What `setStore` was told: whether the session is fresh, and how long it
|
|
496
|
+
* lives. Read-only — changing it after the fact would not move the entry
|
|
497
|
+
* already written.
|
|
498
|
+
*/
|
|
499
|
+
get config(): { fresh: boolean; ttl: number } {
|
|
500
|
+
return { fresh: this.#fresh, ttl: this.#ttl }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* The key the flash bag is stored under inside the session (AdonisJS
|
|
505
|
+
* `flashKey`).
|
|
506
|
+
*
|
|
507
|
+
* Exposed so a store that has to reason about the payload — a migration, a
|
|
508
|
+
* debug screen — does not have to hardcode the string.
|
|
509
|
+
*/
|
|
510
|
+
get flashKey(): string {
|
|
511
|
+
return FLASH_KEY
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* What has been flashed for the NEXT request (AdonisJS
|
|
516
|
+
* `responseFlashMessages`).
|
|
517
|
+
*
|
|
518
|
+
* A copy: flashing goes through `flash()`, which is where the value checks
|
|
519
|
+
* live. `flashMessages` is the other half — what the PREVIOUS request left
|
|
520
|
+
* for this one.
|
|
521
|
+
*/
|
|
522
|
+
get responseFlashMessages(): Record<string, unknown> {
|
|
523
|
+
return { ...this.#flashData }
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Whether `regenerate()` ran this request (AdonisJS `hasRegeneratedSession`).
|
|
528
|
+
*
|
|
529
|
+
* The id changed, so anything holding the old one — a cookie already
|
|
530
|
+
* written, an external store keyed by session id — is stale. It was tracked
|
|
531
|
+
* internally and never exposed, so only this class could act on it.
|
|
532
|
+
*/
|
|
533
|
+
get hasRegeneratedSession(): boolean {
|
|
534
|
+
return this.#regenerated
|
|
535
|
+
}
|
|
536
|
+
|
|
475
537
|
get readonly(): boolean {
|
|
476
538
|
return false
|
|
477
539
|
}
|
|
@@ -496,7 +558,11 @@ export class Session {
|
|
|
496
558
|
async initiate(): Promise<void> {
|
|
497
559
|
if (this.#initiated || this.#driver === undefined) return
|
|
498
560
|
this.#initiated = true
|
|
499
|
-
|
|
561
|
+
const stored = await this.#driver.read(this.#sessionId)
|
|
562
|
+
// The store is the authority on existence: a cookie can outlive the row it
|
|
563
|
+
// points at, and that session is fresh however old its id is.
|
|
564
|
+
if (stored === null) this.#fresh = true
|
|
565
|
+
this.#hydrate(stored ?? {})
|
|
500
566
|
}
|
|
501
567
|
|
|
502
568
|
/**
|
|
@@ -106,17 +106,25 @@ export default class SessionMiddleware {
|
|
|
106
106
|
const rolling = this.#config.rolling
|
|
107
107
|
const isProduction = process.env.NODE_ENV === 'production'
|
|
108
108
|
|
|
109
|
-
// Read session
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
// Read the session cookie — pre-parsed by the Rust HyperServer.
|
|
110
|
+
//
|
|
111
|
+
// The COOKIE driver's value is its own AEAD ciphertext, so it is read raw
|
|
112
|
+
// and the driver authenticates it. The server-side drivers store a bare id,
|
|
113
|
+
// which is SIGNED on the way out (see the write below): `request.cookie()`
|
|
114
|
+
// verifies the HMAC and returns null when it does not hold, so a planted or
|
|
115
|
+
// foreign id is refused here instead of reaching the store.
|
|
116
|
+
let sessionId = this.#cookieDriver
|
|
117
|
+
? ctx.request.plainCookie<string>(cookieName, undefined, { encoded: false })
|
|
118
|
+
: ctx.request.cookie(cookieName)
|
|
113
119
|
const hadIncomingCookie = sessionId !== null
|
|
114
120
|
|
|
115
121
|
// Cookie driver: the cookie value IS the session data (encrypted)
|
|
116
122
|
if (this.#cookieDriver) {
|
|
117
123
|
const data = await this.#driver.read(sessionId ?? '')
|
|
118
124
|
sessionId = sessionId ?? generateSessionId()
|
|
119
|
-
|
|
125
|
+
// `null` means the cookie carried nothing readable — an empty session,
|
|
126
|
+
// not an absent one, since for this driver the cookie IS the store.
|
|
127
|
+
const session = new Session(sessionId, data ?? {})
|
|
120
128
|
// No store to commit to: the cookie written below IS the storage, so the
|
|
121
129
|
// session is seated without one and `commit()` has nothing to do.
|
|
122
130
|
session.setStore(this.#driver, { fresh: !hadIncomingCookie, ttl: maxAge })
|
|
@@ -193,10 +201,12 @@ export default class SessionMiddleware {
|
|
|
193
201
|
// defeats HTTP caching, and leaks a Set-Cookie to CDNs. This mirrors
|
|
194
202
|
// the cookie-driver path above (which already omits `isNew`).
|
|
195
203
|
if (session.isDirty() || regenerated || (rolling && hadIncomingCookie)) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
// SIGNED, as AdonisJS signs it (`response.cookie(cookieName, id, …)`).
|
|
205
|
+
// The id is 192 bits of entropy, so it was never guessable — what the
|
|
206
|
+
// signature buys is that a cookie the app did not issue is rejected on
|
|
207
|
+
// read, before any store lookup, and that an id from another deployment
|
|
208
|
+
// (a different APP_KEY) cannot be presented here.
|
|
209
|
+
ctx.response.cookie(cookieName, session.sessionId, {
|
|
200
210
|
maxAge: this.#config.clearWithBrowser ? undefined : maxAge,
|
|
201
211
|
path: '/',
|
|
202
212
|
httpOnly: true,
|
|
@@ -18,14 +18,14 @@ export class CookieDriver implements SessionDriver {
|
|
|
18
18
|
this.signer = new CookieSigner(secret)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
async read(sessionId: string): Promise<Record<string, unknown
|
|
22
|
-
if (!sessionId) return
|
|
21
|
+
async read(sessionId: string): Promise<Record<string, unknown> | null> {
|
|
22
|
+
if (!sessionId) return null
|
|
23
23
|
const decrypted = this.signer.decrypt(sessionId)
|
|
24
|
-
if (!decrypted) return
|
|
24
|
+
if (!decrypted) return null
|
|
25
25
|
try {
|
|
26
26
|
return JSON.parse(decrypted)
|
|
27
27
|
} catch {
|
|
28
|
-
return
|
|
28
|
+
return null
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -70,16 +70,16 @@ export class DatabaseDriver implements SessionDriverWithTagging {
|
|
|
70
70
|
this.#table = table
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
async read(sessionId: string): Promise<Record<string, unknown
|
|
73
|
+
async read(sessionId: string): Promise<Record<string, unknown> | null> {
|
|
74
74
|
const rows = await this.#db.query<{ data: string; expires_at: number | string }>(
|
|
75
75
|
`SELECT data, expires_at FROM ${this.#table} WHERE id = ?`,
|
|
76
76
|
[sessionId],
|
|
77
77
|
)
|
|
78
78
|
const row = rows[0]
|
|
79
|
-
if (!row) return
|
|
79
|
+
if (!row) return null
|
|
80
80
|
if (Number(row.expires_at) < Date.now()) {
|
|
81
81
|
await this.destroy(sessionId)
|
|
82
|
-
return
|
|
82
|
+
return null
|
|
83
83
|
}
|
|
84
84
|
return parseData(row.data)
|
|
85
85
|
}
|
|
@@ -36,26 +36,26 @@ export class FileDriver implements SessionDriver {
|
|
|
36
36
|
return path.join(this.#location, `${name}.json`)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
async read(sessionId: string): Promise<Record<string, unknown
|
|
39
|
+
async read(sessionId: string): Promise<Record<string, unknown> | null> {
|
|
40
40
|
let raw: string
|
|
41
41
|
try {
|
|
42
42
|
raw = await fs.promises.readFile(this.#pathFor(sessionId), 'utf8')
|
|
43
43
|
} catch {
|
|
44
|
-
return
|
|
44
|
+
return null
|
|
45
45
|
}
|
|
46
46
|
try {
|
|
47
47
|
const parsed: unknown = JSON.parse(raw)
|
|
48
|
-
if (typeof parsed !== 'object' || parsed === null) return
|
|
48
|
+
if (typeof parsed !== 'object' || parsed === null) return null
|
|
49
49
|
const entry = parsed as { expiresAt?: number; data?: Record<string, unknown> }
|
|
50
50
|
if (typeof entry.expiresAt === 'number' && entry.expiresAt < Date.now()) {
|
|
51
51
|
await this.destroy(sessionId)
|
|
52
|
-
return
|
|
52
|
+
return null
|
|
53
53
|
}
|
|
54
54
|
return entry.data ?? {}
|
|
55
55
|
} catch {
|
|
56
56
|
// A truncated or hand-edited file is a session we cannot trust; treat it
|
|
57
57
|
// as absent rather than handing a half-parsed object to the app.
|
|
58
|
-
return
|
|
58
|
+
return null
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -76,7 +76,9 @@ export class FileDriver implements SessionDriver {
|
|
|
76
76
|
|
|
77
77
|
async touch(sessionId: string, ttl: number): Promise<void> {
|
|
78
78
|
const data = await this.read(sessionId)
|
|
79
|
-
|
|
79
|
+
// Nothing stored: there is no expiry to slide, and writing here would
|
|
80
|
+
// create the very session that was not there.
|
|
81
|
+
if (data === null || Object.keys(data).length === 0) return
|
|
80
82
|
await this.write(sessionId, data, ttl)
|
|
81
83
|
}
|
|
82
84
|
}
|
|
@@ -29,11 +29,11 @@ export class MemoryDriver implements SessionDriverWithTagging {
|
|
|
29
29
|
this.#maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_ENTRIES)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
async read(sessionId: string): Promise<Record<string, unknown
|
|
32
|
+
async read(sessionId: string): Promise<Record<string, unknown> | null> {
|
|
33
33
|
const entry = this.#store.get(sessionId)
|
|
34
34
|
if (!entry || entry.expiresAt < Date.now()) {
|
|
35
35
|
this.#store.delete(sessionId)
|
|
36
|
-
return
|
|
36
|
+
return null
|
|
37
37
|
}
|
|
38
38
|
return { ...entry.data }
|
|
39
39
|
}
|
|
@@ -114,16 +114,16 @@ export class RedisDriver implements SessionDriverWithTagging {
|
|
|
114
114
|
* A corrupt value would otherwise 500 every request carrying that cookie,
|
|
115
115
|
* with no way for the visitor to recover — a new session is the safe answer.
|
|
116
116
|
*/
|
|
117
|
-
async read(sessionId: string): Promise<Record<string, unknown
|
|
117
|
+
async read(sessionId: string): Promise<Record<string, unknown> | null> {
|
|
118
118
|
const client = await this.#client()
|
|
119
119
|
const raw = await client.get(this.#key(sessionId))
|
|
120
|
-
if (raw === null) return
|
|
120
|
+
if (raw === null) return null
|
|
121
121
|
|
|
122
122
|
try {
|
|
123
123
|
const parsed: unknown = JSON.parse(raw)
|
|
124
|
-
return isPlainRecord(parsed) ? parsed :
|
|
124
|
+
return isPlainRecord(parsed) ? parsed : null
|
|
125
125
|
} catch {
|
|
126
|
-
return
|
|
126
|
+
return null
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -169,7 +169,14 @@ export class RedisDriver implements SessionDriverWithTagging {
|
|
|
169
169
|
stale.push(id)
|
|
170
170
|
continue
|
|
171
171
|
}
|
|
172
|
-
|
|
172
|
+
// The EXISTS check above already established the key is there; a null
|
|
173
|
+
// here means it expired in the gap, and that device is no longer active.
|
|
174
|
+
const data = await this.read(id)
|
|
175
|
+
if (data === null) {
|
|
176
|
+
stale.push(id)
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
sessions.push({ id, data })
|
|
173
180
|
}
|
|
174
181
|
if (stale.length > 0) await client.srem(key, ...stale)
|
|
175
182
|
return sessions
|
package/tsconfig.app.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"extends": "./tsconfig.base.json",
|
|
3
4
|
"compilerOptions": {
|
|
4
|
-
"strict": true,
|
|
5
|
-
"target": "ES2022",
|
|
6
|
-
"module": "NodeNext",
|
|
7
|
-
"moduleResolution": "NodeNext",
|
|
8
5
|
"lib": ["ES2023"],
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"forceConsistentCasingInFileNames": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"experimentalDecorators": true,
|
|
13
|
-
"emitDecoratorMetadata": true,
|
|
14
|
-
"resolveJsonModule": true,
|
|
15
|
-
"isolatedModules": true,
|
|
16
6
|
"declaration": false,
|
|
17
7
|
"sourceMap": true,
|
|
18
|
-
"
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
// `${configDir}` resolves against the config that EXTENDS this one, not
|
|
10
|
+
// against this file. Without it every path here pointed inside
|
|
11
|
+
// `node_modules/@c9up/ream`: an app's build output landed in ream's own
|
|
12
|
+
// dist, and its node_modules was never excluded.
|
|
13
|
+
"outDir": "${configDir}/dist"
|
|
19
14
|
},
|
|
20
|
-
"
|
|
15
|
+
"include": ["${configDir}/**/*"],
|
|
16
|
+
"exclude": [
|
|
17
|
+
"${configDir}/node_modules",
|
|
18
|
+
"${configDir}/dist",
|
|
19
|
+
"${configDir}/build"
|
|
20
|
+
]
|
|
21
21
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Shared by tsconfig.app / tsconfig.package / tsconfig.client. Emit options
|
|
3
|
+
// (declaration, sourceMap, outDir) belong to the leaf that knows what it is
|
|
4
|
+
// building — a base that decides them makes every leaf fight it.
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"strict": true,
|
|
7
|
+
"target": "ES2022",
|
|
8
|
+
"module": "NodeNext",
|
|
9
|
+
"moduleResolution": "NodeNext",
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"experimentalDecorators": true,
|
|
14
|
+
"emitDecoratorMetadata": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"isolatedModules": true
|
|
17
|
+
},
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"extends": "./tsconfig.base.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
// Browser code is bundled, so resolution follows the bundler rather than
|
|
6
|
+
// Node, and the DOM libs replace the Node types.
|
|
7
|
+
"moduleResolution": "Bundler",
|
|
8
|
+
"module": "ESNext",
|
|
9
|
+
"lib": ["DOM", "DOM.Iterable", "ES2023"],
|
|
10
|
+
"types": [],
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"jsx": "react-jsx"
|
|
13
|
+
},
|
|
14
|
+
"include": ["${configDir}/**/*"],
|
|
15
|
+
"exclude": ["${configDir}/node_modules", "${configDir}/dist"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"extends": "./tsconfig.base.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
// A package ships its types, and keeps the doc-comments that document them.
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"removeComments": false,
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
"types": ["node"],
|
|
11
|
+
"outDir": "${configDir}/dist"
|
|
12
|
+
},
|
|
13
|
+
"include": ["${configDir}/src/**/*"],
|
|
14
|
+
"exclude": ["${configDir}/node_modules", "${configDir}/dist"]
|
|
15
|
+
}
|