feature: the key of a credential store may be a path of keys, so stores of several kinds share one object of config.json.
Release / release (push) Failing after 16m30s

This commit is contained in:
slawek committed 2026-10-28 23:44:36 +01:00
1 parent 7959c90012
commit 97c46ff0d8
6 files changed
+71 -12

No files matched your search

+28 -8
View File
@@ -8,7 +8,8 @@ import { decrypt, encrypt, encryptionKey } from './secrets.ts'
/**
* A credential store: the list under key of config.json at configPath, every entry of the kind the application gives,
* with the names reserved for its own credentials. With missingAsEmpty a missing config.json reads as {}, and the
* with the names reserved for its own credentials. key is a key of config.json or a path of keys separated by dots,
* such as connections.pg, for a list in nested objects. With missingAsEmpty a missing config.json reads as {}, and the
* first write creates it.
*/
export type CredentialStore = {
@@ -114,9 +115,30 @@ export function credentialProblems(entries: unknown, store: Pick<CredentialStore
return problems
}
const isObject = (value: unknown): value is Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value)
/** The value of data at key, a key or a path of keys separated by dots; undefined when it is not there. */
function valueAt(data: Record<string, unknown>, key: string): unknown {
return key.split('.').reduce<unknown>((value, part) => (isObject(value) ? value[part] : undefined), data)
}
/**
* data with value at key, a key or a path of keys separated by dots, after the other keys of its object; the objects
* of the path that are missing, or are not objects, are created, and their other keys kept.
*/
function withValue(data: Record<string, unknown>, key: string, value: unknown): Record<string, unknown> {
const [first, ...rest] = key.split('.')
if (rest.length > 0) {
return { ...data, [first]: withValue(isObject(data[first]) ? data[first] : {}, rest.join('.'), value) }
}
const others = { ...data }
delete others[first]
return { ...others, [first]: value }
}
/** The entries of the store's key in config.json; a value that is not a list reads as none. */
function entriesOf(data: Record<string, unknown>, store: CredentialStore): Record<string, unknown>[] {
const entries = data[store.key]
const entries = valueAt(data, store.key)
return Array.isArray(entries) ? entries.filter((entry) => entry !== null && typeof entry === 'object') : []
}
@@ -185,7 +207,7 @@ function toEntries(profiles: CredentialProfile[], data: Record<string, unknown>,
*/
export function credentialValues(store: CredentialStore, name: string): CredentialValues {
const { data } = read(store)
const [problem] = Object.entries(credentialProblems(data[store.key], store))
const [problem] = Object.entries(credentialProblems(valueAt(data, store.key), store))
if (problem !== undefined) {
throw new HttpError(500, `${problem[0]} in ${store.configPath} ${problem[1]}.`)
}
@@ -234,8 +256,8 @@ export function registerCredentialRoutes(app: FastifyInstance, store: Credential
function problemsOf(value: CredentialProfile[], data: Record<string, unknown>): Record<string, string> {
const problems: Record<string, string> = {}
for (const [path, message] of Object.entries(credentialProblems(toEntries(value, data, store, () => 'new'), store))) {
const [, index, field] = path.split('.')
problems[field === undefined ? index ?? store.key : `${index}.${field}`] = `${field ?? 'The credential'} ${message}.`
const [index, field] = path.slice(store.key.length + 1).split('.')
problems[field === undefined ? index || store.key : `${index}.${field}`] = `${field ?? 'The credential'} ${message}.`
}
if (key() === null) {
value.forEach((profile, index) => {
@@ -279,11 +301,9 @@ export function registerCredentialRoutes(app: FastifyInstance, store: Credential
throw new HttpError(422, 'The credentials are not valid.', problems)
}
const sealing = key()!
const others = { ...file.data }
delete others[store.key]
const entries = toEntries(request.body.value, file.data, store, (secret) => encrypt(secret, sealing))
try {
writeFileSync(store.configPath, `${JSON.stringify({ ...others, [store.key]: entries }, null, 2)}\n`)
writeFileSync(store.configPath, `${JSON.stringify(withValue(file.data, store.key, entries), null, 2)}\n`)
} catch (error) {
throw new HttpError(500, `${store.configPath} cannot be written: ${(error as Error).message}`)
}