Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@repo/db": "workspace:*",
"@repo/env": "workspace:*",
"@repo/ui": "workspace:*",
"@rsbuild/core": "catalog:",
"@tanstack/react-form-start": "catalog:",
"@tanstack/react-query": "catalog:",
"@tanstack/react-router": "catalog:",
Expand Down
52 changes: 52 additions & 0 deletions apps/web/src/lib/csp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export type CspDirectives = Record<string, string[]>;

export function buildBaseCsp(opts: {
nonce: string;
isDev: boolean;
pathname: string;
reportUrl: string;
}): CspDirectives {
const { nonce, isDev, pathname, reportUrl } = opts;
const needsScannerCompatEval = /^\/admin\/walls\/[^/]+\/devices\/?$/.test(pathname);
const styleSrcElem = ["'self'", "'unsafe-inline'"];

return {
'upgrade-insecure-requests': [],
'default-src': ["'none'"],
'base-uri': ["'self'"],
'object-src': ["'none'"],
'form-action': ["'self'"],
'connect-src': ["'self'", 'ws:', 'wss:', 'https:', ...(isDev ? ['http:'] : [])],
'manifest-src': ["'self'"],
'frame-src': ["'self'", 'https:', ...(isDev ? ['http:'] : [])],
'img-src': ["'self'", 'data:', 'blob:', 'https:'],
'media-src': ["'self'", 'data:', 'blob:', 'https:', ...(isDev ? ['http:'] : [])],
'font-src': ["'self'", 'data:', 'https:', ...(isDev ? ['http:'] : [])],
'worker-src': ["'self'", 'blob:'],
'script-src': [
"'strict-dynamic'",
`'nonce-${nonce}'`,
// Required by modern engines for WebAssembly compilation without opening
// JS eval permissions.
"'wasm-unsafe-eval'",
// Compatibility fallback for engines that still gate WASM compile behind
// 'unsafe-eval' (kept narrowly scoped to scanner route in production).
...(isDev || needsScannerCompatEval ? ["'unsafe-eval'"] : [])
],
'style-src': styleSrcElem,
'style-src-elem': styleSrcElem,
'style-src-attr': ["'unsafe-inline'"],
'report-uri': [reportUrl],
'report-to': ['csp-endpoint']
};
}

export function modifyCsp(base: CspDirectives, overrides: CspDirectives): CspDirectives {
return { ...base, ...overrides };
}

export function serializeCsp(directives: CspDirectives): string {
return Object.entries(directives)
.map(([name, values]) => (values.length ? `${name} ${values.join(' ')}` : name))
.join('; ');
}
30 changes: 30 additions & 0 deletions apps/web/src/lib/portalHttp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export function getCorsHeaders(request: Request) {
const origin = request.headers.get('origin');
return {
'Access-Control-Allow-Origin': origin ?? '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Authorization, Content-Type',
'Access-Control-Max-Age': '86400',
Vary: 'Origin'
} as const;
}

export function json(request: Request, status: number, payload: unknown) {
return new Response(JSON.stringify(payload), {
status,
headers: {
'Content-Type': 'application/json',
...getCorsHeaders(request)
}
});
}

export function getBearerToken(request: Request): string | null {
const auth = request.headers.get('authorization');
if (auth && auth.toLowerCase().startsWith('bearer ')) {
return auth.slice(7).trim();
}
const url = new URL(request.url);
const fallback = url.searchParams.get('_gem_t');
return fallback && fallback.trim().length > 0 ? fallback.trim() : null;
}
2 changes: 1 addition & 1 deletion apps/web/src/lib/serverAssetUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import sharp from 'sharp';

import { ASSET_DIR } from './serverVariables';

const VARIANT_WIDTHS = [50, 200, 800, 1600];
const VARIANT_WIDTHS = [50, 200, 800, 1600, 2400, 3200];

/** Compute a blurhash from an image file on disk */
export async function computeBlurhash(imagePath: string): Promise<string | null> {
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/lib/serverVariables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export const APP_DATA_DIR = env.APP_DATA_DIR;
export const UPLOAD_DIR = env.UPLOAD_DIR || join(APP_DATA_DIR, 'uploads');
export const TMP_DIR = env.TMP_DIR || join(APP_DATA_DIR, 'tmp');
export const ASSET_DIR = env.ASSET_DIR || join(APP_DATA_DIR, 'assets');
export const CONTROLLER_DIR = env.CONTROLLER_DIR || join(APP_DATA_DIR, 'controllers');
4 changes: 4 additions & 0 deletions apps/web/src/lib/wallEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export class WallEngine {
return () => this.layoutCallbacks.delete(callback);
}

public onReady(callback: () => void) {
return this.bus.onReady(callback);
}

public registerLayer(layer: LayerWithWallComponentState, el: HTMLElement) {
let layerPtr = this.layers.get(layer.numericId);
if (!layerPtr) {
Expand Down
Loading