@coralreefventures/site-tools

中文文档
0.7.0发布于 2026/10/20MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 2035

Command-line tools for a Coral Reef public site: serve the static export as the host does, gate it with Lighthouse, and lock it behind the door.

原始使用文档
文档目录3 节
  1. The door gate
  2. The coming-soon page
  3. The library

@coralreefventures/site-tools

Three command-line tools for a Coral Reef public site, which is a Next.js static export served by Amplify Hosting. Both sites carried the first two as scripts/serve.ts and scripts/lighthouse.ts; they are bins now, and the products' scripts go. The third locks a site behind the door.

reef-serve [--root out] [--port 3140]
reef-lighthouse [--root out] [--reports lighthouse] [--port 3160] [--threshold 90] [--retries 2] [--shard 1/3] [--chrome <path>] [path ...]
reef-door-bundle --site <id> [--out out] --door-url <url> --hosts <h1,h2> --jwks-url <url> [--signout-chain id=host,...]
                 [--name <display name>] [--mark <file>] [--mark-dark <file>] [--accent #rrggbb] [--accent-dark #rrggbb]

reef-serve serves the export the way Amplify does, for Playwright and Lighthouse: a directory answers with its index.html, a directory path without its slash redirects to it, anything else is the 404 page with status 404, and text is compressed as the CDN compresses it. PORT is read when --port is not given, so a Playwright webServer command can pass either:

webServer: { command: `pnpm exec reef-serve --port ${port}`, port },

reef-lighthouse is the merge gate: Lighthouse on every page in the export's sitemap (or the paths given), with its default mobile emulation, each of performance, accessibility, best practices and SEO at the threshold or above. A page under it is measured again, up to --retries more times (2 by default), and the median of its runs decides: a performance score swings a few points between runs of the same page on a shared runner, and one low run should not fail a gate three runs would pass, while a page every run finds low still fails. A page under it then fails the run and leaves its last report in --reports. A category's own floor (--performance 85) stands in for the threshold for that category alone, where a site's shell puts every page a few points under 90 on a shared runner while the other three categories hold at 90. --shard <index>/<count> measures one share of the pages, every count-th from the index-th (1-based, as Playwright's --shard), so a site whose gate outgrows one runner splits it across a CI matrix: Lighthouse measures one page at a time, about 14 seconds each on a CI runner, and pages measured side by side on one runner would skew each other's performance scores. Lighthouse itself is not a dependency, because it bundles axe-core (MPL-2.0), which is outside the family's licence allowlist; it runs through pnpm dlx, pinned (--lighthouse-version). Chrome is Playwright's Chromium when @playwright/test is installed beside this package, or --chrome.

The door gate

reef-door-bundle turns a static export into an Amplify deployment that serves nothing without a session from the door. It writes .amplify-hosting/ with the export inside the gate's compute bundle and a deployment manifest with exactly one route, /* to Compute, and no static route, so every byte (pages, RSC payloads, _next/static chunks, images, social cards, the 404 page) is answered by the gate. The app runs on platform WEB_COMPUTE with the cache type AMPLIFY_MANAGED, so cookies are in the CDN's cache key.

reef-door-bundle --site <id> --door-url https://<door> --hosts <host> --jwks-url https://<door>/.well-known/crv-door-jwks.json \
  --signout-chain <id>=<host>,<id>=<host>

Each flag falls back to an environment variable, so a build spec can set them per app: CRV_DOOR_URL, CRV_DOOR_HOSTS, CRV_DOOR_JWKS_URL, CRV_DOOR_SIGNOUT_CHAIN and CRV_DOOR_HOST_HEADER (host, the default, or x-forwarded-host if the platform rewrites Host). The bin fetches the JWKS and fails the build if it is unreachable, empty, or anything but P-256 public keys; it refuses a static/ directory, a second route and a bundle over 200 MB.

