Skip to content

Publish to scoutos.live#24

Merged
twilson63 merged 5 commits into
mainfrom
feat/publish-to-scoutos-live
Jun 12, 2026
Merged

Publish to scoutos.live#24
twilson63 merged 5 commits into
mainfrom
feat/publish-to-scoutos-live

Conversation

@twilson63

Copy link
Copy Markdown
Collaborator

Implements docs/scoutos-live-prd.md — a signed-in user clicks Publish, the project is packaged server-side and deployed to Scout Live, and they get a stable live URL at {subdomain}.scoutos.live. Republishing the same project updates the same URL.

What's in here, by phase

  • Phase 0 — ports smoke test: verified hyper-zepto 0.1.0's remote adapters against scout-live's sidecar + data-router source; all six data ops the template uses match. Env mapping documented in the PRD (strip the trailing /_ports from SCOUT_PORTS_URL; no token — the sidecar injects auth). smoke/phase0-live-ports/ is the deployable smoke app; round trip verified locally against a platform-shaped stub.
  • Phase 1 — production template: the /api/db bridge moved to zepto-bridge.js (written once), mounted by vite.config.ts in dev and the new server.js (static dist/ + SPA fallback) in production. Remote vs local adapters selected from the Scout Live env. Both template copies, both prompt copies, and both test suites updated in sync.
  • Phase 2 — credentials: user_credentials table (PGlite, existing AES-256-GCM keyCrypto), PUT/GET/DELETE /api/credentials. Keys are write-only — responses carry a presence flag only.
  • Phase 3 — packaging + publish proxy: hand-rolled USTAR+gzip writer (~70 lines, no new dependency), Dockerfile/.dockerignore injection (numeric USER 1000 for runAsNonRoot; npm install because project maps carry no lockfile — image built and CRUD-tested under real Docker), deployments table, POST /api/publish + GET /api/publish/:buildId with distinct error codes (no_scoutos_key, subdomain_taken, scoutlive_rate_limited + Retry-After...), injectable scoutlive.ts client. Tests assert keys and publish codes never appear in responses or logs.
  • Phase 4 — Gleam UI: publish actor state machine, publish modal, toolbar rocket button (managed mode only), write-only ScoutOS key field in the account panel. Client-side subdomain validation mirrors the platform's rules + reserved list. 21 new Gleam tests; scripts/verify-publish-ui.mjs drives headless Chrome against the dev server with a faked backend (8/8 checks).
  • Phase 5 — rollout docs + gating: README section; non-managed mode shows no publish affordances (asserted headless).

Test status

  • Root: 70 Gleam + 56 vitest green. Server: 99 vitest green.

Remaining before merge (need a sk_live key — none was available locally)

  • Phase 0 live deploy: SCOUTOS_API_KEY=sk_live_... smoke/phase0-live-ports/deploy.sh <name>, then curl -X POST https://<name>.scoutos.live/verify, then republish with the printed publishCode.
  • One real POST /api/build acceptance of a server-generated tar.gz.
  • End-to-end on a PR preview (sign in → save key → build → publish → edit → republish).

🤖 Generated with Claude Code

twilson63 and others added 5 commits June 12, 2026 10:01
…rver

Phase 0 (PRD docs/scoutos-live-prd.md): verified hyper-zepto 0.1.0 remote
adapters against scout-live's ports sidecar and data router source. All six
data ops the template uses match; documented the env mapping (strip the
trailing /_ports from SCOUT_PORTS_URL, no token) in the PRD. Added
smoke/phase0-live-ports/, a deployable hyper-zepto app that exercises data
CRUD through the sidecar; live deploy awaits an sk_live key.

Phase 1: starter template now production-deployable. The /api/db bridge
moved to zepto-bridge.js (written once), mounted by vite.config.ts in dev
and by the new server.js in production, which serves dist/ with SPA
fallback. resolvePorts() picks remote adapters when SCOUT_PORTS_URL/
SCOUTOS_PORTS_URL is set, local .zepto otherwise. Both template copies,
both prompt copies, and both test suites updated in sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
user_credentials table in the embedded PGlite store (keyed by clerk_user_id
+ provider), encrypted with the existing AES-256-GCM keyCrypto. Endpoints:
PUT /api/credentials/scoutos (validate sk_live_/sk_test_ shape, encrypt,
upsert), GET /api/credentials (presence flag only), DELETE. Keys are
write-only — no response ever contains one; tests assert the flag-only
contract, 401/400 paths, PGlite round-trip, and migration idempotency.
Migration now runs one statement per query() call since PGlite rejects
multi-statement prepared statements.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- tar.ts: minimal USTAR + gzip writer (~70 lines, no new dependency);
  deterministic headers, prefix-field splitting for long paths, verified
  against bsdtar extraction.
