@stacksjs/bun-router 0.0.15 → 0.0.16
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/{chunk-j0e7z7hd.js → chunk-0145g1me.js} +6 -0
- package/dist/cli.js +12 -11
- package/dist/index.js +63 -26
- package/package.json +1 -1
- package/src/middleware/csrf.ts +10 -0
- package/src/utils.ts +98 -33
|
@@ -7043,6 +7043,7 @@ class Logger3 {
|
|
|
7043
7043
|
}
|
|
7044
7044
|
}
|
|
7045
7045
|
var logger3 = new Logger3("stacks");
|
|
7046
|
+
|
|
7046
7047
|
class BunfigError2 extends Error {
|
|
7047
7048
|
timestamp;
|
|
7048
7049
|
context;
|
|
@@ -9233,6 +9234,11 @@ class Csrf {
|
|
|
9233
9234
|
headers: newHeaders
|
|
9234
9235
|
});
|
|
9235
9236
|
}
|
|
9237
|
+
const authHeader = req.headers.get("authorization") ?? req.headers.get("Authorization");
|
|
9238
|
+
if (authHeader && /^Bearer\s+\S/i.test(authHeader)) {
|
|
9239
|
+
const response2 = await next();
|
|
9240
|
+
return response2 || new Response("Not Found", { status: 404 });
|
|
9241
|
+
}
|
|
9236
9242
|
const token = req.headers.get("X-CSRF-TOKEN") || req.jsonBody?.csrf_token || req.jsonBody?._token || null;
|
|
9237
9243
|
const cookies = this.parseCookies(req);
|
|
9238
9244
|
const cookieToken = cookies[csrfConfig?.cookie?.name || "csrf-token"];
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
middleware
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-0145g1me.js";
|
|
5
5
|
|
|
6
6
|
// src/cli/index.ts
|
|
7
7
|
import process15 from "process";
|
|
@@ -572,11 +572,9 @@ class Command {
|
|
|
572
572
|
isMatched(name) {
|
|
573
573
|
if (this.aliasNames.includes(name))
|
|
574
574
|
return true;
|
|
575
|
-
if (this.
|
|
576
|
-
return
|
|
577
|
-
|
|
578
|
-
return true;
|
|
579
|
-
return false;
|
|
575
|
+
if (this.namespace)
|
|
576
|
+
return `${this.namespace}:${this.name}` === name;
|
|
577
|
+
return this.name === name;
|
|
580
578
|
}
|
|
581
579
|
get isDefaultCommand() {
|
|
582
580
|
return this.name === "" || this.aliasNames.includes("!");
|
|
@@ -584,6 +582,9 @@ class Command {
|
|
|
584
582
|
get isGlobalCommand() {
|
|
585
583
|
return this instanceof GlobalCommand;
|
|
586
584
|
}
|
|
585
|
+
get displayName() {
|
|
586
|
+
return this.namespace ? `${this.namespace}:${this.name}` : this.name;
|
|
587
|
+
}
|
|
587
588
|
hasOption(name) {
|
|
588
589
|
name = name.split(".")[0];
|
|
589
590
|
return !!this.options.find((option) => {
|
|
@@ -649,7 +650,7 @@ class Command {
|
|
|
649
650
|
});
|
|
650
651
|
sections.push({
|
|
651
652
|
title: `For more info, run any command with the \`--help\` flag`,
|
|
652
|
-
body: commands.map((command) => ` $ ${name}${command.
|
|
653
|
+
body: commands.map((command) => ` $ ${name}${command.displayName === "" ? "" : ` ${command.displayName}`} --help`).join(`
|
|
653
654
|
`)
|
|
654
655
|
});
|
|
655
656
|
}
|
|
@@ -1004,8 +1005,8 @@ Received ${signal}, cleaning up...`);
|
|
|
1004
1005
|
if (this.enableDidYouMean) {
|
|
1005
1006
|
const allCommandNames = [];
|
|
1006
1007
|
for (const command of this.commands) {
|
|
1007
|
-
if (command.
|
|
1008
|
-
allCommandNames.push(command.
|
|
1008
|
+
if (command.displayName) {
|
|
1009
|
+
allCommandNames.push(command.displayName);
|
|
1009
1010
|
}
|
|
1010
1011
|
if (command.aliasNames) {
|
|
1011
1012
|
allCommandNames.push(...command.aliasNames);
|
|
@@ -1961,7 +1962,7 @@ class GroupMultiSelectPrompt extends Prompt {
|
|
|
1961
1962
|
this.value = [];
|
|
1962
1963
|
}
|
|
1963
1964
|
if (item.group === true) {
|
|
1964
|
-
const group = item.value;
|
|
1965
|
+
const group = String(item.value);
|
|
1965
1966
|
const groupedItems = this.getGroupItems(group);
|
|
1966
1967
|
if (this.isGroupSelected(group)) {
|
|
1967
1968
|
this.value = this.value.filter((v) => groupedItems.findIndex((i) => i.value === v) === -1);
|
|
@@ -2044,7 +2045,7 @@ function getPrefix() {
|
|
|
2044
2045
|
}
|
|
2045
2046
|
var prefix = getPrefix();
|
|
2046
2047
|
// package.json
|
|
2047
|
-
var version = "0.0.
|
|
2048
|
+
var version = "0.0.16";
|
|
2048
2049
|
|
|
2049
2050
|
// src/cli/middleware.ts
|
|
2050
2051
|
import fs2 from "fs/promises";
|
package/dist/index.js
CHANGED
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
signRequest,
|
|
67
67
|
singleFileUpload,
|
|
68
68
|
verifySignature
|
|
69
|
-
} from "./chunk-
|
|
69
|
+
} from "./chunk-0145g1me.js";
|
|
70
70
|
|
|
71
71
|
// src/auth.ts
|
|
72
72
|
function base64UrlEncode(str) {
|
|
@@ -6199,46 +6199,83 @@ function createPathRegex(pattern) {
|
|
|
6199
6199
|
const regexPattern = pattern.replace(/\{([^}]+)\}/g, "([^/]+)");
|
|
6200
6200
|
return new RegExp(`^${regexPattern}$`);
|
|
6201
6201
|
}
|
|
6202
|
-
|
|
6202
|
+
var PARAM_SEGMENT_RE = /\{([^}:]+)(?::[^}]+)?(\?)?\}/;
|
|
6203
|
+
var OPTIONAL_SEGMENT_RE = /\{[^}]+\?\}/;
|
|
6204
|
+
var compiledPatternCache = new Map;
|
|
6205
|
+
var constraintRegexCache = new Map;
|
|
6206
|
+
function compilePattern(pattern) {
|
|
6207
|
+
const cached = compiledPatternCache.get(pattern);
|
|
6208
|
+
if (cached)
|
|
6209
|
+
return cached;
|
|
6210
|
+
const isWildcard = pattern.endsWith("/*");
|
|
6203
6211
|
const normalizedPattern = normalizePath(pattern);
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
const
|
|
6207
|
-
if (
|
|
6208
|
-
|
|
6209
|
-
|
|
6212
|
+
let optionalCount = 0;
|
|
6213
|
+
const segments = normalizedPattern.split("/").filter(Boolean).map((seg) => {
|
|
6214
|
+
const isOptional = OPTIONAL_SEGMENT_RE.test(seg);
|
|
6215
|
+
if (isOptional)
|
|
6216
|
+
optionalCount++;
|
|
6217
|
+
const paramMatch = seg.match(PARAM_SEGMENT_RE);
|
|
6218
|
+
return {
|
|
6219
|
+
isParam: !!paramMatch,
|
|
6220
|
+
paramName: paramMatch ? paramMatch[1].replace("?", "") : "",
|
|
6221
|
+
isOptional,
|
|
6222
|
+
literal: seg
|
|
6223
|
+
};
|
|
6224
|
+
});
|
|
6225
|
+
const compiled = {
|
|
6226
|
+
isWildcard,
|
|
6227
|
+
wildcardBase: isWildcard ? pattern.slice(0, -2) : "",
|
|
6228
|
+
segments,
|
|
6229
|
+
optionalCount
|
|
6230
|
+
};
|
|
6231
|
+
compiledPatternCache.set(pattern, compiled);
|
|
6232
|
+
return compiled;
|
|
6233
|
+
}
|
|
6234
|
+
function getConstraintRegex(constraint) {
|
|
6235
|
+
const cached = constraintRegexCache.get(constraint);
|
|
6236
|
+
if (cached !== undefined)
|
|
6237
|
+
return cached;
|
|
6238
|
+
let regex;
|
|
6239
|
+
try {
|
|
6240
|
+
regex = new RegExp(`^${constraint}$`);
|
|
6241
|
+
} catch {
|
|
6242
|
+
regex = null;
|
|
6243
|
+
}
|
|
6244
|
+
constraintRegexCache.set(constraint, regex);
|
|
6245
|
+
return regex;
|
|
6246
|
+
}
|
|
6247
|
+
function matchPath(pattern, path, params, constraints) {
|
|
6248
|
+
const compiled = compilePattern(pattern);
|
|
6249
|
+
if (compiled.isWildcard && path.startsWith(compiled.wildcardBase)) {
|
|
6250
|
+
return true;
|
|
6210
6251
|
}
|
|
6211
|
-
const patternSegments =
|
|
6212
|
-
const pathSegments =
|
|
6213
|
-
|
|
6214
|
-
if (pathSegments.length < patternSegments.length - optionalParamCount || pathSegments.length > patternSegments.length) {
|
|
6252
|
+
const patternSegments = compiled.segments;
|
|
6253
|
+
const pathSegments = normalizePath(path).split("/").filter(Boolean);
|
|
6254
|
+
if (pathSegments.length < patternSegments.length - compiled.optionalCount || pathSegments.length > patternSegments.length) {
|
|
6215
6255
|
return false;
|
|
6216
6256
|
}
|
|
6217
6257
|
for (let i = 0;i < patternSegments.length; i++) {
|
|
6218
|
-
const
|
|
6258
|
+
const seg = patternSegments[i];
|
|
6219
6259
|
const pathSegment = pathSegments[i];
|
|
6220
6260
|
if (pathSegment === undefined) {
|
|
6221
|
-
if (
|
|
6261
|
+
if (seg.isOptional) {
|
|
6222
6262
|
continue;
|
|
6223
|
-
} else {
|
|
6224
|
-
return false;
|
|
6225
6263
|
}
|
|
6264
|
+
return false;
|
|
6226
6265
|
}
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
try {
|
|
6233
|
-
const regex = new RegExp(`^${constraints[paramName]}$`);
|
|
6266
|
+
if (seg.isParam) {
|
|
6267
|
+
params[seg.paramName] = pathSegment;
|
|
6268
|
+
if (constraints && constraints[seg.paramName]) {
|
|
6269
|
+
const regex = getConstraintRegex(constraints[seg.paramName]);
|
|
6270
|
+
if (regex) {
|
|
6234
6271
|
if (!regex.test(pathSegment)) {
|
|
6235
6272
|
return false;
|
|
6236
6273
|
}
|
|
6237
|
-
}
|
|
6238
|
-
console.warn(`Invalid constraint regex for parameter ${paramName}: ${constraints[paramName]}`);
|
|
6274
|
+
} else {
|
|
6275
|
+
console.warn(`Invalid constraint regex for parameter ${seg.paramName}: ${constraints[seg.paramName]}`);
|
|
6239
6276
|
}
|
|
6240
6277
|
}
|
|
6241
|
-
} else if (
|
|
6278
|
+
} else if (seg.literal !== pathSegment) {
|
|
6242
6279
|
return false;
|
|
6243
6280
|
}
|
|
6244
6281
|
}
|
package/package.json
CHANGED
package/src/middleware/csrf.ts
CHANGED
|
@@ -60,6 +60,16 @@ export default class Csrf {
|
|
|
60
60
|
})
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// Bearer-authed requests can't be CSRF'd — the token doesn't
|
|
64
|
+
// ride on cookies, so a hostile origin can't trick a browser into
|
|
65
|
+
// sending it. Mirrors Laravel Sanctum / Django REST framework /
|
|
66
|
+
// express-csurf semantics. Filed against stacksjs/stacks#1922.
|
|
67
|
+
const authHeader = req.headers.get('authorization') ?? req.headers.get('Authorization')
|
|
68
|
+
if (authHeader && /^Bearer\s+\S/i.test(authHeader)) {
|
|
69
|
+
const response = await next()
|
|
70
|
+
return response || new Response('Not Found', { status: 404 })
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
// For unsafe methods, verify the CSRF token
|
|
64
74
|
// Check for token in headers or in the request body
|
|
65
75
|
const token = req.headers.get('X-CSRF-TOKEN')
|
package/src/utils.ts
CHANGED
|
@@ -83,6 +83,83 @@ export function createPathRegex(pattern: string): RegExp {
|
|
|
83
83
|
return new RegExp(`^${regexPattern}$`)
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Per-pattern matching data. A route's pattern never changes, so the
|
|
88
|
+
* segment split, param-name extraction, and optional-param count are derived
|
|
89
|
+
* once and cached — turning `matchPath` from "regex + split on every request"
|
|
90
|
+
* into a tight array walk. Only the incoming `path` is processed per call.
|
|
91
|
+
*/
|
|
92
|
+
interface CompiledSegment {
|
|
93
|
+
isParam: boolean
|
|
94
|
+
paramName: string
|
|
95
|
+
isOptional: boolean
|
|
96
|
+
literal: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface CompiledPattern {
|
|
100
|
+
isWildcard: boolean
|
|
101
|
+
wildcardBase: string
|
|
102
|
+
segments: CompiledSegment[]
|
|
103
|
+
optionalCount: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const PARAM_SEGMENT_RE = /\{([^}:]+)(?::[^}]+)?(\?)?\}/
|
|
107
|
+
const OPTIONAL_SEGMENT_RE = /\{[^}]+\?\}/
|
|
108
|
+
|
|
109
|
+
const compiledPatternCache = new Map<string, CompiledPattern>()
|
|
110
|
+
// Cached compiled constraint regexes; `null` marks an invalid pattern so we
|
|
111
|
+
// don't re-attempt construction on every request.
|
|
112
|
+
const constraintRegexCache = new Map<string, RegExp | null>()
|
|
113
|
+
|
|
114
|
+
function compilePattern(pattern: string): CompiledPattern {
|
|
115
|
+
const cached = compiledPatternCache.get(pattern)
|
|
116
|
+
if (cached)
|
|
117
|
+
return cached
|
|
118
|
+
|
|
119
|
+
const isWildcard = pattern.endsWith('/*')
|
|
120
|
+
const normalizedPattern = normalizePath(pattern)
|
|
121
|
+
let optionalCount = 0
|
|
122
|
+
const segments: CompiledSegment[] = normalizedPattern
|
|
123
|
+
.split('/')
|
|
124
|
+
.filter(Boolean)
|
|
125
|
+
.map((seg) => {
|
|
126
|
+
const isOptional = OPTIONAL_SEGMENT_RE.test(seg)
|
|
127
|
+
if (isOptional)
|
|
128
|
+
optionalCount++
|
|
129
|
+
const paramMatch = seg.match(PARAM_SEGMENT_RE)
|
|
130
|
+
return {
|
|
131
|
+
isParam: !!paramMatch,
|
|
132
|
+
paramName: paramMatch ? paramMatch[1].replace('?', '') : '',
|
|
133
|
+
isOptional,
|
|
134
|
+
literal: seg,
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const compiled: CompiledPattern = {
|
|
139
|
+
isWildcard,
|
|
140
|
+
wildcardBase: isWildcard ? pattern.slice(0, -2) : '',
|
|
141
|
+
segments,
|
|
142
|
+
optionalCount,
|
|
143
|
+
}
|
|
144
|
+
compiledPatternCache.set(pattern, compiled)
|
|
145
|
+
return compiled
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function getConstraintRegex(constraint: string): RegExp | null {
|
|
149
|
+
const cached = constraintRegexCache.get(constraint)
|
|
150
|
+
if (cached !== undefined)
|
|
151
|
+
return cached
|
|
152
|
+
let regex: RegExp | null
|
|
153
|
+
try {
|
|
154
|
+
regex = new RegExp(`^${constraint}$`)
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
regex = null
|
|
158
|
+
}
|
|
159
|
+
constraintRegexCache.set(constraint, regex)
|
|
160
|
+
return regex
|
|
161
|
+
}
|
|
162
|
+
|
|
86
163
|
/**
|
|
87
164
|
* Matches a path against a pattern and extracts parameters
|
|
88
165
|
* @param pattern The path pattern (e.g., '/users/{id}')
|
|
@@ -97,71 +174,59 @@ export function matchPath(
|
|
|
97
174
|
params: Record<string, string>,
|
|
98
175
|
constraints?: Record<string, string>,
|
|
99
176
|
): boolean {
|
|
100
|
-
|
|
101
|
-
const normalizedPattern = normalizePath(pattern)
|
|
102
|
-
const normalizedPath = normalizePath(path)
|
|
177
|
+
const compiled = compilePattern(pattern)
|
|
103
178
|
|
|
104
|
-
// Special case for wildcard patterns
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
179
|
+
// Special case for wildcard patterns. Matches the original semantics:
|
|
180
|
+
// a prefix hit short-circuits to true, otherwise we fall through to the
|
|
181
|
+
// per-segment match below.
|
|
182
|
+
if (compiled.isWildcard && path.startsWith(compiled.wildcardBase)) {
|
|
183
|
+
return true
|
|
110
184
|
}
|
|
111
185
|
|
|
112
|
-
|
|
113
|
-
const
|
|
114
|
-
const pathSegments = normalizedPath.split('/').filter(Boolean)
|
|
186
|
+
const patternSegments = compiled.segments
|
|
187
|
+
const pathSegments = normalizePath(path).split('/').filter(Boolean)
|
|
115
188
|
|
|
116
189
|
// Quick check - if segments don't match (accounting for optional params) then no match
|
|
117
190
|
// For mandatory parameters, the pattern and path must have same number of segments
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (pathSegments.length < patternSegments.length - optionalParamCount
|
|
191
|
+
if (pathSegments.length < patternSegments.length - compiled.optionalCount
|
|
121
192
|
|| pathSegments.length > patternSegments.length) {
|
|
122
193
|
return false
|
|
123
194
|
}
|
|
124
195
|
|
|
125
196
|
// Match each segment
|
|
126
197
|
for (let i = 0; i < patternSegments.length; i++) {
|
|
127
|
-
const
|
|
198
|
+
const seg = patternSegments[i]
|
|
128
199
|
const pathSegment = pathSegments[i]
|
|
129
200
|
|
|
130
201
|
// If we've run out of path segments and this is not an optional parameter
|
|
131
202
|
if (pathSegment === undefined) {
|
|
132
|
-
if (
|
|
203
|
+
if (seg.isOptional) {
|
|
133
204
|
// This is an optional parameter and we have no path segment for it
|
|
134
205
|
continue
|
|
135
206
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return false
|
|
139
|
-
}
|
|
207
|
+
// Required parameter or static segment with no matching path segment
|
|
208
|
+
return false
|
|
140
209
|
}
|
|
141
210
|
|
|
142
|
-
|
|
143
|
-
const paramMatch = patternSegment.match(/\{([^}:]+)(?::[^}]+)?(\?)?\}/)
|
|
144
|
-
if (paramMatch) {
|
|
145
|
-
// This is a parameter segment, extract param name
|
|
146
|
-
const paramName = paramMatch[1].replace('?', '')
|
|
211
|
+
if (seg.isParam) {
|
|
147
212
|
// Store the parameter value
|
|
148
|
-
params[paramName] = pathSegment
|
|
213
|
+
params[seg.paramName] = pathSegment
|
|
149
214
|
|
|
150
215
|
// Check constraints if they exist
|
|
151
|
-
if (constraints && constraints[paramName]) {
|
|
152
|
-
|
|
153
|
-
|
|
216
|
+
if (constraints && constraints[seg.paramName]) {
|
|
217
|
+
const regex = getConstraintRegex(constraints[seg.paramName])
|
|
218
|
+
if (regex) {
|
|
154
219
|
if (!regex.test(pathSegment)) {
|
|
155
220
|
return false
|
|
156
221
|
}
|
|
157
222
|
}
|
|
158
|
-
|
|
223
|
+
else {
|
|
159
224
|
// If regex is invalid, treat as no constraint
|
|
160
|
-
console.warn(`Invalid constraint regex for parameter ${paramName}: ${constraints[paramName]}`)
|
|
225
|
+
console.warn(`Invalid constraint regex for parameter ${seg.paramName}: ${constraints[seg.paramName]}`)
|
|
161
226
|
}
|
|
162
227
|
}
|
|
163
228
|
}
|
|
164
|
-
else if (
|
|
229
|
+
else if (seg.literal !== pathSegment) {
|
|
165
230
|
// Static segment doesn't match
|
|
166
231
|
return false
|
|
167
232
|
}
|