@iobroker/adapter-core
Version:
Core module to be used in ioBroker adapters. Acts as the bridge to js-controller.
136 lines (135 loc) • 5.41 kB
JavaScript
/**
* Central credential storage for ioBroker.
*
* Credentials are stored as objects with IDs like `system.credentials.<name>`,
* e.g. `system.credentials.anthropic`. The admin adapter provides the UI to manage them
* (Settings -> Credentials) and a JsonConfig component `credential` so adapters can let
* the user pick a credential by ID in the instance configuration.
*
* All information lives in `native`:
* - `native.type` - the credential type (email, cloud, ai, custom) - a pure category used for filtering
* - `native.version` - version of the data format (for future migrations)
* - `native.encryptedFields` - names of the fields in `native` that are stored encrypted
* with the system secret
* - all other keys in `native` are the credential fields themselves
*
* Every credential has one of two forms:
* - `login`: a `login` and a `password` field (password encrypted)
* - `key`: a single `key` field (encrypted), e.g. an API key
*
* This module is intentionally free of Node.js imports so it can also be consumed by
* browser builds (admin UI, json-config).
*/
/** Prefix of all credential object IDs */
export const CREDENTIALS_PREFIX = 'system.credentials.';
/** Current version of the credential data format */
export const CREDENTIALS_VERSION = 1;
/** Keys in `native` that are reserved for metadata and are not credential fields */
export const CREDENTIAL_META_FIELDS = ['type', 'version', 'encryptedFields'];
/**
* Registry of the two credential forms and their fields.
* This is the single source of truth - the admin UI renders its dialogs from it.
*/
export const CREDENTIAL_FORMS = {
login: [
{ name: 'login', type: 'text', required: true },
{ name: 'password', type: 'password', encrypted: true, required: true },
],
key: [{ name: 'key', type: 'password', encrypted: true, required: true }],
};
/**
* Detects the form of a credential from its fields.
*
* @param values The credential fields (e.g. `CredentialInfo.values`)
*/
export function getCredentialForm(values) {
return values.key !== undefined ? 'key' : 'login';
}
function getText(text, lang) {
if (!text) {
return '';
}
if (typeof text === 'string') {
return text;
}
return text[lang || 'en'] || text.en || Object.values(text)[0] || '';
}
function decodeCredentialObject(adapter, obj) {
const native = obj.native || {};
const encryptedFields = Array.isArray(native.encryptedFields) ? native.encryptedFields : [];
const values = {};
for (const [key, value] of Object.entries(native)) {
if (CREDENTIAL_META_FIELDS.includes(key)) {
continue;
}
values[key] =
encryptedFields.includes(key) && typeof value === 'string' && value ? adapter.decrypt(value) : value;
}
return {
id: obj._id,
name: getText(obj.common?.name),
type: native.type || '',
values: values,
};
}
/**
* Reads one credential and decrypts all encrypted fields with the system secret.
*
* @param adapter The adapter instance
* @param id The credential ID, e.g. `system.credentials.anthropic` (usually taken from the
* instance configuration where the user selected it via the `credential` JsonConfig component)
*/
export async function getCredentials(adapter, id) {
if (!id || !id.startsWith(CREDENTIALS_PREFIX)) {
throw new Error(`Invalid credential ID "${id}": must start with "${CREDENTIALS_PREFIX}"`);
}
const obj = await adapter.getForeignObjectAsync(id);
if (!obj) {
throw new Error(`Credentials "${id}" not found. Maybe they were deleted in the admin UI.`);
}
return decodeCredentialObject(adapter, obj);
}
/**
* Lists all stored credentials (without decrypting any secrets).
*
* @param adapter The adapter instance
* @param type Optional: only list credentials of this type, e.g. 'ai'
*/
export async function listCredentials(adapter, type) {
const objs = await adapter.getForeignObjectsAsync(`${CREDENTIALS_PREFIX}*`, 'config');
return Object.values(objs)
.filter(obj => !!obj && (!type || obj.native?.type === type))
.map(obj => {
const native = obj.native || {};
return {
id: obj._id,
name: getText(obj.common?.name),
type: native.type || '',
};
});
}
/**
* Subscribes to changes of one credential, so the adapter can e.g. reconnect
* when the user edits the credential in the admin UI.
*
* @param adapter The adapter instance
* @param id The credential ID, e.g. `system.credentials.anthropic`
* @param handler Called with the decrypted credential on every change, or with `null` if the credential was deleted
* @returns A function that unsubscribes again
*/
export async function subscribeCredentials(adapter, id, handler) {
if (!id || !id.startsWith(CREDENTIALS_PREFIX)) {
throw new Error(`Invalid credential ID "${id}": must start with "${CREDENTIALS_PREFIX}"`);
}
const listener = (changedId, obj) => {
if (changedId === id) {
handler(id, obj ? decodeCredentialObject(adapter, obj) : null);
}
};
adapter.on('objectChange', listener);
await adapter.subscribeForeignObjectsAsync(id);
return async () => {
adapter.removeListener('objectChange', listener);
await adapter.unsubscribeForeignObjectsAsync(id);
};
}