viz-js-lib 0.18.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
package/lib/agent.js ADDED
@@ -0,0 +1,237 @@
1
+ 'use strict';
2
+
3
+ var _bluebird = require('bluebird');
4
+
5
+ var _bluebird2 = _interopRequireDefault(_bluebird);
6
+
7
+ var _key_public = require('./auth/ecc/src/key_public');
8
+
9
+ var _key_public2 = _interopRequireDefault(_key_public);
10
+
11
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+
13
+ /** Mirrors the node's never_delegable_operation_names(): a row holding one of these grants nothing. */
14
+ /**
15
+ * VIZ agent access (HF15, op-id 105 `set_agent_permission`) — read-side helpers.
16
+ *
17
+ * A VIZ key is no longer only an authority key. A principal may register up to
18
+ * CHAIN_AGENT_MAX_PER_ACCOUNT live agents: a label plus a public key that may sign an explicit,
19
+ * frozen list of that principal's operations, optionally scoped to off-chain addons, optionally
20
+ * expiring (epoch `1970-01-01T00:00:00` = perpetual).
21
+ *
22
+ * The old check — "getAccount and look at active_authority.key_auths" — silently rejects agent
23
+ * keys, because an agent is not an account and never appears in any authority. The recipe now is
24
+ * authority OR agents, and `check()` does both in one call.
25
+ *
26
+ * Mirrors `VIZ\Agent` of viz-php-lib; the node (`agent_evaluator.cpp::usable_agent_row`) is the
27
+ * source of truth:
28
+ * - an agent answers direct active/regular requirements only, and only for operations on its list;
29
+ * the master authority is never satisfied by an agent, nor is a nested/other authority;
30
+ * - a row is unusable when expired or when its list holds a never-delegable name;
31
+ * - one key belongs to one agent of a principal; a key change of master/active, a recovery or a
32
+ * sale wipes every agent of the principal.
33
+ *
34
+ * @module viz/agent
35
+ */
36
+
37
+ var NEVER_DELEGABLE = ['set_agent_permission', 'proposal_update', 'account_update', 'recover_account', 'change_recovery_account', 'set_account_price', 'set_subaccount_price', 'target_account_sale'];
38
+
39
+ /** Authority roles a key can belong to, most privileged first. Agents answer for none of them. */
40
+ var ROLES = ['master', 'active', 'regular'];
41
+
42
+ module.exports = function (VIZ_API) {
43
+ /** Agent rows of a principal as the node returns them (expired rows included; it sweeps them lazily). */
44
+ function get(account) {
45
+ return VIZ_API.getAgentPermissionsAsync(account);
46
+ }
47
+
48
+ /** First live agent row signed by `key`, or false. A failed request reads as "no agents". */
49
+ function find(account, key) {
50
+ return _bluebird2.default.resolve(get(account)).then(function (rows) {
51
+ if (!Array.isArray(rows)) {
52
+ return false;
53
+ }
54
+ for (var i = 0; i < rows.length; i++) {
55
+ var row = rows[i];
56
+ if (row && row.agent_key && !isExpired(row) && keyEquals(row.agent_key, key)) {
57
+ return row;
58
+ }
59
+ }
60
+ return false;
61
+ }).catch(function () {
62
+ return false;
63
+ });
64
+ }
65
+
66
+ /** Resolves true when `key` is a live agent of `account` — and, when `operation` is given, may sign it. */
67
+ function has(account, key, operation) {
68
+ return find(account, key).then(function (row) {
69
+ if (false === row) {
70
+ return false;
71
+ }
72
+ if (null === operation || undefined === operation || '' === operation) {
73
+ return true;
74
+ }
75
+ return grants(row, operation);
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Unified check: may `key` act for `account`? Answers for the authority roles first, then for the
81
+ * agents. Resolves to an object:
82
+ * allowed — bool
83
+ * source — 'authority' | 'agent' | 'none'
84
+ * role — authority role the key matched, when source is 'authority', else false
85
+ * agent — the matched agent row, when source is 'agent', else false
86
+ * operation — the operation the caller asked about (null = any)
87
+ *
88
+ * With `operation` omitted an authority key is answered by membership alone, while an agent is
89
+ * only answered as "this is a live agent key": its grants are per-operation, so pass the wire
90
+ * name (`transfer`, `pm_place_bet`, ...) to check a specific operation. A key that is only in a
91
+ * nested authority (account_auths) is NOT resolved here — use `verify_authority` with a signed
92
+ * transaction for the full recursive check. Note that a node without HF15 answers every agent
93
+ * question with "none": the check then degrades to authorities only.
94
+ */
95
+ function check(account, key, operation) {
96
+ var result = {
97
+ allowed: false,
98
+ source: 'none',
99
+ role: false,
100
+ agent: false,
101
+ operation: null === operation ? null : operation
102
+ };
103
+
104
+ return _bluebird2.default.resolve(VIZ_API.getAccountAsync(account, '')).catch(function () {
105
+ return false;
106
+ }).then(function (accountObject) {
107
+ if (accountObject && 'string' === typeof accountObject.name && accountObject.name === account) {
108
+ for (var i = 0; i < ROLES.length; i++) {
109
+ var role = ROLES[i];
110
+ if (inAuthority(accountObject[role + '_authority'], key)) {
111
+ result.allowed = true;
112
+ result.source = 'authority';
113
+ result.role = role;
114
+ return result;
115
+ }
116
+ }
117
+ }
118
+ return find(account, key).then(function (row) {
119
+ if (false !== row && (null === operation || undefined === operation || '' === operation || grants(row, operation))) {
120
+ result.allowed = true;
121
+ result.source = 'agent';
122
+ result.agent = row;
123
+ }
124
+ return result;
125
+ });
126
+ });
127
+ }
128
+
129
+ /** True when the agent row is alive at head block time. The node's own flag wins; the timestamp is the fallback. */
130
+ function isExpired(row) {
131
+ if (undefined !== row.expired) {
132
+ return !!row.expired;
133
+ }
134
+ if (!row.expiration) {
135
+ return false;
136
+ }
137
+ // The chain reports UTC without a trailing 'Z', so parse it as UTC explicitly.
138
+ var time = Date.parse(row.expiration + 'Z');
139
+ if (isNaN(time) || time <= 0) {
140
+ return false; // epoch = perpetual; an unparsable value is not a reason to deny the key
141
+ }
142
+ return time <= Date.now();
143
+ }
144
+
145
+ /** True when the row's list covers `operation` and the row is not poisoned by a never-delegable name. */
146
+ function grants(row, operation) {
147
+ if (!Array.isArray(row.operations) || -1 === row.operations.indexOf(operation)) {
148
+ return false;
149
+ }
150
+ return !poisoned(row);
151
+ }
152
+
153
+ /**
154
+ * True when the row's list holds a never-delegable name. The node refuses such a row WHOLE
155
+ * (`usable_agent_row` bails on the first forbidden name), it does not merely drop that name — so
156
+ * the row grants nothing at all, including its off-chain addons.
157
+ */
158
+ function poisoned(row) {
159
+ if (!Array.isArray(row.operations)) {
160
+ return false;
161
+ }
162
+ for (var i = 0; i < row.operations.length; i++) {
163
+ if (-1 !== NEVER_DELEGABLE.indexOf(row.operations[i])) {
164
+ return true;
165
+ }
166
+ }
167
+ return false;
168
+ }
169
+
170
+ /**
171
+ * True when the row is scoped to `scope`. Addons are opaque off-chain scopes: the node stores
172
+ * them and never interprets them, so a service matches its OWN scope here, by exact string
173
+ * (case-sensitive — the chain does not normalise them for us).
174
+ */
175
+ function addonMatches(row, scope) {
176
+ if (!Array.isArray(row.addons)) {
177
+ return false;
178
+ }
179
+ return -1 !== row.addons.indexOf(scope);
180
+ }
181
+
182
+ /** True when `key` is a member of the authority with enough weight to meet its threshold. */
183
+ function inAuthority(authority, key) {
184
+ if (!authority || !Array.isArray(authority.key_auths)) {
185
+ return false;
186
+ }
187
+ var threshold = parseInt(authority.weight_threshold, 10) || 0;
188
+ var weight = 0;
189
+ for (var i = 0; i < authority.key_auths.length; i++) {
190
+ var member = authority.key_auths[i];
191
+ if (member && member.length > 1 && keyEquals(member[0], key)) {
192
+ weight += parseInt(member[1], 10) || 0;
193
+ }
194
+ }
195
+ return weight >= threshold && weight > 0;
196
+ }
197
+
198
+ /** Compare two encoded keys, tolerating whitespace, a missing 'VIZ' prefix and base58 spelling differences. */
199
+ function keyEquals(a, b) {
200
+ if ('string' !== typeof a || 'string' !== typeof b) {
201
+ return false;
202
+ }
203
+ if (a === b) {
204
+ return true;
205
+ }
206
+ return normalizeKey(a) === normalizeKey(b);
207
+ }
208
+
209
+ /** Canonical `VIZ...` form of an encoded public key; the trimmed input when it cannot be parsed. */
210
+ function normalizeKey(key) {
211
+ var value = String(key === null || undefined === key ? '' : key).trim();
212
+ if ('' === value) {
213
+ return '';
214
+ }
215
+ if (0 !== value.indexOf('VIZ')) {
216
+ value = 'VIZ' + value;
217
+ }
218
+ var parsed = _key_public2.default.fromString(value);
219
+ return parsed ? parsed.toString() : String(key).trim();
220
+ }
221
+
222
+ return {
223
+ NEVER_DELEGABLE: NEVER_DELEGABLE,
224
+ ROLES: ROLES,
225
+ get: get,
226
+ find: find,
227
+ has: has,
228
+ check: check,
229
+ isExpired: isExpired,
230
+ grants: grants,
231
+ poisoned: poisoned,
232
+ addonMatches: addonMatches,
233
+ inAuthority: inAuthority,
234
+ keyEquals: keyEquals,
235
+ normalizeKey: normalizeKey
236
+ };
237
+ };
package/lib/browser.js CHANGED
@@ -4,6 +4,7 @@ var api = require('./api');
4
4
  var auth = require('./auth');
5
5
  var broadcast = require('./broadcast');
6
6
  var formatter = require('./formatter')(api);
7
+ var agent = require('./agent')(api);
7
8
  var memo = require('./auth/memo');
8
9
  var aes = require('./auth/ecc/src/aes');
9
10
  var config = require('./config');
@@ -15,6 +16,7 @@ var viz = {
15
16
  broadcast: broadcast,
16
17
  config: config,
17
18
  formatter: formatter,
19
+ agent: agent,
18
20
  memo: memo,
19
21
  aes: aes,
20
22
  utils: utils
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@ var api = require('./api');
4
4
  var auth = require('./auth');
5
5
  var broadcast = require('./broadcast');
6
6
  var formatter = require('./formatter')(api);
7
+ var agent = require('./agent')(api);
7
8
  var memo = require('./auth/memo');
8
9
  var aes = require('./auth/ecc/src/aes');
9
10
  var config = require('./config');
@@ -15,6 +16,7 @@ module.exports = {
15
16
  auth: auth,
16
17
  broadcast: broadcast,
17
18
  formatter: formatter,
19
+ agent: agent,
18
20
  memo: memo,
19
21
  aes: aes,
20
22
  config: config,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viz-js-lib",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
4
4
  "description": "viz.js the JavaScript Library with API support for VIZ blockchain",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Offline tests for the HF15 agent helpers. The decision rules are pure functions on purpose:
3
+ * they mirror the node's agent_evaluator.cpp::usable_agent_row and must be testable without a node.
4
+ */
5
+
6
+ require('babel-polyfill');
7
+ const assert = require('assert');
8
+ const agentFactory = require('../src/agent');
9
+
10
+ const KEY = 'VIZ7iiYUk6hFuieveBJcTVTV33aNuC2vZauXuYc9Hki3moxsm9WXe';
11
+ const OTHER_KEY = 'VIZ7FzBqZJmTdve9iuHXWvB3Ncu7VeJWKwpA1FpbLi4X4ZdsVAYjC';
12
+
13
+ function row(overrides) {
14
+ return Object.assign({
15
+ account: 'alice',
16
+ agent_name: 'shop',
17
+ agent_key: KEY,
18
+ operations: ['transfer'],
19
+ expiration: '1970-01-01T00:00:00',
20
+ addons: ['wallet'],
21
+ expired: false,
22
+ }, overrides || {});
23
+ }
24
+
25
+ describe('viz.agent', function () {
26
+ const agent = agentFactory({});
27
+
28
+ describe('isExpired', function () {
29
+ it('treats the epoch as perpetual, not as expired', function () {
30
+ assert.strictEqual(agent.isExpired(row()), false);
31
+ });
32
+
33
+ it('flags a past expiration when the node sent no flag', function () {
34
+ //the timestamp path is the fallback for a node that does not compute `expired`
35
+ assert.strictEqual(
36
+ agent.isExpired(row({ expiration: '2020-01-01T00:00:00', expired: undefined })),
37
+ true
38
+ );
39
+ });
40
+
41
+ it('keeps a future expiration live', function () {
42
+ assert.strictEqual(agent.isExpired(row({ expiration: '2099-01-01T00:00:00' })), false);
43
+ });
44
+
45
+ it('lets the node flag outrank the client clock', function () {
46
+ assert.strictEqual(
47
+ agent.isExpired(row({ expiration: '2099-01-01T00:00:00', expired: true })),
48
+ true
49
+ );
50
+ });
51
+ });
52
+
53
+ describe('grants', function () {
54
+ it('covers only listed operations', function () {
55
+ assert.strictEqual(agent.grants(row(), 'transfer'), true);
56
+ assert.strictEqual(agent.grants(row(), 'pm_place_bet'), false);
57
+ });
58
+
59
+ it('refuses a row holding a never-delegable name whole', function () {
60
+ const poisoned = row({ operations: ['transfer', 'account_update'] });
61
+ assert.strictEqual(agent.grants(poisoned, 'transfer'), false);
62
+ });
63
+
64
+ it('refuses a malformed row', function () {
65
+ assert.strictEqual(agent.grants(row({ operations: undefined }), 'transfer'), false);
66
+ });
67
+ });
68
+
69
+ describe('poisoned', function () {
70
+ it('flags a row holding a never-delegable name', function () {
71
+ assert.strictEqual(agent.poisoned(row()), false);
72
+ assert.strictEqual(agent.poisoned(row({ operations: ['transfer', 'account_update'] })), true);
73
+ });
74
+
75
+ it('does not call a row without an operation list poisoned', function () {
76
+ assert.strictEqual(agent.poisoned(row({ operations: undefined })), false);
77
+ });
78
+ });
79
+
80
+ describe('addonMatches', function () {
81
+ it('matches the service scope exactly and case-sensitively', function () {
82
+ assert.strictEqual(agent.addonMatches(row(), 'wallet'), true);
83
+ assert.strictEqual(agent.addonMatches(row(), 'Wallet'), false);
84
+ assert.strictEqual(agent.addonMatches(row(), 'exchange'), false);
85
+ assert.strictEqual(agent.addonMatches(row(), ''), false);
86
+ });
87
+
88
+ it('treats a row without addons as no match, not as any scope', function () {
89
+ assert.strictEqual(agent.addonMatches(row({ addons: [] }), 'wallet'), false);
90
+ assert.strictEqual(agent.addonMatches(row({ addons: undefined }), 'wallet'), false);
91
+ });
92
+ });
93
+
94
+ describe('inAuthority', function () {
95
+ it('accepts a member that alone meets the threshold', function () {
96
+ const authority = {
97
+ weight_threshold: 2,
98
+ key_auths: [[KEY, 2], [OTHER_KEY, 1]],
99
+ };
100
+ assert.strictEqual(agent.inAuthority(authority, KEY), true);
101
+ });
102
+
103
+ it('refuses a member below the threshold', function () {
104
+ const authority = { weight_threshold: 2, key_auths: [[KEY, 1]] };
105
+ assert.strictEqual(agent.inAuthority(authority, KEY), false);
106
+ });
107
+
108
+ it('refuses a missing authority', function () {
109
+ assert.strictEqual(agent.inAuthority(false, KEY), false);
110
+ assert.strictEqual(agent.inAuthority(undefined, KEY), false);
111
+ });
112
+ });
113
+
114
+ describe('keyEquals / normalizeKey', function () {
115
+ it('tolerates whitespace and a missing prefix', function () {
116
+ assert.strictEqual(agent.keyEquals(KEY, ` ${KEY} `), true);
117
+ assert.strictEqual(agent.keyEquals(KEY, KEY.slice(3)), true);
118
+ assert.strictEqual(agent.keyEquals(KEY, OTHER_KEY), false);
119
+ });
120
+
121
+ it('returns the canonical form', function () {
122
+ assert.strictEqual(agent.normalizeKey(KEY.slice(3)), KEY);
123
+ assert.strictEqual(agent.normalizeKey(KEY), KEY);
124
+ });
125
+
126
+ it('hands back an unparsable value trimmed', function () {
127
+ assert.strictEqual(agent.normalizeKey(' not-a-key '), 'not-a-key');
128
+ });
129
+ });
130
+
131
+ describe('find / has / check against a stubbed node', function () {
132
+ function stub(rows, accountObject) {
133
+ return agentFactory({
134
+ getAgentPermissionsAsync: () => Promise.resolve(rows),
135
+ getAccountAsync: () => Promise.resolve(accountObject),
136
+ });
137
+ }
138
+
139
+ it('finds the key and applies the operation list', function () {
140
+ const stubbed = stub([row()], false);
141
+ return stubbed.has('alice', KEY, 'transfer').then((allowed) => {
142
+ assert.strictEqual(allowed, true);
143
+ return stubbed.has('alice', KEY, 'pm_place_bet');
144
+ }).then((allowed) => {
145
+ assert.strictEqual(allowed, false);
146
+ return stubbed.has('alice', OTHER_KEY, 'transfer');
147
+ }).then((allowed) => {
148
+ assert.strictEqual(allowed, false);
149
+ });
150
+ });
151
+
152
+ it('skips expired rows', function () {
153
+ const stubbed = stub([row({ expired: true })], false);
154
+ return stubbed.has('alice', KEY, 'transfer').then((allowed) => {
155
+ assert.strictEqual(allowed, false);
156
+ });
157
+ });
158
+
159
+ it('answers an authority key from the account object', function () {
160
+ const stubbed = stub([], {
161
+ name: 'alice',
162
+ active_authority: { weight_threshold: 1, key_auths: [[KEY, 1]] },
163
+ });
164
+ return stubbed.check('alice', KEY, 'transfer').then((verdict) => {
165
+ assert.strictEqual(verdict.allowed, true);
166
+ assert.strictEqual(verdict.source, 'authority');
167
+ assert.strictEqual(verdict.role, 'active');
168
+ });
169
+ });
170
+
171
+ it('answers an agent key from the rows', function () {
172
+ const stubbed = stub([row()], false);
173
+ return stubbed.check('alice', KEY, 'transfer').then((verdict) => {
174
+ assert.strictEqual(verdict.allowed, true);
175
+ assert.strictEqual(verdict.source, 'agent');
176
+ assert.strictEqual(verdict.agent.agent_name, 'shop');
177
+ });
178
+ });
179
+
180
+ it('denies a key matching neither', function () {
181
+ const stubbed = stub([], { name: 'alice' });
182
+ return stubbed.check('alice', OTHER_KEY, 'transfer').then((verdict) => {
183
+ assert.strictEqual(verdict.allowed, false);
184
+ assert.strictEqual(verdict.source, 'none');
185
+ });
186
+ });
187
+
188
+ it('degrades to authorities when the node refuses the agent call', function () {
189
+ const stubbed = agentFactory({
190
+ getAgentPermissionsAsync: () => Promise.reject(new Error('unknown method')),
191
+ getAccountAsync: () => Promise.resolve({ name: 'alice' }),
192
+ });
193
+ return stubbed.check('alice', KEY, 'transfer').then((verdict) => {
194
+ assert.strictEqual(verdict.allowed, false);
195
+ assert.strictEqual(verdict.source, 'none');
196
+ });
197
+ });
198
+ });
199
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The browser entry (src/browser.js) has its own export list, separate from src/index.js, and
3
+ * webpack builds dist/viz.min.js from it. In 0.18.3 the agent module existed in the Node build but
4
+ * was missing here, so the released browser bundle silently lacked `viz.agent` while the READMEs
5
+ * said it was a Node-only module. This test pins the shared surface of the two entries.
6
+ */
7
+
8
+ require('babel-polyfill');
9
+ const assert = require('assert');
10
+ const browser = require('../src/browser');
11
+ const node = require('../src/index');
12
+
13
+ const SHARED = ['api', 'auth', 'broadcast', 'config', 'formatter', 'agent', 'memo', 'aes', 'utils'];
14
+
15
+ describe('browser entry exports', function () {
16
+ SHARED.forEach(function (key) {
17
+ it('exposes ' + key + ', like the Node entry does', function () {
18
+ assert.ok(browser[key], key + ' is missing from src/browser.js');
19
+ assert.strictEqual(typeof browser[key], typeof node[key]);
20
+ });
21
+ });
22
+
23
+ it('exposes the agent helpers the browser bundle is documented to carry', function () {
24
+ ['check', 'get', 'find', 'has', 'grants', 'poisoned', 'addonMatches', 'isExpired', 'inAuthority']
25
+ .forEach(function (name) {
26
+ assert.strictEqual(typeof browser.agent[name], 'function', 'viz.agent.' + name + ' is missing');
27
+ });
28
+ });
29
+
30
+ it('publishes viz on window/global, which is how a browser page reaches it', function () {
31
+ assert.strictEqual(global.viz, browser);
32
+ });
33
+ });
@@ -11,6 +11,8 @@ export const methods_test = [
11
11
  "get_accounts_on_auction",
12
12
  "get_accounts_on_sale",
13
13
  "get_agent_permissions",
14
+ "get_key_history",
15
+ "get_key_history_by_key",
14
16
  "get_active_paid_subscriptions",
15
17
  "get_active_validators",
16
18
  "get_active_witnesses",