146 lines
6.6 KiB
JavaScript
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);
|
|
}
|