@sticklightapp/tanstack-vite-config 0.0.7 → 0.0.9

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.
@@ -1 +1 @@
1
- {"version":3,"file":"define-config.d.ts","sourceRoot":"","sources":["../src/define-config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAClE,OAAO,SAAS,MAAM,sBAAsB,CAAC;AAG7C,OAAO,EAAgB,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AACxF,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAGtE,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAA4C,KAAK,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG5G,KAAK,oBAAoB,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7E,KAAK,YAAY,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEjE,MAAM,WAAW,yBAAyB;IACzC,+GAA+G;IAC/G,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC,kCAAkC;IAClC,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,2GAA2G;IAC3G,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,sDAAsD;IACtD,WAAW,CAAC,EAAE,kBAAkB,CAAC;CACjC;AAED,wBAAgB,YAAY,CAAC,OAAO,GAAE,yBAA8B,IACrD,KAAK,SAAS,KAAG,OAAO,CAAC,UAAU,CAAC,CAmBlD"}
1
+ {"version":3,"file":"define-config.d.ts","sourceRoot":"","sources":["../src/define-config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAClE,OAAO,SAAS,MAAM,sBAAsB,CAAC;AAG7C,OAAO,EAAgB,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AACxF,OAAO,EAAyB,KAAK,YAAY,EAAE,MAAM,YAAY,CAAC;AAItE,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACvF,OAAO,EAA4C,KAAK,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG5G,KAAK,oBAAoB,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7E,KAAK,YAAY,GAAG,WAAW,CAAC,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAEjE,MAAM,WAAW,yBAAyB;IACzC,+GAA+G;IAC/G,IAAI,CAAC,EAAE,iBAAiB,CAAC;IACzB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,8EAA8E;IAC9E,aAAa,CAAC,EAAE,oBAAoB,CAAC;IACrC,kCAAkC;IAClC,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,2GAA2G;IAC3G,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,sDAAsD;IACtD,WAAW,CAAC,EAAE,kBAAkB,CAAC;CACjC;AAED,wBAAgB,YAAY,CAAC,OAAO,GAAE,yBAA8B,IACrD,KAAK,SAAS,KAAG,OAAO,CAAC,UAAU,CAAC,CAmBlD"}
package/define-config.js CHANGED
@@ -8,6 +8,7 @@ import { createLogger } from 'vite';
8
8
  import { buildOnlyNitroPlugins } from './nitro.js';
9
9
  import { DEV_SCRIPTS_INJECTOR, injectDevToolsPlugin } from './plugins/dev-tools.js';
10
10
  import { structuredLoggerPlugin } from './plugins/structured-logger.js';
11
+ import { svgSecurityHeadersPlugin } from './plugins/svg-security-headers.js';
11
12
  import { tailwindCdnPlugin } from './plugins/tailwind-cdn.js';
12
13
  import { resolveViteConfig, SANDBOX_WATCH_IGNORES } from './resolve-vite-config.js';
13
14
  import { captureConsoleErrors, emit } from './sandbox-log.js';
@@ -71,6 +72,7 @@ async function createPlugins(env, options) {
71
72
  viteReact(options.react),
72
73
  injectDevToolsPlugin(),
73
74
  structuredLoggerPlugin(),
75
+ svgSecurityHeadersPlugin(),
74
76
  ...(options.plugins ?? []),
75
77
  ];
76
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sticklightapp/tanstack-vite-config",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Vite configuration for Sticklight TanStack Start apps",
5
5
  "keywords": [
6
6
  "vite",
@@ -0,0 +1,11 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Adds SVG security headers to all SVG responses.
4
+ *
5
+ * SVGs can contain scripts that execute when embedded via object/embed/iframe.
6
+ * These headers provide defense-in-depth:
7
+ * - Content-Security-Policy: sandbox prevents script execution
8
+ * - X-Content-Type-Options: nosniff prevents MIME-sniffing attacks
9
+ */
10
+ export declare function svgSecurityHeadersPlugin(): Plugin;
11
+ //# sourceMappingURL=svg-security-headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svg-security-headers.d.ts","sourceRoot":"","sources":["../../src/plugins/svg-security-headers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAInC;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAUjD"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Adds SVG security headers to all SVG responses.
3
+ *
4
+ * SVGs can contain scripts that execute when embedded via object/embed/iframe.
5
+ * These headers provide defense-in-depth:
6
+ * - Content-Security-Policy: sandbox prevents script execution
7
+ * - X-Content-Type-Options: nosniff prevents MIME-sniffing attacks
8
+ */
9
+ export function svgSecurityHeadersPlugin() {
10
+ return {
11
+ name: 'svg-security-headers',
12
+ configureServer(server) {
13
+ applySvgSecurityMiddleware(server.middlewares);
14
+ },
15
+ configurePreviewServer(server) {
16
+ applySvgSecurityMiddleware(server.middlewares);
17
+ },
18
+ };
19
+ }
20
+ function applySvgSecurityMiddleware(middlewares) {
21
+ middlewares.use((req, res, next) => {
22
+ const originalWriteHead = res.writeHead.bind(res);
23
+ // Override writeHead to intercept and modify headers for SVG responses
24
+ res.writeHead = function (statusCode, ...args) {
25
+ // Parse arguments: (statusCode), (statusCode, headers), or (statusCode, statusMessage, headers)
26
+ let statusMessage;
27
+ let headers;
28
+ if (typeof args[0] === 'string') {
29
+ // (statusCode, statusMessage, headers)
30
+ statusMessage = args[0];
31
+ headers = args[1];
32
+ }
33
+ else if (typeof args[0] === 'object') {
34
+ // (statusCode, headers)
35
+ headers = args[0];
36
+ }
37
+ // Detect if this is an SVG response
38
+ const isSvg = isSvgResponse(req, res, headers);
39
+ if (isSvg) {
40
+ // Clone headers or create new object to avoid mutating caller's object
41
+ const modifiedHeaders = headers ? { ...headers } : {};
42
+ // Remove any existing CSP or X-Content-Type-Options headers (case-insensitive)
43
+ for (const key of Object.keys(modifiedHeaders)) {
44
+ const lowerKey = key.toLowerCase();
45
+ if (lowerKey === 'content-security-policy' || lowerKey === 'x-content-type-options') {
46
+ delete modifiedHeaders[key];
47
+ }
48
+ }
49
+ // Apply SVG security headers
50
+ modifiedHeaders['Content-Security-Policy'] = "sandbox; script-src 'none'";
51
+ modifiedHeaders['X-Content-Type-Options'] = 'nosniff';
52
+ // Call original with modified headers
53
+ if (statusMessage) {
54
+ return originalWriteHead(statusCode, statusMessage, modifiedHeaders);
55
+ }
56
+ else {
57
+ return originalWriteHead(statusCode, modifiedHeaders);
58
+ }
59
+ }
60
+ // Not an SVG, pass through unchanged
61
+ if (statusMessage) {
62
+ return originalWriteHead(statusCode, statusMessage, headers);
63
+ }
64
+ else if (headers) {
65
+ return originalWriteHead(statusCode, headers);
66
+ }
67
+ else {
68
+ return originalWriteHead(statusCode);
69
+ }
70
+ };
71
+ next();
72
+ });
73
+ }
74
+ /**
75
+ * Check if the request/response is for an SVG file.
76
+ * Checks both content-type header and URL path extension.
77
+ */
78
+ function isSvgResponse(req, res, headersArg) {
79
+ // Check content-type from multiple sources (case-insensitive)
80
+ const contentType = getContentType(res, headersArg);
81
+ if (contentType) {
82
+ const mimeType = contentType.split(';')[0]?.trim().toLowerCase();
83
+ if (mimeType === 'image/svg+xml') {
84
+ return true;
85
+ }
86
+ }
87
+ // Check URL path extension
88
+ if (req.url) {
89
+ return isSvgPath(req.url);
90
+ }
91
+ return false;
92
+ }
93
+ /**
94
+ * Extract content-type header case-insensitively from headers argument or res.getHeader.
95
+ */
96
+ function getContentType(res, headersArg) {
97
+ // Check headers argument (object or array format)
98
+ if (headersArg) {
99
+ if (Array.isArray(headersArg)) {
100
+ // Array of [name, value] pairs
101
+ for (const [name, value] of headersArg) {
102
+ if (name.toLowerCase() === 'content-type') {
103
+ return Array.isArray(value) ? value[0] : value;
104
+ }
105
+ }
106
+ }
107
+ else {
108
+ // Object format
109
+ for (const key of Object.keys(headersArg)) {
110
+ if (key.toLowerCase() === 'content-type') {
111
+ const value = headersArg[key];
112
+ return Array.isArray(value) ? value[0] : value;
113
+ }
114
+ }
115
+ }
116
+ }
117
+ // Fallback to res.getHeader
118
+ const header = res.getHeader('content-type');
119
+ if (header) {
120
+ return Array.isArray(header) ? header[0] : String(header);
121
+ }
122
+ return undefined;
123
+ }
124
+ /**
125
+ * Check if a URL path ends with .svg extension.
126
+ * Handles query strings, fragments, percent-encoding, and case-insensitivity.
127
+ */
128
+ function isSvgPath(url) {
129
+ try {
130
+ // Parse URL to extract pathname, stripping query and hash
131
+ // Using URL constructor with a dummy base for relative URLs
132
+ const parsed = new URL(url, 'http://localhost');
133
+ const pathname = parsed.pathname;
134
+ // Decode percent-encoding (e.g., %2Esvg -> .svg)
135
+ // decodeURIComponent can throw on invalid encoding
136
+ const decoded = decodeURIComponent(pathname);
137
+ // Check if pathname ends with .svg (case-insensitive)
138
+ return decoded.toLowerCase().endsWith('.svg');
139
+ }
140
+ catch {
141
+ // If URL parsing or decoding fails, fall back to simple check
142
+ // Strip query and hash manually
143
+ const pathOnly = url.split('?')[0]?.split('#')[0] ?? '';
144
+ return pathOnly.toLowerCase().endsWith('.svg');
145
+ }
146
+ }