@c9up/ream 0.2.2 → 0.2.4
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/container/Container.d.ts +18 -0
- package/dist/container/Container.d.ts.map +1 -1
- package/dist/container/Container.js +29 -0
- package/dist/container/Container.js.map +1 -1
- package/dist/env/Env.d.ts +11 -0
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +33 -17
- package/dist/env/Env.js.map +1 -1
- package/dist/env/Secret.d.ts +17 -0
- package/dist/env/Secret.d.ts.map +1 -1
- package/dist/env/Secret.js +23 -0
- package/dist/env/Secret.js.map +1 -1
- package/dist/errors/aggregate.d.ts +3 -0
- package/dist/errors/aggregate.d.ts.map +1 -1
- package/dist/errors/aggregate.js +3 -0
- package/dist/errors/aggregate.js.map +1 -1
- package/dist/events/Emitter.d.ts +20 -1
- package/dist/events/Emitter.d.ts.map +1 -1
- package/dist/events/Emitter.js +28 -6
- package/dist/events/Emitter.js.map +1 -1
- package/dist/helpers/MessageBuilder.d.ts +29 -0
- package/dist/helpers/MessageBuilder.d.ts.map +1 -0
- package/dist/helpers/MessageBuilder.js +63 -0
- package/dist/helpers/MessageBuilder.js.map +1 -0
- package/dist/helpers/VerificationToken.d.ts +56 -0
- package/dist/helpers/VerificationToken.d.ts.map +1 -0
- package/dist/helpers/VerificationToken.js +70 -0
- package/dist/helpers/VerificationToken.js.map +1 -0
- package/dist/helpers/defineStaticProperty.d.ts +15 -0
- package/dist/helpers/defineStaticProperty.d.ts.map +1 -0
- package/dist/helpers/defineStaticProperty.js +47 -0
- package/dist/helpers/defineStaticProperty.js.map +1 -0
- package/dist/helpers/duration.d.ts +20 -0
- package/dist/helpers/duration.d.ts.map +1 -1
- package/dist/helpers/duration.js +71 -0
- package/dist/helpers/duration.js.map +1 -1
- package/dist/helpers/fs.d.ts +39 -0
- package/dist/helpers/fs.d.ts.map +1 -0
- package/dist/helpers/fs.js +70 -0
- package/dist/helpers/fs.js.map +1 -0
- package/dist/helpers/index.d.ts +8 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/index.js +7 -0
- package/dist/helpers/index.js.map +1 -1
- package/dist/helpers/safeTiming.d.ts +14 -0
- package/dist/helpers/safeTiming.d.ts.map +1 -0
- package/dist/helpers/safeTiming.js +42 -0
- package/dist/helpers/safeTiming.js.map +1 -0
- package/dist/helpers/string.d.ts +85 -0
- package/dist/helpers/string.d.ts.map +1 -1
- package/dist/helpers/string.js +127 -1
- package/dist/helpers/string.js.map +1 -1
- package/dist/http/HttpContext.d.ts +14 -1
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts +16 -0
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +34 -9
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +5 -30
- package/dist/http/Request.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/router/Router.d.ts +54 -0
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +72 -3
- package/dist/router/Router.js.map +1 -1
- package/dist/session/ReadOnlyValuesStore.d.ts +11 -0
- package/dist/session/ReadOnlyValuesStore.d.ts.map +1 -1
- package/dist/session/ReadOnlyValuesStore.js +17 -0
- package/dist/session/ReadOnlyValuesStore.js.map +1 -1
- package/dist/session/Session.d.ts +11 -3
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +15 -6
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.js +2 -1
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/errors.d.ts +25 -0
- package/dist/session/errors.d.ts.map +1 -0
- package/dist/session/errors.js +32 -0
- package/dist/session/errors.js.map +1 -0
- package/dist/types/{container.d.ts → index.d.ts} +47 -18
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +14 -0
- package/dist/types/index.js.map +1 -0
- package/events.darwin-arm64.node +0 -0
- package/events.darwin-x64.node +0 -0
- package/events.linux-arm64-gnu.node +0 -0
- package/events.linux-x64-gnu.node +0 -0
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +7 -3
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/container/Container.ts +31 -0
- package/src/env/Env.ts +34 -15
- package/src/env/Secret.ts +26 -0
- package/src/errors/aggregate.ts +3 -0
- package/src/events/Emitter.ts +33 -5
- package/src/helpers/MessageBuilder.ts +69 -0
- package/src/helpers/VerificationToken.ts +93 -0
- package/src/helpers/defineStaticProperty.ts +51 -0
- package/src/helpers/duration.ts +75 -0
- package/src/helpers/fs.ts +104 -0
- package/src/helpers/index.ts +8 -0
- package/src/helpers/safeTiming.ts +45 -0
- package/src/helpers/string.ts +145 -1
- package/src/http/HttpContext.ts +14 -1
- package/src/http/RedirectBuilder.ts +38 -8
- package/src/http/Request.ts +5 -29
- package/src/index.ts +11 -2
- package/src/router/Router.ts +86 -3
- package/src/session/ReadOnlyValuesStore.ts +20 -0
- package/src/session/Session.ts +16 -11
- package/src/session/SessionMiddleware.ts +2 -1
- package/src/session/errors.ts +39 -0
- package/src/types/{container.ts → index.ts} +48 -17
- package/dist/types/container.d.ts.map +0 -1
- package/dist/types/container.js +0 -23
- package/dist/types/container.js.map +0 -1
package/src/helpers/string.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto'
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto'
|
|
2
2
|
import bytes from './bytes.js'
|
|
3
|
+
import { milliseconds, seconds } from './duration.js'
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Internal helpers
|
|
@@ -79,6 +80,135 @@ export function titleCase(s: string): string {
|
|
|
79
80
|
.join(' ')
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Space-separated lowercase words.
|
|
85
|
+
* @example noCase('userName') // 'user name'
|
|
86
|
+
*/
|
|
87
|
+
export function noCase(s: string): string {
|
|
88
|
+
return splitWords(s).join(' ')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Dot-separated lowercase words.
|
|
93
|
+
* @example dotCase('userName') // 'user.name'
|
|
94
|
+
*/
|
|
95
|
+
export function dotCase(s: string): string {
|
|
96
|
+
return splitWords(s).join('.')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Every word capitalised, space separated.
|
|
101
|
+
* @example capitalCase('user_name') // 'User Name'
|
|
102
|
+
*/
|
|
103
|
+
export function capitalCase(s: string): string {
|
|
104
|
+
return titleCase(s)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* First word capitalised, the rest lowercase.
|
|
109
|
+
* @example sentenceCase('userName') // 'User name'
|
|
110
|
+
*/
|
|
111
|
+
export function sentenceCase(s: string): string {
|
|
112
|
+
const words = splitWords(s)
|
|
113
|
+
if (words.length === 0) return ''
|
|
114
|
+
const [first, ...rest] = words
|
|
115
|
+
return [(first ?? '').charAt(0).toUpperCase() + (first ?? '').slice(1), ...rest].join(' ')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Join values the way a sentence does.
|
|
120
|
+
* @example sentence(['a', 'b', 'c']) // 'a, b, and c'
|
|
121
|
+
*/
|
|
122
|
+
export function sentence(
|
|
123
|
+
values: readonly unknown[],
|
|
124
|
+
options: { separator?: string; pairSeparator?: string; lastSeparator?: string } = {},
|
|
125
|
+
): string {
|
|
126
|
+
const parts = values.map((value) => String(value))
|
|
127
|
+
if (parts.length === 0) return ''
|
|
128
|
+
if (parts.length === 1) return parts[0] ?? ''
|
|
129
|
+
const separator = options.separator ?? ', '
|
|
130
|
+
const pairSeparator = options.pairSeparator ?? ' and '
|
|
131
|
+
const lastSeparator = options.lastSeparator ?? ', and '
|
|
132
|
+
if (parts.length === 2) return parts.join(pairSeparator)
|
|
133
|
+
return parts.slice(0, -1).join(separator) + lastSeparator + parts[parts.length - 1]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Wrap text at `width`, breaking on spaces.
|
|
138
|
+
*
|
|
139
|
+
* A word longer than the width is left whole rather than cut: breaking an
|
|
140
|
+
* identifier or a URL mid-token is worse than an over-long line.
|
|
141
|
+
*/
|
|
142
|
+
export function wordWrap(
|
|
143
|
+
value: string,
|
|
144
|
+
options: { width: number; indent?: string; newLine?: string },
|
|
145
|
+
): string {
|
|
146
|
+
const width = Math.max(1, Math.floor(options.width))
|
|
147
|
+
const indent = options.indent ?? ''
|
|
148
|
+
const newLine = options.newLine ?? '\n'
|
|
149
|
+
const lines: string[] = []
|
|
150
|
+
let line = ''
|
|
151
|
+
for (const word of value.split(/\s+/).filter(Boolean)) {
|
|
152
|
+
if (line.length === 0) {
|
|
153
|
+
line = word
|
|
154
|
+
} else if (line.length + 1 + word.length <= width) {
|
|
155
|
+
line += ` ${word}`
|
|
156
|
+
} else {
|
|
157
|
+
lines.push(line)
|
|
158
|
+
line = word
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (line.length > 0) lines.push(line)
|
|
162
|
+
return lines.map((entry, index) => (index === 0 ? entry : indent + entry)).join(newLine)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Pad every column to the same width.
|
|
167
|
+
* @example justify(['a', 'bb'], { width: 4 }) // ['a ', 'bb ']
|
|
168
|
+
*/
|
|
169
|
+
export function justify(
|
|
170
|
+
columns: readonly string[],
|
|
171
|
+
options: { width: number; align?: 'left' | 'right'; indent?: string },
|
|
172
|
+
): string[] {
|
|
173
|
+
const width = Math.max(0, Math.floor(options.width))
|
|
174
|
+
const indent = options.indent ?? ''
|
|
175
|
+
return columns.map((column) => {
|
|
176
|
+
const padded =
|
|
177
|
+
options.align === 'right' ? column.padStart(width, ' ') : column.padEnd(width, ' ')
|
|
178
|
+
return indent + padded
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** A RFC 4122 v4 UUID. */
|
|
183
|
+
export function uuid(): string {
|
|
184
|
+
return randomUUID()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** `ordinal` under the name AdonisJS also ships it as. */
|
|
188
|
+
export function ordinalize(value: number): string {
|
|
189
|
+
return ordinal(value)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** `sentence` under the name AdonisJS also ships it as. */
|
|
193
|
+
export function toSentence(
|
|
194
|
+
values: readonly unknown[],
|
|
195
|
+
options?: { separator?: string; pairSeparator?: string; lastSeparator?: string },
|
|
196
|
+
): string {
|
|
197
|
+
return sentence(values, options)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** `escapeHTML` under the name @poppinss/string ships it as. */
|
|
201
|
+
export function htmlEscape(value: string): string {
|
|
202
|
+
return escapeHTML(value)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Pluralise `word`, honouring a count: `pluralize('item', 1)` stays singular.
|
|
207
|
+
*/
|
|
208
|
+
export function pluralize(word: string, count?: number): string {
|
|
209
|
+
return count === 1 ? singular(word) : plural(word)
|
|
210
|
+
}
|
|
211
|
+
|
|
82
212
|
/**
|
|
83
213
|
* Converts a snake_case/camelCase string to a human-readable form.
|
|
84
214
|
* @example humanize('user_name') // 'User name'
|
|
@@ -466,6 +596,20 @@ const string = {
|
|
|
466
596
|
prettyHrTime,
|
|
467
597
|
toUnixSlash,
|
|
468
598
|
interpolate,
|
|
599
|
+
noCase,
|
|
600
|
+
dotCase,
|
|
601
|
+
capitalCase,
|
|
602
|
+
sentenceCase,
|
|
603
|
+
sentence,
|
|
604
|
+
toSentence,
|
|
605
|
+
wordWrap,
|
|
606
|
+
justify,
|
|
607
|
+
uuid,
|
|
608
|
+
ordinalize,
|
|
609
|
+
htmlEscape,
|
|
610
|
+
pluralize,
|
|
611
|
+
seconds,
|
|
612
|
+
milliseconds,
|
|
469
613
|
}
|
|
470
614
|
|
|
471
615
|
export default string
|
package/src/http/HttpContext.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { MatchedParams } from '../router/Router.js'
|
|
|
14
14
|
import type { CookieSigner } from '../security/CookieSigner.js'
|
|
15
15
|
import type { SignedUrl } from '../security/SignedUrl.js'
|
|
16
16
|
import type { Session } from '../session/Session.js'
|
|
17
|
+
import type { Authenticators } from '../types/index.js'
|
|
17
18
|
import { Macroable } from '../utils/Macroable.js'
|
|
18
19
|
import type { RouteUrlResolver } from './RedirectBuilder.js'
|
|
19
20
|
import { RedirectBuilder } from './RedirectBuilder.js'
|
|
@@ -47,7 +48,19 @@ export interface AuthState {
|
|
|
47
48
|
check?(): Promise<boolean>
|
|
48
49
|
authenticateUsing?(guards?: string[], options?: { loginRoute?: string }): Promise<void>
|
|
49
50
|
getUserOrFail?(): AuthState['user']
|
|
50
|
-
|
|
51
|
+
/**
|
|
52
|
+
* The guard behind `name` (AdonisJS `auth.use('web')`).
|
|
53
|
+
*
|
|
54
|
+
* Typed through the augmentable `Authenticators` interface in
|
|
55
|
+
* `@c9up/ream/types`: an app whose auth package augments it gets the guard's
|
|
56
|
+
* real type back, while a host that augments nothing still resolves to
|
|
57
|
+
* `unknown` rather than failing to compile. Without this, reaching a guard
|
|
58
|
+
* needed a cast that lies about a contract ream does not own.
|
|
59
|
+
*/
|
|
60
|
+
use?: {
|
|
61
|
+
<Name extends keyof Authenticators>(name: Name): Authenticators[Name]
|
|
62
|
+
(name: string): unknown
|
|
63
|
+
}
|
|
51
64
|
readonly authenticationAttempted?: boolean
|
|
52
65
|
readonly authenticatedViaGuard?: string
|
|
53
66
|
}
|
|
@@ -13,16 +13,27 @@ import type { Response } from './Response.js'
|
|
|
13
13
|
|
|
14
14
|
export type RouteUrlResolver = (name: string, params?: Record<string, string>) => string
|
|
15
15
|
|
|
16
|
-
function isSameOriginOrRelative(
|
|
16
|
+
function isSameOriginOrRelative(
|
|
17
|
+
referer: string,
|
|
18
|
+
requestUrl?: string,
|
|
19
|
+
allowedHosts: readonly string[] = [],
|
|
20
|
+
): boolean {
|
|
17
21
|
// A relative path is trusted only when it starts with a single "/" and has no
|
|
18
22
|
// backslash: browsers normalise "\" to "/", so "/\evil.com" would become the
|
|
19
23
|
// protocol-relative "//evil.com" that a lone `!startsWith("//")` check misses.
|
|
20
24
|
if (referer.startsWith('/') && !referer.startsWith('//') && !referer.includes('\\')) return true
|
|
25
|
+
let ref: URL
|
|
26
|
+
try {
|
|
27
|
+
ref = new URL(referer)
|
|
28
|
+
} catch {
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
// An explicitly allowed host is trusted whatever the request's own origin is
|
|
32
|
+
// — that is what the list is for (AdonisJS `allowedHosts`).
|
|
33
|
+
if (allowedHosts.includes(ref.host)) return true
|
|
21
34
|
if (!requestUrl) return false
|
|
22
35
|
try {
|
|
23
|
-
|
|
24
|
-
const req = new URL(requestUrl)
|
|
25
|
-
return ref.origin === req.origin
|
|
36
|
+
return ref.origin === new URL(requestUrl).origin
|
|
26
37
|
} catch {
|
|
27
38
|
return false
|
|
28
39
|
}
|
|
@@ -35,6 +46,14 @@ export interface IntendedUrlStore {
|
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
export class RedirectBuilder {
|
|
49
|
+
/**
|
|
50
|
+
* Hosts a redirect-back may leave the origin for (AdonisJS `allowedHosts`).
|
|
51
|
+
*
|
|
52
|
+
* Empty by default, which keeps `back()` same-origin only — an open-redirect
|
|
53
|
+
* is exactly what an unvalidated Referer buys.
|
|
54
|
+
*/
|
|
55
|
+
allowedHosts: string[] = []
|
|
56
|
+
|
|
38
57
|
#response: Response
|
|
39
58
|
#status = 302
|
|
40
59
|
#qs: Record<string, unknown> = {}
|
|
@@ -193,12 +212,23 @@ export class RedirectBuilder {
|
|
|
193
212
|
* to `fallback` to prevent open redirect attacks.
|
|
194
213
|
*/
|
|
195
214
|
back(fallback = '/'): void {
|
|
215
|
+
this.toPath(this.getPreviousUrl(fallback))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Where "back" goes (AdonisJS `getPreviousUrl`).
|
|
220
|
+
*
|
|
221
|
+
* Reads the Referer and validates its host, returning `fallback` when there
|
|
222
|
+
* is nothing trustworthy. Public and overridable on purpose: resolving the
|
|
223
|
+
* previous URL from the session instead of the header is the documented
|
|
224
|
+
* reason to replace it.
|
|
225
|
+
*/
|
|
226
|
+
getPreviousUrl(fallback = '/'): string {
|
|
196
227
|
const referer = this.#requestReferer
|
|
197
|
-
if (referer && isSameOriginOrRelative(referer, this.#requestUrl)) {
|
|
198
|
-
|
|
199
|
-
} else {
|
|
200
|
-
this.toPath(fallback)
|
|
228
|
+
if (referer && isSameOriginOrRelative(referer, this.#requestUrl, this.allowedHosts)) {
|
|
229
|
+
return referer
|
|
201
230
|
}
|
|
231
|
+
return fallback
|
|
202
232
|
}
|
|
203
233
|
|
|
204
234
|
/**
|
package/src/http/Request.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @implements FR21
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { parseQueryString } from '../bodyparser/qsParse.js'
|
|
10
11
|
import type { CookieSigner } from '../security/CookieSigner.js'
|
|
11
12
|
import type { SignedUrl } from '../security/SignedUrl.js'
|
|
12
13
|
import type { Dict } from '../types/helpers.js'
|
|
@@ -701,6 +702,10 @@ export class Request extends Macroable {
|
|
|
701
702
|
/** Get parsed query string as an object. */
|
|
702
703
|
qs(): Dict<unknown> {
|
|
703
704
|
if (!this.#parsedQs) {
|
|
705
|
+
// The same parser the body parser uses. A local one lived here and split
|
|
706
|
+
// on "&" only: `?filter[status]=open` stayed the literal key
|
|
707
|
+
// "filter[status]", and `?tags[]=a&tags[]=b` silently kept only "b" —
|
|
708
|
+
// both are ordinary shapes for a filter or an HTML multi-select.
|
|
704
709
|
this.#parsedQs = parseQueryString(this.#raw.query)
|
|
705
710
|
}
|
|
706
711
|
return { ...this.#parsedQs }
|
|
@@ -999,35 +1004,6 @@ function isIpLiteral(host: string): boolean {
|
|
|
999
1004
|
return /^(\d{1,3}\.){3}\d{1,3}$/.test(host)
|
|
1000
1005
|
}
|
|
1001
1006
|
|
|
1002
|
-
/**
|
|
1003
|
-
* Decode a percent-encoded segment without throwing on malformed input.
|
|
1004
|
-
* Browsers happily send `q=%E0%A4%A` (truncated UTF-8 sequences) — refusing
|
|
1005
|
-
* the whole request would let any client knock the route handler offline.
|
|
1006
|
-
*/
|
|
1007
|
-
function safeDecode(value: string): string {
|
|
1008
|
-
try {
|
|
1009
|
-
return decodeURIComponent(value)
|
|
1010
|
-
} catch {
|
|
1011
|
-
return value
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
function parseQueryString(qs: string): Dict<unknown> {
|
|
1016
|
-
if (!qs) return {}
|
|
1017
|
-
const result: Dict<unknown> = {}
|
|
1018
|
-
for (const pair of qs.split('&')) {
|
|
1019
|
-
const eqIdx = pair.indexOf('=')
|
|
1020
|
-
if (eqIdx === -1) {
|
|
1021
|
-
result[safeDecode(pair)] = ''
|
|
1022
|
-
} else {
|
|
1023
|
-
const key = safeDecode(pair.slice(0, eqIdx))
|
|
1024
|
-
const value = safeDecode(pair.slice(eqIdx + 1))
|
|
1025
|
-
result[key] = value
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
return result
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
1007
|
interface AcceptEntry {
|
|
1032
1008
|
value: string
|
|
1033
1009
|
q: number
|
package/src/index.ts
CHANGED
|
@@ -81,6 +81,10 @@ export type {
|
|
|
81
81
|
SessionRedisClientSource,
|
|
82
82
|
} from './session/drivers/RedisDriver.js'
|
|
83
83
|
export { RedisDriver as SessionRedisDriver } from './session/drivers/RedisDriver.js'
|
|
84
|
+
export {
|
|
85
|
+
E_SESSION_NOT_READY,
|
|
86
|
+
E_SESSION_TAGGING_NOT_SUPPORTED,
|
|
87
|
+
} from './session/errors.js'
|
|
84
88
|
export type { ValuePath } from './session/ReadOnlyValuesStore.js'
|
|
85
89
|
export { ReadOnlyValuesStore } from './session/ReadOnlyValuesStore.js'
|
|
86
90
|
export type {
|
|
@@ -252,7 +256,12 @@ export { ConsoleKernel, defineConfig, Ignitor, prettyPrintError } from './Ignito
|
|
|
252
256
|
export { errors } from './errors/aggregate.js'
|
|
253
257
|
export type { AppContext, ProviderContract } from './Provider.js'
|
|
254
258
|
export { Provider } from './Provider.js'
|
|
255
|
-
export type {
|
|
259
|
+
export type {
|
|
260
|
+
AuthenticatorName,
|
|
261
|
+
Authenticators,
|
|
262
|
+
ContainerBinding,
|
|
263
|
+
ContainerBindings,
|
|
264
|
+
} from './types/index.js'
|
|
256
265
|
|
|
257
266
|
// ─── Codemods ───────────────────────────────────────────────
|
|
258
267
|
|
|
@@ -362,4 +371,4 @@ export {
|
|
|
362
371
|
StatsTracker,
|
|
363
372
|
} from './scheduler/index.js'
|
|
364
373
|
|
|
365
|
-
export const VERSION = '0.2.
|
|
374
|
+
export const VERSION = '0.2.4'
|
package/src/router/Router.ts
CHANGED
|
@@ -402,6 +402,54 @@ export class RouteBuilder extends Macroable {
|
|
|
402
402
|
getDefinition(): RouteDefinition {
|
|
403
403
|
return this.#primary()
|
|
404
404
|
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* The handler this route dispatches to (AdonisJS `route.getHandler`).
|
|
408
|
+
*
|
|
409
|
+
* Either the inline function or the controller/method pair — a route always
|
|
410
|
+
* has exactly one of the two.
|
|
411
|
+
*/
|
|
412
|
+
getHandler(): RouteHandlerFunction | { target: AnyConstructor; method: string } | null {
|
|
413
|
+
const route = this.#primary()
|
|
414
|
+
return route.controller ?? route.handler
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Every middleware attached to this route (AdonisJS `route.getMiddleware`).
|
|
419
|
+
*
|
|
420
|
+
* Named entries and inline functions in one list, in the order they run.
|
|
421
|
+
*/
|
|
422
|
+
getMiddleware(): Array<string | MiddlewareFunction> {
|
|
423
|
+
const route = this.#primary()
|
|
424
|
+
return [...route.middleware, ...route.inlineMiddleware]
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* A serialisable description of this route (AdonisJS `route.toJSON`).
|
|
429
|
+
*
|
|
430
|
+
* What a routes-list command or a route-manifest generator reads, and why it
|
|
431
|
+
* carries names rather than the functions themselves.
|
|
432
|
+
*/
|
|
433
|
+
toJSON(): {
|
|
434
|
+
name?: string
|
|
435
|
+
pattern: string
|
|
436
|
+
methods: string[]
|
|
437
|
+
handler: RouteHandlerFunction | { target: AnyConstructor; method: string } | null
|
|
438
|
+
middleware: string[]
|
|
439
|
+
domain?: string
|
|
440
|
+
matchers: Record<string, ParamMatcher>
|
|
441
|
+
} {
|
|
442
|
+
const route = this.#primary()
|
|
443
|
+
return {
|
|
444
|
+
...(route.name === undefined ? {} : { name: route.name }),
|
|
445
|
+
pattern: route.path,
|
|
446
|
+
methods: this.#routes.map((entry) => entry.method),
|
|
447
|
+
handler: this.getHandler(),
|
|
448
|
+
middleware: [...route.middleware],
|
|
449
|
+
...(route.domain === undefined ? {} : { domain: route.domain }),
|
|
450
|
+
matchers: { ...route.matchers },
|
|
451
|
+
}
|
|
452
|
+
}
|
|
405
453
|
}
|
|
406
454
|
|
|
407
455
|
// ─── GroupBuilder ───────────────────────────────────────────
|
|
@@ -415,6 +463,16 @@ export class GroupBuilder extends Macroable {
|
|
|
415
463
|
this.#routes = routes
|
|
416
464
|
}
|
|
417
465
|
|
|
466
|
+
/**
|
|
467
|
+
* The routes this group wraps (AdonisJS `group.routes`).
|
|
468
|
+
*
|
|
469
|
+
* Read-only view: a group applies to its members, it does not hand out the
|
|
470
|
+
* backing array for someone else to splice.
|
|
471
|
+
*/
|
|
472
|
+
get routes(): readonly RouteDefinition[] {
|
|
473
|
+
return [...this.#routes]
|
|
474
|
+
}
|
|
475
|
+
|
|
418
476
|
/** Set URL prefix for all routes in the group. */
|
|
419
477
|
prefix(p: string): this {
|
|
420
478
|
for (const route of this.#routes) {
|
|
@@ -705,6 +763,8 @@ function isViewEngine(value: unknown): value is ViewEngine {
|
|
|
705
763
|
export class OnRouteBuilder {
|
|
706
764
|
#router: Router
|
|
707
765
|
#path: string
|
|
766
|
+
/** The route built by `render`/`redirect`/`redirectToPath`/`setHandler`. */
|
|
767
|
+
#route: RouteBuilder | undefined
|
|
708
768
|
|
|
709
769
|
constructor(router: Router, path: string) {
|
|
710
770
|
this.#router = router
|
|
@@ -721,7 +781,7 @@ export class OnRouteBuilder {
|
|
|
721
781
|
* seeds its own engine there.
|
|
722
782
|
*/
|
|
723
783
|
render(view: string, data?: Record<string, unknown>): RouteBuilder {
|
|
724
|
-
|
|
784
|
+
this.#route = this.#router.get(this.#path, async (ctx) => {
|
|
725
785
|
const candidate = Reflect.get(Object(ctx), 'view') ?? ctx.store.get('view')
|
|
726
786
|
if (!isViewEngine(candidate)) {
|
|
727
787
|
throw new Error(
|
|
@@ -732,6 +792,7 @@ export class OnRouteBuilder {
|
|
|
732
792
|
const html = await candidate.render(view, data)
|
|
733
793
|
ctx.response.type('text/html; charset=utf-8').send(html)
|
|
734
794
|
})
|
|
795
|
+
return this.#route
|
|
735
796
|
}
|
|
736
797
|
|
|
737
798
|
/**
|
|
@@ -740,16 +801,38 @@ export class OnRouteBuilder {
|
|
|
740
801
|
* for a fixed URL.
|
|
741
802
|
*/
|
|
742
803
|
redirect(name: string, params?: Record<string, string>, status = 302): RouteBuilder {
|
|
743
|
-
|
|
804
|
+
this.#route = this.#router.get(this.#path, async (ctx) => {
|
|
744
805
|
ctx.response.redirect().status(status).toRoute(name, params)
|
|
745
806
|
})
|
|
807
|
+
return this.#route
|
|
746
808
|
}
|
|
747
809
|
|
|
748
810
|
/** Redirect to a fixed path/URL (AdonisJS brisk `redirectToPath`). */
|
|
749
811
|
redirectToPath(target: string, status = 302): RouteBuilder {
|
|
750
|
-
|
|
812
|
+
this.#route = this.#router.get(this.#path, async (ctx) => {
|
|
751
813
|
ctx.response.redirect().status(status).toPath(target)
|
|
752
814
|
})
|
|
815
|
+
return this.#route
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Register a handler for this path (AdonisJS brisk `setHandler`).
|
|
820
|
+
*
|
|
821
|
+
* The escape hatch from the canned briskness: `render` and `redirect` cover
|
|
822
|
+
* the two common cases, this covers the third without going back through
|
|
823
|
+
* `router.get()`.
|
|
824
|
+
*/
|
|
825
|
+
setHandler(handler: RouteHandlerFunction): RouteBuilder {
|
|
826
|
+
this.#route = this.#router.get(this.#path, handler)
|
|
827
|
+
return this.#route
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* The route the last `render`/`redirect`/`setHandler` created, or
|
|
832
|
+
* `undefined` while none has been called (AdonisJS brisk `route`).
|
|
833
|
+
*/
|
|
834
|
+
get route(): RouteBuilder | undefined {
|
|
835
|
+
return this.#route
|
|
753
836
|
}
|
|
754
837
|
}
|
|
755
838
|
|
|
@@ -49,4 +49,24 @@ export class ReadOnlyValuesStore {
|
|
|
49
49
|
has(path: ValuePath): boolean {
|
|
50
50
|
return this.get(path) !== undefined
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Every value, as a plain object (AdonisJS `toObject`).
|
|
55
|
+
*
|
|
56
|
+
* Same content as {@link all}; the two names exist because AdonisJS ships
|
|
57
|
+
* both and a migrated template may reach for either.
|
|
58
|
+
*/
|
|
59
|
+
toObject(): Record<string, unknown> {
|
|
60
|
+
return this.all()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Every value — what `JSON.stringify(flashMessages)` emits. */
|
|
64
|
+
toJSON(): Record<string, unknown> {
|
|
65
|
+
return this.all()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** JSON text, so `{{ flashMessages }}` in a template renders the contents. */
|
|
69
|
+
toString(): string {
|
|
70
|
+
return JSON.stringify(this.#values)
|
|
71
|
+
}
|
|
52
72
|
}
|
package/src/session/Session.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { SessionRedisClientSource } from './drivers/RedisDriver.js'
|
|
14
|
+
import { E_SESSION_NOT_READY, E_SESSION_TAGGING_NOT_SUPPORTED } from './errors.js'
|
|
15
|
+
import { ReadOnlyValuesStore } from './ReadOnlyValuesStore.js'
|
|
14
16
|
|
|
15
17
|
export interface SessionDriver {
|
|
16
18
|
/**
|
|
@@ -435,9 +437,16 @@ export class Session {
|
|
|
435
437
|
this.forget(INTENDED_URL_KEY)
|
|
436
438
|
}
|
|
437
439
|
|
|
438
|
-
/**
|
|
439
|
-
|
|
440
|
-
|
|
440
|
+
/**
|
|
441
|
+
* What the PREVIOUS request flashed for this one (AdonisJS `flashMessages`).
|
|
442
|
+
*
|
|
443
|
+
* A property holding a store, not a method returning a record: that is the
|
|
444
|
+
* AdonisJS shape, and it is what lets a controller or template read a nested
|
|
445
|
+
* failure with `session.flashMessages.get('errors.email')`. `all()` gives
|
|
446
|
+
* back the plain object.
|
|
447
|
+
*/
|
|
448
|
+
get flashMessages(): ReadOnlyValuesStore {
|
|
449
|
+
return new ReadOnlyValuesStore(this.#previousFlash)
|
|
441
450
|
}
|
|
442
451
|
|
|
443
452
|
/** Get a specific flash message from the previous request. */
|
|
@@ -519,8 +528,8 @@ export class Session {
|
|
|
519
528
|
* live. `flashMessages` is the other half — what the PREVIOUS request left
|
|
520
529
|
* for this one.
|
|
521
530
|
*/
|
|
522
|
-
get responseFlashMessages():
|
|
523
|
-
return
|
|
531
|
+
get responseFlashMessages(): ReadOnlyValuesStore {
|
|
532
|
+
return new ReadOnlyValuesStore(this.#flashData)
|
|
524
533
|
}
|
|
525
534
|
|
|
526
535
|
/**
|
|
@@ -625,14 +634,10 @@ export class Session {
|
|
|
625
634
|
*/
|
|
626
635
|
#taggingDriver(method: string): SessionDriverWithTagging {
|
|
627
636
|
if (this.#driver === undefined) {
|
|
628
|
-
throw new
|
|
629
|
-
`session.${method}() needs a session store — this session was built without one.`,
|
|
630
|
-
)
|
|
637
|
+
throw new E_SESSION_NOT_READY(method)
|
|
631
638
|
}
|
|
632
639
|
if (!supportsTagging(this.#driver)) {
|
|
633
|
-
throw new
|
|
634
|
-
`session.${method}() is not supported by the configured session store. Use the memory, redis, or database store.`,
|
|
635
|
-
)
|
|
640
|
+
throw new E_SESSION_TAGGING_NOT_SUPPORTED(method)
|
|
636
641
|
}
|
|
637
642
|
return this.#driver
|
|
638
643
|
}
|
|
@@ -232,7 +232,8 @@ function shareSessionWithView(ctx: HttpContext, session: Session): void {
|
|
|
232
232
|
const view = Reflect.get(Object(ctx), 'view')
|
|
233
233
|
const share = Reflect.get(Object(view), 'share')
|
|
234
234
|
if (typeof share !== 'function') return
|
|
235
|
-
|
|
235
|
+
// The session hands back a store already — no need to rebuild one here.
|
|
236
|
+
const flashMessages = session.flashMessages
|
|
236
237
|
share.call(view, {
|
|
237
238
|
session: new ReadOnlyValuesStore(session.all()),
|
|
238
239
|
flashMessages,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session exceptions, under the codes AdonisJS raises.
|
|
3
|
+
*
|
|
4
|
+
* The conditions were already guarded — they just threw bare `Error`s, so an
|
|
5
|
+
* app could only match on the message text.
|
|
6
|
+
*
|
|
7
|
+
* `E_SESSION_NOT_MUTABLE` has no counterpart here on purpose: it is what
|
|
8
|
+
* AdonisJS raises for a session initiated in read-only mode, and ream has no
|
|
9
|
+
* read-only mode. Declaring the class with nothing able to throw it would be
|
|
10
|
+
* worse than its absence — an app would guard a branch that never runs.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Exception } from '../http/Exception.js'
|
|
14
|
+
|
|
15
|
+
/** The session was used before a store was attached to it. */
|
|
16
|
+
export class E_SESSION_NOT_READY extends Exception {
|
|
17
|
+
static override status = 500
|
|
18
|
+
static override code = 'E_SESSION_NOT_READY'
|
|
19
|
+
|
|
20
|
+
constructor(method: string) {
|
|
21
|
+
super(`session.${method}() needs a session store — this session was built without one.`, {
|
|
22
|
+
status: 500,
|
|
23
|
+
code: 'E_SESSION_NOT_READY',
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The configured store cannot tag sessions (cookie and file stores cannot). */
|
|
29
|
+
export class E_SESSION_TAGGING_NOT_SUPPORTED extends Exception {
|
|
30
|
+
static override status = 500
|
|
31
|
+
static override code = 'E_SESSION_TAGGING_NOT_SUPPORTED'
|
|
32
|
+
|
|
33
|
+
constructor(method: string) {
|
|
34
|
+
super(
|
|
35
|
+
`session.${method}() is not supported by the configured session store. Use the memory, redis, or database store.`,
|
|
36
|
+
{ status: 500, code: 'E_SESSION_TAGGING_NOT_SUPPORTED' },
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
}
|