From 444bb4c1161ac7e68ed665d11794e97bea42ec8a Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Wed, 20 May 2026 05:02:12 +0200 Subject: [PATCH] feat(firmware): allow env import token --- docker-compose.yml | 4 ++++ .../plugins/service-admin-http/middleware.ts | 18 ++++++++++++++++++ .../service-admin-http/routes-firmware.ts | 7 ++++--- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 724e232..e0eafcd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,10 @@ services: - BF_SELF_URL=http://server:18080 # Optional: paste Ed25519 PEM private key here for firmware signing. # - BF_FIRMWARE_SIGNING_KEY= + # Optional: single-purpose Bearer token for GitHub Actions firmware import. + # Set GitHub's BF_AUTOIMPORT_API_KEY secret to the same value. + # Generate with: openssl rand -base64 32 + # - BF_FIRMWARE_IMPORT_API_KEY= # Optional MQTT telemetry bridge (ThingsBoard / HA / Influx / etc). # - BF_MQTT_URL=mqtt://broker:1883 # - BF_MQTT_USERNAME= diff --git a/server/src/plugins/service-admin-http/middleware.ts b/server/src/plugins/service-admin-http/middleware.ts index 68c1f7f..90c151e 100644 --- a/server/src/plugins/service-admin-http/middleware.ts +++ b/server/src/plugins/service-admin-http/middleware.ts @@ -6,6 +6,7 @@ * record so downstream handlers (which always read `event.context.user`) * keep working unchanged. */ +import { createHash, timingSafeEqual } from "node:crypto"; import { type H3, getCookie, getRequestPath } from "h3"; import type { AdminDeps } from "./index.js"; import type { User, Session } from "../../shared/types.js"; @@ -36,6 +37,14 @@ function syntheticApiKeyUser(keyPrefix: string): User { }; } +function tokenMatchesEnv(token: string, envName: string): boolean { + const expected = process.env[envName]?.trim(); + if (!expected || expected.length < 32 || token.length < 32) return false; + const a = createHash("sha256").update(token).digest(); + const b = createHash("sha256").update(expected).digest(); + return timingSafeEqual(a, b); +} + export function registerMiddleware(app: H3, deps: AdminDeps): void { app.use(async (event) => { const path = getRequestPath(event); @@ -70,6 +79,15 @@ export function registerMiddleware(app: H3, deps: AdminDeps): void { const authz = event.req.headers.get("authorization"); if (authz && authz.startsWith("Bearer ")) { const token = authz.slice(7); + if ( + path === "/api/admin/firmware/import" && + tokenMatchesEnv(token, "BF_FIRMWARE_IMPORT_API_KEY") + ) { + event.context.user = syntheticApiKeyUser("fw-import"); + event.context.apiKeyPrefix = "fw-import"; + return; + } + const key = await deps.auth.verifyApiKey(token, event.req.headers.get("x-real-ip")); if (!key || !key.scopes.includes("admin")) { return new Response(null, { status: 401 }); diff --git a/server/src/plugins/service-admin-http/routes-firmware.ts b/server/src/plugins/service-admin-http/routes-firmware.ts index 76fd7ba..cdcd8f8 100644 --- a/server/src/plugins/service-admin-http/routes-firmware.ts +++ b/server/src/plugins/service-admin-http/routes-firmware.ts @@ -3,10 +3,11 @@ * * Upload path supports: * - browser multipart form ("upload from your machine") - * - CI auto-import via API key (header X-BetterFrame-API-Key: bf-…) + * - CI auto-import via Authorization: Bearer . The token may be a + * DB-backed admin API key or the single-purpose BF_FIRMWARE_IMPORT_API_KEY. * POST /api/admin/firmware/import with JSON {version, channel, arch, - * signature, sha256, release_notes, content_b64} so GitHub Actions can - * publish releases without a session. + * release_notes, content_b64} so GitHub Actions can publish releases + * without a session. */ import { type H3, getRouterParam, readBody, createError } from "h3"; import { randomUUID } from "node:crypto";