Files
Hankan/.github/workflows/ci.yml
MechaCat02 b48a5f8fb1 fix(server): CORS, without which the phone could never reach the Pi
The Android build always talks to the Pi cross-origin -- a Capacitor
webview serves the app from http://localhost, not from your domain -- so
the browser sends a preflight OPTIONS first. It is not permitted to attach
an Authorization header to that. Auth ran before anything else, so the
preflight came back 401 and the real request was never attempted.

There were no Access-Control-Allow-* headers either, so even a successful
preflight would not have helped. Verified from an actual page before the
fix: GET /api/sync and POST /api/tutor both "Failed to fetch" -- an opaque
network error that points at the network rather than at middleware order.

CORS now runs first and answers OPTIONS itself. Any origin is allowed by
default, which is not a hole: the gate is a bearer token rather than a
cookie, so a hostile page gains nothing from being allowed to send a
request it cannot authenticate. HANKAN_ALLOWED_ORIGINS narrows it.

backends/echo.ts is a keyless backend that reflects the request back in
chunks. Deploying involves a container, a reverse proxy, a token, CORS and
an SSE stream that has to survive compression -- five things that break
independently, none of which involve Anthropic. HANKAN_TUTOR_BACKEND=echo
proves all five from the phone before a key exists and before anything is
billed. CI now runs the server that way, so the tutor endpoint is
exercised over real HTTP rather than only against an injected mock.

test/server/http.test.ts covers the preflight, the allow-origin header on
real responses, Vary: Origin, that a bad token is still refused, and that
the SSE stream parses and terminates with a done event.

Verified end to end in a browser: the Pi configured through the settings
panel, sync pushing 2 rows and a second sync moving 0 (the pushedAt
watermark holding), the header switching from "local stand-in" to
"connected", and a turn streaming back over SSE with the 8,859-character
system prompt intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 21:47:18 +02:00

72 lines
2.1 KiB
YAML

name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# The curriculum gate comes first, before anything else can run.
# validate.mjs reads only data/ and lib/ and exits non-zero on a
# blocking failure. Baseline: PASS — 0 blocking, 0 advisory.
- name: Curriculum validation
run: node validate.mjs
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Typecheck
run: npm run typecheck
# The sync round-trip needs a real Postgres and a running server, so a
# fake would not exercise the change_seq trigger, the last-write-wins
# upsert, or cursor paging — which is where sync actually goes wrong.
- name: Start Postgres
run: |
docker run -d --name hankan-pg \
-e POSTGRES_PASSWORD=test -e POSTGRES_DB=hankan \
-p 55432:5432 postgres:16-alpine
for i in $(seq 1 30); do
docker exec hankan-pg pg_isready -U postgres -d hankan && break
sleep 1
done
- name: Start the sync server
env:
DATABASE_URL: postgres://postgres:test@localhost:55432/hankan
HANKAN_TOKEN: test-token
HANKAN_TEST_MODE: "1"
# Keyless, so the tutor endpoint is exercised over real HTTP —
# SSE framing, CORS and auth — without an Anthropic key in CI.
HANKAN_TUTOR_BACKEND: echo
PORT: "8788"
run: |
node --experimental-strip-types server/src/main.ts &
for i in $(seq 1 30); do
curl -sf http://localhost:8788/health && break
sleep 1
done
- name: Tests
env:
HANKAN_TEST_SERVER: http://localhost:8788
run: npm test
# Every word in every unit's words[] must resolve in lemma or surface.
# Runs against the committed band files in app/public/dict/.
- name: Roadmap word coverage
run: npm run dict:assert
- name: Build
run: npm run build