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
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# Neuro Atlas is a separately deployed Next.js zone, built at /neuro-atlas.
# Origin only: HTTPS, no credentials, path, query or fragment. Rebuild after changes.
# Defaults to https://neuro-atlas-app.vercel.app when unset.
NEURO_ATLAS_ORIGIN=https://neuro-atlas-app.vercel.app
# Local integration QA only: set to 1 to allow an explicit loopback origin, even
# for next build/start. Rejected when any VERCEL / VERCEL_ENV / VERCEL_URL is set.
# NEURO_ATLAS_LOCAL_QA=1

# Session encryption secret (must be at least 32 characters)
COOKIE_SECRET=your-32-character-secret-here-change-me

Expand Down
13 changes: 13 additions & 0 deletions docs/neuro-atlas-integration-evidence/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Local integration evidence — September 18, 2026

Real headed-Chrome captures of two locally running Next applications; not hosted deployment proof. Atlas runtime: `f7a85ae26b85976e5fe8c914ff39e8df36d5adbd`, in an isolated loopback-only fixture with the Basic-auth proxy omitted. That fixture is not included in either PR. PLRD captures precede the middleware-only mixed-case cookie correction at `89ccfc0`; visible UI is identical. The correction receives a separate upstream privacy replay.

The static Neurotech card image (`public/images/neuro-atlas-preview.png` in PLRD) is a genuine local Atlas interface screenshot at that Atlas runtime revision. It is not live data or a copied/editable dataset.

Local paired QA covered prefix root/deep routes; JS/CSS/fonts/logos; retained PL Neuro Website link plus separate Atlas link; 1440/390/320px layout; browser Back/reload; chart hash/deep-link/modal/Escape; funding search and company detail. See PR verification for final native tests/build and upstream Cookie/Authorization probe results.

Hosted paired validation remains a launch prerequisite: reconnect the existing Vercel project to the transferred repository; deploy exact-SHA prefixed Atlas preview; pair with PLRD preview; resolve anonymous access separately. No merge, production configuration, auth change, permanent redirect, or collaborator invitation is included.

## Old links at cutover

Preserve page-to-page links after the new public route is verified: old `/milestones` → `https://www.plrd.org/neuro-atlas/milestones`; likewise `/funding`, `/field-velocity`, and `/methodology`. Preserve query strings and browser fragments; legacy `/regulatory-landscape` maps to Milestones. Keep `/ecosystem`'s current external destination unless separately changed. Use temporary redirects during validation; distinguish direct legacy-host traffic from the internal proxy hop to avoid loops. Test both paths before permanent redirects. These PRs do not activate the cutover redirects.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 45 additions & 0 deletions docs/neuro-atlas-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Neuro Atlas zone: paired preview and launch

