@agoric/access-token 0.4.22-upgrade-14-dev-0169c7e.0 → 0.4.22-upgrade-16-dev-8879538.0
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.
- package/CHANGELOG.md +0 -8
- package/package.json +17 -7
- package/src/access-token.js +23 -10
- package/src/json-store.js +36 -8
- package/test/{test-state.js → state.test.js} +3 -19
- package/test/tmp.js +19 -0
- package/test/token.test.js +17 -0
- package/{jsconfig.json → tsconfig.json} +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,14 +3,6 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
### [0.4.22-u11wf.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/access-token@0.4.21...@agoric/access-token@0.4.22-u11wf.0) (2023-09-23)
|
|
7
|
-
|
|
8
|
-
**Note:** Version bump only for package @agoric/access-token
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
6
|
### [0.4.21](https://github.com/Agoric/agoric-sdk/compare/@agoric/access-token@0.4.20...@agoric/access-token@0.4.21) (2023-05-19)
|
|
15
7
|
|
|
16
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agoric/access-token",
|
|
3
|
-
"version": "0.4.22-upgrade-
|
|
3
|
+
"version": "0.4.22-upgrade-16-dev-8879538.0+8879538",
|
|
4
4
|
"description": "Persistent credentials for Agoric users, backed by a simple JSON file",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/access-token.js",
|
|
@@ -12,26 +12,36 @@
|
|
|
12
12
|
"test": "ava",
|
|
13
13
|
"test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js",
|
|
14
14
|
"test:xs": "exit 0",
|
|
15
|
+
"lint": "run-s --continue-on-error lint:*",
|
|
15
16
|
"lint-fix": "yarn lint:eslint --fix",
|
|
16
|
-
"lint:eslint": "eslint ."
|
|
17
|
+
"lint:eslint": "eslint .",
|
|
18
|
+
"lint:types": "tsc"
|
|
17
19
|
},
|
|
18
20
|
"dependencies": {
|
|
19
|
-
"@agoric/assert": "0.6.1-upgrade-14-dev-0169c7e.0+0169c7e",
|
|
20
21
|
"n-readlines": "^1.0.0",
|
|
22
|
+
"proper-lockfile": "^4.1.2",
|
|
21
23
|
"tmp": "^0.2.1"
|
|
22
24
|
},
|
|
23
25
|
"devDependencies": {
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
+
"@types/n-readlines": "^1.0.3",
|
|
27
|
+
"@types/proper-lockfile": "^4.1.2",
|
|
28
|
+
"ava": "^5.3.0",
|
|
29
|
+
"c8": "^9.1.0"
|
|
26
30
|
},
|
|
27
31
|
"publishConfig": {
|
|
28
32
|
"access": "public"
|
|
29
33
|
},
|
|
30
34
|
"ava": {
|
|
31
35
|
"files": [
|
|
32
|
-
"test
|
|
36
|
+
"test/**/*.test.*"
|
|
37
|
+
],
|
|
38
|
+
"require": [
|
|
39
|
+
"@endo/init/debug.js"
|
|
33
40
|
],
|
|
34
41
|
"timeout": "2m"
|
|
35
42
|
},
|
|
36
|
-
"
|
|
43
|
+
"typeCoverage": {
|
|
44
|
+
"atLeast": 83.97
|
|
45
|
+
},
|
|
46
|
+
"gitHead": "8879538cd1d125a08346f02dd5701d0d70c90bb8"
|
|
37
47
|
}
|
package/src/access-token.js
CHANGED
|
@@ -6,6 +6,11 @@ import path from 'path';
|
|
|
6
6
|
import { openJSONStore } from './json-store.js';
|
|
7
7
|
|
|
8
8
|
// Adapted from https://stackoverflow.com/a/43866992/14073862
|
|
9
|
+
/**
|
|
10
|
+
* @param {object} opts
|
|
11
|
+
* @param {BufferEncoding} [opts.stringBase]
|
|
12
|
+
* @param {number} [opts.byteLength]
|
|
13
|
+
*/
|
|
9
14
|
export function generateAccessToken({
|
|
10
15
|
stringBase = 'base64url',
|
|
11
16
|
byteLength = 48,
|
|
@@ -28,9 +33,13 @@ export function generateAccessToken({
|
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
35
|
* @param {string|number} port
|
|
36
|
+
* @param {string} [sharedStateDir]
|
|
31
37
|
* @returns {Promise<string>}
|
|
32
38
|
*/
|
|
33
|
-
export async function getAccessToken(
|
|
39
|
+
export async function getAccessToken(
|
|
40
|
+
port,
|
|
41
|
+
sharedStateDir = path.join(os.homedir(), '.agoric'),
|
|
42
|
+
) {
|
|
34
43
|
if (typeof port === 'string') {
|
|
35
44
|
const match = port.match(/^(.*:)?(\d+)$/);
|
|
36
45
|
if (match) {
|
|
@@ -39,18 +48,22 @@ export async function getAccessToken(port) {
|
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
// Ensure we're protected with a unique accessToken for this basedir.
|
|
42
|
-
const sharedStateDir = path.join(os.homedir(), '.agoric');
|
|
43
51
|
await fs.promises.mkdir(sharedStateDir, { mode: 0o700, recursive: true });
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
const { storage, commit, close } = await openJSONStore(sharedStateDir);
|
|
54
|
+
let accessToken;
|
|
55
|
+
try {
|
|
56
|
+
// Ensure an access token exists.
|
|
57
|
+
const accessTokenKey = `accessToken/${port}`;
|
|
58
|
+
if (!storage.has(accessTokenKey)) {
|
|
59
|
+
storage.set(accessTokenKey, await generateAccessToken());
|
|
60
|
+
await commit();
|
|
61
|
+
}
|
|
62
|
+
accessToken = storage.get(accessTokenKey);
|
|
63
|
+
} finally {
|
|
64
|
+
await close();
|
|
51
65
|
}
|
|
52
|
-
|
|
53
|
-
await close();
|
|
66
|
+
|
|
54
67
|
if (typeof accessToken !== 'string') {
|
|
55
68
|
throw Error(`Could not find access token for ${port}`);
|
|
56
69
|
}
|
package/src/json-store.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import process from 'process';
|
|
5
|
+
import lockfile from 'proper-lockfile';
|
|
5
6
|
import Readlines from 'n-readlines';
|
|
6
7
|
|
|
7
8
|
// TODO: Update this when we make a breaking change.
|
|
@@ -11,6 +12,8 @@ import Readlines from 'n-readlines';
|
|
|
11
12
|
// changed when you're just trying to use the wallet from the browser.
|
|
12
13
|
const DATA_FILE = 'swingset-kernel-state.jsonlines';
|
|
13
14
|
|
|
15
|
+
const DEFAULT_LOCK_RETRIES = 10;
|
|
16
|
+
|
|
14
17
|
/**
|
|
15
18
|
* @typedef {ReturnType<typeof makeStorageInMemory>['storage']} JSONStore
|
|
16
19
|
*/
|
|
@@ -156,20 +159,37 @@ function makeStorageInMemory() {
|
|
|
156
159
|
* @param {string} [dirPath] Path to a directory in which database files may be kept, or
|
|
157
160
|
* null.
|
|
158
161
|
* @param {boolean} [forceReset] If true, initialize the database to an empty state
|
|
162
|
+
* @param {null | import('proper-lockfile').LockOptions['retries']} [lockRetries] If null, do not lock the database.
|
|
159
163
|
*
|
|
160
|
-
* @returns {{
|
|
164
|
+
* @returns {Promise<{
|
|
161
165
|
* storage: JSONStore, // a storage API object to load and store data
|
|
162
166
|
* commit: () => Promise<void>, // commit changes made since the last commit
|
|
163
167
|
* close: () => Promise<void>, // shutdown the store, abandoning any uncommitted changes
|
|
164
|
-
* }}
|
|
168
|
+
* }>}
|
|
165
169
|
*/
|
|
166
|
-
function makeJSONStore(
|
|
170
|
+
async function makeJSONStore(
|
|
171
|
+
dirPath,
|
|
172
|
+
forceReset = false,
|
|
173
|
+
lockRetries = DEFAULT_LOCK_RETRIES,
|
|
174
|
+
) {
|
|
175
|
+
await null;
|
|
167
176
|
const { storage, state } = makeStorageInMemory();
|
|
168
177
|
|
|
178
|
+
let releaseLock = async () => {};
|
|
169
179
|
let storeFile;
|
|
170
180
|
if (dirPath) {
|
|
171
181
|
fs.mkdirSync(dirPath, { recursive: true });
|
|
172
182
|
storeFile = path.resolve(dirPath, DATA_FILE);
|
|
183
|
+
|
|
184
|
+
if (lockRetries !== null) {
|
|
185
|
+
// We need to lock the database to prevent multiple divergent instances.
|
|
186
|
+
releaseLock = await lockfile.lock(dirPath, {
|
|
187
|
+
// @ts-expect-error TS(2345) lockFilePath really does exist on LockOptions
|
|
188
|
+
lockFilePath: `${storeFile}.lock`,
|
|
189
|
+
retries: lockRetries,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
173
193
|
if (forceReset) {
|
|
174
194
|
safeUnlink(storeFile);
|
|
175
195
|
} else {
|
|
@@ -186,8 +206,7 @@ function makeJSONStore(dirPath, forceReset = false) {
|
|
|
186
206
|
if (lines) {
|
|
187
207
|
let line = lines.next();
|
|
188
208
|
while (line) {
|
|
189
|
-
|
|
190
|
-
const [key, value] = JSON.parse(line);
|
|
209
|
+
const [key, value] = JSON.parse(line.toString());
|
|
191
210
|
storage.set(key, value);
|
|
192
211
|
line = lines.next();
|
|
193
212
|
}
|
|
@@ -195,18 +214,25 @@ function makeJSONStore(dirPath, forceReset = false) {
|
|
|
195
214
|
}
|
|
196
215
|
}
|
|
197
216
|
|
|
217
|
+
const assertNotClosed = () => {
|
|
218
|
+
if (!dirPath || storeFile) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
throw Error('JSON store is already closed');
|
|
222
|
+
};
|
|
223
|
+
|
|
198
224
|
/**
|
|
199
225
|
* Commit unsaved changes.
|
|
200
226
|
*/
|
|
201
227
|
async function commit() {
|
|
228
|
+
assertNotClosed();
|
|
202
229
|
if (dirPath) {
|
|
203
230
|
const tempFile = `${storeFile}-${process.pid}.tmp`;
|
|
204
231
|
const fd = fs.openSync(tempFile, 'w');
|
|
205
232
|
|
|
206
233
|
for (const [key, value] of state.entries()) {
|
|
207
234
|
const line = JSON.stringify([key, value]);
|
|
208
|
-
fs.writeSync(fd, line);
|
|
209
|
-
fs.writeSync(fd, '\n');
|
|
235
|
+
fs.writeSync(fd, `${line}\n`);
|
|
210
236
|
}
|
|
211
237
|
fs.closeSync(fd);
|
|
212
238
|
fs.renameSync(tempFile, storeFile);
|
|
@@ -218,7 +244,9 @@ function makeJSONStore(dirPath, forceReset = false) {
|
|
|
218
244
|
* (if you want to save them, call commit() first).
|
|
219
245
|
*/
|
|
220
246
|
async function close() {
|
|
221
|
-
|
|
247
|
+
assertNotClosed();
|
|
248
|
+
storeFile = undefined;
|
|
249
|
+
await releaseLock();
|
|
222
250
|
}
|
|
223
251
|
|
|
224
252
|
return { storage, commit, close };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import tmp from 'tmp';
|
|
2
|
-
|
|
3
1
|
import test from 'ava';
|
|
2
|
+
import { tmpDir } from './tmp.js';
|
|
4
3
|
import {
|
|
5
4
|
initJSONStore,
|
|
6
5
|
openJSONStore,
|
|
@@ -8,21 +7,6 @@ import {
|
|
|
8
7
|
isJSONStore,
|
|
9
8
|
} from '../src/json-store.js';
|
|
10
9
|
|
|
11
|
-
/**
|
|
12
|
-
* @param {string} [prefix]
|
|
13
|
-
* @returns {Promise<[string, () => void]>}
|
|
14
|
-
*/
|
|
15
|
-
const tmpDir = prefix =>
|
|
16
|
-
new Promise((resolve, reject) => {
|
|
17
|
-
tmp.dir({ unsafeCleanup: true, prefix }, (err, name, removeCallback) => {
|
|
18
|
-
if (err) {
|
|
19
|
-
reject(err);
|
|
20
|
-
} else {
|
|
21
|
-
resolve([name, removeCallback]);
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
10
|
function testStorage(t, storage) {
|
|
27
11
|
t.falsy(storage.has('missing'));
|
|
28
12
|
t.is(storage.get('missing'), undefined);
|
|
@@ -58,14 +42,14 @@ test('storageInFile', async t => {
|
|
|
58
42
|
const [dbDir, cleanup] = await tmpDir('testdb');
|
|
59
43
|
t.teardown(cleanup);
|
|
60
44
|
t.is(isJSONStore(dbDir), false);
|
|
61
|
-
const { storage, commit, close } = initJSONStore(dbDir);
|
|
45
|
+
const { storage, commit, close } = await initJSONStore(dbDir);
|
|
62
46
|
testStorage(t, storage);
|
|
63
47
|
await commit();
|
|
64
48
|
const before = getAllState(storage);
|
|
65
49
|
await close();
|
|
66
50
|
t.is(isJSONStore(dbDir), true);
|
|
67
51
|
|
|
68
|
-
const { storage: after } = openJSONStore(dbDir);
|
|
52
|
+
const { storage: after } = await openJSONStore(dbDir);
|
|
69
53
|
t.deepEqual(getAllState(after), before, 'check state after reread');
|
|
70
54
|
t.is(isJSONStore(dbDir), true);
|
|
71
55
|
});
|
package/test/tmp.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import tmp from 'tmp';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {string} [prefix]
|
|
5
|
+
* @returns {Promise<[string, () => void]>}
|
|
6
|
+
*/
|
|
7
|
+
export const tmpDir = prefix =>
|
|
8
|
+
new Promise((resolve, reject) => {
|
|
9
|
+
// We use `unsafeCleanup` because we want to remove the directory even if it
|
|
10
|
+
// still contains files.
|
|
11
|
+
const unsafeCleanup = true;
|
|
12
|
+
tmp.dir({ unsafeCleanup, prefix }, (err, name, removeCallback) => {
|
|
13
|
+
if (err) {
|
|
14
|
+
reject(err);
|
|
15
|
+
} else {
|
|
16
|
+
resolve([name, removeCallback]);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import { tmpDir } from './tmp.js';
|
|
3
|
+
|
|
4
|
+
import { getAccessToken } from '../src/access-token.js';
|
|
5
|
+
|
|
6
|
+
test('access tokens', async t => {
|
|
7
|
+
const [sharedStateDir, removeCallback] = await tmpDir('access-token-test');
|
|
8
|
+
const [a, b, c] = await Promise.all([
|
|
9
|
+
getAccessToken(1234, sharedStateDir),
|
|
10
|
+
getAccessToken(1234, sharedStateDir),
|
|
11
|
+
getAccessToken(1234, sharedStateDir),
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
t.is(a, b);
|
|
15
|
+
t.is(a, c);
|
|
16
|
+
await removeCallback();
|
|
17
|
+
});
|