@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 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-14-dev-0169c7e.0+0169c7e",
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
- "ava": "^5.2.0",
25
- "c8": "^7.13.0"
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/**/test-*.js"
36
+ "test/**/*.test.*"
37
+ ],
38
+ "require": [
39
+ "@endo/init/debug.js"
33
40
  ],
34
41
  "timeout": "2m"
35
42
  },
36
- "gitHead": "0169c7e505099fdcfa8e4d1436e5d3b372f1c320"
43
+ "typeCoverage": {
44
+ "atLeast": 83.97
45
+ },
46
+ "gitHead": "8879538cd1d125a08346f02dd5701d0d70c90bb8"
37
47
  }
@@ -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(port) {
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
- // Ensure an access token exists.
46
- const { storage, commit, close } = openJSONStore(sharedStateDir);
47
- const accessTokenKey = `accessToken/${port}`;
48
- if (!storage.has(accessTokenKey)) {
49
- storage.set(accessTokenKey, await generateAccessToken());
50
- await commit();
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
- const accessToken = storage.get(accessTokenKey);
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(dirPath, forceReset = false) {
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
- // @ts-expect-error JSON.parse can take a Buffer
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
- // Nothing to do here.
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
+ });
@@ -1,6 +1,9 @@
1
1
  // This file can contain .js-specific Typescript compiler config.
2
2
  {
3
3
  "extends": "../../tsconfig.json",
4
+ "compilerOptions": {
5
+ "allowSyntheticDefaultImports": true
6
+ },
4
7
  "include": [
5
8
  "src/**/*.js",
6
9
  "test/**/*.js"