docs(openspec): archive fpagent-release-installer and sync specs
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-02
|
||||
@@ -0,0 +1,85 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
See proposal.md — Why. The `fingerprint-agent` repository and its releases are
|
||||
private: an unauthenticated request to the forge's releases API answers `404`.
|
||||
Releases already publish, per build, a generic bundle
|
||||
(`fpagent-<version>-linux-x86_64.tar.gz`), per-Ubuntu offline bundles
|
||||
(`fpagent-<version>-ubuntu-<codename>-x86_64.tar.gz`) with the libfprint `.deb`
|
||||
closure vendored, and a `.sha256` beside each. Every bundle already contains
|
||||
the fpagent binary, the udev rule, and its own `install.sh` that handles
|
||||
libfprint, udev, `plugdev` and the systemd service. The tools app is an Express
|
||||
server with a hardcoded-configuration policy whose only sanctioned environment
|
||||
variables were the Image Cropper credentials.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- A field laptop installs or updates fpagent with one public command.
|
||||
- The forge credential never reaches the field laptop.
|
||||
- Reuse the bundle's installer rather than reimplementing hardware setup.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Replacing the bundle installer, or the forge's own release tooling.
|
||||
- Authenticating the installer: it is intentionally public.
|
||||
- Supporting architectures other than x86_64 (the bundle installer already
|
||||
requires it).
|
||||
|
||||
## Decisions
|
||||
|
||||
1. **Proxy the release through the app; do not embed the key in the script.**
|
||||
The credential lives in `GITEA_RELEASE_INSTALL_KEY` and the app fetches the
|
||||
private release server-side. The alternative — rendering the key into the
|
||||
served script so the client could call the forge directly — publishes a
|
||||
repository credential to every installer and any `curl`; rejected.
|
||||
|
||||
2. **Two thin routes, no proxy framework.** `/fpagent/releases/latest` returns
|
||||
a trimmed `{tag, assets:[{name,url}]}` with same-origin URLs, and
|
||||
`/fpagent/releases/download/:tag/:name` streams one asset. Both are
|
||||
registered before the session middleware so they stay public and do no
|
||||
per-request PocketBase work. A single combined "give me the right tarball"
|
||||
route was considered but needs the server to guess the client's OS; the
|
||||
client knows its own codename, so selection happens in the script.
|
||||
|
||||
3. **The installer delegates to the bundle's own `install.sh`.** All hardware
|
||||
and dependency logic already exists and is versioned with the binary;
|
||||
reimplementing it in the hosted script would duplicate it and drift. The
|
||||
hosted script only fetches, verifies, extracts, and forwards flags.
|
||||
|
||||
4. **Bundle selection prefers the codename offline bundle.** When the host's
|
||||
`/etc/os-release` `VERSION_CODENAME` matches a published offline bundle, use
|
||||
it (no network needed for the dependency closure); otherwise fall back to
|
||||
the generic online bundle, whose installer apt-gets libfprint.
|
||||
|
||||
5. **Checksum verification, not signature verification.** The release publishes
|
||||
sha256 only, so the installer verifies sha256 over TLS from the same origin
|
||||
it trusts for the script itself. GPG would require release signing that does
|
||||
not exist yet; deferred.
|
||||
|
||||
6. **Asset names are validated server-side** against a conservative pattern,
|
||||
so a crafted tag or name cannot walk the forge path or reach another host.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [A compromised tools app serves a malicious installer] → the app is already
|
||||
trusted to serve the tools pages and holds the operator session; the
|
||||
installer is not a new trust boundary. TLS to `tools.yrka.app` is the same
|
||||
trust as the app.
|
||||
- [The forge changes its bundle naming] → selection falls back to the generic
|
||||
bundle, and a release with no matching asset fails loudly rather than
|
||||
installing something wrong.
|
||||
- [The app buffers an asset in memory] → bundles are tens of MB; acceptable,
|
||||
and requests are rare. Streaming can replace it if that changes.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Add `GITEA_RELEASE_INSTALL_KEY` to `.env`/`.env.production` (already supplied)
|
||||
and deploy the app; the installer and proxy are new routes and change no
|
||||
existing behavior. Rollback = remove the routes; nothing else depends on them.
|
||||
|
||||
## Open Questions
|
||||
|
||||
None.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Proposal: fpagent-release-installer
|
||||
|
||||
## Why
|
||||
|
||||
Field laptops need `fpagent` installed and kept current, but the
|
||||
`fingerprint-agent` repository is private: its releases require a forge token
|
||||
that operators do not have. Today an operator must obtain a bundle out of band
|
||||
and run its installer by hand. A public one-line installer hosted by the tools
|
||||
app removes that step while keeping the forge credential server-side.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a public installer script served at `/fpagent/install.sh`, intended to be
|
||||
piped into `bash`. It fetches the latest release, selects the bundle matching
|
||||
the host (the Ubuntu-codename offline bundle when present, else the generic
|
||||
online bundle), verifies the published sha256, extracts it, and runs the
|
||||
bundle's own installer, forwarding flags such as `--enable-service`,
|
||||
`--check` and `--uninstall`.
|
||||
- Add same-origin proxy routes `/fpagent/releases/latest` and
|
||||
`/fpagent/releases/download/:tag/:name` that read the private release from the
|
||||
forge using the `GITEA_RELEASE_INSTALL_KEY` environment variable and stream
|
||||
assets to the client. The token and the forge host are never exposed.
|
||||
- Extend the app's sanctioned environment configuration to include the installer
|
||||
key, alongside the existing Image Cropper credentials.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `fpagent-installer`: the public installer surface — the download/verify/handoff
|
||||
script and the token-guarded same-origin release proxy that backs it.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `tools-app`: the hardcoded-configuration requirement now sanctions a second
|
||||
environment variable (the fpagent installer key) in addition to the Image
|
||||
Cropper credentials.
|
||||
|
||||
## Impact
|
||||
|
||||
- `tools/server.js`: `GITEA_URL` / `FPAGENT_REPO` constants, the installer key
|
||||
read, and two public routes registered before the session middleware.
|
||||
- `tools/public/fpagent/install.sh`: new installer script.
|
||||
- `tools/test/fpagent-install.test.js`: proxy and script tests.
|
||||
- `.env` / `.env.production` hold the key; `.env.example` and `README.md`
|
||||
document the install flow.
|
||||
- Depends on the private `fingerprint-agent` releases and their bundle naming
|
||||
(`fpagent-<version>-{linux-x86_64|ubuntu-<codename>-x86_64}.tar.gz`) plus the
|
||||
matching `.sha256` assets.
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
# fpagent-installer Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Lets a field laptop install the latest private `fpagent` release with a single
|
||||
public command, without ever handling the forge credential — the tools app
|
||||
proxies the private release and serves the installer.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Public installer endpoint
|
||||
|
||||
The app SHALL serve an installer script at `/fpagent/install.sh` that is
|
||||
reachable without authentication. When run, the script SHALL fetch the latest
|
||||
release metadata, choose a bundle for the host, verify the bundle's published
|
||||
sha256 checksum, extract it, and hand off to the extracted bundle's own
|
||||
installer. The script SHALL be served uncached.
|
||||
|
||||
#### Scenario: Installer is fetched anonymously
|
||||
|
||||
- **WHEN** a client requests `/fpagent/install.sh` without a session
|
||||
- **THEN** the app returns the installer script and does not redirect to login
|
||||
|
||||
#### Scenario: Host-appropriate bundle is chosen
|
||||
|
||||
- **WHEN** the latest release offers both a bundle for the host's Ubuntu
|
||||
codename and a generic bundle
|
||||
- **THEN** the installer uses the codename bundle
|
||||
|
||||
#### Scenario: Generic bundle is the fallback
|
||||
|
||||
- **WHEN** the latest release offers no bundle matching the host's Ubuntu
|
||||
codename
|
||||
- **THEN** the installer uses the generic bundle
|
||||
|
||||
#### Scenario: Checksum is verified before extraction
|
||||
|
||||
- **WHEN** the installer downloads a bundle and its published checksum
|
||||
- **THEN** it verifies the checksum before extracting and refuses to install
|
||||
when the checksum does not match
|
||||
|
||||
#### Scenario: Options are forwarded to the bundled installer
|
||||
|
||||
- **WHEN** the installer is invoked with options such as `--enable-service` or
|
||||
`--check`
|
||||
- **THEN** it passes those options through to the extracted bundle's installer
|
||||
|
||||
#### Scenario: No compatible bundle
|
||||
|
||||
- **WHEN** the latest release contains no bundle for the host architecture
|
||||
- **THEN** the installer exits with a failure message and installs nothing
|
||||
|
||||
### Requirement: Release metadata endpoint
|
||||
|
||||
The app SHALL expose a same-origin endpoint that returns the latest release's
|
||||
tag and the names of its downloadable assets as same-origin URLs. The app SHALL
|
||||
authenticate to the private forge using the configured release key, SHALL omit
|
||||
asset names that are not valid asset names, and SHALL NOT reveal the release
|
||||
key or the forge host to clients.
|
||||
|
||||
#### Scenario: Latest release is returned
|
||||
|
||||
- **WHEN** a client requests the latest-release endpoint and the forge returns
|
||||
a release
|
||||
- **THEN** the response carries the release tag and each asset's name and a
|
||||
same-origin URL under `/fpagent/releases/download/`
|
||||
|
||||
#### Scenario: Unsafe asset names are omitted
|
||||
|
||||
- **WHEN** the forge release lists an asset whose name is not a valid asset
|
||||
name
|
||||
- **THEN** that asset does not appear in the response
|
||||
|
||||
#### Scenario: Release key is not exposed
|
||||
|
||||
- **WHEN** any client fetches the latest-release endpoint
|
||||
- **THEN** the response body contains neither the release key nor the forge host
|
||||
|
||||
#### Scenario: Installer not configured
|
||||
|
||||
- **WHEN** the release key is not configured
|
||||
- **THEN** the endpoint answers that the installer is not configured
|
||||
|
||||
#### Scenario: Forge unavailable
|
||||
|
||||
- **WHEN** the forge cannot be reached or returns an error for the latest
|
||||
release
|
||||
- **THEN** the endpoint answers with a gateway error
|
||||
|
||||
### Requirement: Release asset download proxy
|
||||
|
||||
The app SHALL stream a requested release asset to the client through a
|
||||
same-origin route, authenticating the upstream request with the release key so
|
||||
the client sends no credentials. It SHALL reject asset names that are not valid
|
||||
asset names and SHALL report a missing asset as not found.
|
||||
|
||||
#### Scenario: Asset is streamed without client credentials
|
||||
|
||||
- **WHEN** a client requests a release asset by tag and name without any
|
||||
credential
|
||||
- **THEN** the app fetches it from the forge with the release key and streams
|
||||
its bytes to the client
|
||||
|
||||
#### Scenario: Invalid asset name is rejected
|
||||
|
||||
- **WHEN** a client requests an asset whose tag or name is not a valid asset
|
||||
name
|
||||
- **THEN** the app rejects the request without contacting the forge
|
||||
|
||||
#### Scenario: Missing asset
|
||||
|
||||
- **WHEN** the forge reports the requested asset as not found
|
||||
- **THEN** the app answers with not found
|
||||
|
||||
#### Scenario: Installer not configured
|
||||
|
||||
- **WHEN** the release key is not configured
|
||||
- **THEN** the endpoint answers that the installer is not configured
|
||||
|
||||
### Requirement: Installer credential stays server-side
|
||||
|
||||
The app SHALL read the private-forge release key from the
|
||||
`GITEA_RELEASE_INSTALL_KEY` environment variable. The key SHALL NOT appear in
|
||||
the installer script, in any release metadata response, or in any streamed
|
||||
asset.
|
||||
|
||||
#### Scenario: Served installer carries no credential
|
||||
|
||||
- **WHEN** a client fetches `/fpagent/install.sh`
|
||||
- **THEN** the response contains no release key
|
||||
|
||||
#### Scenario: Credential supplied by the environment
|
||||
|
||||
- **WHEN** the release key is supplied in the environment
|
||||
- **THEN** the installer and proxy endpoints use it without any change to the
|
||||
hardcoded configuration
|
||||
@@ -0,0 +1,34 @@
|
||||
# tools-app Specification
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Configuration is hardcoded
|
||||
|
||||
The app SHALL hardcode all configuration in source: port `4173`, portal
|
||||
`https://portal.yrka.app`, PocketBase `https://pb.itcass.net`, a2loft
|
||||
`https://apis.a2loft.net`, agent `http://127.0.0.1:9000`, and the fingerprint
|
||||
agent release forge `https://sources.yrka.app`. There SHALL be no runtime
|
||||
configuration via environment variables or config files, with two exceptions:
|
||||
the Image Cropper's service credentials (Minio keys, Azure storage connection
|
||||
string, nodered credentials, cleanup-service key) SHALL come from environment
|
||||
variables and SHALL NOT be stored in source; and the Fingerprint Agent
|
||||
installer's release key (`GITEA_RELEASE_INSTALL_KEY`) SHALL come from the
|
||||
environment and SHALL NOT be stored in source.
|
||||
|
||||
#### Scenario: No configuration required to run
|
||||
|
||||
- **WHEN** the app is started with no environment variables or config files
|
||||
- **THEN** it serves on port 4173 with the hardcoded external endpoints
|
||||
|
||||
#### Scenario: Cropper credentials from the environment
|
||||
|
||||
- **WHEN** the cropper credentials are supplied as environment variables
|
||||
- **THEN** the cropper routes use them without any change to the hardcoded
|
||||
URL/port configuration
|
||||
|
||||
#### Scenario: Installer key from the environment
|
||||
|
||||
- **WHEN** the fpagent installer release key is supplied as an environment
|
||||
variable
|
||||
- **THEN** the installer and release-proxy routes use it without any change to
|
||||
the hardcoded URL/port configuration
|
||||
@@ -0,0 +1,23 @@
|
||||
# Tasks
|
||||
|
||||
- [x] 1. Server: `GITEA_URL`/`FPAGENT_REPO` constants, the installer key read
|
||||
from `GITEA_RELEASE_INSTALL_KEY`, and two public proxy routes registered
|
||||
before the session middleware: `GET /fpagent/releases/latest` (trimmed
|
||||
`{tag, assets:[{name,url}]}`, same-origin URLs, unsafe names dropped) and
|
||||
`GET /fpagent/releases/download/:tag/:name` (validated names, streamed with
|
||||
the key, 404/502 mapping, 503 when unconfigured).
|
||||
- [x] 2. Installer script `tools/public/fpagent/install.sh`: fetch metadata,
|
||||
prefer the host-codename offline bundle over the generic one, download and
|
||||
verify sha256, extract, and forward all options to the bundle's `install.sh`.
|
||||
Served uncached at `/fpagent/install.sh`.
|
||||
- [x] 3. Tests (`tools/test/fpagent-install.test.js`): latest proxy shape and
|
||||
token non-disclosure, asset streaming with upstream auth, invalid-name
|
||||
rejection, missing-asset 404, unconfigured 503, public script serving, and
|
||||
`bash -n` on the script.
|
||||
- [x] 4. Docs/config: `GITEA_RELEASE_INSTALL_KEY` in `.env.example`, installer
|
||||
and configuration notes in `README.md`.
|
||||
- [x] 5. Verified: 55 tests pass (48 existing + 7 new); routes exercised live
|
||||
against the forge (latest release `v1.0.0`, proxied checksum matches the
|
||||
publisher's, invalid name refused); the installer run end to end against a
|
||||
fake release base — codename bundle chosen, checksum pass and fail, options
|
||||
forwarded to the extracted bundle installer.
|
||||
@@ -0,0 +1,136 @@
|
||||
# fpagent-installer Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Lets a field laptop install the latest private `fpagent` release with a single
|
||||
public command, without ever handling the forge credential — the tools app
|
||||
proxies the private release and serves the installer.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Public installer endpoint
|
||||
|
||||
The app SHALL serve an installer script at `/fpagent/install.sh` that is
|
||||
reachable without authentication. When run, the script SHALL fetch the latest
|
||||
release metadata, choose a bundle for the host, verify the bundle's published
|
||||
sha256 checksum, extract it, and hand off to the extracted bundle's own
|
||||
installer. The script SHALL be served uncached.
|
||||
|
||||
#### Scenario: Installer is fetched anonymously
|
||||
|
||||
- **WHEN** a client requests `/fpagent/install.sh` without a session
|
||||
- **THEN** the app returns the installer script and does not redirect to login
|
||||
|
||||
#### Scenario: Host-appropriate bundle is chosen
|
||||
|
||||
- **WHEN** the latest release offers both a bundle for the host's Ubuntu
|
||||
codename and a generic bundle
|
||||
- **THEN** the installer uses the codename bundle
|
||||
|
||||
#### Scenario: Generic bundle is the fallback
|
||||
|
||||
- **WHEN** the latest release offers no bundle matching the host's Ubuntu
|
||||
codename
|
||||
- **THEN** the installer uses the generic bundle
|
||||
|
||||
#### Scenario: Checksum is verified before extraction
|
||||
|
||||
- **WHEN** the installer downloads a bundle and its published checksum
|
||||
- **THEN** it verifies the checksum before extracting and refuses to install
|
||||
when the checksum does not match
|
||||
|
||||
#### Scenario: Options are forwarded to the bundled installer
|
||||
|
||||
- **WHEN** the installer is invoked with options such as `--enable-service` or
|
||||
`--check`
|
||||
- **THEN** it passes those options through to the extracted bundle's installer
|
||||
|
||||
#### Scenario: No compatible bundle
|
||||
|
||||
- **WHEN** the latest release contains no bundle for the host architecture
|
||||
- **THEN** the installer exits with a failure message and installs nothing
|
||||
|
||||
### Requirement: Release metadata endpoint
|
||||
|
||||
The app SHALL expose a same-origin endpoint that returns the latest release's
|
||||
tag and the names of its downloadable assets as same-origin URLs. The app SHALL
|
||||
authenticate to the private forge using the configured release key, SHALL omit
|
||||
asset names that are not valid asset names, and SHALL NOT reveal the release
|
||||
key or the forge host to clients.
|
||||
|
||||
#### Scenario: Latest release is returned
|
||||
|
||||
- **WHEN** a client requests the latest-release endpoint and the forge returns
|
||||
a release
|
||||
- **THEN** the response carries the release tag and each asset's name and a
|
||||
same-origin URL under `/fpagent/releases/download/`
|
||||
|
||||
#### Scenario: Unsafe asset names are omitted
|
||||
|
||||
- **WHEN** the forge release lists an asset whose name is not a valid asset
|
||||
name
|
||||
- **THEN** that asset does not appear in the response
|
||||
|
||||
#### Scenario: Release key is not exposed
|
||||
|
||||
- **WHEN** any client fetches the latest-release endpoint
|
||||
- **THEN** the response body contains neither the release key nor the forge host
|
||||
|
||||
#### Scenario: Installer not configured
|
||||
|
||||
- **WHEN** the release key is not configured
|
||||
- **THEN** the endpoint answers that the installer is not configured
|
||||
|
||||
#### Scenario: Forge unavailable
|
||||
|
||||
- **WHEN** the forge cannot be reached or returns an error for the latest
|
||||
release
|
||||
- **THEN** the endpoint answers with a gateway error
|
||||
|
||||
### Requirement: Release asset download proxy
|
||||
|
||||
The app SHALL stream a requested release asset to the client through a
|
||||
same-origin route, authenticating the upstream request with the release key so
|
||||
the client sends no credentials. It SHALL reject asset names that are not valid
|
||||
asset names and SHALL report a missing asset as not found.
|
||||
|
||||
#### Scenario: Asset is streamed without client credentials
|
||||
|
||||
- **WHEN** a client requests a release asset by tag and name without any
|
||||
credential
|
||||
- **THEN** the app fetches it from the forge with the release key and streams
|
||||
its bytes to the client
|
||||
|
||||
#### Scenario: Invalid asset name is rejected
|
||||
|
||||
- **WHEN** a client requests an asset whose tag or name is not a valid asset
|
||||
name
|
||||
- **THEN** the app rejects the request without contacting the forge
|
||||
|
||||
#### Scenario: Missing asset
|
||||
|
||||
- **WHEN** the forge reports the requested asset as not found
|
||||
- **THEN** the app answers with not found
|
||||
|
||||
#### Scenario: Installer not configured
|
||||
|
||||
- **WHEN** the release key is not configured
|
||||
- **THEN** the endpoint answers that the installer is not configured
|
||||
|
||||
### Requirement: Installer credential stays server-side
|
||||
|
||||
The app SHALL read the private-forge release key from the
|
||||
`GITEA_RELEASE_INSTALL_KEY` environment variable. The key SHALL NOT appear in
|
||||
the installer script, in any release metadata response, or in any streamed
|
||||
asset.
|
||||
|
||||
#### Scenario: Served installer carries no credential
|
||||
|
||||
- **WHEN** a client fetches `/fpagent/install.sh`
|
||||
- **THEN** the response contains no release key
|
||||
|
||||
#### Scenario: Credential supplied by the environment
|
||||
|
||||
- **WHEN** the release key is supplied in the environment
|
||||
- **THEN** the installer and proxy endpoints use it without any change to the
|
||||
hardcoded configuration
|
||||
@@ -119,11 +119,14 @@ session; the OTP SHALL NOT be accepted without one.
|
||||
### Requirement: Configuration is hardcoded
|
||||
The app SHALL hardcode all configuration in source: port `4173`, portal
|
||||
`https://portal.yrka.app`, PocketBase `https://pb.itcass.net`, a2loft
|
||||
`https://apis.a2loft.net`, and agent `http://127.0.0.1:9000`. There SHALL be
|
||||
no runtime configuration via environment variables or config files, with one
|
||||
exception: the Image Cropper's service credentials (Minio keys, Azure storage
|
||||
connection string, nodered credentials, cleanup-service key) SHALL come from
|
||||
environment variables and SHALL NOT be stored in source.
|
||||
`https://apis.a2loft.net`, agent `http://127.0.0.1:9000`, and the fingerprint
|
||||
agent release forge `https://sources.yrka.app`. There SHALL be no runtime
|
||||
configuration via environment variables or config files, with two exceptions:
|
||||
the Image Cropper's service credentials (Minio keys, Azure storage connection
|
||||
string, nodered credentials, cleanup-service key) SHALL come from environment
|
||||
variables and SHALL NOT be stored in source; and the Fingerprint Agent
|
||||
installer's release key (`GITEA_RELEASE_INSTALL_KEY`) SHALL come from the
|
||||
environment and SHALL NOT be stored in source.
|
||||
|
||||
#### Scenario: No configuration required to run
|
||||
- **WHEN** the app is started with no environment variables or config files
|
||||
@@ -134,6 +137,12 @@ environment variables and SHALL NOT be stored in source.
|
||||
- **THEN** the cropper routes use them without any change to the hardcoded
|
||||
URL/port configuration
|
||||
|
||||
#### Scenario: Installer key from the environment
|
||||
- **WHEN** the fpagent installer release key is supplied as an environment
|
||||
variable
|
||||
- **THEN** the installer and release-proxy routes use it without any change to
|
||||
the hardcoded URL/port configuration
|
||||
|
||||
### Requirement: Carbon Design System UI
|
||||
The tools pages SHALL be built on the Carbon Design System: Carbon web
|
||||
components (version-pinned via CDN) for buttons, notifications, toasts, tags,
|
||||
|
||||
Reference in New Issue
Block a user