@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.
Files changed (215) hide show
  1. package/dist/Application.d.ts +68 -0
  2. package/dist/Application.d.ts.map +1 -1
  3. package/dist/Application.js +142 -6
  4. package/dist/Application.js.map +1 -1
  5. package/dist/ConfigLoader.d.ts +4 -0
  6. package/dist/ConfigLoader.d.ts.map +1 -1
  7. package/dist/ConfigLoader.js +0 -8
  8. package/dist/ConfigLoader.js.map +1 -1
  9. package/dist/HealthCheck.d.ts +14 -7
  10. package/dist/HealthCheck.d.ts.map +1 -1
  11. package/dist/HealthCheck.js +21 -12
  12. package/dist/HealthCheck.js.map +1 -1
  13. package/dist/HttpKernel.d.ts +12 -0
  14. package/dist/HttpKernel.d.ts.map +1 -1
  15. package/dist/HttpKernel.js +15 -4
  16. package/dist/HttpKernel.js.map +1 -1
  17. package/dist/Ignitor.d.ts +8 -0
  18. package/dist/Ignitor.d.ts.map +1 -1
  19. package/dist/Ignitor.js +11 -0
  20. package/dist/Ignitor.js.map +1 -1
  21. package/dist/bodyparser/BodyParserMiddleware.d.ts +13 -2
  22. package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
  23. package/dist/bodyparser/BodyParserMiddleware.js +50 -30
  24. package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
  25. package/dist/bodyparser/MultipartFile.d.ts +65 -6
  26. package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
  27. package/dist/bodyparser/MultipartFile.js +100 -7
  28. package/dist/bodyparser/MultipartFile.js.map +1 -1
  29. package/dist/console/BaseCommand.d.ts +8 -1
  30. package/dist/console/BaseCommand.d.ts.map +1 -1
  31. package/dist/console/BaseCommand.js +1 -1
  32. package/dist/console/BaseCommand.js.map +1 -1
  33. package/dist/console/Kernel.d.ts +15 -0
  34. package/dist/console/Kernel.d.ts.map +1 -1
  35. package/dist/console/Kernel.js +17 -0
  36. package/dist/console/Kernel.js.map +1 -1
  37. package/dist/directories.d.ts +52 -0
  38. package/dist/directories.d.ts.map +1 -0
  39. package/dist/directories.js +38 -0
  40. package/dist/directories.js.map +1 -0
  41. package/dist/events/Emitter.d.ts +49 -6
  42. package/dist/events/Emitter.d.ts.map +1 -1
  43. package/dist/events/Emitter.js +117 -7
  44. package/dist/events/Emitter.js.map +1 -1
  45. package/dist/events/EventsBuffer.d.ts +47 -0
  46. package/dist/events/EventsBuffer.d.ts.map +1 -0
  47. package/dist/events/EventsBuffer.js +107 -0
  48. package/dist/events/EventsBuffer.js.map +1 -0
  49. package/dist/events/index.d.ts +2 -0
  50. package/dist/events/index.d.ts.map +1 -1
  51. package/dist/events/index.js +1 -0
  52. package/dist/events/index.js.map +1 -1
  53. package/dist/health/BaseCheck.d.ts +19 -0
  54. package/dist/health/BaseCheck.d.ts.map +1 -0
  55. package/dist/health/BaseCheck.js +24 -0
  56. package/dist/health/BaseCheck.js.map +1 -0
  57. package/dist/health/HealthChecks.d.ts +15 -0
  58. package/dist/health/HealthChecks.d.ts.map +1 -0
  59. package/dist/health/HealthChecks.js +69 -0
  60. package/dist/health/HealthChecks.js.map +1 -0
  61. package/dist/health/Result.d.ts +27 -0
  62. package/dist/health/Result.d.ts.map +1 -0
  63. package/dist/health/Result.js +55 -0
  64. package/dist/health/Result.js.map +1 -0
  65. package/dist/health/checks/DiskSpaceCheck.d.ts +29 -0
  66. package/dist/health/checks/DiskSpaceCheck.d.ts.map +1 -0
  67. package/dist/health/checks/DiskSpaceCheck.js +58 -0
  68. package/dist/health/checks/DiskSpaceCheck.js.map +1 -0
  69. package/dist/health/checks/MemoryHeapCheck.d.ts +21 -0
  70. package/dist/health/checks/MemoryHeapCheck.d.ts.map +1 -0
  71. package/dist/health/checks/MemoryHeapCheck.js +55 -0
  72. package/dist/health/checks/MemoryHeapCheck.js.map +1 -0
  73. package/dist/health/checks/MemoryRSSCheck.d.ts +21 -0
  74. package/dist/health/checks/MemoryRSSCheck.d.ts.map +1 -0
  75. package/dist/health/checks/MemoryRSSCheck.js +55 -0
  76. package/dist/health/checks/MemoryRSSCheck.js.map +1 -0
  77. package/dist/health/checks/memory.d.ts +24 -0
  78. package/dist/health/checks/memory.d.ts.map +1 -0
  79. package/dist/health/checks/memory.js +88 -0
  80. package/dist/health/checks/memory.js.map +1 -0
  81. package/dist/health/index.d.ts +9 -0
  82. package/dist/health/index.d.ts.map +1 -0
  83. package/dist/health/index.js +8 -0
  84. package/dist/health/index.js.map +1 -0
  85. package/dist/health/tracing.d.ts +12 -0
  86. package/dist/health/tracing.d.ts.map +1 -0
  87. package/dist/health/tracing.js +11 -0
  88. package/dist/health/tracing.js.map +1 -0
  89. package/dist/health/types.d.ts +51 -0
  90. package/dist/health/types.d.ts.map +1 -0
  91. package/dist/health/types.js +5 -0
  92. package/dist/health/types.js.map +1 -0
  93. package/dist/helpers/bytes.d.ts +28 -0
  94. package/dist/helpers/bytes.d.ts.map +1 -0
  95. package/dist/helpers/bytes.js +63 -0
  96. package/dist/helpers/bytes.js.map +1 -0
  97. package/dist/helpers/string.d.ts +4 -0
  98. package/dist/helpers/string.d.ts.map +1 -1
  99. package/dist/helpers/string.js +2 -0
  100. package/dist/helpers/string.js.map +1 -1
  101. package/dist/http/HttpContext.d.ts +38 -2
  102. package/dist/http/HttpContext.d.ts.map +1 -1
  103. package/dist/http/HttpContext.js +81 -3
  104. package/dist/http/HttpContext.js.map +1 -1
  105. package/dist/http/Request.d.ts +57 -3
  106. package/dist/http/Request.d.ts.map +1 -1
  107. package/dist/http/Request.js +51 -38
  108. package/dist/http/Request.js.map +1 -1
  109. package/dist/http/Response.d.ts +82 -2
  110. package/dist/http/Response.d.ts.map +1 -1
  111. package/dist/http/Response.js +240 -13
  112. package/dist/http/Response.js.map +1 -1
  113. package/dist/http/SseStream.d.ts +10 -0
  114. package/dist/http/SseStream.d.ts.map +1 -1
  115. package/dist/http/SseStream.js.map +1 -1
  116. package/dist/index.d.ts +7 -2
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +4 -1
  119. package/dist/index.js.map +1 -1
  120. package/dist/migrations/MigrationRegistry.d.ts +28 -0
  121. package/dist/migrations/MigrationRegistry.d.ts.map +1 -0
  122. package/dist/migrations/MigrationRegistry.js +41 -0
  123. package/dist/migrations/MigrationRegistry.js.map +1 -0
  124. package/dist/migrations/types.d.ts +64 -0
  125. package/dist/migrations/types.d.ts.map +1 -0
  126. package/dist/migrations/types.js +15 -0
  127. package/dist/migrations/types.js.map +1 -0
  128. package/dist/router/Router.d.ts +112 -9
  129. package/dist/router/Router.d.ts.map +1 -1
  130. package/dist/router/Router.js +206 -28
  131. package/dist/router/Router.js.map +1 -1
  132. package/dist/security/CookieSigner.d.ts +43 -4
  133. package/dist/security/CookieSigner.d.ts.map +1 -1
  134. package/dist/security/CookieSigner.js +68 -10
  135. package/dist/security/CookieSigner.js.map +1 -1
  136. package/dist/session/Session.d.ts +46 -1
  137. package/dist/session/Session.d.ts.map +1 -1
  138. package/dist/session/Session.js +57 -4
  139. package/dist/session/Session.js.map +1 -1
  140. package/dist/session/SessionMiddleware.d.ts.map +1 -1
  141. package/dist/session/SessionMiddleware.js +19 -9
  142. package/dist/session/SessionMiddleware.js.map +1 -1
  143. package/dist/session/drivers/CookieDriver.d.ts +1 -1
  144. package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
  145. package/dist/session/drivers/CookieDriver.js +3 -3
  146. package/dist/session/drivers/CookieDriver.js.map +1 -1
  147. package/dist/session/drivers/DatabaseDriver.d.ts +1 -1
  148. package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
  149. package/dist/session/drivers/DatabaseDriver.js +2 -2
  150. package/dist/session/drivers/DatabaseDriver.js.map +1 -1
  151. package/dist/session/drivers/FileDriver.d.ts +1 -1
  152. package/dist/session/drivers/FileDriver.d.ts.map +1 -1
  153. package/dist/session/drivers/FileDriver.js +7 -5
  154. package/dist/session/drivers/FileDriver.js.map +1 -1
  155. package/dist/session/drivers/MemoryDriver.d.ts +1 -1
  156. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  157. package/dist/session/drivers/MemoryDriver.js +1 -1
  158. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  159. package/dist/session/drivers/RedisDriver.d.ts +1 -1
  160. package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
  161. package/dist/session/drivers/RedisDriver.js +11 -4
  162. package/dist/session/drivers/RedisDriver.js.map +1 -1
  163. package/events.win32-x64-msvc.node +0 -0
  164. package/index.darwin-arm64.node +0 -0
  165. package/index.darwin-x64.node +0 -0
  166. package/index.linux-arm64-gnu.node +0 -0
  167. package/index.linux-x64-gnu.node +0 -0
  168. package/index.win32-x64-msvc.node +0 -0
  169. package/package.json +5 -2
  170. package/scheduler.win32-x64-msvc.node +0 -0
  171. package/src/Application.ts +169 -6
  172. package/src/ConfigLoader.ts +6 -0
  173. package/src/HealthCheck.ts +27 -14
  174. package/src/HttpKernel.ts +27 -4
  175. package/src/Ignitor.ts +17 -0
  176. package/src/bodyparser/BodyParserMiddleware.ts +70 -36
  177. package/src/bodyparser/MultipartFile.ts +125 -8
  178. package/src/console/BaseCommand.ts +8 -1
  179. package/src/console/Kernel.ts +18 -0
  180. package/src/directories.ts +81 -0
  181. package/src/events/Emitter.ts +129 -13
  182. package/src/events/EventsBuffer.ts +131 -0
  183. package/src/events/index.ts +7 -0
  184. package/src/health/BaseCheck.ts +30 -0
  185. package/src/health/HealthChecks.ts +83 -0
  186. package/src/health/Result.ts +69 -0
  187. package/src/health/checks/DiskSpaceCheck.ts +69 -0
  188. package/src/health/checks/MemoryHeapCheck.ts +68 -0
  189. package/src/health/checks/MemoryRSSCheck.ts +68 -0
  190. package/src/health/checks/memory.ts +115 -0
  191. package/src/health/index.ts +13 -0
  192. package/src/health/tracing.ts +15 -0
  193. package/src/health/types.ts +51 -0
  194. package/src/helpers/bytes.ts +71 -0
  195. package/src/helpers/string.ts +2 -0
  196. package/src/http/HttpContext.ts +86 -4
  197. package/src/http/Request.ts +82 -10
  198. package/src/http/Response.ts +283 -14
  199. package/src/http/SseStream.ts +10 -0
  200. package/src/index.ts +27 -1
  201. package/src/migrations/MigrationRegistry.ts +49 -0
  202. package/src/migrations/types.ts +68 -0
  203. package/src/router/Router.ts +241 -32
  204. package/src/security/CookieSigner.ts +92 -18
  205. package/src/session/Session.ts +71 -5
  206. package/src/session/SessionMiddleware.ts +19 -9
  207. package/src/session/drivers/CookieDriver.ts +4 -4
  208. package/src/session/drivers/DatabaseDriver.ts +3 -3
  209. package/src/session/drivers/FileDriver.ts +8 -6
  210. package/src/session/drivers/MemoryDriver.ts +2 -2
  211. package/src/session/drivers/RedisDriver.ts +12 -5
  212. package/tsconfig.app.json +13 -13
  213. package/tsconfig.base.json +19 -0
  214. package/tsconfig.client.json +16 -0
  215. package/tsconfig.package.json +15 -0
@@ -13,7 +13,16 @@
13
13
  import type { SessionRedisClientSource } from './drivers/RedisDriver.js'
14
14
 
15
15
  export interface SessionDriver {
16
- read(sessionId: string): Promise<Record<string, unknown>>
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.__flash
195
+ const flash = data[FLASH_KEY]
179
196
  this.#previousFlash = isPlainRecord(flash) ? { ...flash } : {}
180
- delete this.#data.__flash
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.__flash = this.#flashData
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
- this.#hydrate(await this.#driver.read(this.#sessionId))
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 ID from cookie — pre-parsed by the Rust HyperServer.
110
- let sessionId = ctx.request.plainCookie<string>(cookieName, undefined, {
111
- encoded: false,
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
- const session = new Session(sessionId, data)
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
- ctx.response.plainCookie(cookieName, session.sessionId, {
197
- // An opaque id — packing it would invalidate every session cookie
198
- // already in a browser for no gain.
199
- encode: false,
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
- if (Object.keys(data).length === 0) return
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
- sessions.push({ id, data: await this.read(id) })
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
- "outDir": "./dist"
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
- "exclude": ["node_modules", "dist", "build"]
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
+ }