@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.
@@ -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-j0e7z7hd.js";
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.name === name)
576
- return true;
577
- if (this.namespace && `${this.namespace}:${this.name}` === name)
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.name === "" ? "" : ` ${command.name}`} --help`).join(`
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.name) {
1008
- allCommandNames.push(command.name);
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.15";
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-j0e7z7hd.js";
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
- function matchPath(pattern, path, params, constraints) {
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
- const normalizedPath = normalizePath(path);
6205
- if (pattern.endsWith("/*")) {
6206
- const basePattern = pattern.slice(0, -2);
6207
- if (path.startsWith(basePattern)) {
6208
- return true;
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 = normalizedPattern.split("/").filter(Boolean);
6212
- const pathSegments = normalizedPath.split("/").filter(Boolean);
6213
- const optionalParamCount = (normalizedPattern.match(/\{[^}]+\?\}/g) || []).length;
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 patternSegment = patternSegments[i];
6258
+ const seg = patternSegments[i];
6219
6259
  const pathSegment = pathSegments[i];
6220
6260
  if (pathSegment === undefined) {
6221
- if (patternSegment.match(/\{[^}]+\?\}/)) {
6261
+ if (seg.isOptional) {
6222
6262
  continue;
6223
- } else {
6224
- return false;
6225
6263
  }
6264
+ return false;
6226
6265
  }
6227
- const paramMatch = patternSegment.match(/\{([^}:]+)(?::[^}]+)?(\?)?\}/);
6228
- if (paramMatch) {
6229
- const paramName = paramMatch[1].replace("?", "");
6230
- params[paramName] = pathSegment;
6231
- if (constraints && constraints[paramName]) {
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
- } catch {
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 (patternSegment !== pathSegment) {
6278
+ } else if (seg.literal !== pathSegment) {
6242
6279
  return false;
6243
6280
  }
6244
6281
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/bun-router",
3
3
  "type": "module",
4
- "version": "0.0.15",
4
+ "version": "0.0.16",
5
5
  "description": "A fast, type-safe router for Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -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
- // Both paths should be normalized
101
- const normalizedPattern = normalizePath(pattern)
102
- const normalizedPath = normalizePath(path)
177
+ const compiled = compilePattern(pattern)
103
178
 
104
- // Special case for wildcard patterns
105
- if (pattern.endsWith('/*')) {
106
- const basePattern = pattern.slice(0, -2)
107
- if (path.startsWith(basePattern)) {
108
- return true
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
- // Handle patterns with parameters
113
- const patternSegments = normalizedPattern.split('/').filter(Boolean)
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
- const optionalParamCount = (normalizedPattern.match(/\{[^}]+\?\}/g) || []).length
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 patternSegment = patternSegments[i]
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 (patternSegment.match(/\{[^}]+\?\}/)) {
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
- else {
137
- // Required parameter or static segment with no matching path segment
138
- return false
139
- }
207
+ // Required parameter or static segment with no matching path segment
208
+ return false
140
209
  }
141
210
 
142
- // Check if this segment is a parameter
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
- try {
153
- const regex = new RegExp(`^${constraints[paramName]}$`)
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
- catch {
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 (patternSegment !== pathSegment) {
229
+ else if (seg.literal !== pathSegment) {
165
230
  // Static segment doesn't match
166
231
  return false
167
232
  }