PLRD remains the site shell; [protocol/neuro-atlas](https://github.com/protocol/neuro-atlas) remains an independent application and contribution repository. Only `/neuro-atlas` and `/neuro-atlas/*` proxy to Atlas, retaining the prefix. PLRD root routes, `/_next`, `/api`, robots, and auth are not redirected. The Neurotech page has contextual HTML anchors (no Next prefetch across zones), not a main-menu entry.

## Configuration

- `NEURO_ATLAS_ORIGIN`: deployment **origin**, default `https://neuro-atlas-app.vercel.app`. HTTPS only, with optional trailing slash; no credentials, path, query, or fragment. Invalid values fail the build. Use a canonical URL spelling; values are not silently repaired. This is build-time routing configuration: rebuild when it changes.
- Atlas must be built with `NEXT_PUBLIC_BASE_PATH=/neuro-atlas` and its canonical-site configuration set for the intended home. The destination includes that prefix, including assets, fonts, raw files, sitemap, and nested routes. A standalone root build is not a compatible target.
- Local QA only: `NEURO_ATLAS_LOCAL_QA=1` permits `http://127.0.0.1:3491`, `http://localhost:3491`, or `http://[::1]:3491`, including production-mode `next build` / `next start`. Loopback requires this flag even for HTTPS. The exception is rejected whenever `VERCEL`, `VERCEL_ENV`, or `VERCEL_URL` is defined. Remote HTTP is always rejected. Never set the QA flag on a hosted deployment.

## Security and launch blockers

Atlas currently has its own HTTP Basic-auth gate. This change preserves Authorization and does **not** bypass, remove, or replace that gate. Public anonymous launch requires a separate explicit owner approval to remove or replace Basic auth in Atlas. Do not use credentials found in source. Vercel deployment protection can independently block paired previews: resolve access through an owner-approved deployment policy, never inject bypass tokens into the proxy.

The narrowly scoped middleware strips inbound `Cookie` headers before forwarding every Atlas request, including static assets. PLRD session and consent cookies are not needed upstream. It does not set PLRD consent cookies on Atlas requests. Verify this through the actual proxy with an upstream header probe, not just middleware unit tests.

**This is still a shared-origin frontend trust boundary, not a sandbox.** Atlas JavaScript can make same-origin requests to PLRD; responses may also set cookies. Cookie stripping is defense-in-depth only. Review contributor changes before production and do not share PLRD secrets or deployment write access with Atlas contributors. Public source is not a blanket license for underlying datasets. Preserve consent parity and configure any shared analytics separately and explicitly.

## Local verification

Use the repository's native pnpm 10 lockfile; do not switch package managers or regenerate it:

```sh
npx --yes pnpm@10.34.5 install --frozen-lockfile
npx --yes pnpm@10.34.5 test
npx --yes pnpm@10.34.5 exec tsc --noEmit
NEURO_ATLAS_ORIGIN=http://127.0.0.1:3491 NEURO_ATLAS_LOCAL_QA=1 \
UV_THREADPOOL_SIZE=1 taskset -c 2,3 npx --yes pnpm@10.34.5 build
NEURO_ATLAS_ORIGIN=http://127.0.0.1:3491 NEURO_ATLAS_LOCAL_QA=1 \
npx --yes pnpm@10.34.5 start --hostname 127.0.0.1 --port 3492
```

Run Atlas on port 3491 with the same `/neuro-atlas` build prefix. Any gate-free browser fixture must be isolated, loopback-only, and never deployed. Check root and deep Atlas URLs, reload/history, query/hash/filter state, modal interactions, return navigation, chunks/CSS/fonts/raw downloads, and auth challenge forwarding. Probe inbound Cookie removal and preserved Authorization on both pages and assets. Check PLRD home, Neurotech, E&G, existing assets, and auth routes for regressions. Inspect the Neurotech preview at 1440, 390, and 320 px and keyboard focus.

The preview uses `public/images/neuro-atlas-preview.png`: a genuine screenshot of the exact new Atlas interface supplied during paired QA, with its source revision and capture details recorded alongside the image. It is labeled “Static interface preview”, not a live dashboard; no dataset or synthetic chart is copied into PLRD. Missing image/provenance is a release blocker, not permission to fabricate it.

## Hosted sequence and rollback

1. Review both exact commits and approve the shared-origin trust model. Before deployment, authorize the Vercel GitHub integration for `protocol/neuro-atlas` and reconnect/verify its Git deployment linkage after the repository transfer. Confirm a new commit can produce a deployment; an existing live deployment alone does not prove this connection works. The deployment origin does not change just because the GitHub owner changes.
2. Deploy a prefixed Atlas preview first; confirm its exact SHA, gate/protection behavior, namespace, canonical metadata, sitemap, and analytics/consent settings. Default standalone builds remain unchanged until deliberately configured.
3. Build a PLRD preview using that exact Atlas preview origin. Verify both deployed SHAs and rerun the paired checks over HTTPS. Local success is not hosted proof.
4. Capture the real screenshot and provenance; include captioned commit-pinned screenshots in review. Do not publish a PR claiming production readiness before the paired hosted proof and the separate anonymous-access decision.
5. After approval, merge/configure Atlas deliberately, verify the prefixed production deployment, then merge/deploy PLRD with the approved origin. Never merge PLRD first against a root-only Atlas build.
6. Check `https://www.plrd.org/neuro-atlas/`, deep paths, and the standalone domain. Atlas owns its prefixed sitemap; contextual links provide discovery. This patch deliberately leaves the PLRD root sitemap/robots unchanged rather than duplicating Atlas route catalogs. No permanent old-origin redirects are introduced; consider them only after migration is verified.
7. Roll back PLRD to the prior verified deployment (or revert the integration and rebuild) to remove the links and rewrites. Revert Atlas build configuration/deployment separately if needed. Avoid irreversible redirects so standalone operation remains recoverable.
23 changes: 23 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,29 @@ const nextConfig: NextConfig = {
images: {
unoptimized: true,
},
async rewrites() {
const value = process.env.NEURO_ATLAS_ORIGIN ?? 'https://neuro-atlas-app.vercel.app'
let url: URL
try {
url = new URL(value)
} catch {
throw new Error('NEURO_ATLAS_ORIGIN must be an HTTPS origin without credentials, path, query or fragment')
}
// Compare the original input, not just URL.pathname: URL parsing normalizes
// dot segments, whitespace and backslashes that must not be accepted here.
const loopback = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)
const localQA = process.env.NEURO_ATLAS_LOCAL_QA === '1'
&& !['VERCEL', 'VERCEL_ENV', 'VERCEL_URL'].some((key) => process.env[key] !== undefined)
const allowedProtocol = url.protocol === 'https:' || (url.protocol === 'http:' && loopback && localQA)
if ((value !== url.origin && value !== `${url.origin}/`) || !allowedProtocol || (loopback && !localQA)) {
throw new Error('NEURO_ATLAS_ORIGIN must be an HTTPS origin without credentials, path, query or fragment; loopback requires NEURO_ATLAS_LOCAL_QA=1 outside Vercel')
}
const origin = url.origin
return [
{ source: '/neuro-atlas', destination: `${origin}/neuro-atlas` },
{ source: '/neuro-atlas/:path*', destination: `${origin}/neuro-atlas/:path*` },
]
},
async redirects() {
return [
// Preserve shared preview links when the approved Neuro article goes live.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"build": "node scripts/build-content.mjs && next build",
"start": "next start",
"generate-jwk": "node scripts/generate-jwk.js",
"test": "node --test scripts/velocity/*.test.mjs scripts/neuro-content.test.mjs scripts/neuro-publication.test.mjs scripts/blog-metadata.test.mjs scripts/radar-local.test.mjs scripts/homepage-order.test.mjs scripts/team-openings.test.mjs"
"test": "node --test scripts/velocity/*.test.mjs scripts/neuro-content.test.mjs scripts/neuro-publication.test.mjs scripts/blog-metadata.test.mjs scripts/radar-local.test.mjs scripts/homepage-order.test.mjs scripts/team-openings.test.mjs scripts/neuro-atlas*.test.mjs"
},
"dependencies": {
"@atproto/api": "^0.18.17",
Expand Down
Binary file added public/images/neuro-atlas-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions scripts/neuro-atlas-middleware.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { NextRequest } from 'next/server.js'
import { unstable_doesMiddlewareMatch } from 'next/experimental/testing/server.js'
import { source } from './velocity/test-source-loader.mjs'

const { middleware, config } = source('middleware.ts')
const { COOKIE_CONSENT_ENABLED } = source('lib/cookie-consent.ts')
// Next external rewrites match case-insensitively; isolation must cover that same set.
const atlasPaths = ['/neuro-atlas', '/neuro-atlas/', '/neuro-atlas/methods/', '/neuro-atlas/_next/static/chunks/app.js', '/neuro-atlas/fonts/test.woff2', '/neuro-atlas/data.csv', '/Neuro-Atlas/', '/NEURO-ATLAS/milestones', '/NeUrO-aTlAs/_next/static/chunks/app.js', '/NEURO-ATLAS/logos/test.png']

test('Atlas namespace strips inbound cookies but preserves Basic Authorization on every route and asset', () => {
for (const path of atlasPaths) {
const req = new NextRequest(`https://www.plrd.org${path}`, { headers: {
cookie: 'pl-session=private; pl-consent-region=required',
authorization: 'Basic dGVzdDp0ZXN0',
'x-vercel-ip-country': 'DE',
} })
const res = middleware(req)
assert.ok(res.headers.has('x-middleware-override-headers'), path)
assert.equal(res.headers.get('x-middleware-request-cookie'), null, path)
assert.equal(res.headers.get('x-middleware-request-authorization'), 'Basic dGVzdDp0ZXN0', path)
assert.equal(res.headers.get('set-cookie'), null, 'Atlas must not trigger PLRD consent cookies')
assert.equal(req.headers.get('cookie'), 'pl-session=private; pl-consent-region=required', 'clone, do not mutate input')
}
})

test('middleware explicitly matches Atlas static assets excluded from ordinary page matching', () => {
for (const path of atlasPaths) {
assert.equal(unstable_doesMiddlewareMatch({ config, nextConfig: {}, url: `https://www.plrd.org${path}` }), true, path)
}
})

test('PLRD routes including near-prefix names retain original consent and request behavior', () => {
for (const path of ['/', '/areas/neurotech/', '/neuro-atlas-other/', '/api/auth/callback/', '/_next/static/app.js']) {
const req = new NextRequest(`https://www.plrd.org${path}`, { headers: { cookie: 'pl-session=private', 'x-vercel-ip-country': 'DE' } })
const res = middleware(req)
assert.equal(res.headers.get('x-middleware-override-headers'), null, path)
assert.equal(req.headers.get('cookie'), 'pl-session=private')
if (COOKIE_CONSENT_ENABLED) assert.match(res.headers.get('set-cookie'), /pl-consent-region=required/)
else assert.equal(res.headers.get('set-cookie'), null)
}
for (const path of ['/api/auth/callback/', '/_next/static/app.js', '/images/test.png']) {
assert.equal(unstable_doesMiddlewareMatch({ config, nextConfig: {}, url: `https://www.plrd.org${path}` }), false, path)
}
})
62 changes: 62 additions & 0 deletions scripts/neuro-atlas-ui.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { renderToStaticMarkup } from 'react-dom/server'
import { JSDOM } from 'jsdom'
import { source } from './velocity/test-source-loader.mjs'

const { default: AreaHeroActions } = source('components/AreaHeroActions.tsx')
const { default: AreaPage } = source('app/areas/[slug]/page.tsx')
const { areas } = source('lib/content.ts')

test('Neurotech Explore has one accessible static preview after strategy and before Insights', async () => {
const tree = await AreaPage({ params: Promise.resolve({ slug: 'neurotech' }) })
const all = nodes(tree)
const section = all.find(node => node.type === 'section' && node.props['aria-labelledby'] === 'neuro-atlas-explore')
assert.ok(section, 'Neurotech needs its contextual Explore preview')
const links = nodes(section).filter(node => node.props.href === '/neuro-atlas/')
assert.equal(links.length, 1)
assert.equal(links[0].type, 'a', 'the entire visual card is a hard navigation across zones')
const { document } = new JSDOM(renderToStaticMarkup(section)).window
assert.equal(document.querySelector('h2').textContent, 'Explore')
assert.equal(document.querySelector('h3').textContent, 'Neuro Atlas')
assert.equal(document.querySelector('p').textContent, 'Follow neurotechnology milestones, explore BCI funding, and track the field’s progress.')
assert.match(document.body.textContent, /Interactive atlas/)
const image = document.querySelector('img')
assert.equal(image.getAttribute('src'), '/images/neuro-atlas-preview.png')
assert.match(image.getAttribute('alt'), /Neuro Atlas/)
assert.equal(image.getAttribute('loading'), 'lazy')
assert.match(document.querySelector('figcaption').textContent, /Static interface preview/)
assert.equal(document.querySelectorAll('iframe, canvas').length, 0)
const html = document.querySelector('a')
assert.match(html.className, /focus-visible:/)
assert.ok(all.indexOf(section) > all.findIndex(node => node.props.id === 'opportunity-spaces'))
assert.ok(all.indexOf(section) < all.findIndex(node => node.type === 'h2' && node.props.children === 'Insights'))
const neuro = areas.find(area => area.slug === 'neurotech')
assert.ok(all.some(node => node.props.dangerouslySetInnerHTML?.__html === neuro.html), 'existing page copy remains intact')
for (const slug of ['ai-robotics', 'digital-human-rights']) {
const other = await AreaPage({ params: Promise.resolve({ slug }) })
assert.equal(nodes(other).filter(node => node.props.href === '/neuro-atlas/').length, 0)
}
})

function nodes(node) {
if (Array.isArray(node)) return node.flatMap(nodes)
if (!node || typeof node !== 'object') return []
return [node, ...nodes(node.props?.children)]
}

test('Neurotech adds a normal Atlas anchor without replacing its Website or strategy links', () => {
const props = { areaSlug: 'neurotech', showOpportunitySpaces: true, opportunityHref: '#opportunity-spaces' }
const tree = AreaHeroActions(props)
const anchor = nodes(tree).find(node => node.props.href === '/neuro-atlas/')
assert.ok(anchor, 'distinct small Neuro Atlas link must exist')
assert.equal(anchor.type, 'a', 'cross-zone navigation must not use Next Link')
assert.equal(anchor.props.target, undefined)
const { document } = new JSDOM(renderToStaticMarkup(tree)).window
assert.equal(document.querySelector('a[href="/neuro-atlas/"]').textContent.trim(), 'Neuro Atlas →')
assert.match(document.querySelector('a[href="https://www.plneuro.xyz/"]').textContent, /Website/)
assert.ok(document.querySelector('a[href="#opportunity-spaces"]'))
for (const areaSlug of ['ai-robotics', 'digital-human-rights', 'economies-governance']) {
assert.equal(nodes(AreaHeroActions({ ...props, areaSlug })).filter(node => node.props.href === '/neuro-atlas/').length, 0)
}
})
Loading