@c9up/ream 0.2.27 → 0.2.29
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/README.md +1 -0
- package/dist/Codemods.d.ts +16 -0
- package/dist/Codemods.d.ts.map +1 -1
- package/dist/Codemods.js +182 -0
- package/dist/Codemods.js.map +1 -1
- package/dist/Ignitor.d.ts +11 -1
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +27 -21
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.d.ts.map +1 -1
- package/dist/bodyparser/BodyParserMiddleware.js +3 -0
- package/dist/bodyparser/BodyParserMiddleware.js.map +1 -1
- package/dist/bodyparser/qsParse.d.ts +10 -1
- package/dist/bodyparser/qsParse.d.ts.map +1 -1
- package/dist/bodyparser/qsParse.js +53 -15
- package/dist/bodyparser/qsParse.js.map +1 -1
- package/dist/codemods/source.d.ts +57 -0
- package/dist/codemods/source.d.ts.map +1 -0
- package/dist/codemods/source.js +282 -0
- package/dist/codemods/source.js.map +1 -0
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +3 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/http/Exception.d.ts +14 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +10 -2
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/HttpContext.d.ts +11 -0
- package/dist/http/HttpContext.d.ts.map +1 -1
- package/dist/http/HttpContext.js +2 -1
- package/dist/http/HttpContext.js.map +1 -1
- package/dist/http/RedirectBuilder.d.ts +9 -3
- package/dist/http/RedirectBuilder.d.ts.map +1 -1
- package/dist/http/RedirectBuilder.js +39 -37
- package/dist/http/RedirectBuilder.js.map +1 -1
- package/dist/http/Request.d.ts +42 -17
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +65 -29
- package/dist/http/Request.js.map +1 -1
- package/dist/http/SseStream.js +6 -7
- package/dist/http/SseStream.js.map +1 -1
- package/dist/http/permissions.d.ts +19 -0
- package/dist/http/permissions.d.ts.map +1 -0
- package/dist/http/permissions.js +28 -0
- package/dist/http/permissions.js.map +1 -0
- package/dist/http-client/HttpClient.d.ts +111 -0
- package/dist/http-client/HttpClient.d.ts.map +1 -0
- package/dist/http-client/HttpClient.js +386 -0
- package/dist/http-client/HttpClient.js.map +1 -0
- package/dist/http-client/addresses.d.ts +14 -0
- package/dist/http-client/addresses.d.ts.map +1 -0
- package/dist/http-client/addresses.js +57 -0
- package/dist/http-client/addresses.js.map +1 -0
- package/dist/http-client/fake.d.ts +50 -0
- package/dist/http-client/fake.d.ts.map +1 -0
- package/dist/http-client/fake.js +95 -0
- package/dist/http-client/fake.js.map +1 -0
- package/dist/http-client/index.d.ts +7 -0
- package/dist/http-client/index.d.ts.map +1 -0
- package/dist/http-client/index.js +7 -0
- package/dist/http-client/index.js.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/Pipeline.d.ts.map +1 -1
- package/dist/middleware/Pipeline.js +6 -3
- package/dist/middleware/Pipeline.js.map +1 -1
- package/dist/native/generated.d.ts +108 -3
- package/dist/native/generated.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.d.ts +6 -0
- package/dist/rpc/RpcProvider.d.ts.map +1 -1
- package/dist/rpc/RpcProvider.js +4 -1
- package/dist/rpc/RpcProvider.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts +27 -14
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +95 -38
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/locks/RedisLockBackend.d.ts.map +1 -1
- package/dist/scheduler/locks/RedisLockBackend.js +13 -2
- package/dist/scheduler/locks/RedisLockBackend.js.map +1 -1
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +17 -3
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/security/SignedUrl.d.ts.map +1 -1
- package/dist/security/SignedUrl.js +6 -1
- package/dist/security/SignedUrl.js.map +1 -1
- package/dist/server/Server.d.ts +75 -9
- package/dist/server/Server.d.ts.map +1 -1
- package/dist/server/Server.js +60 -9
- package/dist/server/Server.js.map +1 -1
- package/dist/server/nativeGrpc.d.ts +27 -0
- package/dist/server/nativeGrpc.d.ts.map +1 -0
- package/dist/server/nativeGrpc.js +47 -0
- package/dist/server/nativeGrpc.js.map +1 -0
- package/dist/server/nativeLimits.d.ts +49 -0
- package/dist/server/nativeLimits.d.ts.map +1 -0
- package/dist/server/nativeLimits.js +67 -0
- package/dist/server/nativeLimits.js.map +1 -0
- package/dist/session/Session.d.ts +45 -0
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js +31 -2
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +52 -13
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/session/config.d.ts +5 -0
- package/dist/session/config.d.ts.map +1 -1
- package/dist/session/config.js +8 -0
- package/dist/session/config.js.map +1 -1
- package/dist/session/drivers/CookieDriver.d.ts +12 -3
- package/dist/session/drivers/CookieDriver.d.ts.map +1 -1
- package/dist/session/drivers/CookieDriver.js +20 -6
- package/dist/session/drivers/CookieDriver.js.map +1 -1
- package/dist/session/drivers/DatabaseDriver.d.ts +2 -0
- package/dist/session/drivers/DatabaseDriver.d.ts.map +1 -1
- package/dist/session/drivers/DatabaseDriver.js +6 -0
- package/dist/session/drivers/DatabaseDriver.js.map +1 -1
- package/dist/session/drivers/FileDriver.d.ts +8 -0
- package/dist/session/drivers/FileDriver.d.ts.map +1 -1
- package/dist/session/drivers/FileDriver.js +85 -11
- package/dist/session/drivers/FileDriver.js.map +1 -1
- package/dist/session/drivers/MemoryDriver.d.ts +2 -0
- package/dist/session/drivers/MemoryDriver.d.ts.map +1 -1
- package/dist/session/drivers/MemoryDriver.js +21 -5
- package/dist/session/drivers/MemoryDriver.js.map +1 -1
- package/dist/session/drivers/RedisDriver.d.ts +2 -0
- package/dist/session/drivers/RedisDriver.d.ts.map +1 -1
- package/dist/session/drivers/RedisDriver.js +6 -0
- package/dist/session/drivers/RedisDriver.js.map +1 -1
- package/dist/session/errors.d.ts +27 -0
- package/dist/session/errors.d.ts.map +1 -1
- package/dist/session/errors.js +33 -0
- package/dist/session/errors.js.map +1 -1
- package/dist/session/templateTags.d.ts +14 -0
- package/dist/session/templateTags.d.ts.map +1 -1
- package/dist/session/templateTags.js +45 -17
- package/dist/session/templateTags.js.map +1 -1
- package/dist/storage/StaticMiddleware.d.ts.map +1 -1
- package/dist/storage/StaticMiddleware.js +29 -18
- package/dist/storage/StaticMiddleware.js.map +1 -1
- 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.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 +11 -2
- package/scheduler.darwin-arm64.node +0 -0
- package/scheduler.darwin-x64.node +0 -0
- package/scheduler.linux-arm64-gnu.node +0 -0
- package/scheduler.linux-x64-gnu.node +0 -0
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Codemods.ts +265 -0
- package/src/Ignitor.ts +48 -20
- package/src/bodyparser/BodyParserMiddleware.ts +3 -0
- package/src/bodyparser/qsParse.ts +69 -14
- package/src/codemods/source.ts +303 -0
- package/src/graphql/GraphQLEngine.ts +3 -2
- package/src/http/Exception.ts +17 -2
- package/src/http/HttpContext.ts +14 -1
- package/src/http/RedirectBuilder.ts +48 -37
- package/src/http/Request.ts +87 -29
- package/src/http/SseStream.ts +6 -7
- package/src/http/permissions.ts +31 -0
- package/src/http-client/HttpClient.ts +505 -0
- package/src/http-client/addresses.ts +58 -0
- package/src/http-client/fake.ts +141 -0
- package/src/http-client/index.ts +21 -0
- package/src/index.ts +9 -1
- package/src/middleware/Pipeline.ts +7 -3
- package/src/native/generated.ts +123 -3
- package/src/rpc/RpcProvider.ts +10 -1
- package/src/rpc/RpcRouter.ts +126 -42
- package/src/scheduler/locks/RedisLockBackend.ts +12 -4
- package/src/security/CookieSigner.ts +17 -3
- package/src/security/SignedUrl.ts +6 -1
- package/src/server/Server.ts +103 -9
- package/src/server/nativeGrpc.ts +59 -0
- package/src/server/nativeLimits.ts +94 -0
- package/src/session/Session.ts +72 -2
- package/src/session/SessionMiddleware.ts +53 -14
- package/src/session/config.ts +7 -0
- package/src/session/drivers/CookieDriver.ts +21 -6
- package/src/session/drivers/DatabaseDriver.ts +10 -0
- package/src/session/drivers/FileDriver.ts +91 -11
- package/src/session/drivers/MemoryDriver.ts +20 -5
- package/src/session/drivers/RedisDriver.ts +7 -0
- package/src/session/errors.ts +48 -0
- package/src/session/templateTags.ts +57 -0
- package/src/storage/StaticMiddleware.ts +28 -18
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Just enough of a TypeScript scanner for the configure codemods: find a
|
|
3
|
+
* call or an object/array literal in CODE (never inside a string, a template
|
|
4
|
+
* or a comment), split a literal into its top-level entries, and append to
|
|
5
|
+
* one. Upstream does this with a full AST; ream ships no compiler, and the
|
|
6
|
+
* files these codemods edit (`start/env.ts`, `reamrc.ts`) are object and
|
|
7
|
+
* array literals.
|
|
8
|
+
*
|
|
9
|
+
* Not handled: regular-expression literals (a quote inside one reads as the
|
|
10
|
+
* start of a string). Neither file has a reason to contain one.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const CLOSERS: Record<string, string> = { '{': '}', '[': ']', '(': ')' }
|
|
14
|
+
|
|
15
|
+
function isIdentifierChar(char: string | undefined): boolean {
|
|
16
|
+
return char !== undefined && /[A-Za-z0-9_$]/.test(char)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The index just past the string, template or comment starting at `i`, or
|
|
21
|
+
* `i` itself when none starts there. An unterminated one runs to the end.
|
|
22
|
+
*/
|
|
23
|
+
function skipNonCode(src: string, i: number): number {
|
|
24
|
+
const char = src[i]
|
|
25
|
+
const next = src[i + 1]
|
|
26
|
+
if (char === '/' && next === '/') {
|
|
27
|
+
const end = src.indexOf('\n', i)
|
|
28
|
+
return end === -1 ? src.length : end
|
|
29
|
+
}
|
|
30
|
+
if (char === '/' && next === '*') {
|
|
31
|
+
const end = src.indexOf('*/', i + 2)
|
|
32
|
+
return end === -1 ? src.length : end + 2
|
|
33
|
+
}
|
|
34
|
+
if (char === "'" || char === '"') {
|
|
35
|
+
let j = i + 1
|
|
36
|
+
while (j < src.length && src[j] !== char && src[j] !== '\n') {
|
|
37
|
+
j += src[j] === '\\' ? 2 : 1
|
|
38
|
+
}
|
|
39
|
+
return j + 1
|
|
40
|
+
}
|
|
41
|
+
if (char === '`') {
|
|
42
|
+
let j = i + 1
|
|
43
|
+
while (j < src.length && src[j] !== '`') {
|
|
44
|
+
if (src[j] === '\\') {
|
|
45
|
+
j += 2
|
|
46
|
+
} else if (src[j] === '$' && src[j + 1] === '{') {
|
|
47
|
+
const close = matchClose(src, j + 1)
|
|
48
|
+
j = close === -1 ? src.length : close + 1
|
|
49
|
+
} else {
|
|
50
|
+
j += 1
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return j + 1
|
|
54
|
+
}
|
|
55
|
+
return i
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Whether the string, template or block comment starting at `i` is closed. */
|
|
59
|
+
function isTerminated(src: string, i: number): boolean {
|
|
60
|
+
const char = src[i]
|
|
61
|
+
if (char === '/') return src.indexOf('*/', i + 2) !== -1
|
|
62
|
+
const end = skipNonCode(src, i)
|
|
63
|
+
return end <= src.length && end - 1 > i && src[end - 1] === char
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Index of the bracket closing the one at `open`, or -1. */
|
|
67
|
+
export function matchClose(src: string, open: number): number {
|
|
68
|
+
const first = CLOSERS[src[open] ?? '']
|
|
69
|
+
if (first === undefined) return -1
|
|
70
|
+
const stack = [first]
|
|
71
|
+
let i = open + 1
|
|
72
|
+
while (i < src.length) {
|
|
73
|
+
const skipped = skipNonCode(src, i)
|
|
74
|
+
if (skipped !== i) {
|
|
75
|
+
i = skipped
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
78
|
+
const char = src[i] ?? ''
|
|
79
|
+
const closer = CLOSERS[char]
|
|
80
|
+
if (closer !== undefined) {
|
|
81
|
+
stack.push(closer)
|
|
82
|
+
} else if (char === stack[stack.length - 1]) {
|
|
83
|
+
stack.pop()
|
|
84
|
+
if (stack.length === 0) return i
|
|
85
|
+
} else if (char === '}' || char === ']' || char === ')') {
|
|
86
|
+
return -1
|
|
87
|
+
}
|
|
88
|
+
i += 1
|
|
89
|
+
}
|
|
90
|
+
return -1
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** First code character at or after `i` (before `end`), skipping whitespace and comments. */
|
|
94
|
+
function firstCode(src: string, i: number, end: number): number {
|
|
95
|
+
let j = i
|
|
96
|
+
while (j < end) {
|
|
97
|
+
const char = src[j] ?? ''
|
|
98
|
+
if (/\s/.test(char)) {
|
|
99
|
+
j += 1
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
if (char === '/' && (src[j + 1] === '/' || src[j + 1] === '*')) {
|
|
103
|
+
j = skipNonCode(src, j)
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
return j
|
|
107
|
+
}
|
|
108
|
+
return end
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One top-level entry of a literal: the text between two commas. */
|
|
112
|
+
export interface Segment {
|
|
113
|
+
start: number
|
|
114
|
+
end: number
|
|
115
|
+
/** First code character, or `end` when the entry is only trivia. */
|
|
116
|
+
codeStart: number
|
|
117
|
+
/** Just past the last code character (comments and whitespace excluded). */
|
|
118
|
+
codeEnd: number
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Split `src[start, end)` on its top-level commas. */
|
|
122
|
+
export function segments(src: string, start: number, end: number): Segment[] {
|
|
123
|
+
const out: Segment[] = []
|
|
124
|
+
let segmentStart = start
|
|
125
|
+
let codeEnd = start
|
|
126
|
+
let depth = 0
|
|
127
|
+
let i = start
|
|
128
|
+
const close = (at: number) => {
|
|
129
|
+
const codeStart = firstCode(src, segmentStart, at)
|
|
130
|
+
out.push({ start: segmentStart, end: at, codeStart, codeEnd: Math.max(codeEnd, codeStart) })
|
|
131
|
+
}
|
|
132
|
+
while (i < end) {
|
|
133
|
+
const char = src[i] ?? ''
|
|
134
|
+
if (char === '/' && (src[i + 1] === '/' || src[i + 1] === '*')) {
|
|
135
|
+
i = skipNonCode(src, i)
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
const skipped = skipNonCode(src, i)
|
|
139
|
+
if (skipped !== i) {
|
|
140
|
+
i = skipped
|
|
141
|
+
codeEnd = i
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (char === ',' && depth === 0) {
|
|
145
|
+
close(i)
|
|
146
|
+
segmentStart = i + 1
|
|
147
|
+
codeEnd = i + 1
|
|
148
|
+
i += 1
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
if (CLOSERS[char] !== undefined) depth += 1
|
|
152
|
+
else if (char === '}' || char === ']' || char === ')') depth -= 1
|
|
153
|
+
if (!/\s/.test(char)) codeEnd = i + 1
|
|
154
|
+
i += 1
|
|
155
|
+
}
|
|
156
|
+
close(end)
|
|
157
|
+
return out
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether `code` is one complete expression that cannot reach outside the
|
|
162
|
+
* slot it is written into: brackets balanced and never closing past where
|
|
163
|
+
* they opened, strings and comments terminated, no top-level comma.
|
|
164
|
+
*/
|
|
165
|
+
export function isSingleExpression(code: string): boolean {
|
|
166
|
+
if (code.trim() === '') return false
|
|
167
|
+
let depth = 0
|
|
168
|
+
let i = 0
|
|
169
|
+
while (i < code.length) {
|
|
170
|
+
const skipped = skipNonCode(code, i)
|
|
171
|
+
if (skipped !== i) {
|
|
172
|
+
// A line comment would swallow the comma written after the value.
|
|
173
|
+
const lineComment = code[i] === '/' && code[i + 1] === '/'
|
|
174
|
+
if (lineComment || !isTerminated(code, i)) return false
|
|
175
|
+
i = skipped
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
const char = code[i] ?? ''
|
|
179
|
+
if (CLOSERS[char] !== undefined) depth += 1
|
|
180
|
+
else if (char === '}' || char === ']' || char === ')') {
|
|
181
|
+
depth -= 1
|
|
182
|
+
if (depth < 0) return false
|
|
183
|
+
} else if (char === ',' && depth === 0) return false
|
|
184
|
+
i += 1
|
|
185
|
+
}
|
|
186
|
+
return depth === 0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Index of the `(` of the first call to `callee` in code, or -1. A mention
|
|
191
|
+
* inside a string or a comment, or as the tail of a longer name
|
|
192
|
+
* (`MyEnv.create`), does not count.
|
|
193
|
+
*/
|
|
194
|
+
export function findCall(src: string, callee: string): number {
|
|
195
|
+
let i = 0
|
|
196
|
+
while (i < src.length) {
|
|
197
|
+
const skipped = skipNonCode(src, i)
|
|
198
|
+
if (skipped !== i) {
|
|
199
|
+
i = skipped
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
const before = src[i - 1]
|
|
203
|
+
if (src.startsWith(callee, i) && !isIdentifierChar(before) && before !== '.') {
|
|
204
|
+
const paren = firstCode(src, i + callee.length, src.length)
|
|
205
|
+
if (src[paren] === '(') return paren
|
|
206
|
+
}
|
|
207
|
+
i += 1
|
|
208
|
+
}
|
|
209
|
+
return -1
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Index of the `{` after the first `export default` in code, or -1. */
|
|
213
|
+
export function findExportDefaultObject(src: string): number {
|
|
214
|
+
let i = 0
|
|
215
|
+
while (i < src.length) {
|
|
216
|
+
const skipped = skipNonCode(src, i)
|
|
217
|
+
if (skipped !== i) {
|
|
218
|
+
i = skipped
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
if (src.startsWith('export', i) && !isIdentifierChar(src[i - 1])) {
|
|
222
|
+
const word = firstCode(src, i + 'export'.length, src.length)
|
|
223
|
+
if (src.startsWith('default', word) && !isIdentifierChar(src[word + 'default'.length])) {
|
|
224
|
+
const brace = firstCode(src, word + 'default'.length, src.length)
|
|
225
|
+
return src[brace] === '{' ? brace : -1
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
i += 1
|
|
229
|
+
}
|
|
230
|
+
return -1
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** A `key: value` entry of an object literal. */
|
|
234
|
+
export interface Property {
|
|
235
|
+
key: string
|
|
236
|
+
valueStart: number
|
|
237
|
+
valueEnd: number
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The `key: value` entries of the object literal opening at `open`. */
|
|
241
|
+
export function properties(src: string, open: number, close: number): Property[] {
|
|
242
|
+
const out: Property[] = []
|
|
243
|
+
for (const segment of segments(src, open + 1, close)) {
|
|
244
|
+
if (segment.codeStart >= segment.end) continue
|
|
245
|
+
const text = src.slice(segment.codeStart, segment.end)
|
|
246
|
+
const match = /^(?:([A-Za-z_$][\w$]*)|'([^'\\\n]*)'|"([^"\\\n]*)")\s*:/.exec(text)
|
|
247
|
+
if (match === null) continue
|
|
248
|
+
const key = match[1] ?? match[2] ?? match[3] ?? ''
|
|
249
|
+
const valueStart = firstCode(src, segment.codeStart + match[0].length, segment.end)
|
|
250
|
+
out.push({ key, valueStart, valueEnd: segment.codeEnd })
|
|
251
|
+
}
|
|
252
|
+
return out
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Leading whitespace of the line holding `index`. */
|
|
256
|
+
function lineIndent(src: string, index: number): string {
|
|
257
|
+
const lineStart = src.lastIndexOf('\n', index - 1) + 1
|
|
258
|
+
return /^[ \t]*/.exec(src.slice(lineStart))?.[0] ?? ''
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Append entries to the literal `src[open..close]` and return the new text.
|
|
263
|
+
*
|
|
264
|
+
* `build` receives the indentation of the existing entries (or one level
|
|
265
|
+
* deeper than the closing bracket when there are none) and returns the lines
|
|
266
|
+
* to add, each already ending with its comma. `blankLine` separates the new
|
|
267
|
+
* entries from existing ones. A missing comma after the last existing entry
|
|
268
|
+
* is added.
|
|
269
|
+
*/
|
|
270
|
+
export function appendToLiteral(
|
|
271
|
+
src: string,
|
|
272
|
+
open: number,
|
|
273
|
+
close: number,
|
|
274
|
+
eol: string,
|
|
275
|
+
build: (indent: string) => string[],
|
|
276
|
+
blankLine = false,
|
|
277
|
+
): string {
|
|
278
|
+
const all = segments(src, open + 1, close)
|
|
279
|
+
const entries = all.filter((s) => s.codeStart < s.end)
|
|
280
|
+
const first = entries[0]
|
|
281
|
+
const closeIndent = lineIndent(src, close)
|
|
282
|
+
const firstOnOwnLine =
|
|
283
|
+
first !== undefined &&
|
|
284
|
+
/^[ \t]*$/.test(src.slice(src.lastIndexOf('\n', first.codeStart - 1) + 1, first.codeStart))
|
|
285
|
+
const indent =
|
|
286
|
+
first !== undefined && firstOnOwnLine ? lineIndent(src, first.codeStart) : `${closeIndent} `
|
|
287
|
+
const lines = build(indent)
|
|
288
|
+
const separator = blankLine && entries.length > 0 ? eol : ''
|
|
289
|
+
|
|
290
|
+
const closeLineStart = src.lastIndexOf('\n', close - 1) + 1
|
|
291
|
+
const closeOnOwnLine = /^[ \t]*$/.test(src.slice(closeLineStart, close))
|
|
292
|
+
let out = closeOnOwnLine
|
|
293
|
+
? `${src.slice(0, closeLineStart)}${separator}${lines.join(eol)}${eol}${src.slice(closeLineStart)}`
|
|
294
|
+
: `${src.slice(0, close)}${eol}${separator}${lines.join(eol)}${eol}${closeIndent}${src.slice(close)}`
|
|
295
|
+
|
|
296
|
+
// The comma goes in last, as it sits before every insertion point above.
|
|
297
|
+
// No comma after the last entry when it is also the last segment.
|
|
298
|
+
const last = entries[entries.length - 1]
|
|
299
|
+
if (last !== undefined && all[all.length - 1] === last) {
|
|
300
|
+
out = `${out.slice(0, last.codeEnd)},${out.slice(last.codeEnd)}`
|
|
301
|
+
}
|
|
302
|
+
return out
|
|
303
|
+
}
|
|
@@ -25,6 +25,7 @@ import * as fs from 'node:fs'
|
|
|
25
25
|
import { currentNodeEnv } from '../env/nodeEnv.js'
|
|
26
26
|
import { loadNapi } from '../helpers/napi-loader.js'
|
|
27
27
|
import type { HttpContext } from '../http/HttpContext.js'
|
|
28
|
+
import { holdsPermissions } from '../http/permissions.js'
|
|
28
29
|
|
|
29
30
|
/** Rust parser output (the `ream-graphql` crate via the `index` NAPI binary). */
|
|
30
31
|
interface RustParsedField {
|
|
@@ -455,9 +456,9 @@ export class GraphQLEngine {
|
|
|
455
456
|
}
|
|
456
457
|
}
|
|
457
458
|
|
|
459
|
+
// A token-authenticated request also needs the token to allow each one.
|
|
458
460
|
if (permissions.length > 0) {
|
|
459
|
-
|
|
460
|
-
if (!permissions.every((p) => userPerms.includes(p))) {
|
|
461
|
+
if (!holdsPermissions(ctx.auth, permissions)) {
|
|
461
462
|
return 'Insufficient permissions'
|
|
462
463
|
}
|
|
463
464
|
}
|
package/src/http/Exception.ts
CHANGED
|
@@ -259,6 +259,12 @@ export type HttpError = {
|
|
|
259
259
|
report?: (error: unknown, ctx: HttpContext) => Promise<void> | void
|
|
260
260
|
/** A hint appended to the message, when the exception carries one. */
|
|
261
261
|
help?: string
|
|
262
|
+
/**
|
|
263
|
+
* Set by a thrown value that is not an {@link Exception} but whose message is
|
|
264
|
+
* written for clients (the `http-errors` convention). Shown outside debug
|
|
265
|
+
* mode even on a 5xx.
|
|
266
|
+
*/
|
|
267
|
+
expose?: boolean
|
|
262
268
|
}
|
|
263
269
|
|
|
264
270
|
/** Renders an HTML error page for a status (AdonisJS `StatusPageRenderer`). */
|
|
@@ -352,13 +358,22 @@ export class ExceptionHandler extends Macroable {
|
|
|
352
358
|
* The message a client is allowed to see.
|
|
353
359
|
*
|
|
354
360
|
* DEVIATION (named): outside debug mode ream only echoes the message of a
|
|
355
|
-
* deliberate `Exception
|
|
361
|
+
* deliberate `Exception`, of a client error (status below 500, which a
|
|
362
|
+
* package raises on purpose: 401, 403, 413…), or of an error flagged
|
|
363
|
+
* `expose: true`; anything else collapses to a generic line. AdonisJS
|
|
356
364
|
* echoes `error.message` whatever was thrown, which leaks the text of driver
|
|
357
365
|
* and library errors to the client.
|
|
366
|
+
*
|
|
367
|
+
* A `code` is NOT a reason to show the message: Node system errors
|
|
368
|
+
* (`ECONNREFUSED`, `ENOENT`) and database drivers (`23505`) carry one, and
|
|
369
|
+
* their messages hold internal hosts, file paths, constraint names and
|
|
370
|
+
* stored values. The error itself is left untouched for reporting.
|
|
358
371
|
*/
|
|
359
372
|
protected publicMessage(error: HttpError, ctx: HttpContext): string {
|
|
360
373
|
if (this.isDebuggingEnabled(ctx)) return error.message
|
|
361
|
-
if (error.
|
|
374
|
+
if (error instanceof Exception || error.status < 500 || error.expose === true) {
|
|
375
|
+
return error.message
|
|
376
|
+
}
|
|
362
377
|
return 'An internal error occurred'
|
|
363
378
|
}
|
|
364
379
|
|
package/src/http/HttpContext.ts
CHANGED
|
@@ -33,7 +33,14 @@ export interface AuthState {
|
|
|
33
33
|
id: string
|
|
34
34
|
email?: string
|
|
35
35
|
roles?: string[]
|
|
36
|
+
/** The user's own permissions — never widened by a token's abilities. */
|
|
36
37
|
permissions?: string[]
|
|
38
|
+
/**
|
|
39
|
+
* Set by a token guard (AdonisJS `user.currentAccessToken`): the token this
|
|
40
|
+
* request authenticated with. Its abilities restrict the user's
|
|
41
|
+
* permissions on every `.permission()` gate; they never add to them.
|
|
42
|
+
*/
|
|
43
|
+
currentAccessToken?: AccessTokenAbilities
|
|
37
44
|
[key: string]: unknown
|
|
38
45
|
}
|
|
39
46
|
roles?: string[]
|
|
@@ -65,6 +72,11 @@ export interface AuthState {
|
|
|
65
72
|
readonly authenticatedViaGuard?: string
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
/** What a permission gate needs from an access token: whether it allows an ability. */
|
|
76
|
+
export interface AccessTokenAbilities {
|
|
77
|
+
allows(ability: string): boolean
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
/**
|
|
69
81
|
* Per-request authorization entry point — populated by an authorization
|
|
70
82
|
* middleware (e.g. `@c9up/warden`'s Bouncer initializer). A structural
|
|
@@ -440,8 +452,9 @@ export class HttpContext extends Macroable {
|
|
|
440
452
|
this.response.setRedirectFactory(
|
|
441
453
|
() =>
|
|
442
454
|
new RedirectBuilder(this.response, {
|
|
443
|
-
requestUrl: this.request.url(),
|
|
455
|
+
requestUrl: this.request.url(true),
|
|
444
456
|
requestReferer: this.request.header('referer'),
|
|
457
|
+
requestAuthority: this.request.authority() ?? undefined,
|
|
445
458
|
routeUrlResolver: this.#routeUrlResolver,
|
|
446
459
|
}),
|
|
447
460
|
)
|
|
@@ -13,30 +13,36 @@ import type { Response } from './Response.js'
|
|
|
13
13
|
|
|
14
14
|
export type RouteUrlResolver = (name: string, params?: Record<string, string>) => string
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* AdonisJS `isValidRedirectUrl`: a relative path, or an absolute URL whose host
|
|
18
|
+
* is the request authority or an allowed host.
|
|
19
|
+
*/
|
|
20
|
+
function isTrustedRedirect(
|
|
21
|
+
url: string,
|
|
22
|
+
authority: string | undefined,
|
|
23
|
+
allowedHosts: readonly string[],
|
|
20
24
|
): boolean {
|
|
21
25
|
// A relative path is trusted only when it starts with a single "/" and has no
|
|
22
26
|
// backslash: browsers normalise "\" to "/", so "/\evil.com" would become the
|
|
23
27
|
// protocol-relative "//evil.com" that a lone `!startsWith("//")` check misses.
|
|
24
|
-
if (
|
|
25
|
-
let
|
|
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
|
|
34
|
-
if (!requestUrl) return false
|
|
28
|
+
if (url.startsWith('/') && !url.startsWith('//') && !url.includes('\\')) return true
|
|
29
|
+
let parsed: URL
|
|
35
30
|
try {
|
|
36
|
-
|
|
31
|
+
parsed = new URL(url)
|
|
37
32
|
} catch {
|
|
38
33
|
return false
|
|
39
34
|
}
|
|
35
|
+
if (allowedHosts.includes(parsed.host)) return true
|
|
36
|
+
// Upstream accepts any absolute URL when it knows no host to compare with;
|
|
37
|
+
// refusing it is the safer reading of an attacker-controlled header.
|
|
38
|
+
return authority !== undefined && parsed.host === authority
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The query string of a path or URL, without the "?". */
|
|
42
|
+
function searchOf(url: string | undefined): string {
|
|
43
|
+
if (url === undefined) return ''
|
|
44
|
+
const index = url.indexOf('?')
|
|
45
|
+
return index === -1 ? '' : url.slice(index + 1)
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/** The slice of the session the intended-URL methods use. */
|
|
@@ -60,6 +66,7 @@ export class RedirectBuilder {
|
|
|
60
66
|
#forwardQs = false
|
|
61
67
|
#requestUrl?: string
|
|
62
68
|
#requestReferer?: string
|
|
69
|
+
#requestAuthority?: string
|
|
63
70
|
#routeUrlResolver?: RouteUrlResolver
|
|
64
71
|
#session?: IntendedUrlStore
|
|
65
72
|
#isAjax = false
|
|
@@ -68,8 +75,14 @@ export class RedirectBuilder {
|
|
|
68
75
|
constructor(
|
|
69
76
|
response: Response,
|
|
70
77
|
options?: {
|
|
78
|
+
/** The request path with its query string. */
|
|
71
79
|
requestUrl?: string
|
|
72
80
|
requestReferer?: string
|
|
81
|
+
/**
|
|
82
|
+
* The request authority (`:authority` or `Host`, never X-Forwarded-Host)
|
|
83
|
+
* an absolute redirect target must match.
|
|
84
|
+
*/
|
|
85
|
+
requestAuthority?: string
|
|
73
86
|
routeUrlResolver?: RouteUrlResolver
|
|
74
87
|
/** The request session, for the intended-URL methods. */
|
|
75
88
|
session?: IntendedUrlStore
|
|
@@ -82,6 +95,7 @@ export class RedirectBuilder {
|
|
|
82
95
|
this.#response = response
|
|
83
96
|
this.#requestUrl = options?.requestUrl
|
|
84
97
|
this.#requestReferer = options?.requestReferer
|
|
98
|
+
this.#requestAuthority = options?.requestAuthority
|
|
85
99
|
this.#routeUrlResolver = options?.routeUrlResolver
|
|
86
100
|
this.#session = options?.session
|
|
87
101
|
this.#isAjax = options?.isAjax ?? false
|
|
@@ -135,13 +149,9 @@ export class RedirectBuilder {
|
|
|
135
149
|
|
|
136
150
|
#safeIntended<T extends string | null>(intended: string | null, fallback: T): string | T {
|
|
137
151
|
if (!intended) return fallback
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
// A protocol-relative `//evil.com` is absolute to a browser even though it
|
|
142
|
-
// looks relative — reject it whether or not a request URL is known.
|
|
143
|
-
if (intended.startsWith('//')) return fallback
|
|
144
|
-
return intended
|
|
152
|
+
return isTrustedRedirect(intended, this.#requestAuthority, this.allowedHosts)
|
|
153
|
+
? intended
|
|
154
|
+
: fallback
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
/** Set redirect status code. */
|
|
@@ -191,7 +201,12 @@ export class RedirectBuilder {
|
|
|
191
201
|
|
|
192
202
|
/** Redirect to an absolute or relative path. */
|
|
193
203
|
toPath(path: string): void {
|
|
194
|
-
|
|
204
|
+
this.#send(path, searchOf(this.#requestUrl))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Redirect with `withQs()` forwarding `forwardedSearch` (a query string without "?"). */
|
|
208
|
+
#send(path: string, forwardedSearch: string): void {
|
|
209
|
+
const url = this.#appendQs(path, forwardedSearch)
|
|
195
210
|
this.#response.status(this.#status)
|
|
196
211
|
this.#response.header('location', url)
|
|
197
212
|
this.#response.setBody('')
|
|
@@ -207,12 +222,13 @@ export class RedirectBuilder {
|
|
|
207
222
|
}
|
|
208
223
|
|
|
209
224
|
/**
|
|
210
|
-
* Redirect back to the previous page (
|
|
211
|
-
*
|
|
212
|
-
*
|
|
225
|
+
* Redirect back to the previous page (AdonisJS `back`): the path of
|
|
226
|
+
* {@link getPreviousUrl}, so the redirect never leaves the site; `withQs()`
|
|
227
|
+
* forwards the previous page's query string, not the current request's.
|
|
213
228
|
*/
|
|
214
229
|
back(fallback = '/'): void {
|
|
215
|
-
|
|
230
|
+
const previous = new URL(this.getPreviousUrl(fallback), 'http://localhost')
|
|
231
|
+
this.#send(previous.pathname, previous.search.slice(1))
|
|
216
232
|
}
|
|
217
233
|
|
|
218
234
|
/**
|
|
@@ -225,7 +241,7 @@ export class RedirectBuilder {
|
|
|
225
241
|
*/
|
|
226
242
|
getPreviousUrl(fallback = '/'): string {
|
|
227
243
|
const referer = this.#requestReferer
|
|
228
|
-
if (referer &&
|
|
244
|
+
if (referer && isTrustedRedirect(referer, this.#requestAuthority, this.allowedHosts)) {
|
|
229
245
|
return referer
|
|
230
246
|
}
|
|
231
247
|
return fallback
|
|
@@ -236,15 +252,10 @@ export class RedirectBuilder {
|
|
|
236
252
|
* uses, so `withQs()` plus `withQs('page', 2)` overrides an inbound `page`
|
|
237
253
|
* rather than being overridden by it.
|
|
238
254
|
*/
|
|
239
|
-
#appendQs(path: string): string {
|
|
255
|
+
#appendQs(path: string, forwardedSearch: string): string {
|
|
240
256
|
const params = new URLSearchParams()
|
|
241
|
-
if (this.#forwardQs
|
|
242
|
-
const
|
|
243
|
-
if (qsIdx !== -1) {
|
|
244
|
-
for (const [key, value] of new URLSearchParams(this.#requestUrl.slice(qsIdx + 1))) {
|
|
245
|
-
params.append(key, value)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
257
|
+
if (this.#forwardQs) {
|
|
258
|
+
for (const [key, value] of new URLSearchParams(forwardedSearch)) params.append(key, value)
|
|
248
259
|
}
|
|
249
260
|
for (const [key, value] of Object.entries(this.#qs)) {
|
|
250
261
|
if (value === undefined || value === null) continue
|