The gate is a Node server with no dependencies (it imports Node's own modules and nothing else, which a test holds), listening on port 3000. For every request, in order:

  1. A path with .., a backslash or a control character, raw or percent-encoded, is a 400; a method other than GET, HEAD or POST is a 405.
  2. A Host not in --hosts is an empty 403.
  3. /robots.txt disallows everything; /_door/health answers ok.
  4. POST /_door takes the door's ticket from a form body: an ES256 JWS whose kid is in the JWKS, issued by the door for this exact host, unexpired and living at most an hour, bound to the __Host-crv_door_state cookie, and naming the same canonical next as the form. It becomes the __Host-crv_door cookie (HttpOnly; Secure; SameSite=Lax, for the ticket's remaining life) and a 303 to next, and sets __Host-crv_door_seen=1 (HttpOnly; Secure; SameSite=Lax, 30 days), a marker with no identity in it that this browser has had a session here. Anything wrong is a 303 back to the door with ?error=ticket. GET /_door/signin?next=<path> starts a sign-in: it runs next through Canonical next (a missing, repeated or refused one becomes /), sets a fresh 32-byte __Host-crv_door_state cookie (SameSite=None, ten minutes) and answers a 302 to the door with site, host, next and state. The coming-soon page's button links here. Only a top-level navigation starts one (a navigation, or a request without Sec-Fetch-* headers that accepts HTML, and Sec-Fetch-Dest: document when sent); an image, a frame or a fetch gets an empty 401 and no cookie, so another site cannot overwrite a visitor's state mid-sign-in.
  5. /_door/signout?then=<id> clears all three cookies (an empty value and an Expires at the epoch, with the attributes each was set with and no Max-Age, which Firefox can keep for a moment as a cookie expiring now) and goes on by a fixed map: to the gate of the site <id> in --signout-chain, naming the site after it, or, for anything else (door, the last stop), to the door's /signout/. It never redirects to a URL from the query.
  6. With a valid session the export is served as Amplify maps it, HTML and .txt as private, no-store and /_next/static/** as private, max-age=31536000, immutable.
  7. Without one, a page load (a navigation, or a request without Sec-Fetch-* headers that accepts HTML) gets the coming-soon page, below, as a 401; anything else gets an empty 401. Neither sets a cookie. The exception is a top-level page load carrying the seen marker: an invitee whose hour is up is sent straight to the door (a fresh state cookie and the 302 /_door/signin gives), which issues a new ticket at once while its own sign-in lives, so renewal stays silent. The marker opens nothing; a fetch carrying it still gets the empty 401. Both 401s carry WWW-Authenticate: Cookie realm="<id>", form-action="/_door/signin", cookie-name="__Host-crv_door".
  8. An exception is an empty 500. The gate fails closed.

Every response carries X-Robots-Tag: noindex, nofollow, HSTS, nosniff, X-Frame-Options: DENY and a Cache-Control with no-store or private. next is canonicalised by parsing it against the host, re-serialising it as path and query, and refusing it if it leaves the origin or holds a backslash or control character (canonicalNext, exported, so the door's ticket issuer can run the same steps).

The coming-soon page

What a visitor without a session sees on the product's own domain: the product's mark and name, " is coming soon.", "Open for now to invited guests.", a "Have an invitation? Sign in" button to /_door/signin carrying the request's canonical next (so sign-in happens at the door and the ticket brings the invitee back to the page they asked for), and a "Get involved" link to <door>/get-involved/?site=<id>.

It is the gate's own HTML and carries nothing of the site: one inline stylesheet, the mark as a data: URI, no script, no request to anywhere. It is built once when the gate starts and is the same bytes for every path except the next its button carries. Its headers are the gate's security headers, X-Robots-Tag: noindex, nofollow, Cache-Control: no-store, and a Content-Security-Policy that allows exactly that: default-src 'none'; style-src 'sha256-<its stylesheet>'; img-src data: (the last only with a mark), and base-uri, form-action and frame-ancestors all 'none'. HEAD gets the same headers and no body. It follows prefers-color-scheme, reads at 320px, and every color pair meets WCAG AA.

Why 401. No request without a session gets a 2xx, so the leak check and any probe can tell a locked site from an open one by status alone. A 200 would read as the site itself to a crawler or an uptime monitor, and a 503 reads as an outage. RFC 9110 requires a 401 to carry a WWW-Authenticate challenge, and no registered scheme describes a session cookie from a sign-in page, so the gate uses the shape drafted for exactly that (draft-broyer-http-cookie-auth). Browsers prompt only for schemes they implement, so for this one they render the body, which the browser suite proves in Chromium, Firefox and WebKit.

Its content comes from the bin's flags, stored in gate.config.json as page and checked when the bundle is made:

FlagDefaultChecked
--namethe site id, capitalized1 to 60 characters, no control character, no space at either end
--marknone: the name stands alonean .svg or .png path inside the export (a link out of it is refused), at most 64 KB, inlined as base64
--mark-darknone: the dark scheme shows --mark on a light tileas --mark; only with --mark
--accent#1f2933#rrggbb, at least 3:1 on the light page (#ffffff)
--accent-dark#d5dbe1#rrggbb, at least 3:1 on the dark page (#11161b)

The button's label is white or black, whichever reads better on the accent; one of the two always reaches 4.5:1. The mark is a file of the site that every visitor then sees, so name only a file meant to be public: the logo, never anything from behind the door. If the Amplify app's custom headers also set a Content-Security-Policy, Amplify's wins or both apply; the products' policy allows inline styles and data: images, so the page renders either way.

The gate holds only public keys, takes all of its configuration as data and names no product. The ticket issuer, the door and the site registry are the door's own.

The library

The library entry exports serve, createStaticServer, runLighthouse and pagesFromSitemap for a script that wants them in-process, and the gate's parts: createGate, startGate, verifyTicket, parseJwks, canonicalNext, parseGateConfig, bundleDoor, and the coming-soon page's comingSoon and parsePageConfig.

This is the one package in the repository with a build: a bin has to be JavaScript, because Node refuses to strip types from a file under node_modules. tsc writes dist/ on prepack, and the door bundle copies the gate's compiled files from there.

版本与发布记录

@coralreefventures/site-tools 共发布 11 个版本,首次发布于 2026/10/05;过去 12 个月发布了 11 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.7.0latest2026/10/2025 天前
0.6.02026/10/1926 天前
0.5.02026/10/1827 天前
0.4.32026/10/1827 天前
0.4.22026/10/131 个月前
0.4.12026/10/131 个月前

关于 @coralreefventures/site-tools 的常见问题

@coralreefventures/site-tools 是什么?

Command-line tools for a Coral Reef public site: serve the static export as the host does, gate it with Lighthouse, and lock it behind the door.

如何安装 @coralreefventures/site-tools?

在项目目录运行 npm install @coralreefventures/site-tools,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 reef-serve、reef-lighthouse、reef-door-bundle,可以用 npx reef-serve 直接运行。

@coralreefventures/site-tools 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@coralreefventures/site-tools 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@coralreefventures/site-tools 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@coralreefventures/site-tools 可以商用吗?

@coralreefventures/site-tools 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@coralreefventures/site-tools 的下载量有多少?

根据 npm 官方下载统计,@coralreefventures/site-tools 在 2026/10/11 – 2026/11/09 这 30 天内被下载 2,035 次,日均约 68 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@coralreefventures/site-tools 还在维护吗?

最新版本 0.7.0 发布于 2026/10/20(25 天前);至今共发布 11 个版本,其中过去 12 个月发布了 11 个;首次发布于 2026/10/05。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。