Compare commits
4
Commits
1ddf61b59f
...
e3392c7750
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3392c7750 | ||
|
|
46027acec1 | ||
|
|
6d3909c334 | ||
|
|
b3b75ffac0 |
@@ -18,3 +18,4 @@ API_BASE="encrypted:BIJm9rXCjB90I6zZFK57kTTjx2KY0+V42NF8zk38YVcSNTu4PB5Dxdz6iOEz
|
||||
IMAGE_FOLDER="encrypted:BEGDDzGErTbitrApWXiq5TuUQrtJCcRq89fuagmNp92npdAubBxA1vtTQwwJ3OGKSPwDW1gQceczY3NWwaeGZbc46L1rut6oP+yE/BATMkJ/7IcoKn7S27By/00GmsbHjWqp9prtLOv5dCgCpw=="
|
||||
NODERED_HTTP_USERNAME="encrypted:BCjXvS6rg+eCpV+S4/mypzw6Xu4vablBSqEZZeQN/oCfl7P7mVWzgaLYzNYOHit+4wP5soE1jw/jdk4OqBIiulcXWbkkEJPNvSqct9iL/rNfxufNuozOSFnii7j9bfW9dvjju2egpxNb16RHLtI+"
|
||||
NODERED_HTTP_PASSWORD=encrypted:BKHA3R+VWxrEkpYhgPfXuZJroarXuIpdMSSI+cpEP6AcQS1vuE1alOtqD0sp5TFU0o5jMx9KWwE6L/WG9LdRQAyEJiotl93oYK46p1L3VgrRkUS+xpX5z5u5VYCLYdCfjhuCU75kGDmRaqId5fbgQ9NmIzJAvYUb2C4zEz0DADNy8/JHmfXKHVorexRAB0gJXoUPfLXk8gcJbdQPNoGobBIonngLREllEmGN
|
||||
GITEA_RELEASE_INSTALL_KEY="encrypted:BNgr8Q5ZURg8EhuMNk7gdZy7DvzjC98tGRciLsQQFpAWnZ5eEGCM/S8DgJTPpQR1OA+L4LUUbW8QhBwsrUwjzqG3/Z8Tnqwma4bIwtXr1s9RERfimZGJ+1XEieY6b7hU3icl2aUBScdOYGFSfx4/Cj94Uz0onzeiMfzS9ZdGy6TUrVzXWCJsS9g="
|
||||
|
||||
+9
-3
@@ -1,6 +1,6 @@
|
||||
# Image Cropper service credentials — the ONLY environment variables this app
|
||||
# uses. Everything else (URLs, port, domain catalog) is hardcoded in source.
|
||||
# Without these the rest of the app works and /cropper answers 503.
|
||||
# Image Cropper service credentials. Everything else (URLs, port, domain
|
||||
# catalog) is hardcoded in source. Without these the rest of the app works and
|
||||
# /cropper answers 503.
|
||||
#
|
||||
# This repository uses dotenvx-encrypted env files (.env, .env.production);
|
||||
# the private keys live in .env.keys (gitignored). To set up or change a value:
|
||||
@@ -16,3 +16,9 @@ AZURE_STORAGE_CONNECTION_STRING=
|
||||
NODERED_HTTP_USERNAME=
|
||||
NODERED_HTTP_PASSWORD=
|
||||
CARVEKIT_API_KEY=
|
||||
|
||||
# Token the server uses to read the private fpagent releases from the forge
|
||||
# (https://sources.yrka.app). It stays server-side: /fpagent/releases/* proxies
|
||||
# the bundles for the public installer at /fpagent/install.sh. Without it the
|
||||
# installer answers 503.
|
||||
GITEA_RELEASE_INSTALL_KEY=
|
||||
|
||||
@@ -18,3 +18,4 @@ API_BASE="encrypted:BFVQaDK474EqpNvSw6iiP8KYjRqyXOMQLL/m5lbQHI30QCrNoMfsxmC7W2mH
|
||||
IMAGE_FOLDER="encrypted:BDNYAfiof6lw9vYxB5jm3GZrtESoA+BNsrktU/YoWlV0HHdAOU+yWGX8FTRTq2/3Qv5AW02IO77Y7Fec9DGVHG6MmzEfuNVZ1Gz7e8XxGatmfp3X+p/REq04bZs4UXflh/YC8a9vYGpelpyVlw=="
|
||||
NODERED_HTTP_USERNAME="encrypted:BApJWB9AcOzsbkS+GxJ24uaVBBD5rAO/q3z6EIhLDutOmiIcolwdl1kHJGgkkRGVKc/PvNlMD1fAYtEblEiMdSwJPKciV4gWdVNWnVVZHK/WFpP3Rj8JLBRq5ysfFgH2PBPHZFUAnBl3FbzVuJfc"
|
||||
NODERED_HTTP_PASSWORD=encrypted:BGgnc8FDXIyZl/vjOwTiG2LLu1uwz72OCy78obR0h6yvm3NBv/pGr0ZeR1ZzDFBntN1WSg0qtroztpZxmdPcNgjO0/osmegUvNRM89uj6CsFtBe2SlDJLN66VH5ZGduB4m2CO0iNm14EZg3mLbkwCZv28Pb9uJqfPePt9EbK3Wwzu8IfJt2FImEe6nqdBng79Rbie4X7Z2WZX/m1nzcJb0WiMuzwGAT3e0aT
|
||||
GITEA_RELEASE_INSTALL_KEY="encrypted:BOYECnGcqfZlj8pRvTldqB/UF6HnBlMCfOymdMd3eRvGFo+HwPRViboFteB/tdMAF9uxL5q80OrgB9qbC6LaDTa/ociZg2kVR+R00YB/ZEV4epamIZ1ZzXWrg8J9D4zTGkJJ5iRiqyE4qGoPxDhn3OhTrs5RfBl/7pJa6Wi9wTU+1xhT3oBJr7E="
|
||||
|
||||
@@ -42,6 +42,8 @@ tools/
|
||||
├── cropper.js # /cropper router (queue, review, save) — injectable clients
|
||||
├── public/
|
||||
│ ├── index.html # landing page shell (tiles injected per session)
|
||||
│ ├── fpagent/
|
||||
│ │ └── install.sh # public /fpagent/install.sh (proxies private releases)
|
||||
│ └── assets/
|
||||
│ ├── style.css # shared Carbon-styled layout + g100 theme
|
||||
│ ├── help.html # /help docs page
|
||||
@@ -119,9 +121,10 @@ record, crop the photo, optionally **Clean background**, then accept or reject
|
||||
with reasons. Reads may use the cached `crop_image` verdict; saving re-verifies
|
||||
live and uploads through the queue.
|
||||
|
||||
Its service credentials are the **only** environment variables this app uses.
|
||||
They live in dotenvx-encrypted files at the repository root — `.env` for
|
||||
development, `.env.production` for deployment:
|
||||
Its service credentials live in dotenvx-encrypted files at the repository root
|
||||
— `.env` for development, `.env.production` for deployment. The fpagent
|
||||
installer key is read from the same files (see
|
||||
[Installing the agent](#installing-the-agent)):
|
||||
|
||||
```sh
|
||||
MINIO_ACCESS_KEY=… MINIO_SECRET_KEY=…
|
||||
@@ -149,13 +152,31 @@ The `/fingerprint` wizard walks the operator through:
|
||||
travel with every `fprint-enroll` to fpagent.
|
||||
3. **Save** — both templates are posted to a2loft with a fresh OTP.
|
||||
|
||||
### Installing the agent
|
||||
|
||||
`/fpagent/install.sh` is a public installer (also served for `curl | bash`). It
|
||||
fetches the newest bundle from the private
|
||||
[`fingerprint-agent`](https://sources.yrka.app/YOUreka/fingerprint-agent/releases)
|
||||
forge, verifies its sha256, then runs the bundle's own installer, which handles
|
||||
libfprint, the udev rule, `plugdev` and the systemd service. On a field laptop:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --enable-service
|
||||
curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --check # report only
|
||||
```
|
||||
|
||||
The forge is private, so the operator's laptop needs no credentials: the app
|
||||
reads the release with `GITEA_RELEASE_INSTALL_KEY` and proxies it through
|
||||
`/fpagent/releases/*`. Without that key the installer answers `503`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Everything except the cropper credentials is hardcoded in `tools/server.js` by
|
||||
design — 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 domain catalog. Change them there; there are no
|
||||
other environment variables or config files.
|
||||
Everything except the credentials is hardcoded in `tools/server.js` by design —
|
||||
port `4173`, portal `https://portal.yrka.app`, PocketBase `https://pb.itcass.net`,
|
||||
a2loft `https://apis.a2loft.net`, agent `http://127.0.0.1:9000`, forge
|
||||
`https://sources.yrka.app` and the domain catalog. Change them there. The only
|
||||
environment variables are the cropper credentials and the fpagent installer key
|
||||
`GITEA_RELEASE_INSTALL_KEY` above.
|
||||
|
||||
## Deployment
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
# install.sh — download and install the latest YOUreka Fingerprint Agent
|
||||
# (fpagent) release on this machine.
|
||||
#
|
||||
# Hosted at https://tools.yrka.app/fpagent/install.sh and meant to be piped
|
||||
# straight into bash on a field laptop:
|
||||
#
|
||||
# curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash
|
||||
#
|
||||
# It downloads the bundle, verifies its sha256, then hands off to the bundle's
|
||||
# own installer, which handles libfprint, the udev rule, plugdev and the
|
||||
# systemd service. Every flag is forwarded to that installer, e.g.:
|
||||
#
|
||||
# curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --enable-service
|
||||
# curl -fsSL https://tools.yrka.app/fpagent/install.sh | bash -s -- --check
|
||||
#
|
||||
# The private release is reached through the tools app, so no credentials are
|
||||
# needed here.
|
||||
set -euo pipefail
|
||||
|
||||
BASE="${FPAGENT_BASE_URL:-https://tools.yrka.app/fpagent}"
|
||||
|
||||
say() { printf '%s\n' "$*"; }
|
||||
die() { printf '[FAIL] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: curl -fsSL ${BASE}/install.sh | bash -s -- [installer options]
|
||||
|
||||
Downloads the latest fpagent release, verifies it, and runs its installer.
|
||||
Options are forwarded to that installer:
|
||||
--check preflight and report only; change nothing
|
||||
--offline never touch the network; fail if dependencies are missing
|
||||
--enable-service install and enable a systemd service (starts at boot)
|
||||
--service-user NAME user for the service (default: the invoking user)
|
||||
--install-path PATH binary destination (default: /usr/local/bin/fpagent)
|
||||
--uninstall remove the binary, the udev rule and the service
|
||||
-h, --help the installer's help
|
||||
|
||||
Environment:
|
||||
FPAGENT_BASE_URL override the download base (default: ${BASE})
|
||||
EOF
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
for cmd in curl tar sha256sum python3; do
|
||||
command -v "$cmd" >/dev/null 2>&1 || die "$cmd is required but not installed"
|
||||
done
|
||||
|
||||
# Offline bundles ship the libfprint .deb closure for a specific Ubuntu
|
||||
# release; prefer the one matching this machine, else fall back to the generic
|
||||
# online bundle (whose installer apt-get's the dependency).
|
||||
codename="$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release 2>/dev/null | tr -d '"')"
|
||||
|
||||
say "Fetching the latest Fingerprint Agent release..."
|
||||
meta="$(curl -fsSL "${BASE}/releases/latest")" || die "could not fetch the release list from ${BASE}"
|
||||
|
||||
readout="$(python3 -c '
|
||||
import json, sys
|
||||
codename = sys.argv[1]
|
||||
release = json.loads(sys.argv[2])
|
||||
assets = [a["name"] for a in release.get("assets", [])]
|
||||
|
||||
def pick(suffix):
|
||||
return next((n for n in assets if n.endswith(suffix) and not n.endswith(".sha256")), "")
|
||||
|
||||
name = pick("-ubuntu-" + codename + "-x86_64.tar.gz") if codename else ""
|
||||
if not name:
|
||||
name = pick("-linux-x86_64.tar.gz")
|
||||
print(release.get("tag", "") + "\t" + name)
|
||||
' "$codename" "$meta")" || die "could not parse the release metadata"
|
||||
|
||||
tag="${readout%%$'\t'*}"
|
||||
asset="${readout#*$'\t'}"
|
||||
[ -n "$tag" ] && [ -n "$asset" ] || die "no compatible bundle in the latest release (x86_64 required)"
|
||||
|
||||
say " release ${tag}"
|
||||
say " bundle ${asset}"
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
get() { curl -fsSL "$1" -o "$2" || die "download failed: $1"; }
|
||||
|
||||
say "Downloading and verifying..."
|
||||
get "${BASE}/releases/download/${tag}/${asset}" "${tmp}/${asset}"
|
||||
get "${BASE}/releases/download/${tag}/${asset}.sha256" "${tmp}/${asset}.sha256"
|
||||
(cd "$tmp" && sha256sum -c "${asset}.sha256") || die "checksum verification failed for ${asset}"
|
||||
|
||||
tar xzf "${tmp}/${asset}" -C "$tmp" || die "could not extract ${asset}"
|
||||
[ -x "${tmp}/install.sh" ] || die "the bundle does not contain install.sh"
|
||||
|
||||
say "Running the installer (pass --enable-service to start at boot)..."
|
||||
"${tmp}/install.sh" "$@"
|
||||
+80
-1
@@ -20,6 +20,11 @@ export const PORTAL_URL = 'https://portal.yrka.app'; // web-auth + openfga/verif
|
||||
export const PB_URL = 'https://pb.itcass.net'; // PocketBase session refresh
|
||||
export const A2LOFT_BASE = 'https://apis.a2loft.net'; // staff lookup + fingerprint save
|
||||
export const AGENT_URL = 'http://127.0.0.1:9000'; // fpagent Socket.IO
|
||||
export const GITEA_URL = 'https://sources.yrka.app'; // fpagent release forge (private)
|
||||
export const FPAGENT_REPO = 'YOUreka/fingerprint-agent';
|
||||
// The Gitea token used to read the private fpagent releases. The operator's
|
||||
// laptop never sees it: /fpagent/* proxies the bundles through this server.
|
||||
const GITEA_RELEASE_KEY = process.env.GITEA_RELEASE_INSTALL_KEY || '';
|
||||
export const DOMAINS = [
|
||||
{ code: 'uc', name: 'UNICAL' },
|
||||
{ code: 'mtr', name: 'Metropolitan Hotel' },
|
||||
@@ -272,6 +277,10 @@ export function createApp(overrides = {}) {
|
||||
agentUrl: AGENT_URL,
|
||||
domains: DOMAINS,
|
||||
ttlMs: DOMAIN_TTL_MS,
|
||||
giteaUrl: GITEA_URL,
|
||||
fpagentRepo: FPAGENT_REPO,
|
||||
giteaToken: GITEA_RELEASE_KEY,
|
||||
giteaFetch: fetch,
|
||||
fetch: fetch,
|
||||
now: Date.now,
|
||||
...overrides,
|
||||
@@ -314,12 +323,82 @@ export function createApp(overrides = {}) {
|
||||
setHeaders: (res, filePath) => {
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
filePath.endsWith('.html') ? 'no-store' : 'public, max-age=31536000, immutable'
|
||||
filePath.endsWith('.html') || filePath.endsWith('.sh')
|
||||
? 'no-store'
|
||||
: 'public, max-age=31536000, immutable'
|
||||
);
|
||||
},
|
||||
})
|
||||
); // assets only; '/' is session-aware
|
||||
|
||||
// ── fpagent installer: public proxy for the private release bundles ───────
|
||||
// The install script (public/fpagent/install.sh) runs on a field laptop with
|
||||
// no Gitea credentials, so this server reads the private release with the
|
||||
// token and streams the bundle back. The token and forge host never leave
|
||||
// here. Registered before the session middleware: the installer is public.
|
||||
const giteaBase = () => cfg.giteaUrl.replace(/\/+$/, '');
|
||||
const giteaHeaders = () => (cfg.giteaToken ? { authorization: `token ${cfg.giteaToken}` } : {});
|
||||
const SAFE_ASSET = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
||||
const installerNotConfigured = (res) =>
|
||||
res.status(503).json({
|
||||
ok: false,
|
||||
message: 'The Fingerprint Agent installer is not configured on this deployment.',
|
||||
});
|
||||
|
||||
app.get('/fpagent/releases/latest', async (req, res) => {
|
||||
if (!cfg.giteaToken) return installerNotConfigured(res);
|
||||
try {
|
||||
const r = await cfg.giteaFetch(`${giteaBase()}/api/v1/repos/${cfg.fpagentRepo}/releases/latest`, {
|
||||
headers: giteaHeaders(),
|
||||
});
|
||||
if (!r.ok) {
|
||||
return res.status(502).json({ ok: false, message: 'Could not reach the Fingerprint Agent releases.' });
|
||||
}
|
||||
const release = await r.json();
|
||||
const tag = String(release?.tag_name || '');
|
||||
const assets = (Array.isArray(release?.assets) ? release.assets : [])
|
||||
.map((a) => String(a?.name || ''))
|
||||
.filter((name) => SAFE_ASSET.test(name))
|
||||
.map((name) => ({
|
||||
name,
|
||||
url: `/fpagent/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(name)}`,
|
||||
}));
|
||||
if (!tag || assets.length === 0) {
|
||||
return res
|
||||
.status(502)
|
||||
.json({ ok: false, message: 'The Fingerprint Agent release has no downloadable bundles.' });
|
||||
}
|
||||
res.set('Cache-Control', 'no-store').json({ ok: true, tag, assets });
|
||||
} catch {
|
||||
res.status(502).json({ ok: false, message: 'Could not reach the Fingerprint Agent releases.' });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/fpagent/releases/download/:tag/:name', async (req, res) => {
|
||||
if (!cfg.giteaToken) return installerNotConfigured(res);
|
||||
const { tag, name } = req.params;
|
||||
if (!SAFE_ASSET.test(tag) || !SAFE_ASSET.test(name)) {
|
||||
return res.status(400).json({ ok: false, message: 'Unknown release asset.' });
|
||||
}
|
||||
try {
|
||||
const r = await cfg.giteaFetch(
|
||||
`${giteaBase()}/${cfg.fpagentRepo}/releases/download/${encodeURIComponent(tag)}/${encodeURIComponent(name)}`,
|
||||
{ headers: giteaHeaders() }
|
||||
);
|
||||
if (!r.ok) {
|
||||
return res
|
||||
.status(r.status === 404 ? 404 : 502)
|
||||
.json({ ok: false, message: 'Release asset not available.' });
|
||||
}
|
||||
res
|
||||
.set('Content-Type', r.headers.get('content-type') || 'application/octet-stream')
|
||||
.set('Cache-Control', 'no-store')
|
||||
.send(Buffer.from(await r.arrayBuffer()));
|
||||
} catch {
|
||||
res.status(502).json({ ok: false, message: 'Could not reach the Fingerprint Agent releases.' });
|
||||
}
|
||||
});
|
||||
|
||||
// ── session middleware: recover / refresh the YOUreka Auth session ────────
|
||||
app.use(async (req, res, next) => {
|
||||
req.session = null;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
// The /fpagent installer surface: a public script, and a proxy that reads the
|
||||
// private Gitea release with GITEA_RELEASE_INSTALL_KEY so the operator's
|
||||
// laptop never needs credentials.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createApp, FPAGENT_REPO } from '../server.js';
|
||||
import { startServer, stopServer, urlOf } from './helpers.js';
|
||||
|
||||
const TOKEN = 'release-install-secret';
|
||||
const TAG = 'v9.9.9';
|
||||
const ARCHIVE = 'fpagent-deadbee-linux-x86_64.tar.gz';
|
||||
const UBUNTU_ARCHIVE = 'fpagent-deadbee-ubuntu-jammy-x86_64.tar.gz';
|
||||
const RELEASE = {
|
||||
tag_name: TAG,
|
||||
assets: [
|
||||
{ name: ARCHIVE },
|
||||
{ name: `${ARCHIVE}.sha256` },
|
||||
{ name: UBUNTU_ARCHIVE },
|
||||
{ name: 'not a safe name' },
|
||||
{ name: '../escape' },
|
||||
],
|
||||
};
|
||||
|
||||
// Fake Gitea: refuses without the token, serves the release and its assets.
|
||||
async function startFakeGitea({ releaseStatus = 200, downloadStatus = 200 } = {}) {
|
||||
const calls = [];
|
||||
const server = await startServer((req, res) => {
|
||||
calls.push({ url: req.url, auth: req.headers.authorization });
|
||||
if (req.headers.authorization !== `token ${TOKEN}`) {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (req.url === `/api/v1/repos/${FPAGENT_REPO}/releases/latest`) {
|
||||
res.writeHead(releaseStatus, { 'content-type': 'application/json' });
|
||||
res.end(JSON.stringify(RELEASE));
|
||||
return;
|
||||
}
|
||||
if (req.url.startsWith(`/${FPAGENT_REPO}/releases/download/`)) {
|
||||
res.writeHead(downloadStatus, { 'content-type': 'application/gzip' });
|
||||
res.end(Buffer.from('bundle-bytes'));
|
||||
return;
|
||||
}
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
});
|
||||
return { server, calls };
|
||||
}
|
||||
|
||||
async function bootFpagentApp(giteaUrl, overrides = {}) {
|
||||
const app = createApp({
|
||||
giteaUrl,
|
||||
fpagentRepo: FPAGENT_REPO,
|
||||
giteaToken: TOKEN,
|
||||
cropperClients: null,
|
||||
...overrides,
|
||||
});
|
||||
const server = await new Promise((resolve) => {
|
||||
const s = app.listen(0, '127.0.0.1', () => resolve(s));
|
||||
});
|
||||
return { server, base: urlOf(server) };
|
||||
}
|
||||
|
||||
test('latest release is proxied as tools-relative URLs, without the token', async () => {
|
||||
const gitea = await startFakeGitea();
|
||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/latest`);
|
||||
assert.equal(r.status, 200);
|
||||
const body = await r.json();
|
||||
assert.equal(body.tag, TAG);
|
||||
// Unsafe asset names are dropped; the rest point back at this app.
|
||||
assert.deepEqual(
|
||||
body.assets.map((a) => a.name),
|
||||
[ARCHIVE, `${ARCHIVE}.sha256`, UBUNTU_ARCHIVE]
|
||||
);
|
||||
for (const a of body.assets) {
|
||||
assert.equal(a.url, `/fpagent/releases/download/${TAG}/${a.name}`);
|
||||
}
|
||||
assert.ok(!JSON.stringify(body).includes(TOKEN));
|
||||
assert.equal(gitea.calls[0].auth, `token ${TOKEN}`); // server authenticated upstream
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
await stopServer(gitea.server);
|
||||
}
|
||||
});
|
||||
|
||||
test('bundles stream through this app; the client sends no credentials', async () => {
|
||||
const gitea = await startFakeGitea();
|
||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/download/${TAG}/${ARCHIVE}`);
|
||||
assert.equal(r.status, 200);
|
||||
assert.equal(Buffer.from(await r.arrayBuffer()).toString(), 'bundle-bytes');
|
||||
const assetCall = gitea.calls.find((c) => c.url.includes('/releases/download/'));
|
||||
assert.equal(assetCall.auth, `token ${TOKEN}`);
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
await stopServer(gitea.server);
|
||||
}
|
||||
});
|
||||
|
||||
test('unsafe asset names are rejected before reaching the forge', async () => {
|
||||
const gitea = await startFakeGitea();
|
||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/download/${TAG}/-bad`);
|
||||
assert.equal(r.status, 400);
|
||||
assert.equal(gitea.calls.length, 0);
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
await stopServer(gitea.server);
|
||||
}
|
||||
});
|
||||
|
||||
test('a missing bundle surfaces as 404', async () => {
|
||||
const gitea = await startFakeGitea({ downloadStatus: 404 });
|
||||
const { server, base } = await bootFpagentApp(urlOf(gitea.server));
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/download/${TAG}/${ARCHIVE}`);
|
||||
assert.equal(r.status, 404);
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
await stopServer(gitea.server);
|
||||
}
|
||||
});
|
||||
|
||||
test('without GITEA_RELEASE_INSTALL_KEY the installer reports 503', async () => {
|
||||
const { server, base } = await bootFpagentApp('https://sources.yrka.app', { giteaToken: '' });
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/releases/latest`);
|
||||
assert.equal(r.status, 503);
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
}
|
||||
});
|
||||
|
||||
test('install.sh is served publicly at /fpagent/install.sh', async () => {
|
||||
const { server, base } = await bootFpagentApp('https://sources.yrka.app', { giteaToken: '' });
|
||||
try {
|
||||
const r = await fetch(`${base}/fpagent/install.sh`);
|
||||
assert.equal(r.status, 200);
|
||||
assert.match(await r.text(), /FPAGENT_BASE_URL/);
|
||||
} finally {
|
||||
await stopServer(server);
|
||||
}
|
||||
});
|
||||
|
||||
test('install.sh is valid bash', () => {
|
||||
const script = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'..',
|
||||
'public',
|
||||
'fpagent',
|
||||
'install.sh'
|
||||
);
|
||||
execFileSync('bash', ['-n', script], { stdio: 'pipe' });
|
||||
});
|
||||
Reference in New Issue
Block a user