diff --git a/server.cjs b/server.cjs
index 10b7e10..f3e79a0 100644
--- a/server.cjs
+++ b/server.cjs
@@ -1369,6 +1369,23 @@ function isTurnstileSessionVerified(req) {
}
}
+function protectedSiteSessionStatus(req) {
+ const turnstileVerified = isTurnstileSessionVerified(req)
+ const siteVerified = isSiteSessionVerified(req)
+ const canIssueSiteSession = turnstileVerified && !siteVerified && Boolean(SITE_SESSION_HMAC_KEY)
+ return {
+ turnstileVerified,
+ siteVerified,
+ canIssueSiteSession,
+ verified: turnstileVerified && siteVerified,
+ }
+}
+
+function protectedSiteSessionGateState(req) {
+ const sessionStatus = protectedSiteSessionStatus(req)
+ return sessionStatus.verified || sessionStatus.canIssueSiteSession ? 'verified' : 'required'
+}
+
function callTurnstileSiteverify(token, remoteIp, idempotencyKey) {
return new Promise((resolve) => {
const params = new URLSearchParams()
@@ -2776,7 +2793,7 @@ function htmlWithSeo(req, data) {
.replaceAll('__SEO_PUBLISHED_TIME__', escapeHtml(seo.publishedAt || ''))
.replaceAll('__SEO_MODIFIED_TIME__', escapeHtml(seo.publishedAt || ''))
.replaceAll('__SEO_JSON_LD__', routeStructuredData(origin, seo, canonicalUrl).replace(/', `
\n${routeFallbackHtml(seo)}\n
`)
}
@@ -2791,8 +2808,11 @@ function requestPathname(req) {
function sendHtml(req, res, data, status) {
const html = htmlWithSeo(req, data)
const finalStatus = status ?? routeSeo(requestPathname(req)).status ?? 200
+ const sessionStatus = protectedSiteSessionStatus(req)
+ const sessionCookie = sessionStatus.canIssueSiteSession ? buildSiteSessionCookie(req) : ''
send(res, finalStatus, html, {
...securityHeaders(req, { html: true }),
+ ...(sessionCookie ? { 'set-cookie': sessionCookie } : {}),
'content-type': mimeTypes['.html'],
'cache-control': 'no-cache',
})
@@ -3379,7 +3399,10 @@ const server = http.createServer((req, res) => {
sendJson(res, 403, { error: 'Turnstile session check is restricted to this site' })
return
}
- sendJson(res, 200, { verified: isTurnstileSessionVerified(req) })
+ const sessionStatus = protectedSiteSessionStatus(req)
+ const sessionCookie = sessionStatus.canIssueSiteSession ? buildSiteSessionCookie(req) : ''
+ const headers = sessionCookie ? { 'set-cookie': sessionCookie } : {}
+ sendJson(res, 200, { verified: sessionStatus.verified || Boolean(sessionCookie) }, headers)
return
}
diff --git a/webhook.cjs b/webhook.cjs
index 050a856..bf7484f 100644
--- a/webhook.cjs
+++ b/webhook.cjs
@@ -323,14 +323,8 @@ function runCapture(command, args, options = {}) {
})
}
-// Files that, when changed, require a fresh `npm ci`. package.json is included
-// because npm ci refuses to run against a package.json/lockfile that are out of
-// sync, so a change to either should trigger a reinstall.
-const DEPENDENCY_FILES = ['package.json', 'package-lock.json']
-
// Resolve the modules the build relies on. Returns the first missing module's
-// name, or null if all are present. Used both to validate after an install and
-// to decide whether a skipped install left an incomplete node_modules.
+// name, or null if all are present. Used to validate after install.
function missingBuildDependency() {
try {
require.resolve('@vitejs/plugin-react')
@@ -351,49 +345,20 @@ function missingBuildDependency() {
return null
}
-// True when any dependency file changed between previousHead and the current
-// HEAD. Defaults to true (reinstall) whenever we can't determine the range, so
-// an uncertain state never skips a needed install.
-async function dependencyFilesChanged(previousHead) {
- if (!validGitSha(previousHead)) return true
- try {
- const changed = await runCapture('git', [
- 'diff',
- '--name-only',
- `${previousHead}..HEAD`,
- '--',
- ...DEPENDENCY_FILES,
- ])
- return changed.trim().length > 0
- } catch {
- return true
- }
-}
+async function ensureBuildDependencies() {
+ await run('npm', ['ci'], {
+ env: {
+ NODE_ENV: 'development',
+ npm_config_omit: '',
+ npm_config_production: 'false',
+ npm_config_fund: 'false',
+ npm_config_audit: 'false',
+ },
+ })
-async function ensureBuildDependencies(previousHead) {
- const depsChanged = await dependencyFilesChanged(previousHead)
- // Skip the clean reinstall (which wipes node_modules and rebuilds native
- // modules like better-sqlite3) only when no dependency files changed AND the
- // existing node_modules already has everything the build needs.
- const canSkip = !depsChanged && missingBuildDependency() === null
-
- if (canSkip) {
- console.log('deploy step skipped: npm ci (no package.json/package-lock.json changes)')
- } else {
- await run('npm', ['ci'], {
- env: {
- NODE_ENV: 'development',
- npm_config_omit: '',
- npm_config_production: 'false',
- npm_config_fund: 'false',
- npm_config_audit: 'false',
- },
- })
-
- const missing = missingBuildDependency()
- if (missing) {
- throw new Error(`${missing} is missing after npm install; dependencies were not installed`)
- }
+ const missing = missingBuildDependency()
+ if (missing) {
+ throw new Error(`${missing} is missing after npm install; dependencies were not installed`)
}
}
@@ -643,10 +608,9 @@ async function deploy(push) {
try {
await notifyDeployStarted(push)
- const previousHead = await runCapture('git', ['rev-parse', 'HEAD']).catch(() => null)
await run('git', ['pull', '--ff-only'])
diff = await deployDiff(push)
- await ensureBuildDependencies(previousHead)
+ await ensureBuildDependencies()
await run('npm', ['run', 'build', '--', '--outDir', '../dist-next'])
validateBuiltDist()
await run('cargo', ['build', '--manifest-path', 'backend/Cargo.toml', '--release'])
@@ -738,43 +702,54 @@ const webhookServer = http.createServer((req, res) => {
if (tooLarge) return
const rawBody = Buffer.concat(chunks)
const push = safeJsonParse(rawBody)
+ const event = String(req.headers['x-github-event'] || '')
+ const pushRef = String(push?.ref || '')
+ const repoFullName = String(push?.repository?.full_name || '')
+ const delivery = String(req.headers['x-github-delivery'] || 'unknown')
if (!verifySignature(rawBody, req.headers['x-hub-signature-256'])) {
+ console.warn(`webhook delivery rejected: invalid signature delivery=${delivery} event=${event || '(missing)'}`)
json(res, 401, { error: 'Invalid signature' })
return
}
- if (req.headers['x-github-event'] !== 'push') {
+ console.log(`webhook delivery received: delivery=${delivery} event=${event || '(missing)'} ref=${pushRef || '(missing)'} repo=${repoFullName || '(missing)'}`)
+
+ if (event !== 'push') {
+ console.log(`webhook delivery skipped: delivery=${delivery} reason=non-push event=${event || '(missing)'}`)
json(res, 202, { skipped: true, reason: 'Only push events trigger deploys' })
return
}
- const pushRef = String(push?.ref || '')
if (!ALLOWED_REFS.has(pushRef)) {
+ console.log(`webhook delivery skipped: delivery=${delivery} reason=ref ref=${pushRef || '(missing)'} allowed=${[...ALLOWED_REFS].join(',')}`)
json(res, 202, { skipped: true, reason: `Ignoring ref ${pushRef || '(missing)'}` })
return
}
if (ALLOWED_REPOSITORY) {
- const repoFullName = String(push?.repository?.full_name || '')
if (repoFullName !== ALLOWED_REPOSITORY) {
+ console.log(`webhook delivery skipped: delivery=${delivery} reason=repository repo=${repoFullName || '(missing)'} allowed=${ALLOWED_REPOSITORY}`)
json(res, 202, { skipped: true, reason: `Ignoring repository ${repoFullName || '(missing)'}` })
return
}
}
if (push?.deleted) {
+ console.log(`webhook delivery skipped: delivery=${delivery} reason=branch-delete ref=${pushRef}`)
json(res, 202, { skipped: true, reason: 'Ignoring branch-delete push' })
return
}
if (deploying) {
queuedPush = push
+ console.log(`webhook deploy queued: delivery=${delivery} ref=${pushRef} repo=${repoFullName || '(missing)'}`)
json(res, 202, { queued: true, deploying: true })
return
}
deploying = true
+ console.log(`webhook deploy accepted: delivery=${delivery} ref=${pushRef} repo=${repoFullName || '(missing)'} restart_targets=${RESTART_TARGETS.join(',') || 'none'}`)
json(res, 202, { accepted: true, restart_targets: RESTART_TARGETS })
runDeployQueue(push)