@sticklightapp/tanstack-vite-config 0.0.8 → 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.
package/define-config.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
@@ -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
|
+
}
|