Ship production deployment and runtime observability
This commit is contained in:
parent
267665bb35
commit
775dd61963
5 changed files with 3712 additions and 175 deletions
|
|
@ -8,6 +8,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="/runtime-config.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
1
web/public/runtime-config.js
Normal file
1
web/public/runtime-config.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
window.remoraRuntimeConfig = {}
|
||||
226
web/server.mjs
Normal file
226
web/server.mjs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
import { createReadStream, statSync } from 'node:fs'
|
||||
import { createServer } from 'node:http'
|
||||
import { extname, resolve, sep } from 'node:path'
|
||||
|
||||
const root = resolve(process.env.REMORA_WEB_ROOT ?? '/app/dist')
|
||||
const port = numberEnvironment('REMORA_WEB_PORT', 8080)
|
||||
const runtimeConfig = {
|
||||
apiUrl: environment('REMORA_WEB_API_URL', '/v1/'),
|
||||
eventsUrl: environment('REMORA_WEB_EVENTS_URL', ''),
|
||||
oidcAuthority: environment('REMORA_WEB_OIDC_AUTHORITY', '/oidc'),
|
||||
oidcClientId: environment('REMORA_WEB_OIDC_CLIENT_ID', 'remora-web'),
|
||||
oidcRedirectUri: environment('REMORA_WEB_OIDC_REDIRECT_URI', '/'),
|
||||
oidcPostLogoutRedirectUri: environment(
|
||||
'REMORA_WEB_OIDC_POST_LOGOUT_REDIRECT_URI',
|
||||
'/',
|
||||
),
|
||||
oidcScope: environment('REMORA_WEB_OIDC_SCOPE', 'openid profile'),
|
||||
}
|
||||
const startedAt = Date.now()
|
||||
let requests = 0
|
||||
|
||||
createServer((request, response) => {
|
||||
requests += 1
|
||||
const traceId = traceIdentifier(request.headers.traceparent)
|
||||
response.setHeader('X-Content-Type-Options', 'nosniff')
|
||||
response.setHeader('Referrer-Policy', 'no-referrer')
|
||||
response.setHeader('X-Frame-Options', 'DENY')
|
||||
response.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
|
||||
response.setHeader('Content-Security-Policy', contentSecurityPolicy())
|
||||
response.setHeader('Traceparent', `00-${traceId}-0000000000000001-01`)
|
||||
response.once('finish', () => {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'info',
|
||||
component: 'remora-web',
|
||||
trace_id: traceId,
|
||||
method: request.method,
|
||||
path: requestPath(request.url),
|
||||
status: response.statusCode,
|
||||
})}\n`,
|
||||
)
|
||||
})
|
||||
route(request, response)
|
||||
}).listen(port, '0.0.0.0', () => {
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'info',
|
||||
component: 'remora-web',
|
||||
message: 'web application ready',
|
||||
port,
|
||||
})}\n`,
|
||||
)
|
||||
})
|
||||
|
||||
function route(request, response) {
|
||||
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
||||
response.writeHead(405, { Allow: 'GET, HEAD' })
|
||||
response.end()
|
||||
return
|
||||
}
|
||||
const url = new URL(request.url ?? '/', 'http://localhost')
|
||||
if (url.pathname === '/health/live' || url.pathname === '/health/ready') {
|
||||
json(response, 200, { status: 'ready' })
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/metrics') {
|
||||
const body =
|
||||
'# TYPE remora_web_build_info gauge\n' +
|
||||
'remora_web_build_info 1\n' +
|
||||
'# TYPE remora_web_requests_total counter\n' +
|
||||
`remora_web_requests_total ${requests}\n` +
|
||||
'# TYPE remora_web_uptime_seconds gauge\n' +
|
||||
`remora_web_uptime_seconds ${Math.floor((Date.now() - startedAt) / 1000)}\n`
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'text/plain; version=0.0.4',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
})
|
||||
response.end(request.method === 'HEAD' ? undefined : body)
|
||||
return
|
||||
}
|
||||
if (url.pathname === '/runtime-config.js') {
|
||||
const body = `window.remoraRuntimeConfig=${JSON.stringify(
|
||||
normalizedRuntimeConfig(),
|
||||
)};\n`
|
||||
response.writeHead(200, {
|
||||
'Cache-Control': 'no-store',
|
||||
'Content-Type': 'text/javascript; charset=utf-8',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
})
|
||||
response.end(request.method === 'HEAD' ? undefined : body)
|
||||
return
|
||||
}
|
||||
const candidate = safeFile(url.pathname)
|
||||
const file = candidate ?? resolve(root, 'index.html')
|
||||
const body = regularFile(file) ? file : resolve(root, 'index.html')
|
||||
if (!regularFile(body)) {
|
||||
response.writeHead(404)
|
||||
response.end()
|
||||
return
|
||||
}
|
||||
const size = statSync(body).size
|
||||
response.writeHead(200, {
|
||||
'Cache-Control': body.endsWith('index.html')
|
||||
? 'no-cache'
|
||||
: 'public, max-age=31536000, immutable',
|
||||
'Content-Type': contentType(body),
|
||||
'Content-Length': size,
|
||||
})
|
||||
if (request.method === 'HEAD') {
|
||||
response.end()
|
||||
} else {
|
||||
createReadStream(body).pipe(response)
|
||||
}
|
||||
}
|
||||
|
||||
function requestPath(value) {
|
||||
try {
|
||||
return new URL(value ?? '/', 'http://localhost').pathname
|
||||
} catch {
|
||||
return '/'
|
||||
}
|
||||
}
|
||||
|
||||
function normalizedRuntimeConfig() {
|
||||
const origin = requiredEnvironment('REMORA_WEB_PUBLIC_ORIGIN')
|
||||
return {
|
||||
...runtimeConfig,
|
||||
eventsUrl:
|
||||
runtimeConfig.eventsUrl ||
|
||||
absolute(runtimeConfig.apiUrl.replace(/\/?$/, '/events'), origin, true),
|
||||
oidcAuthority: absolute(runtimeConfig.oidcAuthority, origin, false),
|
||||
oidcRedirectUri: absolute(runtimeConfig.oidcRedirectUri, origin, false),
|
||||
oidcPostLogoutRedirectUri: absolute(
|
||||
runtimeConfig.oidcPostLogoutRedirectUri,
|
||||
origin,
|
||||
false,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function absolute(value, origin, websocket) {
|
||||
if (/^[a-z][a-z0-9+.-]*:/i.test(value)) return value
|
||||
if (!origin) return value
|
||||
const url = new URL(value, origin)
|
||||
if (websocket) url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
return url.toString()
|
||||
}
|
||||
|
||||
function safeFile(pathname) {
|
||||
let decoded
|
||||
try {
|
||||
decoded = decodeURIComponent(pathname)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
const candidate = resolve(root, `.${decoded}`)
|
||||
return candidate === root || candidate.startsWith(`${root}${sep}`)
|
||||
? candidate
|
||||
: null
|
||||
}
|
||||
|
||||
function regularFile(path) {
|
||||
try {
|
||||
return statSync(path).isFile()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function json(response, status, value) {
|
||||
const body = JSON.stringify(value)
|
||||
response.writeHead(status, {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(body),
|
||||
})
|
||||
response.end(body)
|
||||
}
|
||||
|
||||
function contentType(path) {
|
||||
return (
|
||||
{
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.json': 'application/json',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.woff2': 'font/woff2',
|
||||
}[extname(path)] ?? 'application/octet-stream'
|
||||
)
|
||||
}
|
||||
|
||||
function contentSecurityPolicy() {
|
||||
const connect = environment('REMORA_WEB_CSP_CONNECT_SRC', "'self'")
|
||||
return `default-src 'self'; base-uri 'none'; frame-ancestors 'none'; object-src 'none'; connect-src ${connect}`
|
||||
}
|
||||
|
||||
function traceIdentifier(traceparent) {
|
||||
const match =
|
||||
typeof traceparent === 'string'
|
||||
? /^00-([0-9a-f]{32})-[0-9a-f]{16}-[0-9a-f]{2}$/.exec(traceparent)
|
||||
: null
|
||||
return match?.[1] ?? crypto.randomUUID().replaceAll('-', '')
|
||||
}
|
||||
|
||||
function environment(name, fallback) {
|
||||
const value = process.env[name]
|
||||
return value === undefined || value === '' ? fallback : value
|
||||
}
|
||||
|
||||
function requiredEnvironment(name) {
|
||||
const value = process.env[name]
|
||||
if (value === undefined || value === '') {
|
||||
throw new Error(`${name} is required`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function numberEnvironment(name, fallback) {
|
||||
const value = Number(environment(name, String(fallback)))
|
||||
if (!Number.isSafeInteger(value) || value < 1 || value > 65535) {
|
||||
throw new Error(`${name} must be a valid port`)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
@ -12,6 +12,12 @@ interface RemoraRuntimeConfig {
|
|||
oidcScope: string
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
remoraRuntimeConfig?: Partial<RemoraRuntimeConfig>
|
||||
}
|
||||
}
|
||||
|
||||
export function createBrowserRuntime(): WebClientRuntime {
|
||||
const config = runtimeConfig()
|
||||
const session = new BrowserOidcSession({
|
||||
|
|
@ -29,23 +35,35 @@ export function createBrowserRuntime(): WebClientRuntime {
|
|||
}
|
||||
|
||||
function runtimeConfig(): RemoraRuntimeConfig {
|
||||
const apiUrl = import.meta.env.VITE_REMORA_API_URL ?? '/api/v1/'
|
||||
const injected = window.remoraRuntimeConfig ?? {}
|
||||
const apiUrl =
|
||||
injected.apiUrl ?? import.meta.env.VITE_REMORA_API_URL ?? '/v1/'
|
||||
return {
|
||||
apiUrl,
|
||||
eventsUrl:
|
||||
injected.eventsUrl ??
|
||||
import.meta.env.VITE_REMORA_EVENTS_URL ??
|
||||
new URL('events', websocketBase(apiUrl)).toString(),
|
||||
oidcAuthority:
|
||||
injected.oidcAuthority ??
|
||||
import.meta.env.VITE_REMORA_OIDC_AUTHORITY ??
|
||||
new URL('/oidc', window.location.origin).toString(),
|
||||
oidcClientId: import.meta.env.VITE_REMORA_OIDC_CLIENT_ID ?? 'remora-web',
|
||||
oidcClientId:
|
||||
injected.oidcClientId ??
|
||||
import.meta.env.VITE_REMORA_OIDC_CLIENT_ID ??
|
||||
'remora-web',
|
||||
oidcRedirectUri:
|
||||
injected.oidcRedirectUri ??
|
||||
import.meta.env.VITE_REMORA_OIDC_REDIRECT_URI ??
|
||||
new URL('/', window.location.origin).toString(),
|
||||
oidcPostLogoutRedirectUri:
|
||||
injected.oidcPostLogoutRedirectUri ??
|
||||
import.meta.env.VITE_REMORA_OIDC_POST_LOGOUT_REDIRECT_URI ??
|
||||
new URL('/', window.location.origin).toString(),
|
||||
oidcScope: import.meta.env.VITE_REMORA_OIDC_SCOPE ?? 'openid profile',
|
||||
oidcScope:
|
||||
injected.oidcScope ??
|
||||
import.meta.env.VITE_REMORA_OIDC_SCOPE ??
|
||||
'openid profile',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const result: MockResult = {
|
|||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.routeWebSocket(/\/api\/v1\/events/, () => {})
|
||||
await page.routeWebSocket(/\/v1\/events/, () => {})
|
||||
})
|
||||
|
||||
test('completes credential, submission, result, history, and lifecycle workflows', async ({
|
||||
|
|
@ -162,10 +162,10 @@ async function mockApi(page: Page): Promise<{
|
|||
}
|
||||
let credential = sourceCredential('active')
|
||||
let currentJob = job('Existing archive')
|
||||
await page.route(/\/api\/v1\//, async (route) => {
|
||||
await page.route(/\/v1\//, async (route) => {
|
||||
const request = route.request()
|
||||
const url = new URL(request.url())
|
||||
const path = url.pathname.replace('/api/v1/', '')
|
||||
const path = url.pathname.replace('/v1/', '')
|
||||
state.paths.push(path)
|
||||
if (
|
||||
path === 'jobs' &&
|
||||
|
|
|
|||
Loading…
Reference in a new issue