morphit/node_modules/@beblurt/dblurt/lib/authority.js

146 lines
6.6 KiB
JavaScript

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.evaluateAuthorityForKey = evaluateAuthorityForKey;
exports.validateAccountAuthority = validateAccountAuthority;
exports.validatePostingAuthority = validatePostingAuthority;
const DEFAULT_MAX_RECURSION = 2;
const DEFAULT_MAX_MEMBERSHIP = 40;
const DEFAULT_MAX_ACCOUNT_AUTHS = 125;
function keyToString(key) {
if (typeof key === 'string') {
return key;
}
if ('createPublic' in key && typeof key.createPublic === 'function') {
return key.createPublic().toString();
}
return key.toString();
}
function authorityKeyToString(key) {
return typeof key === 'string' ? key : key.toString();
}
function unique(values) {
return Array.from(new Set(values));
}
/**
* Evaluate whether one public/private key satisfies an authority object.
*
* This mirrors the Layer 1 `sign_state` single-key semantics used for required
* signatures: key weights are counted directly, account authorities recurse via
* the supplied authority getter, and thresholds decide authorization.
*/
function evaluateAuthorityForKey(authority, key, options = {}) {
const publicKey = keyToString(key);
const maxRecursion = options.maxRecursion ?? DEFAULT_MAX_RECURSION;
const maxMembership = options.maxMembership ?? DEFAULT_MAX_MEMBERSHIP;
const maxAccountAuths = options.maxAccountAuths ?? DEFAULT_MAX_ACCOUNT_AUTHS;
const approvedAccounts = new Set();
const visitedAccounts = [];
const missingAccounts = [];
let limitReason = null;
const evaluate = (auth, depth, accountAuthCount) => {
let totalWeight = 0;
let membership = 0;
const matchedKeys = [];
for (const [authorityKey, weight] of auth.key_auths || []) {
const serialized = authorityKeyToString(authorityKey);
if (serialized === publicKey) {
totalWeight += weight;
matchedKeys.push(serialized);
if (totalWeight >= auth.weight_threshold) {
return { authorized: true, matchedKeys, totalWeight };
}
}
membership++;
if (maxMembership > 0 && membership >= maxMembership) {
limitReason = 'membership_limit_exceeded';
return { authorized: false, matchedKeys, totalWeight };
}
}
for (const [account, weight] of auth.account_auths || []) {
if (approvedAccounts.has(account)) {
totalWeight += weight;
if (totalWeight >= auth.weight_threshold) {
return { authorized: true, matchedKeys, totalWeight };
}
}
else if (depth !== maxRecursion) {
if (maxAccountAuths > 0 && accountAuthCount.value >= maxAccountAuths) {
limitReason = 'account_auths_limit_exceeded';
return { authorized: false, matchedKeys, totalWeight };
}
accountAuthCount.value++;
visitedAccounts.push(account);
const delegatedAuthority = options.getAuthority ? options.getAuthority(account) : undefined;
if (!delegatedAuthority) {
missingAccounts.push(account);
}
else {
const delegated = evaluate(delegatedAuthority, depth + 1, accountAuthCount);
matchedKeys.push(...delegated.matchedKeys);
if (delegated.authorized) {
approvedAccounts.add(account);
totalWeight += weight;
if (totalWeight >= auth.weight_threshold) {
return { authorized: true, matchedKeys, totalWeight };
}
}
}
}
membership++;
if (maxMembership > 0 && membership >= maxMembership) {
limitReason = 'membership_limit_exceeded';
return { authorized: false, matchedKeys, totalWeight };
}
}
return { authorized: totalWeight >= auth.weight_threshold, matchedKeys, totalWeight };
};
const evaluated = evaluate(authority, 0, { value: 0 });
const authorized = evaluated.authorized;
const reason = authorized
? 'authority_satisfied'
: limitReason || (missingAccounts.length ? 'missing_delegated_authority' : 'authority_not_satisfied');
return {
approvedAccounts: Array.from(approvedAccounts),
authorized,
matchedKey: evaluated.matchedKeys[0] || null,
matchedKeys: unique(evaluated.matchedKeys),
missingAccounts: unique(missingAccounts),
reason,
threshold: authority.weight_threshold,
totalWeight: evaluated.totalWeight,
visitedAccounts: unique(visitedAccounts)
};
}
/** Inspect one key against one account authority class plus sibling classes for least-privilege warnings. */
function validateAccountAuthority(account, key, authority, options = {}) {
const getAuthority = (name) => {
const delegated = options.getAccount ? options.getAccount(name) : undefined;
return delegated ? delegated[authority] : undefined;
};
const evaluationOptions = {
getAuthority,
maxAccountAuths: options.maxAccountAuths,
maxMembership: options.maxMembership,
maxRecursion: options.maxRecursion
};
const matches = {
active: evaluateAuthorityForKey(account.active, key, { ...evaluationOptions, getAuthority: name => options.getAccount ? options.getAccount(name)?.active : undefined }),
owner: evaluateAuthorityForKey(account.owner, key, { ...evaluationOptions, getAuthority: name => options.getAccount ? options.getAccount(name)?.owner : undefined }),
posting: evaluateAuthorityForKey(account.posting, key, { ...evaluationOptions, getAuthority: name => options.getAccount ? options.getAccount(name)?.posting : undefined })
};
const selected = matches[authority];
const reason = selected.authorized ? 'authority_satisfied' : authority === 'posting' ? 'posting_authority_not_satisfied' : selected.reason;
return {
account: account.name,
authority,
authorized: selected.authorized,
key: keyToString(key),
matches,
missingAccounts: unique([...matches.owner.missingAccounts, ...matches.active.missingAccounts, ...matches.posting.missingAccounts]),
reason
};
}
/** Validate whether one key satisfies an account's posting authority. */
function validatePostingAuthority(account, key, options = {}) {
return validateAccountAuthority(account, key, 'posting', options);
}