From 01732834bab297d4d98e5a6460af2b5522943915 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 01:03:32 +0000 Subject: [PATCH 1/2] fix: production-readiness blockers (Polar prod fallback, CI gate, base-image pinning) - auth: fail-fast in production when Polar product IDs are unconfigured instead of silently serving sandbox products (which would not charge real cards). Adds a `strict` flag so the webhook inverse-lookup still resolves ids across envs. - ci: add a lightweight PR gate (vp check + vp test via setup-vp), kept separate from the heavy Nix `om ci` workflow which stays workflow_dispatch-only. - nix(containers): refuse to build from an unpinned base image (still lib.fakeSha256) with a clear, actionable error instead of a cryptic hash mismatch; document pinning via nix-prefetch-docker. https://claude.ai/code/session_01TMCMpDmbLomRBZbg3FpvMV --- .github/workflows/ci-checks.yml | 35 +++++++++++++++++++++++++ nix/stackpanel/lib/containers.nix | 6 +++++ packages/auth/src/lib/polar-products.ts | 24 ++++++++++++----- 3 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci-checks.yml diff --git a/.github/workflows/ci-checks.yml b/.github/workflows/ci-checks.yml new file mode 100644 index 00000000..ba6971dd --- /dev/null +++ b/.github/workflows/ci-checks.yml @@ -0,0 +1,35 @@ +name: CI Checks + +# Lightweight PR gate: format + lint + typecheck + tests via the Vite+ (`vp`) +# toolchain this repo uses. This is intentionally separate from the heavy Nix +# `om ci` workflow (which remains workflow_dispatch-only) so that every PR gets +# fast feedback on code health without paying for full Nix builds. +on: + pull_request: + push: + branches: [main] + +concurrency: + group: ci-checks-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: check & test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Vite+ toolchain + uses: voidzero-dev/setup-vp@v1 + with: + cache: true + + - name: Install dependencies + run: vp install + + - name: Format, lint, and typecheck + run: vp check + + - name: Run tests + run: vp test diff --git a/nix/stackpanel/lib/containers.nix b/nix/stackpanel/lib/containers.nix index d1c4ffc1..83ecb19a 100644 --- a/nix/stackpanel/lib/containers.nix +++ b/nix/stackpanel/lib/containers.nix @@ -94,7 +94,13 @@ let else let spec = defaultBaseImageSpecs.${type} or defaultBaseImageSpecs.alpine; + # Refuse to build from an unpinned base image: the unpinned specs carry + # lib.fakeSha256, which would otherwise fail later as a cryptic hash + # mismatch. Pin with `nix-prefetch-docker` (set imageDigest + sha256). + isPinned = (spec.sha256 or lib.fakeSha256) != lib.fakeSha256; in + assert lib.assertMsg isPinned + "stackpanel: base image '${spec.imageName}' is not pinned (still lib.fakeSha256); pin it with nix-prefetch-docker before building a container from it."; nix2containerLib.pullImage spec; # --------------------------------------------------------------------------- diff --git a/packages/auth/src/lib/polar-products.ts b/packages/auth/src/lib/polar-products.ts index 72936c12..5f4119d4 100644 --- a/packages/auth/src/lib/polar-products.ts +++ b/packages/auth/src/lib/polar-products.ts @@ -26,13 +26,25 @@ const SANDBOX_FREE = "6abf1427-cf83-4251-90d7-8852e7e5ca21"; */ export function polarProducts( env: DeployEnv = resolveDeployEnv(), + { strict = true }: { strict?: boolean } = {}, ): Record { switch (env) { - case "production": - return { - pro: process.env.POLAR_PRO_PRODUCT_ID_PRODUCTION ?? SANDBOX_PRO, - free: process.env.POLAR_FREE_PRODUCT_ID_PRODUCTION ?? SANDBOX_FREE, - }; + case "production": { + const pro = process.env.POLAR_PRO_PRODUCT_ID_PRODUCTION; + const free = process.env.POLAR_FREE_PRODUCT_ID_PRODUCTION; + if (pro && free) return { pro, free }; + if (strict) { + throw new Error( + "Polar production product IDs are not configured. Set " + + "POLAR_PRO_PRODUCT_ID_PRODUCTION and POLAR_FREE_PRODUCT_ID_PRODUCTION. " + + "Refusing to silently fall back to sandbox products in production — " + + "that would serve sandbox checkouts to real users and never charge real cards.", + ); + } + // Non-strict callers (e.g. the webhook inverse-lookup, which may run + // outside a production runtime) only need id->plan resolution. + return { pro: pro ?? SANDBOX_PRO, free: free ?? SANDBOX_FREE }; + } case "preview": case "dev": default: @@ -65,7 +77,7 @@ export function planForProduct(productId: string | null | undefined): PlanId | " if (!productId) return "unknown"; const envs: DeployEnv[] = ["production", "preview", "dev"]; for (const env of envs) { - const products = polarProducts(env); + const products = polarProducts(env, { strict: false }); for (const [plan, id] of Object.entries(products) as [PlanId, string][]) { if (id === productId) return plan; } From c6b9d20ef7f24976a977dacc9087cc0e0849b9c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 4 Jun 2026 14:34:11 +0000 Subject: [PATCH 2/2] ci: scope gate to a portable tsc typecheck + restrict token permissions vp check/test can't run in clean CI: vite.config.ts imports `vite-plus`, which the vp toolchain provisions locally but isn't resolvable after setup-vp. Gate on the repo's portable `typecheck` turbo tasks (tsc --noEmit) instead, excluding @stackpanel/sdk (pre-existing, unrelated typecheck failure). Add a typecheck script to @stackpanel/auth so the Polar change is covered. Set an explicit least-privilege permissions block (resolves CodeQL finding). https://claude.ai/code/session_01TMCMpDmbLomRBZbg3FpvMV --- .github/workflows/ci-checks.yml | 40 ++++++++++++++++++++------------- packages/auth/package.json | 4 +++- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci-checks.yml b/.github/workflows/ci-checks.yml index ba6971dd..ddf2a7dc 100644 --- a/.github/workflows/ci-checks.yml +++ b/.github/workflows/ci-checks.yml @@ -1,35 +1,45 @@ name: CI Checks -# Lightweight PR gate: format + lint + typecheck + tests via the Vite+ (`vp`) -# toolchain this repo uses. This is intentionally separate from the heavy Nix -# `om ci` workflow (which remains workflow_dispatch-only) so that every PR gets -# fast feedback on code health without paying for full Nix builds. +# Lightweight, portable PR gate: a TypeScript typecheck via the repo's own +# `typecheck` turbo tasks (tsc --noEmit). +# +# It intentionally does NOT use `vp check` / `vp test`: those load +# vite.config.ts, which imports the `vite-plus` package. `vite-plus` is +# provisioned by the local `vp` toolchain but is not a declared dependency and +# is not resolvable in a clean CI checkout, so any vite-config-loading command +# fails there. This gate is also separate from the heavy Nix `om ci` workflow. +# +# `@stackpanel/sdk` is excluded because its typecheck has a pre-existing failure +# (its tsconfig does not pull in node types) unrelated to this gate. Remove the +# `--filter` exclusion once that's fixed so the gate covers every package that +# defines a `typecheck` script. on: pull_request: push: branches: [main] +# Least-privilege GITHUB_TOKEN (CodeQL: workflows should set explicit permissions). +permissions: + contents: read + concurrency: group: ci-checks-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - check: - name: check & test + typecheck: + name: typecheck runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up Vite+ toolchain - uses: voidzero-dev/setup-vp@v1 + - name: Set up Bun + uses: oven-sh/setup-bun@v2 with: - cache: true + bun-version: "1.3.2" - name: Install dependencies - run: vp install - - - name: Format, lint, and typecheck - run: vp check + run: bun install --frozen-lockfile - - name: Run tests - run: vp test + - name: Typecheck (tsc --noEmit) + run: bunx turbo run typecheck --filter='!@stackpanel/sdk' diff --git a/packages/auth/package.json b/packages/auth/package.json index 28659f6c..6708ee00 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -9,7 +9,9 @@ } }, "type": "module", - "scripts": {}, + "scripts": { + "typecheck": "tsc --noEmit" + }, "devDependencies": { "@stackpanel/config": "workspace:*" },