@c9up/ream 0.1.32 → 0.1.33

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 (188) hide show
  1. package/dist/Application.d.ts +27 -0
  2. package/dist/Application.d.ts.map +1 -1
  3. package/dist/Application.js +29 -0
  4. package/dist/Application.js.map +1 -1
  5. package/dist/ErrorBoundary.d.ts.map +1 -1
  6. package/dist/ErrorBoundary.js +13 -1
  7. package/dist/ErrorBoundary.js.map +1 -1
  8. package/dist/HttpKernel.d.ts +8 -0
  9. package/dist/HttpKernel.d.ts.map +1 -1
  10. package/dist/HttpKernel.js +35 -12
  11. package/dist/HttpKernel.js.map +1 -1
  12. package/dist/Ignitor.d.ts +8 -0
  13. package/dist/Ignitor.d.ts.map +1 -1
  14. package/dist/Ignitor.js +39 -0
  15. package/dist/Ignitor.js.map +1 -1
  16. package/dist/bodyparser/BodyParserMiddleware.d.ts +11 -4
  17. package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
  18. package/dist/bodyparser/BodyParserMiddleware.js +55 -43
  19. package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
  20. package/dist/bodyparser/qsParse.d.ts +52 -0
  21. package/dist/bodyparser/qsParse.d.ts.map +1 -0
  22. package/dist/bodyparser/qsParse.js +161 -0
  23. package/dist/bodyparser/qsParse.js.map +1 -0
  24. package/dist/bootstrap/hyper-server.d.ts.map +1 -1
  25. package/dist/bootstrap/hyper-server.js +2 -8
  26. package/dist/bootstrap/hyper-server.js.map +1 -1
  27. package/dist/container/Container.d.ts +41 -0
  28. package/dist/container/Container.d.ts.map +1 -1
  29. package/dist/container/Container.js +193 -26
  30. package/dist/container/Container.js.map +1 -1
  31. package/dist/container/ContainerResolver.d.ts +46 -0
  32. package/dist/container/ContainerResolver.d.ts.map +1 -0
  33. package/dist/container/ContainerResolver.js +60 -0
  34. package/dist/container/ContainerResolver.js.map +1 -0
  35. package/dist/container/ContextualBindingsBuilder.d.ts +20 -0
  36. package/dist/container/ContextualBindingsBuilder.d.ts.map +1 -0
  37. package/dist/container/ContextualBindingsBuilder.js +31 -0
  38. package/dist/container/ContextualBindingsBuilder.js.map +1 -0
  39. package/dist/container/types.d.ts +9 -6
  40. package/dist/container/types.d.ts.map +1 -1
  41. package/dist/decorators/Service.d.ts +1 -5
  42. package/dist/decorators/Service.d.ts.map +1 -1
  43. package/dist/decorators/Service.js +10 -1
  44. package/dist/decorators/Service.js.map +1 -1
  45. package/dist/events/Emitter.d.ts +66 -6
  46. package/dist/events/Emitter.d.ts.map +1 -1
  47. package/dist/events/Emitter.js +165 -28
  48. package/dist/events/Emitter.js.map +1 -1
  49. package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
  50. package/dist/graphql/GraphQLEngine.js +5 -5
  51. package/dist/graphql/GraphQLEngine.js.map +1 -1
  52. package/dist/helpers/duration.d.ts +32 -0
  53. package/dist/helpers/duration.d.ts.map +1 -0
  54. package/dist/helpers/duration.js +74 -0
  55. package/dist/helpers/duration.js.map +1 -0
  56. package/dist/helpers/napi-loader.d.ts +10 -1
  57. package/dist/helpers/napi-loader.d.ts.map +1 -1
  58. package/dist/helpers/napi-loader.js +18 -3
  59. package/dist/helpers/napi-loader.js.map +1 -1
  60. package/dist/helpers/types.d.ts +1 -1
  61. package/dist/helpers/types.d.ts.map +1 -1
  62. package/dist/http/Exception.d.ts.map +1 -1
  63. package/dist/http/Exception.js +17 -5
  64. package/dist/http/Exception.js.map +1 -1
  65. package/dist/http/HttpContext.d.ts +16 -5
  66. package/dist/http/HttpContext.d.ts.map +1 -1
  67. package/dist/http/HttpContext.js +4 -2
  68. package/dist/http/HttpContext.js.map +1 -1
  69. package/dist/http/RedirectBuilder.d.ts +48 -2
  70. package/dist/http/RedirectBuilder.d.ts.map +1 -1
  71. package/dist/http/RedirectBuilder.js +95 -14
  72. package/dist/http/RedirectBuilder.js.map +1 -1
  73. package/dist/http/Request.d.ts +128 -8
  74. package/dist/http/Request.d.ts.map +1 -1
  75. package/dist/http/Request.js +259 -8
  76. package/dist/http/Request.js.map +1 -1
  77. package/dist/http/Response.d.ts +74 -3
  78. package/dist/http/Response.d.ts.map +1 -1
  79. package/dist/http/Response.js +177 -4
  80. package/dist/http/Response.js.map +1 -1
  81. package/dist/index.d.ts +9 -3
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +5 -1
  84. package/dist/index.js.map +1 -1
  85. package/dist/router/Router.d.ts +36 -7
  86. package/dist/router/Router.d.ts.map +1 -1
  87. package/dist/router/Router.js +66 -22
  88. package/dist/router/Router.js.map +1 -1
  89. package/dist/security/CookieSigner.d.ts +35 -4
  90. package/dist/security/CookieSigner.d.ts.map +1 -1
  91. package/dist/security/CookieSigner.js +99 -10
  92. package/dist/security/CookieSigner.js.map +1 -1
  93. package/dist/server/Server.d.ts +2 -3
  94. package/dist/server/Server.d.ts.map +1 -1
  95. package/dist/server/Server.js +30 -13
  96. package/dist/server/Server.js.map +1 -1
  97. package/dist/session/ReadOnlyValuesStore.d.ts +24 -0
  98. package/dist/session/ReadOnlyValuesStore.d.ts.map +1 -0
  99. package/dist/session/ReadOnlyValuesStore.js +45 -0
  100. package/dist/session/ReadOnlyValuesStore.js.map +1 -0
  101. package/dist/session/Session.d.ts +181 -9
  102. package/dist/session/Session.d.ts.map +1 -1
  103. package/dist/session/Session.js +284 -33
  104. package/dist/session/Session.js.map +1 -1
  105. package/dist/session/SessionMiddleware.d.ts.map +1 -1
  106. package/dist/session/SessionMiddleware.js +87 -33
  107. package/dist/session/SessionMiddleware.js.map +1 -1
  108. package/dist/session/drivers/DatabaseDriver.d.ts +66 -0
  109. package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -0
  110. package/dist/session/drivers/DatabaseDriver.js +132 -0
  111. package/dist/session/drivers/DatabaseDriver.js.map +1 -0
  112. package/dist/session/drivers/FileDriver.d.ts +22 -0
  113. package/dist/session/drivers/FileDriver.d.ts.map +1 -0
  114. package/dist/session/drivers/FileDriver.js +66 -0
  115. package/dist/session/drivers/FileDriver.js.map +1 -0
  116. package/dist/session/drivers/MemoryDriver.d.ts +10 -2
  117. package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
  118. package/dist/session/drivers/MemoryDriver.js +70 -0
  119. package/dist/session/drivers/MemoryDriver.js.map +1 -1
  120. package/dist/session/drivers/RedisDriver.d.ts +10 -2
  121. package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
  122. package/dist/session/drivers/RedisDriver.js +58 -0
  123. package/dist/session/drivers/RedisDriver.js.map +1 -1
  124. package/dist/session/templateTags.d.ts +42 -0
  125. package/dist/session/templateTags.d.ts.map +1 -0
  126. package/dist/session/templateTags.js +101 -0
  127. package/dist/session/templateTags.js.map +1 -0
  128. package/dist/testing/RequestBuilder.d.ts +38 -1
  129. package/dist/testing/RequestBuilder.d.ts.map +1 -1
  130. package/dist/testing/RequestBuilder.js +40 -1
  131. package/dist/testing/RequestBuilder.js.map +1 -1
  132. package/dist/testing/TestClient.d.ts +3 -1
  133. package/dist/testing/TestClient.d.ts.map +1 -1
  134. package/dist/testing/TestClient.js +3 -1
  135. package/dist/testing/TestClient.js.map +1 -1
  136. package/dist/types/helpers.d.ts +1 -1
  137. package/dist/types/helpers.d.ts.map +1 -1
  138. package/dist/utils/Macroable.d.ts +14 -5
  139. package/dist/utils/Macroable.d.ts.map +1 -1
  140. package/dist/utils/Macroable.js +20 -18
  141. package/dist/utils/Macroable.js.map +1 -1
  142. package/events.win32-x64-msvc.node +0 -0
  143. package/index.win32-x64-msvc.node +0 -0
  144. package/package.json +1 -1
  145. package/scheduler.win32-x64-msvc.node +0 -0
  146. package/src/Application.ts +39 -0
  147. package/src/ErrorBoundary.ts +13 -1
  148. package/src/HttpKernel.ts +45 -19
  149. package/src/Ignitor.ts +39 -0
  150. package/src/bodyparser/BodyParserMiddleware.ts +66 -44
  151. package/src/bodyparser/qsParse.ts +179 -0
  152. package/src/bootstrap/hyper-server.ts +2 -9
  153. package/src/container/Container.ts +265 -25
  154. package/src/container/ContainerResolver.ts +84 -0
  155. package/src/container/ContextualBindingsBuilder.ts +41 -0
  156. package/src/container/types.ts +11 -7
  157. package/src/decorators/Service.ts +16 -3
  158. package/src/events/Emitter.ts +200 -25
  159. package/src/graphql/GraphQLEngine.ts +7 -8
  160. package/src/helpers/duration.ts +71 -0
  161. package/src/helpers/napi-loader.ts +19 -3
  162. package/src/helpers/types.ts +4 -1
  163. package/src/http/Exception.ts +17 -5
  164. package/src/http/HttpContext.ts +20 -5
  165. package/src/http/RedirectBuilder.ts +126 -11
  166. package/src/http/Request.ts +283 -13
  167. package/src/http/Response.ts +213 -6
  168. package/src/index.ts +17 -3
  169. package/src/router/Router.ts +98 -33
  170. package/src/security/CookieSigner.ts +102 -10
  171. package/src/server/Server.ts +36 -16
  172. package/src/session/ReadOnlyValuesStore.ts +52 -0
  173. package/src/session/Session.ts +364 -37
  174. package/src/session/SessionMiddleware.ts +93 -30
  175. package/src/session/drivers/DatabaseDriver.ts +184 -0
  176. package/src/session/drivers/FileDriver.ts +82 -0
  177. package/src/session/drivers/MemoryDriver.ts +75 -2
  178. package/src/session/drivers/RedisDriver.ts +77 -2
  179. package/src/session/templateTags.ts +124 -0
  180. package/src/testing/RequestBuilder.ts +76 -0
  181. package/src/testing/TestClient.ts +10 -2
  182. package/src/types/helpers.ts +4 -1
  183. package/src/utils/Macroable.ts +23 -8
  184. package/dist/utils/inflect.d.ts +0 -17
  185. package/dist/utils/inflect.d.ts.map +0 -1
  186. package/dist/utils/inflect.js +0 -32
  187. package/dist/utils/inflect.js.map +0 -1
  188. package/src/utils/inflect.ts +0 -29