- publish.ts: subdomain validation mirroring the platform's rules and
  reserved list, plus Dockerfile/.dockerignore injection (node:20-alpine,
  npm install — project maps carry no lockfile — vite build, numeric
  USER 1000 for the runAsNonRoot securityContext, EXPOSE 3000, node
  server.js). Image built and CRUD-tested in Docker locally.
- scoutlive.ts: injectable Scout Live client shaped like openrouter.ts,
  mapping 409/403/429 to typed results.
- deployments table keyed by (clerk_user_id, project_id); republish reuses
  the recorded subdomain and sends the stored publishCode, which is
  persisted encrypted when the first deployed status reveals it.
- POST /api/publish and GET /api/publish/:buildId in app.ts with distinct
  error codes (no_scoutos_key, invalid_subdomain, reserved_subdomain,
  subdomain_taken, scoutlive_rate_limited + Retry-After, payload_too_large).
  Tests assert keys and publish codes never appear in responses or logs.
- Phase 0 smoke Dockerfile: USER node -> USER 1000 (K8s runAsNonRoot can
  only verify numeric UIDs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New publish actor (state machine: check deployment -> prompt or republish ->
publishing -> polling -> deployed/failed/needs-key) with client-side
subdomain validation mirroring the platform rules and reserved list, so bad
names fail before any network call. The top-level update attaches the
project file map when the actor enters Publishing. Runtime + publish.mjs
external handle /api/credentials, /api/deployments/:projectId (new server
endpoint so republish skips the prompt after a reload), /api/publish, and
status polling via SchedulePoll round-trips.

UI: rocket button in the project toolbar (managed mode only — non-managed
shows no publish affordances), publish modal with prompt/progress/live-URL/
failure-logs states, and a write-only ScoutOS key field in the account
panel showing set/not-set only.

21 new Gleam tests cover the wiring; scripts/verify-publish-ui.mjs drives
headless Chrome against the dev server with a faked publish backend —
happy path, local subdomain feedback, republish-without-prompt, missing-key
routing to settings, and the non-managed gate all pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@twilson63
twilson63 temporarily deployed to feat/publish-to-scoutos-live - build PR #24 June 12, 2026 14:33 — with Render Destroyed

@hyperio-mc hyperio-mc left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed PR #24 — Publish to scoutos.live. +3035/-107 across 41 files. Approving.

Architecture is clean:

  • 5-phase PRD fully implemented with outcomes documented and success criteria checked off
  • Credentials stored encrypted (AES-256-GCM via existing keyCrypto), write-only — responses carry presence flags only, tests assert keys never leak into responses/logs
  • Packaging uses a hand-rolled ~70-line USTAR+gzip writer (tar.ts) with proper prefix/name splitting for long paths — no new dependency. The generated Dockerfile uses npm install (project file maps carry no lockfile) and numeric USER 1000 (required for runAsNonRoot securityContext)
  • scoutlive.ts client mirrors the openrouter.ts injectable pattern — faked in tests, clean error type taxonomy (subdomain_taken, invalid_code, scoutlive_rate_limited + Retry-After)
  • Publish flow: first publish prompts for subdomain (pre-validated client-side against naming rules + reserved list before any network call), republish reuses stored subdomain and sends the persisted publishCode

Security:

  • Subdomain validated server-side with regex + reserved list; client mirrors for pre-flight feedback
  • Content-Length + body size checked against MAX_BODY_BYTES (2MB)
  • PublishCode stored encrypted, decrypted only inside the publish handler
  • No credentials in responses or logs — verified by tests
  • The PUBLISH_DOCKERFILE test asserts no USER root, --privileged, --cap-add, curl|sh patterns — mirrors scout-live validation rules

Client (Gleam):

  • New publish actor state machine with modal + toolbar rocket button
  • Write-only ScoutOS key field in account panel
  • verify-publish-ui.mjs headless Chromium harness — 8/8 checks pass including republish-without-prompt
  • Non-managed mode shows zero publish affordances (asserted headless)

DB:

  • user_credentials table (clerk_user_id + provider PK, key_enc bytea)
  • deployments table (clerk_user_id + project_id, subdomain, publish_code_enc, last_build_id, last_url)
  • Migrations split into individual statements (PGlite rejects multi-statement prepared statements)

Smoke (Phase 0):

  • smoke/phase0-live-ports/ with deploy.sh, Dockerfile, index.js exercising all 6 data-port ops against hyper-zepto remote adapters
  • Wire format verified against scout-live sidecar + data-router sources
  • 3 live checks pending a sk_live key

155 client + 99 server tests pass. Solid end-to-end implementation of the full publish pipeline.

@twilson63
twilson63 marked this pull request as ready for review June 12, 2026 17:20
@twilson63
twilson63 merged commit 7b8ce30 into main Jun 12, 2026
@twilson63
twilson63 deleted the feat/publish-to-scoutos-live branch June 12, 2026 17:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants