# sk-react-toolkit The shared code of the web tools built as a Fastify server in TypeScript and a React application with MUI built by Vite, such as azure-platform's manage-ui and sql-query-webui. It is TypeScript source: Node 26 runs the server helpers, and the application's Vite build compiles the web code. ## Using it The package is `@koszewscy/sk-react-toolkit`, published to the npm registry of Gitea, `https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/`. An application depends on a published version and imports `@koszewscy/sk-react-toolkit/server`, `/web` and `/vite`. Its `.npmrc` names the registry of the scope: ```ini @koszewscy:registry=https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/ ``` ```json { "dependencies": { "@koszewscy/sk-react-toolkit": "^0.2.0" } } ``` The web application adds the toolkit's resolve options and font plugin to its `vite.config.ts`, so React, MUI and Emotion come from its own `node_modules`: ```ts import { font, fontChoice, TOOLKIT_RESOLVE } from '@koszewscy/sk-react-toolkit/vite' import { defineConfig, loadEnv } from 'vite' export default defineConfig(({ mode }) => ({ plugins: [react(), font(fontChoice(loadEnv(mode, import.meta.dirname)))], resolve: TOOLKIT_RESOLVE, })) ``` To change the toolkit and an application together, `npm link --no-save` replaces the installed package with the checkout, the TypeScript source, in each package of the application: ```sh npm link --no-save ../sk-react-toolkit npm --prefix web link --no-save ../../sk-react-toolkit ``` `package.json` and the lock file keep the published version; `npm ci` or `npm install` replaces the link with it again. A change of the toolkit reaches the application through a release of the toolkit and an update of the dependency. The peer dependencies are the application's: `fastify` and `@fastify/static` for the server helpers; `react`, `react-dom`, `@mui/material`, `@mui/icons-material` and Emotion for the web code; `vite` for the Vite helpers. ## sk-react-toolkit/server | Export | Purpose | | --- | --- | | `HttpError` | an error answered with its status and `{ error }`, with `problems` and a `detail` message for a refused write | | `strict(properties)` | the JSON schema of an object with exactly these properties, all required | | `errorHandler(log)` | Fastify's error handler: `{ error }`, or `{ error, problems, message }`; unexpected 5xx errors logged with their stack | | `notFoundHandler(webDir, buildHint)` | 404 for `/api` paths, `index.html` for the others, 503 with the hint while the frontend is not built | | `createLog`, `timestampsOn`, `QUIET`, `Log` | the log of a server, with optional timestamps and requests; `QUIET` for tests | | `fail(message)` | prints the error in red and ends the process with status 2 | | `gitVersion(dir, variable)` | `git describe` of the checkout, else the environment variable, else `dev` | | `exitOnSignals(close)` | closes the server on Ctrl-C and SIGTERM, ending the process after 5 seconds at most | | `readJsonObject(path, options)` | a JSON object file and its version, the SHA-256 of its bytes; optionally `{}` for a missing file | | `encryptionKey()`, `encrypt(text, key)`, `decrypt(encrypted, key)` | the key of the secrets and AES-256-GCM encryption; see [Credential store](#credential-store) | | `CredentialStore`, `credentialProblems`, `credentialValues`, `registerCredentialRoutes`, `POSTGRESQL_KIND` | the credential store; see [Credential store](#credential-store) | ## sk-react-toolkit/web | Export | Purpose | | --- | --- | | `request`, `ApiError`, `Validation` | a JSON call to the backend, its error with the problems of a refused write, and the answer of a validate endpoint | | `appTheme(colorSchemes?)`, `renderApp(options)` | the theme (light, dark and system, Regular font, small text fields) and the startup that retries the first call while showing Connecting | | `REGULAR`, `MONOSPACED`, `verbatim` | the font styles and the attributes of inputs the browser must not change | | `Card` | a centered panel with a title and a message | | `ThemeMenu` | the button cycling light, dark and system theme | | `SwitcherMenu`, `SwitcherItem` | a button naming the item chosen, whose menu chooses another, such as a page or a connection | | `TextInput` | a text field with clear and show/hide buttons | | `RowDialog`, `useValidation` | a dialog that edits a value validated by the server as it changes, and the validation hook | | `createBrowserSettings(keys, event)` | switches kept in localStorage and followed across tabs | | `CredentialsButton` | the dialog of a credential store; see [Credential store](#credential-store) | ## sk-react-toolkit/vite | Export | Purpose | | --- | --- | | `font(choice)`, `fontChoice(env)`, `FONTS` | the font plugin of `VITE_FONT`: `cdn` (Google Fonts links, the default), `bundled` (`@fontsource/roboto`, a dependency of the application) or `none` | | `TOOLKIT_RESOLVE` | the resolve options that keep one copy of React, MUI and Emotion | ## Credential store A credential store is a list of credentials in a key of the application's `config.json`, of the kinds the application gives, such as a database connection or a service principal. Each entry has a `name`, unique and not one of the store's reserved names, a `kind`, and the fields of its kind: ```json { "credentials": [ { "name": "state", "kind": "postgresql", "host": "db.example.com", "port": 5432, "database": "platform", "user": "tofu", "ssl_mode": "verify-full", "password": "" } ] } ``` A kind (`CredentialKind`) names its fields (`CredentialField`), each by its key in `config.json`, with a label, a help text, a type, `text`, `port`, `guid` or `choice` with its `choices`, whether it is required, and whether it is secret. `POSTGRESQL_KIND` is the kind of a PostgreSQL connection: `host`, `port`, `database`, `user`, `ssl_mode` (`disable`, `require` or `verify-full`) and a secret `password`. A secret field is encrypted with AES-256-GCM, stored as base64 of a random IV, the ciphertext and the authentication tag. The key is the SHA-256 of `ENCRYPTION_KEY`, else of the content of the file `ENCRYPTION_KEY_FILE` names, without its final newline. Without either, no secret can be saved or used; a secret encrypted with another key cannot be decrypted and has to be entered again. `CredentialStore` describes a store: the path of `config.json`, the key, the kinds, the reserved names and whether a missing `config.json` reads as `{}`. - `credentialProblems(value, store)`: the problems of the key's value by `..`, for the application's own check of `config.json`. - `credentialValues(store, name)`: a credential with its secrets decrypted; HttpError 404 for a name that does not exist, 400 without a key or for a secret the key does not decrypt. - `registerCredentialRoutes(app, store, prefix, log)`: `GET /read`, `POST /validate` and `POST /write`. Read answers the credentials, `CredentialProfile`, with `values` (the fields that are not secret, as text), `secrets` (null for each secret, never answered) and `stored` (which secrets are kept), the kinds, the reserved names, the path of `config.json`, its version and whether the server has a key. A write takes the version read and the credentials; a secret is null to keep the secret of the credential named `original`, `''` for none, or the new secret, which is encrypted. A file that changed since the version read is 409, credentials with problems 422; the other keys of `config.json` are kept. `CredentialsButton` (`prefix`, `title`, `icon`, `note`, `onSaved`) opens the store's credentials in a dialog: a list at the left with Add for each kind and Remove, and the fields of the credential selected, as its kind names them. A secret shows only whether it is kept: Set or Change replaces it and Remove drops it. ## Development `npm install` installs the peer dependencies as development dependencies; `npm run typecheck` and `npm run lint` check the code. `npm run build` (`build.mjs`) builds the published package in `dist/`: the code compiled to JavaScript with its declarations, README.md, CHANGELOG.md and a `package.json` whose exports name the compiled files. ## Releases `./release.mjs` commits, tags the version of the newest CHANGELOG.md entry and pushes `main` with the tag; `-n` prints the git commands instead. The version of a release of the code, `x.y.z`, is that of `package.json`, so a release raises both. A tag `x.y.z-n` releases changes that are not code changes; its version is already published, so its run publishes nothing. The `Release` workflow, `.gitea/workflows/release.yml`, builds and publishes the package on Gitea Actions for each tag and on demand: it installs the dependencies, typechecks and lints the code, builds the package and publishes `dist/` to the npm registry of the owner `CI_OWNER` on the server `CI_URL`, with `CI_TOKEN`, a token with the package scope; all three are secrets of the repository. A version already published is skipped, as the registry refuses the same version again.