wip-devsec

This commit is contained in:
Doxa Maps 2026-09-26 01:26:01 +00:00
parent 7d4da13721
commit d01e19c90c
3 changed files with 26 additions and 11 deletions

View file

@ -41,14 +41,15 @@ export function resolveDevAsset(rel, root = REPO_ROOT) {
const isFile = (p) => existsSync(p) && statSync(p).isFile()
// The built copy lives INSIDE the output folder (public/<tree>/assets) — public/ holds nothing else
// since BUILD-OUTPUT-IS-ONE-FOLDER. The data/ fallback below keeps a fresh clone working unbuilt.
const shipped = resolve(root, 'public', TREE_NAME, 'assets', rel)
if (isFile(shipped)) return shipped
// data/<rel> is the TRACKED source of truth public/assets mirrors (see .gitignore): generate-tree
// writes the mirror, so on a fresh clone — which is what DX receives — public/assets holds almost
// nothing and every table, boundary and people-groups file 404'd under the dev server until the
// first full build. Measured 2026-09-25 across the studio: 13 plugins mounted with empty data.
// data/<rel> FIRST: it is the tracked source of truth that the built copy mirrors. Serving the built copy
// first meant a data/ edit showed OLD data under the dev server until someone rebuilt (73 files have
// both; found by the multiplayer dev-server spike, 2026-09-26). A dev server shows the source.
const source = resolve(root, 'data', rel)
if (isFile(source)) return source
// The built copy (public/<tree>/assets) still answers for what exists only there: files generate-tree
// derives rather than copies.
const shipped = resolve(root, 'public', TREE_NAME, 'assets', rel)
if (isFile(shipped)) return shipped
const profiles = resolve(root, 'plugin-designs')
for (const name of readdirSync(profiles)) {
if (name.startsWith('_') || name.startsWith('.')) continue
@ -239,6 +240,12 @@ export function autoSpaDevPlugin(token = '') {
configureServer(server) {
server.middlewares.use((req, res, next) => {
const rawPath = (req.url || '').split('?')[0]
// NEVER THE REPOSITORY ITSELF. Vite serves the project root, so a reachable dev server answered
// /.git/… with 200 — the whole history, and any credential in the remote URL (multiplayer spike,
// 2026-09-26). These routes run before Vite's own checks, so the refusal is here.
let _decoded = rawPath
try { _decoded = decodeURIComponent(rawPath) } catch { /* keep raw */ }
if (/(^|\/)\.(git|env)(\/|$)|(^|\/)\.(merge-queue|main-capture)(\/|$)/i.test(_decoded)) { res.statusCode = 404; return res.end() }
// A production embed URL (`<bundle>/<profile>[/<instance>]/index.html`) is rewritten onto
// the `/__leaf/` route BEFORE any branch runs, so the existing leaf renderer answers it
// unchanged — see leafRewrite above for why the dev server must know this shape at all.

View file

@ -28,11 +28,13 @@ import { getCurrentInstance, onMounted } from 'vue'
* @param {string} [label] - Optional label for the <style> tag (for debugging)
*/
export function useShadowStyles(css, label) {
// Generate a stable key from the label or a hash of the CSS
const key = label || hashCode(css)
// Captured in setup (onMounted's callback cannot see it reliably).
const instance = getCurrentInstance()
// A stable key: the label; else, under the dev server, the component's HMR id (so an edited stylesheet
// REPLACES its old copy instead of piling up next to it); else a hash of the CSS (builds: unchanged).
const key = label || (instance && instance.type && instance.type.__hmrId ? 'hmr-' + instance.type.__hmrId : hashCode(css))
onMounted(() => {
const instance = getCurrentInstance()
if (!instance) return
// Walk up from the component's root element to find the shadow root
@ -42,8 +44,10 @@ export function useShadowStyles(css, label) {
const root = el.getRootNode()
if (!(root instanceof ShadowRoot)) return // light DOM — no injection needed
// Skip if already injected (idempotent)
if (root.querySelector(`style[data-shadow-styles="${key}"]`)) return
// Already injected: keep it — unless the CSS changed (a hot update), then replace its text. The early
// return here used to keep the OLD CSS after an edit, under a label.
const existing = root.querySelector(`style[data-shadow-styles="${key}"]`)
if (existing) { if (existing.textContent !== css) existing.textContent = css; return }
const style = document.createElement('style')
style.setAttribute('data-shadow-styles', key)

View file

@ -94,6 +94,10 @@ export default defineConfig(({ command, mode }) => {
// the maintainer's internal platform and must NOT exist in public
// artifacts. Default FALSE strips it from every bundle (dead-branch +
// treeshake); an explicitly-flagged platform build sets DOXA_QA=1.
// The dev server listens on THIS MACHINE ONLY. A reachable one serves the source tree, node_modules
// and the token baked into /; a remote viewer reaches it through an ssh tunnel (ssh -L), never a public
// port. `--host` on the command line still overrides this for a deliberate LAN session.
server: { host: '127.0.0.1' },
define: {
__DOXA_QA__: JSON.stringify(process.env.DOXA_QA === '1'),
// The output tree's name (vite.doxa-maps-build-config.json `name`, the same value OUT_ROOT uses),