79 lines
5.1 KiB
Markdown
79 lines
5.1 KiB
Markdown
# Architecture Decision Records
|
|
|
|
## ADR-1: The source is TypeScript, linked into an application while both change
|
|
|
|
Status: Accepted
|
|
|
|
Context: manage-ui and sql-query-webui share their server helpers and web components. The
|
|
applications run their servers with Node's type stripping and build their web code with Vite.
|
|
They depend on the published package (ADR-2), and a change of the toolkit is tried in them before
|
|
it is released.
|
|
|
|
Decision: The source is `.ts` and `.tsx` files with three entry points: `./server`, `./web` and
|
|
`./vite`. While the toolkit and an application change together, `npm link --no-save` replaces the
|
|
installed package with the checkout, leaving `package.json` and the lock file on the published
|
|
version; Node runs the server helpers from the checkout's real path, where type stripping
|
|
applies, and Vite compiles the web code. The server helpers import only Node's modules at run
|
|
time, and types of Fastify; the Vite helpers import only types of Vite, taking `loadEnv`'s answer
|
|
from the application. The web code's React, MUI and Emotion come from the application, which
|
|
dedupes them with `TOOLKIT_RESOLVE`. The peer dependencies are optional, as an application uses
|
|
only some of the entry points, and are development dependencies for the toolkit's own checks.
|
|
|
|
Consequences: The dependency of an application never names a path; `npm ci` replaces a link
|
|
with the published version. Node refuses type stripping in `node_modules`, so the published
|
|
package is compiled (ADR-2).
|
|
|
|
## ADR-2: The published package is built in dist/ and published to Gitea by a tag
|
|
|
|
Status: Accepted
|
|
|
|
Context: An installed package lives in `node_modules`, where Node does not strip types, so the
|
|
published package has to be JavaScript, while the linked checkout stays TypeScript source. The
|
|
repository is on Gitea, which has an npm registry per owner and Actions mostly compatible with
|
|
GitHub's.
|
|
|
|
Decision: `build.mjs` compiles the toolkit with `tsconfig.build.json`, which rewrites the `.ts`
|
|
import extensions to `.js`, into `dist/` with declarations, and writes `dist/package.json` with
|
|
exports of the compiled files and declarations; the package published is `dist/`, while the
|
|
source `package.json` stays private with exports of the source. The name is
|
|
`@koszewscy/sk-react-toolkit`, scoped to the owner, so an application takes only this scope from
|
|
Gitea; it depends on a published version and imports the package by this name, which is also the
|
|
name `npm link` gives the checkout. The package is built and published only by the
|
|
`Release` workflow, which follows linux-cloud-router's: it runs for each tag and on demand, with
|
|
the secrets `CI_URL`, `CI_OWNER` and `CI_TOKEN`, and skips a version already published.
|
|
`actions/setup-node` gives npm the registry of the package's scope and the token, so the package
|
|
carries no registry; `release.mjs` refuses a tag `x.y.z` that is not the version of
|
|
`package.json`.
|
|
|
|
Consequences: A release raises the version in both CHANGELOG.md and `package.json`, and a
|
|
re-run, or a tag `x.y.z-n`, publishes nothing. The declarations of `renderApp` reference the
|
|
declaration of the `virtual:font` module, which `build.mjs` copies into `dist/`.
|
|
|
|
## ADR-3: Credentials of any kind are kept in config.json, with secrets encrypted by a key of the environment
|
|
|
|
Status: Accepted
|
|
|
|
Context: sql-query-webui kept PostgreSQL connection profiles with encrypted passwords, and
|
|
manage-ui needs a PostgreSQL connection and an Azure service principal without taking them from
|
|
its environment. Other systems will need other kinds of credentials.
|
|
|
|
Decision: A credential store is a list in a key of the application's `config.json`, all of one
|
|
kind; each entry has a name and the fields of the store's kind, and no kind of its own, as the key
|
|
of `config.json`, whose schema the application defines, already says what the entries are. An
|
|
application with several kinds of credentials has a store, a key, for each. The application gives
|
|
the kind as data: each field with its key, label, help, type (text, port, GUID or a choice),
|
|
whether it is required and whether it is secret. The toolkit checks the entries, serves read,
|
|
validate and write routes, and renders the dialog from the kind, so a new kind needs no new code;
|
|
the dialog edits the stores of one `config.json` in tabs, and its Save writes them one after the
|
|
other, each with the version the previous write answers. Secret fields are encrypted with
|
|
AES-256-GCM, a random 12-byte IV each, with the key the SHA-256 of `ENCRYPTION_KEY`, else of the
|
|
content of `ENCRYPTION_KEY_FILE`. A secret is never answered: read gives null and whether it is
|
|
kept, and a write sends null to keep it, `''` to drop it, or a new secret. The applications decide
|
|
how a credential is used: `credentialValues` answers it with its secrets decrypted. The types the
|
|
server and the dialog share are in `shared/credentials.ts`, exported by both entry points.
|
|
|
|
Consequences: Without the key no secret can be saved or used, and a changed key makes the saved
|
|
secrets unusable until they are entered again. SHA-256 does not slow down guessing a weak key, so
|
|
the key has to be long and random. Anyone who reaches the routes can replace credentials, as the
|
|
applications have no authentication.
|