@@ -1,43 +1,134 @@
1
1
  /**
2
2
  * Cookie signing and encryption.
3
3
  * Uses Rust NAPI via crypto facade when available.
4
+ *
5
+ * NAMED DEVIATION — the wire format is NOT AdonisJS's.
6
+ *
7
+ * AdonisJS encrypts with `aes-256-cbc` and appends its own HMAC
8
+ * (`base64url(payload).base64url(iv).hmac`). Ream uses `aes-256-gcm`, where
9
+ * authentication is part of the cipher rather than a second primitive bolted on
10
+ * — encrypt-then-MAC assembled by hand is a well-known source of subtle breaks,
11
+ * and there is no reason to reproduce it.
12
+ *
13
+ * The consequence, stated plainly: a cookie encrypted by an AdonisJS app cannot
14
+ * be decrypted here. Migrating an app invalidates the encrypted cookies already
15
+ * in users' browsers — they are signed out once, at deploy. Everything the app
16
+ * CALLS (`encrypt` / `decrypt` / `sign` / `unsign`, with purpose and expiry)
17
+ * behaves the same.
4
18
  */
5
19
 
6
20
  import { createCipheriv, createDecipheriv, createHmac, randomBytes } from 'node:crypto'
7
21
  import { hmacSign, hmacVerify } from './crypto.js'
