@stacksjs/defaults 0.70.278 → 0.70.280

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.
@@ -97,6 +97,28 @@ export function generateCsrfToken(): string {
97
97
  * - 2-hour `Max-Age` — short enough to limit replay if leaked, long
98
98
  * enough that idle tabs don't constantly re-fetch tokens
99
99
  */
100
+ /**
101
+ * Whether a response is already handing the browser a CSRF token.
102
+ *
103
+ * `getSetCookie` is the only way to read multiple `Set-Cookie` headers back;
104
+ * `get` joins them into one string, which is not something a cookie value can
105
+ * be parsed out of reliably. Older runtimes without it fall back to the joined
106
+ * form, where a substring match is still correct for the question being asked.
107
+ */
108
+ function responseAlreadySeeds(response: Response): boolean {
109
+ const headers = response.headers as Headers & { getSetCookie?: () => string[] }
110
+ const cookies = typeof headers.getSetCookie === 'function'
111
+ ? headers.getSetCookie()
112
+ : [headers.get('set-cookie') || '']
113
+
114
+ return cookies.some(cookie =>
115
+ cookie.startsWith(`${CSRF_COOKIE_NAME}=`)
116
+ || cookie.startsWith('csrf-token=')
117
+ || cookie.includes(`, ${CSRF_COOKIE_NAME}=`)
118
+ || cookie.includes(', csrf-token='),
119
+ )
120
+ }
121
+
100
122
  export function seedCsrfCookieIfMissing(req: Request, response: Response, minted?: string): Response {
101
123
  const cookieHeader = req.headers.get('cookie') || ''
102
124
 
@@ -109,6 +131,15 @@ export function seedCsrfCookieIfMissing(req: Request, response: Response, minted
109
131
  return response
110
132
  }
111
133
 
134
+ // A token already on its way to the browser counts as present, exactly like
135
+ // one in the request. Something upstream can mint before the render - the
136
+ // stx dev server does, so a page's forms carry a token on a first visit -
137
+ // and appending a second here would leave the browser storing the last
138
+ // Set-Cookie while the page embedded the first. Two tokens fail the same way
139
+ // no token does, and are far harder to see.
140
+ if (responseAlreadySeeds(response))
141
+ return response
142
+
112
143
  const token = minted || generateCsrfToken()
113
144
  const isSecure = req.url.startsWith('https://')
114
145
  const cookie = [
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.70.278",
5
+ "version": "0.70.280",
6
6
  "description": "A modern Stacks development environment.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.70.278",
5
+ "version": "0.70.280",
6
6
  "description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
7
7
  "author": "Chris Breuer",
8
8
  "license": "MIT",