feat(firmware): allow env import token

This commit is contained in:
Mitchell R 2026-05-20 05:02:12 +02:00
parent d4abc86999
commit 444bb4c116
No known key found for this signature in database
3 changed files with 26 additions and 3 deletions

View file

@ -38,6 +38,10 @@ services:
- BF_SELF_URL=http://server:18080 - BF_SELF_URL=http://server:18080
# Optional: paste Ed25519 PEM private key here for firmware signing. # Optional: paste Ed25519 PEM private key here for firmware signing.
# - BF_FIRMWARE_SIGNING_KEY= # - 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). # Optional MQTT telemetry bridge (ThingsBoard / HA / Influx / etc).
# - BF_MQTT_URL=mqtt://broker:1883 # - BF_MQTT_URL=mqtt://broker:1883
# - BF_MQTT_USERNAME= # - BF_MQTT_USERNAME=

View file

@ -6,6 +6,7 @@
* record so downstream handlers (which always read `event.context.user`) * record so downstream handlers (which always read `event.context.user`)
* keep working unchanged. * keep working unchanged.
*/ */
import { createHash, timingSafeEqual } from "node:crypto";
import { type H3, getCookie, getRequestPath } from "h3"; import { type H3, getCookie, getRequestPath } from "h3";
import type { AdminDeps } from "./index.js"; import type { AdminDeps } from "./index.js";
import type { User, Session } from "../../shared/types.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 { export function registerMiddleware(app: H3, deps: AdminDeps): void {
app.use(async (event) => { app.use(async (event) => {
const path = getRequestPath(event); const path = getRequestPath(event);
@ -70,6 +79,15 @@ export function registerMiddleware(app: H3, deps: AdminDeps): void {
const authz = event.req.headers.get("authorization"); const authz = event.req.headers.get("authorization");
if (authz && authz.startsWith("Bearer ")) { if (authz && authz.startsWith("Bearer ")) {
const token = authz.slice(7); 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")); const key = await deps.auth.verifyApiKey(token, event.req.headers.get("x-real-ip"));
if (!key || !key.scopes.includes("admin")) { if (!key || !key.scopes.includes("admin")) {
return new Response(null, { status: 401 }); return new Response(null, { status: 401 });

View file

@ -3,10 +3,11 @@
* *
* Upload path supports: * Upload path supports:
* - browser multipart form ("upload from your machine") * - 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 <token>. 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, * POST /api/admin/firmware/import with JSON {version, channel, arch,
* signature, sha256, release_notes, content_b64} so GitHub Actions can * release_notes, content_b64} so GitHub Actions can publish releases
* publish releases without a session. * without a session.
*/ */
import { type H3, getRouterParam, readBody, createError } from "h3"; import { type H3, getRouterParam, readBody, createError } from "h3";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";