Skip to content
Merged
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
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"dependencies": {
"@modelcontextprotocol/sdk": "^1.32.2",
"@profullstack/coinpay-onboard": "^0.2.0",
"@profullstack/footer": "^0.2.6",
"@profullstack/footer": "^0.4.0",
"@profullstack/hqtui": "^0.11.0",
"hono": "^4.13.14",
"qrcode": "^1.5.4",
Expand Down
22 changes: 22 additions & 0 deletions src/ring-footer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* template in the background every 15 minutes, so a page never waits on the CDN and
* a footer release reaches us without a redeploy. A failed read keeps the last copy.
*/
import { createHash } from 'node:crypto'
import { descriptor, footerHtml, footerHtmlSync, type FooterOptions } from '@profullstack/footer'

/** The ring member URL (the web2 hostname; hops are measured from it). */
Expand All @@ -30,6 +31,27 @@ let cached = footerHtmlSync(OPTIONS)
/** The footer HTML to print in the layout. */
export const ringFooter = (): string => cached

let hashedFor = ''
let hashes = ''

/**
* CSP 'sha256-…' sources for the footer's inline scripts (0.4.0+: the newsletter
* form's wireSubscribe, which posts to pit.world and reports inline). script-src has
* no 'unsafe-inline' and the footer is cached across requests, so a per-request nonce
* cannot reach it; a hash of the exact script text can. Recomputed when the cache changes.
*/
export function ringFooterScriptHashes(): string {
if (hashedFor !== cached) {
const out: string[] = []
for (const m of cached.matchAll(/<script(?:\s[^>]*)?>([\s\S]*?)<\/script>/g)) {
out.push(`'sha256-${createHash('sha256').update(m[1] ?? '', 'utf8').digest('base64')}'`)
}
hashes = out.join(' ')
hashedFor = cached
}
return hashes
}

/** /.well-known/openwebring.json */
export const ringDescriptor = () => descriptor({ site: RING_SITE, name: 'ad.network' })

Expand Down
11 changes: 9 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Sealer, Store, type Campaign, type DepositRow, type Site, type User } f
import { adPage, advertisePage, depositPage, helpPage, homePage, installScript, mePage, messagePage, pitchPage, publishPage, sitePage, SITE, type Ctx } from './html.js'
import { startPitFooterRefresh } from './pit-footer.js'
import { RING_PLACEHOLDER, ringBar, startRingRefresh } from './ring.js'
import { ringDescriptor, startRingFooterRefresh } from './ring-footer.js'
import { ringDescriptor, ringFooterScriptHashes, startRingFooterRefresh } from './ring-footer.js'
import { AdNetwork, newApiKey, UserError, type Config } from './service.js'
import { VERSION } from './version.js'

Expand All @@ -25,7 +25,10 @@ const SESSION = 'an_session'
const SCOPES = ['openid', 'profile', 'email', 'wallet:read']

const SECURITY_HEADERS: Record<string, string> = {
'content-security-policy': "default-src 'none'; style-src 'self' 'unsafe-inline'; script-src 'self' https://crawlproof.com; img-src 'self' data: https://built.indie.pit.world https://pwamart.com; connect-src 'self' https://crawlproof.com; frame-src https://crawlproof.com; form-action 'self' https://coinpayportal.com; base-uri 'none'; frame-ancestors 'none'",
// https://pit.world: the footer's newsletter form posts to pit.world/api/subscribe
// (connect-src) and falls back to a plain POST to pit.world/subscribe (form-action).
// script-src also gets the footer's inline-script hashes, added per response below.
'content-security-policy': "default-src 'none'; style-src 'self' 'unsafe-inline'; script-src 'self' https://crawlproof.com; img-src 'self' data: https://built.indie.pit.world https://pwamart.com; connect-src 'self' https://crawlproof.com https://pit.world; frame-src https://crawlproof.com; form-action 'self' https://coinpayportal.com https://pit.world; base-uri 'none'; frame-ancestors 'none'",
'referrer-policy': 'strict-origin-when-cross-origin',
'x-content-type-options': 'nosniff',
'permissions-policy': 'interest-cohort=(), browsing-topics=(), camera=(), microphone=(), geolocation=()',
Expand Down Expand Up @@ -89,6 +92,10 @@ export function createApp(deps: Deps) {
app.use('*', async (c, next) => {
await next()
for (const [k, v] of Object.entries(SECURITY_HEADERS)) c.header(k, v)
const footerScripts = ringFooterScriptHashes()
if (footerScripts) {
c.header('content-security-policy', SECURITY_HEADERS['content-security-policy']!.replace("script-src 'self'", `script-src 'self' ${footerScripts}`))
}
})

app.get('/health', (c) => c.json({ ok: true, version: VERSION }))
Expand Down
Loading