Files
id/Dockerfile
T
zooqueenandHanzo Dev 9111c896cb base: spa 1.4.11 — asset 404s become no-store
Today's sign-in outage: during the 0.2.34 rollout a module fetch for the
new bundle hit an old replica, the bare 404 had no cache-control, and
Cloudflare's edge cached it for four hours. hanzo.id served a black page
with a healthy origin behind it. spa 1.4.11 marks asset-miss 404s
no-store (and reunites the spa lineage that shipped as 1.4.3-1.4.8 from
a branch no remote called main).

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 16:31:50 -07:00

98 lines
5.1 KiB
Docker

# syntax=docker/dockerfile:1.7
# Hanzo ID — Vite SPA built once, served by hanzoai/spa.
FROM node:24-alpine AS build
WORKDIR /build
ENV PNPM_HOME=/pnpm PATH=$PNPM_HOME:$PATH
RUN corepack enable && corepack prepare [email protected] --activate
# THE LOCKFILE SHIPS, and the install is frozen to it.
#
# This used to omit pnpm-lock.yaml and run `--frozen-lockfile=false`, so the
# image resolved the whole tree FRESH on every build while `pnpm test` on the
# runner resolved it from the lockfile. Two different dependency graphs from one
# commit: the tested one, and the shipped one. It went green for as long as free
# resolution happened to agree, and stopped the moment it did not — adding one
# dependency (@hanzo/event) moved vite from the lockfile's
# 7.3.5_@[email protected]_… to 7.3.6_@[email protected] and the build died on
# `Cannot find module '/build/apps/web/node_modules/vite/bin/vite.js'`. Nothing
# was wrong with the source: the same commit builds cleanly when installed from
# the lockfile.
#
# A resolver free to drift ships a bundle no one has run. Frozen, the image gets
# the exact tree the tests passed against, and a lockfile that has gone stale
# fails HERE — loudly, naming the mismatch — instead of silently building
# something else.
#
# EVERY workspace member's package.json must be present before a frozen install:
# pnpm validates the lockfile against all of them and refuses if one is missing.
# apps/account is not built into this image, but it IS in the workspace, so its
# manifest is required for the check to pass.
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./
COPY apps/web/package.json apps/web/
COPY apps/account/package.json apps/account/
COPY pkgs/shared/package.json pkgs/shared/
COPY pkgs/auth/package.json pkgs/auth/
COPY pkgs/connect/package.json pkgs/connect/
COPY pkgs/idv/package.json pkgs/idv/
COPY pkgs/onboarding/package.json pkgs/onboarding/
RUN pnpm install --frozen-lockfile
COPY apps apps
COPY pkgs pkgs
# Publishable event-ingest key (pk-live-…), inlined by Vite into the bundle.
#
# PUBLISHABLE_KEY is the name in KMS (org `hanzo`, path `deploy`, env `prod`)
# and on the --build-arg; the VITE_ prefix is what makes Vite inline it, and it
# is a property of THIS build, so it is applied here and the secret store keeps
# the ONE plain name.
#
# Publishable and write-only by design — it authorizes a write into one org and
# can read nothing — so shipping it in a bundle is the documented use. It is
# still a credential: it comes from KMS via CI. Never commit a value here.
#
# Deliberately NO default. An absent key is not a degraded mode: the door refuses
# the beacon outright. Measured against the live endpoint with and without a
# browser Origin, for pageviews AND exceptions alike: a hard 401
# `ingest_key_required`. Nothing arrives.
#
# This comment used to say the unkeyed beacon took an "anonymous lane" that filed
# rows under a `$public` tenant and answered 200. That lane is not implemented in
# the deployed cloud, and the belief was expensive: it is why "we still get
# errors, just not events" was accepted across four repos while the true answer
# was that a keyless surface reports nothing at all. hanzo.id ran exactly that
# way, which is the failure this gate exists to make loud.
ARG PUBLISHABLE_KEY
ENV VITE_PUBLISHABLE_KEY=$PUBLISHABLE_KEY
# Fail closed, and gate HERE because this is the one path every builder passes
# through — a guard in a workflow protects that lane only.
RUN case "$PUBLISHABLE_KEY" in \
pk-*) : ;; \
'') echo "PUBLISHABLE_KEY is empty - pass --build-arg PUBLISHABLE_KEY=<pk-...> (KMS deploy/PUBLISHABLE_KEY, env prod)" >&2; exit 1 ;; \
*) echo "PUBLISHABLE_KEY is not a publishable key (expected a pk- prefix)" >&2; exit 1 ;; \
esac
# Do NOT re-declare ARG VITE_PUBLISHABLE_KEY below this line. A later ARG of the
# same name shadows the ENV set above with an empty default, so the key resolves,
# passes the gate, and is then blanked before Vite inlines it — every step green,
# the bundle unattributed. That is exactly how hanzo.chat 1.0.58 shipped.
#
# `&&`, not `;`: with `;` the RUN exits with the status of the LAST command and a
# failed build would be masked. Assert on the bytes that actually ship — a key
# present in the environment and absent from the bundle is indistinguishable from
# success everywhere except the warehouse, where the traffic simply stops being
# attributable.
RUN pnpm --filter @hanzo/id-web build && \
{ grep -rqF "$VITE_PUBLISHABLE_KEY" apps/web/dist || \
{ echo "ERROR: the ingest key is not in apps/web/dist - hanzo.id would ship unattributed" >&2; exit 1; }; }
# SPA server stage — hanzoai/spa is the correct base for a Vite SPA:
# history-API fallthrough for client-side routes AND a SPA-safe CSP.
# hanzoai/static defaults to `Content-Security-Policy: default-src 'none'`
# (built for static assets, not an app that loads its own bundle), which
# blocks the SPA's own scripts and leaves a blank page. hanzoai/spa serves
# index.html for all routes with a sane CSP. Defaults: PORT=3000, ROOT=/public.
FROM ghcr.io/hanzoai/spa:1.4.11
COPY --from=build /build/apps/web/dist /public
EXPOSE 3000