8
22
 
23
+ /** Raised when no APP_KEY is configured (AdonisJS `E_MISSING_APP_KEY`). */
24
+ export class E_MISSING_APP_KEY extends Error {
25
+ readonly code = 'E_MISSING_APP_KEY' as const
26
+ constructor() {
27
+ super('Missing APP_KEY. Set it in your environment before encrypting anything.')
28
+ this.name = 'E_MISSING_APP_KEY'
29
+ }
30
+ }
31
+
32
+ /** Raised when the APP_KEY is too short to be worth having. */
33
+ export class E_INSECURE_APP_KEY extends Error {
34
+ readonly code = 'E_INSECURE_APP_KEY' as const
35
+ constructor() {
36
+ super('APP_KEY is too short — it must be at least 16 characters.')
37
+ this.name = 'E_INSECURE_APP_KEY'
38
+ }
39
+ }
40
+
41
+ /** The envelope carried inside an encrypted or signed value. */
42
+ interface Envelope {
43
+ /** The payload. */
44
+ m: string
45
+ /** Epoch-ms expiry, when one was given. */
46
+ e?: number
47
+ /** The purpose it was sealed for, when one was given. */
48
+ p?: string
49
+ }
50
+
9
51
  export class CookieSigner {
10
52
  #secret: string
11
53
  #keyBuffer: Buffer
12
54
 
13
55
  constructor(secret: string) {
56
+ // Validated as AdonisJS does: an absent or trivially short key is a
57
+ // configuration error, not something to discover at decrypt time.
58
+ if (typeof secret !== 'string' || secret.length === 0) throw new E_MISSING_APP_KEY()
59
+ if (secret.length < 16) throw new E_INSECURE_APP_KEY()
14
60
  this.#secret = secret
15
61
  // Derive a 32-byte key for AES-256-GCM via HKDF-like derivation
16
62
  this.#keyBuffer = Buffer.from(createHmac('sha256', secret).update('cookie-key').digest())
17
63
  }
18
64
 
19
- sign(value: string): string {
20
- const sig = hmacSign(value, this.#secret)
21
- return `${value}.${sig}`
65
+ /**
66
+ * Wrap a payload with its expiry and purpose.
67
+ *
68
+ * `purpose` is what stops a value sealed for one use being replayed into
69
+ * another — a password-reset token presented as a session cookie fails here
70
+ * rather than being honoured.
71
+ */
72
+ #seal(value: string, expiresIn?: number, purpose?: string): string {
73
+ const envelope: Envelope = { m: value }
74
+ if (expiresIn !== undefined) envelope.e = Date.now() + expiresIn
75
+ if (purpose !== undefined) envelope.p = purpose
76
+ return JSON.stringify(envelope)
22
77
  }
23
78
 
24
- unsign(signed: string): string | null {
79
+ /** Unwrap a sealed payload, refusing an expired one or the wrong purpose. */
80
+ #open(raw: string, purpose?: string): string | null {
81
+ let envelope: Envelope
82
+ try {
83
+ const parsed: unknown = JSON.parse(raw)
84
+ if (typeof parsed !== 'object' || parsed === null) return null
85
+ envelope = parsed as Envelope
86
+ } catch {
87
+ // Not an envelope — a value sealed before this format existed.
88
+ return raw
89
+ }
90
+ if (typeof envelope.m !== 'string') return raw
91
+ if (envelope.e !== undefined && envelope.e < Date.now()) return null
92
+ if ((envelope.p ?? undefined) !== purpose) return null
93
+ return envelope.m
94
+ }
95
+
96
+ /**
97
+ * Sign a value (AdonisJS `Encryption.verifier.sign`).
98
+ *
99
+ * `expiresIn` is in milliseconds; `purpose` scopes the signature so it cannot
100
+ * be replayed somewhere else.
101
+ */
102
+ sign(value: string, expiresIn?: number, purpose?: string): string {
103
+ const sealed = Buffer.from(this.#seal(value, expiresIn, purpose)).toString('base64url')
104
+ const sig = hmacSign(sealed, this.#secret)
105
+ return `${sealed}.${sig}`
106
+ }
107
+
108
+ /** Verify and unwrap, returning null when tampered, expired, or mis-purposed. */
109
+ unsign(signed: string, purpose?: string): string | null {
25
110
  const lastDot = signed.lastIndexOf('.')
26
111
  if (lastDot === -1) return null
27
- const value = signed.slice(0, lastDot)
112
+ const sealed = signed.slice(0, lastDot)
28
113
  const sig = signed.slice(lastDot + 1)
29
- return hmacVerify(value, sig, this.#secret) ? value : null
114
+ if (!hmacVerify(sealed, sig, this.#secret)) return null
115
+ try {
116
+ return this.#open(Buffer.from(sealed, 'base64url').toString('utf8'), purpose)
117
+ } catch {
118
+ return null
119
+ }
30
120
  }
31
121
 
32
- encrypt(value: string): string {
122
+ encrypt(value: string, expiresIn?: number, purpose?: string): string {
33
123
  const iv = randomBytes(12)
34
124
  const cipher = createCipheriv('aes-256-gcm', this.#keyBuffer, iv)
35
- const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()])
125
+ const sealed = this.#seal(value, expiresIn, purpose)
126
+ const encrypted = Buffer.concat([cipher.update(sealed, 'utf8'), cipher.final()])
36
127
  const tag = cipher.getAuthTag()
37
128
  return `${iv.toString('base64url')}.${encrypted.toString('base64url')}.${tag.toString('base64url')}`
38
129
  }
39
130
 
40
- decrypt(encrypted: string): string | null {
131
+ decrypt(encrypted: string, purpose?: string): string | null {
41
132
  const parts = encrypted.split('.')
42
133
  if (parts.length !== 3) return null
43
134
  try {
@@ -46,7 +137,8 @@ export class CookieSigner {
46
137
  const tag = Buffer.from(parts[2], 'base64url')
47
138
  const decipher = createDecipheriv('aes-256-gcm', this.#keyBuffer, iv)
48
139
  decipher.setAuthTag(tag)
49
- return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8')
140
+ const raw = Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8')
141
+ return this.#open(raw, purpose)
50
142
  } catch {
51
143
  return null
52
144
  }
@@ -23,11 +23,11 @@ export interface MiddlewareClass {
23
23
  /** Lazy import returning a module with a default export. */
24
24
  export type LazyImport<T> = () => Promise<{ default: T }>
25
25
 
26
- // biome-ignore lint/suspicious/noExplicitAny: contravariance — same as ControllerAction
27
- export type ErrorHandlerClass = new (...args: any[]) => ExceptionHandler
26
+ // `never[]` accepts every constructor shape: parameters are contravariant, so a
27
+ // rest of `never` is assignable from any concrete list. No `any` needed.
28
+ export type ErrorHandlerClass = new (...args: never[]) => ExceptionHandler
28
29
 
29
- // biome-ignore lint/suspicious/noExplicitAny: contravariance — middleware class constructors
30
- type MiddlewareClassConstructor = new (...args: any[]) => MiddlewareClass
30
+ export type MiddlewareClassConstructor = new (...args: never[]) => MiddlewareClass
31
31
 
32
32
  /** What server.use() and router.use() accept: lazy imports or direct functions. */
33
33
  export type MiddlewareEntry = LazyImport<MiddlewareClassConstructor> | MiddlewareFunction
@@ -130,6 +130,18 @@ export function resolveMiddlewareEntry(entry: MiddlewareEntry): MiddlewareFuncti
130
130
  return async (ctx: HttpContext, next: () => Promise<void>) => {
131
131
  if (!cachedClass) {
132
132
  const mod = await lazyImport()
133
+ if (mod === null || typeof mod !== 'object' || !('default' in mod)) {
134
+ // The zero-arity ambiguity, reported where it can be acted on: a bound
135
+ // or rest-args middleware lands here because it declares no parameters
136
+ // and was read as an import factory.
137
+ throw new Error(
138
+ '[E_MIDDLEWARE_ENTRY] A middleware entry with no declared parameters was treated as a lazy import, ' +
139
+ 'but it did not resolve to a module with a default export. ' +
140
+ 'If this is an inline middleware (e.g. `handle.bind(this)` or `(...args) => …`), register it on the ' +
141
+ 'route or group — `router.get(...).use(fn)` — rather than in `router.use([...])`, which takes ' +
142
+ 'imports of middleware classes, as AdonisJS does.',
143
+ )
144
+ }
133
145
  cachedClass = mod.default
134
146
  }
135
147
  const instance = ctx.containerResolver
@@ -169,18 +181,26 @@ export function resolveParametrizedMiddlewareEntry(
169
181
  }
170
182
  }
171
183
 
172
- /** Check if an entry is a direct middleware function (not a lazy import). */
184
+ /**
185
+ * Whether an entry is a middleware to run, rather than a module to import.
186
+ *
187
+ * The two are separated by WHERE they are registered, exactly as in AdonisJS:
188
+ * `router.use([...])` takes imports of middleware classes (its own `use()`
189
+ * runs `moduleImporter(one, 'handle')` over every entry), while an inline
190
+ * function goes on a route or group. So this only has to cope with the entries
191
+ * that reach it, and a zero-arity function is an import factory.
192
+ *
193
+ * A middleware that lost its parameters — `handle.bind(this)`, `(...args) =>`
194
+ * — still lands here looking like a factory, which is why the resolver below
195
+ * raises a named error instead of failing somewhere deeper.
196
+ */
173
197
  function isMiddlewareFunction(entry: MiddlewareEntry): entry is MiddlewareFunction {
174
- // A lazy import factory returns a promise (dynamic import). Middleware functions don't.
175
- // We check: if calling it with no args returns a thenable, it's a lazy import.
176
- // Better heuristic: lazy imports are () => import(...) which have .length === 0
177
- // and middleware functions have .length >= 2 (ctx, next).
178
- // Edge case: arrow with defaults has length 1. But lazy import() always has length 0.
179
198
  if (typeof entry !== 'function') return false
180
- // Functions with 2+ params are definitely middleware
181
- if (entry.length >= 2) return true
182
- // Functions with 0 params are likely lazy imports
183
- if (entry.length === 0) return false
184
- // 1 param is ambiguous — treat as middleware (ctx only, no next)
185
- return true
199
+ // A declared parameter means a middleware: `(ctx)`, `(ctx, next)`.
200
+ if (entry.length > 0) return true
201
+ // Zero declared parameters: an import factory. `router.use()` only takes
202
+ // those, so this is not a guess about which kind an entry is — a closure
203
+ // that reaches here was registered in the wrong place, and the resolver
204
+ // says so rather than running it as a module.
205
+ return false
186
206
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * A read-only bag of values addressed by key or key PATH, shared with templates.
3
+ *
4
+ * This is the shape AdonisJS shares as `session` and `flashMessages`, and the
5
+ * shape its `@error` / `@errors` / `@inputError` tags read — `has(['errorsBag',
6
+ * field])` and `get('inputErrorsBag', {})`. A migrated template relies on it, so
7
+ * the path semantics have to match: a dotted string and an array address the
8
+ * same nested value.
9
+ */
10
+
11
+ /** A key, a dotted key path, or the same path already split. */
12
+ export type ValuePath = string | readonly string[]
13
+
14
+ function segments(path: ValuePath): string[] {
15
+ return Array.isArray(path) ? [...path] : String(path).split('.')
16
+ }
17
+
18
+ export class ReadOnlyValuesStore {
19
+ readonly #values: Record<string, unknown>
20
+
21
+ constructor(values: Record<string, unknown> | null) {
22
+ this.#values = values ?? {}
23
+ }
24
+
25
+ /** True when the bag holds nothing — `@if(flashMessages.isEmpty)`. */
26
+ get isEmpty(): boolean {
27
+ return Object.keys(this.#values).length === 0
28
+ }
29
+
30
+ /** Every value, as a shallow copy so a template cannot mutate the store. */
31
+ all(): Record<string, unknown> {
32
+ return { ...this.#values }
33
+ }
34
+
35
+ /** The value at `path`, or `defaultValue` when any segment is missing. */
36
+ get(path: ValuePath, defaultValue?: unknown): unknown {
37
+ let current: unknown = this.#values
38
+ for (const key of segments(path)) {
39
+ if (current === null || typeof current !== 'object') return defaultValue
40
+ // `Reflect.get` rather than indexing: a flashed key named `__proto__`
41
+ // must read an OWN property, never walk the prototype chain.
42
+ if (!Object.hasOwn(current, key)) return defaultValue
43
+ current = Reflect.get(current, key)
44
+ }
45
+ return current === undefined ? defaultValue : current
46
+ }
47
+
48
+ /** Whether `path` resolves to something other than `undefined`. */
49
+ has(path: ValuePath): boolean {
50
+ return this.get(path) !== undefined
51
+ }
52
+ }