Update
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ lib
|
|||||||
docs
|
docs
|
||||||
ref
|
ref
|
||||||
*.opvault
|
*.opvault
|
||||||
|
freddy
|
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
@ -5,11 +5,15 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"type": "pwa-node",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "REPL",
|
"name": "REPL",
|
||||||
"skipFiles": ["<node_internals>/**"],
|
"type": "node",
|
||||||
"program": "repl.ts"
|
"request": "launch",
|
||||||
|
"runtimeExecutable": "node",
|
||||||
|
"runtimeArgs": ["--nolazy", "-r", "ts-node/register/transpile-only"],
|
||||||
|
"args": ["repl.ts"],
|
||||||
|
"cwd": "${workspaceRoot}",
|
||||||
|
"internalConsoleOptions": "openOnSessionStart",
|
||||||
|
"skipFiles": ["<node_internals>/**"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
|
@ -5,19 +5,22 @@
|
|||||||
"repository": "https://git.aet.ac/aet/opvault.js.git",
|
"repository": "https://git.aet.ac/aet/opvault.js.git",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c; prettier --write lib >/dev/null",
|
"build": "rollup -c; cp src/adapters/index.d.ts lib/adapters/; prettier --write lib >/dev/null",
|
||||||
"build:docs": "typedoc --out docs src/index.ts --excludePrivate",
|
"build:docs": "typedoc --out docs src/index.ts --excludePrivate",
|
||||||
"test": "mocha test/**/*.test.ts",
|
"test": "node --expose-gc node_modules/mocha/bin/_mocha test/**/*.test.ts",
|
||||||
"repl": "node -r ts-node/register/transpile-only src/repl.ts"
|
"repl": "node -r ts-node/register/transpile-only src/repl.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/plugin-json": "^4.1.0",
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
"@rollup/plugin-replace": "^3.0.0",
|
||||||
"@types/chai": "^4.2.19",
|
"@types/chai": "^4.2.19",
|
||||||
"@types/chai-as-promised": "^7.1.4",
|
"@types/chai-as-promised": "^7.1.4",
|
||||||
"@types/fs-extra": "^9.0.11",
|
"@types/fs-extra": "^9.0.11",
|
||||||
"@types/mocha": "github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768",
|
"@types/mocha": "github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768",
|
||||||
"@types/node": "^16.0.0",
|
"@types/node": "^16.0.0",
|
||||||
"@types/prompts": "^2.0.13",
|
"@types/prompts": "^2.0.13",
|
||||||
|
"@types/sinon": "^10.0.2",
|
||||||
|
"@types/sinon-chai": "^3.2.5",
|
||||||
"@typescript-eslint/eslint-plugin": "4.28.2",
|
"@typescript-eslint/eslint-plugin": "4.28.2",
|
||||||
"@typescript-eslint/parser": "4.28.2",
|
"@typescript-eslint/parser": "4.28.2",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
@ -37,6 +40,8 @@
|
|||||||
"prompts": "^2.4.1",
|
"prompts": "^2.4.1",
|
||||||
"rollup": "^2.52.7",
|
"rollup": "^2.52.7",
|
||||||
"rollup-plugin-ts": "^1.4.0",
|
"rollup-plugin-ts": "^1.4.0",
|
||||||
|
"sinon": "^11.1.1",
|
||||||
|
"sinon-chai": "^3.7.0",
|
||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"tsconfig-paths": "^3.9.0",
|
"tsconfig-paths": "^3.9.0",
|
||||||
"typedoc": "^0.21.2",
|
"typedoc": "^0.21.2",
|
||||||
|
110
pnpm-lock.yaml
generated
110
pnpm-lock.yaml
generated
@ -2,12 +2,15 @@ lockfileVersion: 5.3
|
|||||||
|
|
||||||
specifiers:
|
specifiers:
|
||||||
'@rollup/plugin-json': ^4.1.0
|
'@rollup/plugin-json': ^4.1.0
|
||||||
|
'@rollup/plugin-replace': ^3.0.0
|
||||||
'@types/chai': ^4.2.19
|
'@types/chai': ^4.2.19
|
||||||
'@types/chai-as-promised': ^7.1.4
|
'@types/chai-as-promised': ^7.1.4
|
||||||
'@types/fs-extra': ^9.0.11
|
'@types/fs-extra': ^9.0.11
|
||||||
'@types/mocha': github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768
|
'@types/mocha': github:whitecolor/mocha-types#da22474cf43f48a56c86f8c23a5a0ea36e295768
|
||||||
'@types/node': ^16.0.0
|
'@types/node': ^16.0.0
|
||||||
'@types/prompts': ^2.0.13
|
'@types/prompts': ^2.0.13
|
||||||
|
'@types/sinon': ^10.0.2
|
||||||
|
'@types/sinon-chai': ^3.2.5
|
||||||
'@typescript-eslint/eslint-plugin': 4.28.2
|
'@typescript-eslint/eslint-plugin': 4.28.2
|
||||||
'@typescript-eslint/parser': 4.28.2
|
'@typescript-eslint/parser': 4.28.2
|
||||||
chai: ^4.3.4
|
chai: ^4.3.4
|
||||||
@ -27,6 +30,8 @@ specifiers:
|
|||||||
prompts: ^2.4.1
|
prompts: ^2.4.1
|
||||||
rollup: ^2.52.7
|
rollup: ^2.52.7
|
||||||
rollup-plugin-ts: ^1.4.0
|
rollup-plugin-ts: ^1.4.0
|
||||||
|
sinon: ^11.1.1
|
||||||
|
sinon-chai: ^3.7.0
|
||||||
tiny-invariant: 1.1.0
|
tiny-invariant: 1.1.0
|
||||||
ts-node: ^10.0.0
|
ts-node: ^10.0.0
|
||||||
tsconfig-paths: ^3.9.0
|
tsconfig-paths: ^3.9.0
|
||||||
@ -40,12 +45,15 @@ dependencies:
|
|||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@rollup/plugin-json': 4.1.0_rollup@2.52.7
|
'@rollup/plugin-json': 4.1.0_rollup@2.52.7
|
||||||
|
'@rollup/plugin-replace': 3.0.0_rollup@2.52.7
|
||||||
'@types/chai': 4.2.19
|
'@types/chai': 4.2.19
|
||||||
'@types/chai-as-promised': 7.1.4
|
'@types/chai-as-promised': 7.1.4
|
||||||
'@types/fs-extra': 9.0.11
|
'@types/fs-extra': 9.0.11
|
||||||
'@types/mocha': github.com/whitecolor/mocha-types/da22474cf43f48a56c86f8c23a5a0ea36e295768
|
'@types/mocha': github.com/whitecolor/mocha-types/da22474cf43f48a56c86f8c23a5a0ea36e295768
|
||||||
'@types/node': 16.0.0
|
'@types/node': 16.0.0
|
||||||
'@types/prompts': 2.0.13
|
'@types/prompts': 2.0.13
|
||||||
|
'@types/sinon': 10.0.2
|
||||||
|
'@types/sinon-chai': 3.2.5
|
||||||
'@typescript-eslint/eslint-plugin': 4.28.2_5031fffb45dfb7117e61c1d8ea1ef3ff
|
'@typescript-eslint/eslint-plugin': 4.28.2_5031fffb45dfb7117e61c1d8ea1ef3ff
|
||||||
'@typescript-eslint/parser': 4.28.2_eslint@7.30.0+typescript@4.3.5
|
'@typescript-eslint/parser': 4.28.2_eslint@7.30.0+typescript@4.3.5
|
||||||
chai: 4.3.4
|
chai: 4.3.4
|
||||||
@ -65,6 +73,8 @@ devDependencies:
|
|||||||
prompts: 2.4.1
|
prompts: 2.4.1
|
||||||
rollup: 2.52.7
|
rollup: 2.52.7
|
||||||
rollup-plugin-ts: 1.4.0_rollup@2.52.7+typescript@4.3.5
|
rollup-plugin-ts: 1.4.0_rollup@2.52.7+typescript@4.3.5
|
||||||
|
sinon: 11.1.1
|
||||||
|
sinon-chai: 3.7.0_chai@4.3.4+sinon@11.1.1
|
||||||
ts-node: 10.0.0_488376d43314e2606bceb2872a37d0ef
|
ts-node: 10.0.0_488376d43314e2606bceb2872a37d0ef
|
||||||
tsconfig-paths: 3.9.0
|
tsconfig-paths: 3.9.0
|
||||||
typedoc: 0.21.2_typescript@4.3.5
|
typedoc: 0.21.2_typescript@4.3.5
|
||||||
@ -1275,6 +1285,16 @@ packages:
|
|||||||
rollup: 2.52.7
|
rollup: 2.52.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@rollup/plugin-replace/3.0.0_rollup@2.52.7:
|
||||||
|
resolution: {integrity: sha512-3c7JCbMuYXM4PbPWT4+m/4Y6U60SgsnDT/cCyAyUKwFHg7pTSfsSQzIpETha3a3ig6OdOKzZz87D9ZXIK3qsDg==}
|
||||||
|
peerDependencies:
|
||||||
|
rollup: ^1.20.0 || ^2.0.0
|
||||||
|
dependencies:
|
||||||
|
'@rollup/pluginutils': 3.1.0_rollup@2.52.7
|
||||||
|
magic-string: 0.25.7
|
||||||
|
rollup: 2.52.7
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@rollup/pluginutils/3.1.0_rollup@2.52.7:
|
/@rollup/pluginutils/3.1.0_rollup@2.52.7:
|
||||||
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
|
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
|
||||||
engines: {node: '>= 8.0.0'}
|
engines: {node: '>= 8.0.0'}
|
||||||
@ -1298,6 +1318,30 @@ packages:
|
|||||||
rollup: 2.52.7
|
rollup: 2.52.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@sinonjs/commons/1.8.3:
|
||||||
|
resolution: {integrity: sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==}
|
||||||
|
dependencies:
|
||||||
|
type-detect: 4.0.8
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@sinonjs/fake-timers/7.1.2:
|
||||||
|
resolution: {integrity: sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==}
|
||||||
|
dependencies:
|
||||||
|
'@sinonjs/commons': 1.8.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@sinonjs/samsam/6.0.2:
|
||||||
|
resolution: {integrity: sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==}
|
||||||
|
dependencies:
|
||||||
|
'@sinonjs/commons': 1.8.3
|
||||||
|
lodash.get: 4.4.2
|
||||||
|
type-detect: 4.0.8
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@sinonjs/text-encoding/0.7.1:
|
||||||
|
resolution: {integrity: sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@tsconfig/node10/1.0.8:
|
/@tsconfig/node10/1.0.8:
|
||||||
resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==}
|
resolution: {integrity: sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -1357,6 +1401,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-E121rHk/4BlcEwANZOwcHl8L/Sl0zyIFXJoyggXkl7FCT/4MTf5u25f+qiphe0V5ELaFIkCptgvbf4whCJUVMA==}
|
resolution: {integrity: sha512-E121rHk/4BlcEwANZOwcHl8L/Sl0zyIFXJoyggXkl7FCT/4MTf5u25f+qiphe0V5ELaFIkCptgvbf4whCJUVMA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/chai/4.2.21:
|
||||||
|
resolution: {integrity: sha512-yd+9qKmJxm496BOV9CMNaey8TWsikaZOwMRwPHQIjcOJM9oV+fi9ZMNw3JsVnbEEbo2gRTDnGEBv8pjyn67hNg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/estree/0.0.39:
|
/@types/estree/0.0.39:
|
||||||
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -1397,6 +1445,19 @@ packages:
|
|||||||
resolution: {integrity: sha512-0caWDWmpCp0uifxFh+FaqK3CuZ2SkRR/ZRxAV5+zNdC3QVUi6wyOJnefhPvtNt8NQWXB5OA93BUvZsXpWat2Xw==}
|
resolution: {integrity: sha512-0caWDWmpCp0uifxFh+FaqK3CuZ2SkRR/ZRxAV5+zNdC3QVUi6wyOJnefhPvtNt8NQWXB5OA93BUvZsXpWat2Xw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/sinon-chai/3.2.5:
|
||||||
|
resolution: {integrity: sha512-bKQqIpew7mmIGNRlxW6Zli/QVyc3zikpGzCa797B/tRnD9OtHvZ/ts8sYXV+Ilj9u3QRaUEM8xrjgd1gwm1BpQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/chai': 4.2.21
|
||||||
|
'@types/sinon': 10.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/@types/sinon/10.0.2:
|
||||||
|
resolution: {integrity: sha512-BHn8Bpkapj8Wdfxvh2jWIUoaYB/9/XhsL0oOvBfRagJtKlSl9NWPcFOz2lRukI9szwGxFtYZCTejJSqsGDbdmw==}
|
||||||
|
dependencies:
|
||||||
|
'@sinonjs/fake-timers': 7.1.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/ua-parser-js/0.7.36:
|
/@types/ua-parser-js/0.7.36:
|
||||||
resolution: {integrity: sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==}
|
resolution: {integrity: sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -2820,6 +2881,10 @@ packages:
|
|||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/isarray/0.0.1:
|
||||||
|
resolution: {integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/isbot/3.0.26:
|
/isbot/3.0.26:
|
||||||
resolution: {integrity: sha512-y1IwTPP6pRGDmQUTrCz1bZ9ZPSmij3eWruBBIiCOARX5ueyLv58xuFxvUGg6uI0k9u1swnOmJR8DKYZbcDXLqQ==}
|
resolution: {integrity: sha512-y1IwTPP6pRGDmQUTrCz1bZ9ZPSmij3eWruBBIiCOARX5ueyLv58xuFxvUGg6uI0k9u1swnOmJR8DKYZbcDXLqQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -2916,6 +2981,10 @@ packages:
|
|||||||
object.assign: 4.1.1
|
object.assign: 4.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/just-extend/4.2.1:
|
||||||
|
resolution: {integrity: sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/kleur/3.0.3:
|
/kleur/3.0.3:
|
||||||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@ -2970,6 +3039,10 @@ packages:
|
|||||||
resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=}
|
resolution: {integrity: sha1-gteb/zCmfEAF/9XiUVMArZyk168=}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash.get/4.4.2:
|
||||||
|
resolution: {integrity: sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/lodash.isempty/4.4.0:
|
/lodash.isempty/4.4.0:
|
||||||
resolution: {integrity: sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=}
|
resolution: {integrity: sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=}
|
||||||
dev: true
|
dev: true
|
||||||
@ -3170,6 +3243,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/nise/5.1.0:
|
||||||
|
resolution: {integrity: sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==}
|
||||||
|
dependencies:
|
||||||
|
'@sinonjs/commons': 1.8.3
|
||||||
|
'@sinonjs/fake-timers': 7.1.2
|
||||||
|
'@sinonjs/text-encoding': 0.7.1
|
||||||
|
just-extend: 4.2.1
|
||||||
|
path-to-regexp: 1.8.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/node-releases/1.1.73:
|
/node-releases/1.1.73:
|
||||||
resolution: {integrity: sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==}
|
resolution: {integrity: sha512-uW7fodD6pyW2FZNZnp/Z3hvWKeEW1Y8R1+1CnErE8cXFXzl5blBOoVB41CvMer6P6Q0S5FXDwcHgFd1Wj0U9zg==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -3383,6 +3466,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/path-to-regexp/1.8.0:
|
||||||
|
resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==}
|
||||||
|
dependencies:
|
||||||
|
isarray: 0.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/path-type/3.0.0:
|
/path-type/3.0.0:
|
||||||
resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
|
resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -3717,6 +3806,27 @@ packages:
|
|||||||
object-inspect: 1.10.3
|
object-inspect: 1.10.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/sinon-chai/3.7.0_chai@4.3.4+sinon@11.1.1:
|
||||||
|
resolution: {integrity: sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g==}
|
||||||
|
peerDependencies:
|
||||||
|
chai: ^4.0.0
|
||||||
|
sinon: '>=4.0.0'
|
||||||
|
dependencies:
|
||||||
|
chai: 4.3.4
|
||||||
|
sinon: 11.1.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/sinon/11.1.1:
|
||||||
|
resolution: {integrity: sha512-ZSSmlkSyhUWbkF01Z9tEbxZLF/5tRC9eojCdFh33gtQaP7ITQVaMWQHGuFM7Cuf/KEfihuh1tTl3/ABju3AQMg==}
|
||||||
|
dependencies:
|
||||||
|
'@sinonjs/commons': 1.8.3
|
||||||
|
'@sinonjs/fake-timers': 7.1.2
|
||||||
|
'@sinonjs/samsam': 6.0.2
|
||||||
|
diff: 5.0.0
|
||||||
|
nise: 5.1.0
|
||||||
|
supports-color: 7.2.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/sisteransi/1.0.5:
|
/sisteransi/1.0.5:
|
||||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
14
repl.ts
14
repl.ts
@ -1,11 +1,13 @@
|
|||||||
#!/usr/bin/env ts-node-transpile-only
|
#!/usr/bin/env node -r ts-node/register/transpile-only
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
import prompts from "prompts"
|
import prompts from "prompts"
|
||||||
import { OnePassword } from "./src/index"
|
import { OnePassword } from "./src/index"
|
||||||
|
|
||||||
|
const path = "./freddy"
|
||||||
|
|
||||||
async function main(args: string[]) {
|
async function main(args: string[]) {
|
||||||
const instance = new OnePassword({ path: args[0] })
|
const instance = new OnePassword({ path })
|
||||||
const profiles = await instance.getProfileNames()
|
const profiles = await instance.getProfileNames()
|
||||||
|
|
||||||
// const { profile } = await prompts({
|
// const { profile } = await prompts({
|
||||||
@ -28,13 +30,17 @@ async function main(args: string[]) {
|
|||||||
|
|
||||||
vault.unlock(password)
|
vault.unlock(password)
|
||||||
|
|
||||||
|
const find = vault.overviews.get("A note with some attachments")!
|
||||||
|
const item = vault.getItem(find.uuid!)
|
||||||
|
|
||||||
const d = vault.decryptAttachment(
|
const d = vault.decryptAttachment(
|
||||||
|
item!.original,
|
||||||
fs.readFileSync(
|
fs.readFileSync(
|
||||||
"./freddy-2013-12-04.opvault/default/1C7D72EFA19A4EE98DB7A9661D2F5732_3B94A1F475014E27BFB00C99A42214DF.attachment"
|
"./freddy/default/F2DB5DA3FCA64372A751E0E85C67A538_AFBDA49A5F684179A78161E40CA2AAD3.attachment"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
fs.writeFileSync("./test", d)
|
fs.writeFileSync("./test2.png", d.file)
|
||||||
|
|
||||||
// console.log(vault.overviews.values())
|
// console.log(vault.overviews.values())
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,28 @@
|
|||||||
import { builtinModules } from "module"
|
import { builtinModules } from "module"
|
||||||
import ts from "rollup-plugin-ts"
|
import ts from "rollup-plugin-ts"
|
||||||
import json from "@rollup/plugin-json"
|
import json from "@rollup/plugin-json"
|
||||||
|
import replace from "@rollup/plugin-replace"
|
||||||
import { dependencies } from "./package.json"
|
import { dependencies } from "./package.json"
|
||||||
|
|
||||||
/** @returns {import("rollup").RollupOptions} */
|
/** @returns {import("rollup").RollupOptions} */
|
||||||
export default () => ({
|
export default () => ({
|
||||||
input: "./src/index.ts",
|
input: {
|
||||||
|
index: "./src/index.ts",
|
||||||
|
"adapters/node": "./src/adapters/node.ts",
|
||||||
|
},
|
||||||
external: builtinModules.concat(Object.keys(dependencies)),
|
external: builtinModules.concat(Object.keys(dependencies)),
|
||||||
output: {
|
output: {
|
||||||
file: "lib/index.js",
|
dir: "lib",
|
||||||
format: "cjs",
|
format: "cjs",
|
||||||
},
|
},
|
||||||
plugins: [ts(), json()],
|
plugins: [
|
||||||
|
ts({ transpileOnly: true }),
|
||||||
|
json(),
|
||||||
|
replace({
|
||||||
|
preventAssignment: true,
|
||||||
|
values: {
|
||||||
|
"process.env.NODE_ENV": '"production"',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
0
src/adapters/browser.ts
Normal file
0
src/adapters/browser.ts
Normal file
43
src/adapters/index.d.ts
vendored
Normal file
43
src/adapters/index.d.ts
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* An object that implements basic file system functionalities.
|
||||||
|
*/
|
||||||
|
export interface IFileSystem {
|
||||||
|
/**
|
||||||
|
* Synchronously tests whether or not the given path exists by checking with the file system.
|
||||||
|
* @param path A path to a file or directory.
|
||||||
|
*/
|
||||||
|
existsSync(path: string): boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously reads the entire contents of a file.
|
||||||
|
* @param path A path to a file.
|
||||||
|
*/
|
||||||
|
readFile(path: string): Promise<string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously writes data to a file, replacing the file if it already exists.
|
||||||
|
* @param path A path to a file.
|
||||||
|
* @param data The data to write.
|
||||||
|
*/
|
||||||
|
writeFile(path: string, data: string): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous readdir(3) - read a directory.
|
||||||
|
* @param path A path to a directory.
|
||||||
|
*/
|
||||||
|
readdir(path: string): Promise<string[]>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronous stat(2) - Get file status.
|
||||||
|
* @param path A path to a file.
|
||||||
|
*/
|
||||||
|
stat(path: string): Promise<{ isDirectory(): boolean }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAdapter {
|
||||||
|
/**
|
||||||
|
* Underlying `fs` module. You can replace it with a wrapper of
|
||||||
|
* `memfs` or any object that implements `IFileSystem`.
|
||||||
|
*/
|
||||||
|
fs: IFileSystem
|
||||||
|
}
|
19
src/adapters/node.ts
Normal file
19
src/adapters/node.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { promises as fs, existsSync } from "fs"
|
||||||
|
|
||||||
|
import type { IAdapter } from "./index"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Node.js adapter. This can be used while using `opvault.js`
|
||||||
|
* in a Node.js environment.
|
||||||
|
*/
|
||||||
|
const nodeAdapter: IAdapter = {
|
||||||
|
fs: {
|
||||||
|
readFile: path => fs.readFile(path, "utf-8"),
|
||||||
|
writeFile: fs.writeFile,
|
||||||
|
readdir: fs.readdir,
|
||||||
|
stat: fs.stat,
|
||||||
|
existsSync,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default nodeAdapter
|
@ -1,9 +1,18 @@
|
|||||||
import { webcrypto } from "crypto"
|
import { webcrypto, createHmac, createDecipheriv, createHash } from "crypto"
|
||||||
|
import { HMACAssertionError } from "./errors"
|
||||||
|
|
||||||
declare module "crypto" {
|
declare module "crypto" {
|
||||||
export const webcrypto: Crypto
|
export const webcrypto: Crypto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Encryption and MAC */
|
||||||
|
export interface Cipher {
|
||||||
|
/** Encryption key */
|
||||||
|
key: Buffer
|
||||||
|
/** HMAC key */
|
||||||
|
hmac: Buffer
|
||||||
|
}
|
||||||
|
|
||||||
async function pbkdf2(password: string, salt: string, iterations = 1000, length = 256) {
|
async function pbkdf2(password: string, salt: string, iterations = 1000, length = 256) {
|
||||||
const encoder = new TextEncoder()
|
const encoder = new TextEncoder()
|
||||||
const key = await webcrypto.subtle.importKey(
|
const key = await webcrypto.subtle.importKey(
|
||||||
@ -25,3 +34,40 @@ async function pbkdf2(password: string, salt: string, iterations = 1000, length
|
|||||||
)
|
)
|
||||||
return bits
|
return bits
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assertHMac(data: Buffer, key: Buffer, expected: Buffer) {
|
||||||
|
const actual = createHmac("sha256", key).update(data).digest()
|
||||||
|
if (!actual.equals(expected)) {
|
||||||
|
throw new HMACAssertionError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const splitPlainText = (derivedKey: Buffer): Cipher => ({
|
||||||
|
key: derivedKey.slice(0, 32),
|
||||||
|
hmac: derivedKey.slice(32, 64),
|
||||||
|
})
|
||||||
|
|
||||||
|
export function decryptKeys(encryptedKey: string, derived: Cipher) {
|
||||||
|
const buffer = Buffer.from(encryptedKey, "base64")
|
||||||
|
const base = decryptOPData(buffer, derived)
|
||||||
|
const digest = createHash("sha512").update(base).digest()
|
||||||
|
return splitPlainText(digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decryptData(key: Buffer, iv: Buffer, data: Buffer) {
|
||||||
|
const cipher = createDecipheriv("aes-256-cbc", key, iv).setAutoPadding(false)
|
||||||
|
return Buffer.concat([cipher.update(data), cipher.final()])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decryptOPData(cipherText: Buffer, cipher: Cipher) {
|
||||||
|
const key = cipherText.slice(0, -32)
|
||||||
|
assertHMac(key, cipher.hmac, cipherText.slice(-32))
|
||||||
|
|
||||||
|
const plaintext = decryptData(cipher.key, key.slice(16, 32), key.slice(32))
|
||||||
|
const size = readUint16(key.slice(8, 16))
|
||||||
|
return plaintext.slice(-size)
|
||||||
|
}
|
||||||
|
|
||||||
|
function readUint16({ buffer, byteOffset, length }: Buffer) {
|
||||||
|
return new DataView(buffer, byteOffset, length).getUint16(0, true)
|
||||||
|
}
|
||||||
|
49
src/ee.ts
Normal file
49
src/ee.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
type EventKeyWithNoArg<T> = {
|
||||||
|
[K in keyof T]: T[K] extends void ? K : never
|
||||||
|
}[keyof T]
|
||||||
|
|
||||||
|
type CallbackSignature<T, K extends keyof T> = K extends EventKeyWithNoArg<T>
|
||||||
|
? () => void
|
||||||
|
: (value: T[K]) => void
|
||||||
|
|
||||||
|
export class EventEmitter<T extends Record</* eventName */ string, /* eventArg */ any>> {
|
||||||
|
#listeners = new Map<keyof T, Set<(...args: any[]) => any>>()
|
||||||
|
|
||||||
|
#getList(key: keyof T) {
|
||||||
|
if (!this.#listeners.has(key)) {
|
||||||
|
this.#listeners.set(key, new Set())
|
||||||
|
}
|
||||||
|
return this.#listeners.get(key)!
|
||||||
|
}
|
||||||
|
|
||||||
|
on<K extends keyof T>(key: K, fn: CallbackSignature<T, K>) {
|
||||||
|
this.#getList(key).add(fn)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
off<K extends keyof T>(key: K, fn: CallbackSignature<T, K>) {
|
||||||
|
this.#getList(key).delete(fn)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
once<K extends keyof T>(key: K, fn: CallbackSignature<T, K>) {
|
||||||
|
const wrapped = (...arg: any[]) => {
|
||||||
|
;(fn as any)(...arg)
|
||||||
|
this.off(key, wrapped)
|
||||||
|
}
|
||||||
|
return this.on(key, wrapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<K extends EventKeyWithNoArg<T>>(key: K): this
|
||||||
|
emit<K extends keyof T>(key: K, value: T[K]): this
|
||||||
|
|
||||||
|
emit<K extends keyof T>(key: K, value?: T[K]) {
|
||||||
|
const listeners = this.#getList(key)
|
||||||
|
if (arguments.length === 1) {
|
||||||
|
listeners.forEach(fn => fn())
|
||||||
|
} else {
|
||||||
|
listeners.forEach(fn => fn(value!))
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
}
|
122
src/fs.ts
122
src/fs.ts
@ -1,94 +1,64 @@
|
|||||||
import { resolve } from "path"
|
import { resolve, extname } from "path"
|
||||||
import invariant from "tiny-invariant"
|
import invariant from "tiny-invariant"
|
||||||
import type { Stats } from "fs"
|
import type { IFileSystem } from "./adapters"
|
||||||
|
import { once } from "./util"
|
||||||
|
|
||||||
/**
|
export type OnePasswordFileManager = ReturnType<typeof OnePasswordFileManager>
|
||||||
* An object that implements basic file system functionalities.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export interface IFileSystem {
|
|
||||||
/**
|
|
||||||
* Synchronously tests whether or not the given path exists by checking with the file system.
|
|
||||||
* @param path A path to a file or directory.
|
|
||||||
*/
|
|
||||||
existsSync(path: string): boolean
|
|
||||||
|
|
||||||
/**
|
export function OnePasswordFileManager(
|
||||||
* Asynchronously reads the entire contents of a file.
|
fs: IFileSystem,
|
||||||
* @param path A path to a file.
|
path: string,
|
||||||
*/
|
profileName: string
|
||||||
readFile(path: string, encoding: BufferEncoding): Promise<string>
|
) {
|
||||||
|
const root = resolve(path, profileName)
|
||||||
/**
|
|
||||||
* Asynchronously writes data to a file, replacing the file if it already exists.
|
|
||||||
* @param path A path to a file.
|
|
||||||
* @param data The data to write.
|
|
||||||
*/
|
|
||||||
writeFile(path: string, data: string): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous readdir(3) - read a directory.
|
|
||||||
* @param path A path to a directory.
|
|
||||||
*/
|
|
||||||
readdir(path: string): Promise<string[]>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronous stat(2) - Get file status.
|
|
||||||
* @param path A path to a file.
|
|
||||||
*/
|
|
||||||
stat(path: string): Promise<Stats>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDefaultFileSystem(): IFileSystem {
|
|
||||||
const fs: typeof import("fs") = require("fs")
|
|
||||||
return { ...fs.promises, existsSync: fs.existsSync }
|
|
||||||
}
|
|
||||||
|
|
||||||
export class OnePasswordFileManager {
|
|
||||||
private root: string
|
|
||||||
|
|
||||||
constructor(private fs: IFileSystem, path: string, profileName: string) {
|
|
||||||
this.root = resolve(path, profileName)
|
|
||||||
invariant(fs.existsSync(path), `Path ${path} does not exist.`)
|
invariant(fs.existsSync(path), `Path ${path} does not exist.`)
|
||||||
invariant(fs.existsSync(this.root), `Profile ${profileName} does not exist.`)
|
invariant(fs.existsSync(root), `Profile ${profileName} does not exist.`)
|
||||||
}
|
|
||||||
|
|
||||||
#hasFile(path: string) {
|
const abs = (path: string) => resolve(root, path)
|
||||||
return this.fs.existsSync(resolve(this.root, path))
|
|
||||||
}
|
|
||||||
|
|
||||||
async #readFile(path: string) {
|
|
||||||
return await this.fs.readFile(resolve(this.root, path), "utf-8")
|
|
||||||
}
|
|
||||||
|
|
||||||
async #writeFile(path: string, value: string) {
|
|
||||||
return await this.fs.writeFile(resolve(this.root, path), value)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const result = {
|
||||||
getProfile() {
|
getProfile() {
|
||||||
return this.#readFile("profile.js")
|
return fs.readFile(abs("profile.js"))
|
||||||
}
|
},
|
||||||
|
|
||||||
getFolders() {
|
getFolders() {
|
||||||
return this.#readFile("folders.js")
|
return fs.readFile(abs("folders.js"))
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAttachments() {
|
||||||
|
const files = await fs.readdir(root)
|
||||||
|
files
|
||||||
|
.filter(name => extname(name) === ".attachment")
|
||||||
|
.forEach(name => {
|
||||||
|
const sep = name.indexOf("_")
|
||||||
|
const path = resolve(root, name)
|
||||||
|
const [itemUUID, fileUUID] = [name.slice(0, sep), name.slice(sep + 1)]
|
||||||
|
return {
|
||||||
|
itemUUID,
|
||||||
|
fileUUID,
|
||||||
|
getFile: once(() => fs.readFile(path)),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
async getBand(name: string) {
|
async getBand(name: string) {
|
||||||
const fileName = `band_${name.toUpperCase()}.js`
|
const path = abs(`band_${name}.js`)
|
||||||
if (this.#hasFile(fileName)) {
|
if (fs.existsSync(path)) {
|
||||||
return await this.#readFile(fileName)
|
return await fs.readFile(path)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
setProfile(profile: string) {
|
async setProfile(profile: string) {
|
||||||
this.#writeFile("profile.js", profile)
|
await fs.writeFile("profile.js", profile)
|
||||||
}
|
},
|
||||||
|
|
||||||
setFolders(folders: string) {
|
async setFolders(folders: string) {
|
||||||
this.#writeFile("folders.js", folders)
|
await fs.writeFile("folders.js", folders)
|
||||||
}
|
},
|
||||||
|
|
||||||
setBand(name: string, band: string) {
|
async setBand(name: string, band: string) {
|
||||||
this.#writeFile(`band_${name}.js`, band)
|
await fs.writeFile(`band_${name}.js`, band)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
5
src/global.d.ts
vendored
Normal file
5
src/global.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
type integer = number
|
||||||
|
|
||||||
|
interface Disposable {
|
||||||
|
dispose(): void
|
||||||
|
}
|
31
src/index.ts
31
src/index.ts
@ -1,22 +1,22 @@
|
|||||||
import { resolve } from "path"
|
import { resolve } from "path"
|
||||||
import type { IFileSystem } from "./fs"
|
import { Vault } from "./models/vault"
|
||||||
import { getDefaultFileSystem } from "./fs"
|
import { invariant } from "./errors"
|
||||||
import { Vault } from "./vault"
|
import type { IAdapter } from "./adapters"
|
||||||
|
import { asyncMap } from "./util"
|
||||||
|
|
||||||
export type { Vault } from "./vault"
|
export type { Vault } from "./models/vault"
|
||||||
export { Category, FieldType } from "./models"
|
export { Category, FieldType } from "./models"
|
||||||
|
|
||||||
interface Options {
|
interface IOptions {
|
||||||
/**
|
/**
|
||||||
* Path to `.opvault` directory
|
* Path to `.opvault` directory
|
||||||
*/
|
*/
|
||||||
path: string
|
path: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Underlying `fs` module. You can replace it with a wrapper of
|
* Adapter used to interact with the file system and cryptography modules
|
||||||
* `memfs` or any object that implements `IFileSystem`.
|
|
||||||
*/
|
*/
|
||||||
fs?: IFileSystem
|
adapter?: IAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,29 +24,28 @@ interface Options {
|
|||||||
*/
|
*/
|
||||||
export class OnePassword {
|
export class OnePassword {
|
||||||
readonly #path: string
|
readonly #path: string
|
||||||
readonly #fs: IFileSystem
|
readonly #adapter: IAdapter
|
||||||
|
|
||||||
constructor({ path, fs = getDefaultFileSystem() }: Options) {
|
constructor({ path, adapter = require("./adapters/node").default }: IOptions) {
|
||||||
this.#fs = fs
|
this.#adapter = adapter
|
||||||
this.#path = path
|
this.#path = path
|
||||||
|
invariant(path, "Path must not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns A list of names of profiles of the current vault.
|
* @returns A list of names of profiles of the current vault.
|
||||||
*/
|
*/
|
||||||
async getProfileNames() {
|
async getProfileNames() {
|
||||||
const [fs, path] = [this.#fs, this.#path]
|
const [fs, path] = [this.#adapter.fs, this.#path]
|
||||||
const children = await fs.readdir(path)
|
const children = await fs.readdir(path)
|
||||||
const profiles: string[] = []
|
const profiles: string[] = []
|
||||||
await Promise.all(
|
await asyncMap(children, async child => {
|
||||||
children.map(async child => {
|
|
||||||
const fullPath = resolve(path, child)
|
const fullPath = resolve(path, child)
|
||||||
const stats = await fs.stat(fullPath)
|
const stats = await fs.stat(fullPath)
|
||||||
if (stats.isDirectory() && fs.existsSync(resolve(fullPath, "profile.js"))) {
|
if (stats.isDirectory() && fs.existsSync(resolve(fullPath, "profile.js"))) {
|
||||||
profiles.push(child)
|
profiles.push(child)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
|
||||||
return profiles
|
return profiles
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +53,6 @@ export class OnePassword {
|
|||||||
* @returns A OnePassword Vault instance.
|
* @returns A OnePassword Vault instance.
|
||||||
*/
|
*/
|
||||||
async getProfile(profileName: string) {
|
async getProfile(profileName: string) {
|
||||||
return await Vault.of(this.#path, profileName, this.#fs)
|
return await Vault.of(this.#path, profileName, this.#adapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
83
src/models/attachment.ts
Normal file
83
src/models/attachment.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import type { Cipher } from "../crypto"
|
||||||
|
import { decryptOPData } from "../crypto"
|
||||||
|
import { invariant } from "../errors"
|
||||||
|
import { cache } from "../util"
|
||||||
|
import type { Item } from "./item"
|
||||||
|
|
||||||
|
type integer = number
|
||||||
|
|
||||||
|
export interface AttachmentMetadata {
|
||||||
|
itemUUID: string
|
||||||
|
contentSize: integer
|
||||||
|
external: boolean
|
||||||
|
updatedAt: integer
|
||||||
|
txTimestamp: integer
|
||||||
|
/** Base64 encoded OPData */
|
||||||
|
overview: string
|
||||||
|
createdAt: integer
|
||||||
|
uuid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Attachment implements Disposable {
|
||||||
|
#item: WeakRef<Item>
|
||||||
|
|
||||||
|
private metadataSize: number
|
||||||
|
private iconSize: number
|
||||||
|
|
||||||
|
constructor(item: Item, private buffer: Buffer) {
|
||||||
|
this.#validate()
|
||||||
|
this.#item = new WeakRef(item)
|
||||||
|
this.metadataSize = buffer.readIntLE(8, 2)
|
||||||
|
this.iconSize = buffer.readIntLE(12, 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate attachment file.
|
||||||
|
*/
|
||||||
|
#validate() {
|
||||||
|
const file = this.buffer
|
||||||
|
invariant(
|
||||||
|
file.slice(0, 6).toString("utf-8") === "OPCLDA",
|
||||||
|
"Attachment must start with OPCLDA"
|
||||||
|
)
|
||||||
|
invariant(
|
||||||
|
file.readIntLE(7, 1) === 1,
|
||||||
|
"The version for this attachment file format is not supported."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@cache()
|
||||||
|
get metadata(): AttachmentMetadata {
|
||||||
|
return JSON.parse(this.buffer.slice(16, 16 + this.metadataSize).toString("utf-8"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@cache()
|
||||||
|
get icon() {
|
||||||
|
const { buffer, metadataSize, iconSize } = this
|
||||||
|
const iconData = buffer.slice(16 + metadataSize, 16 + metadataSize + iconSize)
|
||||||
|
return decryptOPData(iconData, cipher)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt the content of this attachment. This function
|
||||||
|
* shall be called by an `Item` where `cipher` is `deriveConcreteKey(item, master)`.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
decrypt(cipher: Cipher) {
|
||||||
|
const { buffer, metadataSize, iconSize } = this
|
||||||
|
const metadata: AttachmentMetadata = JSON.parse(
|
||||||
|
buffer.slice(16, 16 + metadataSize).toString("utf-8")
|
||||||
|
)
|
||||||
|
const iconData = buffer.slice(16 + metadataSize, 16 + metadataSize + iconSize)
|
||||||
|
|
||||||
|
return {
|
||||||
|
metadata,
|
||||||
|
icon: decryptOPData(iconData, cipher),
|
||||||
|
file: decryptOPData(buffer.slice(16 + metadataSize + iconSize), cipher),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.buffer = null!
|
||||||
|
}
|
||||||
|
}
|
1
src/models/crypto.ts
Normal file
1
src/models/crypto.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export class Crypto {}
|
67
src/models/item.ts
Normal file
67
src/models/item.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { assertHMac, decryptData, decryptOPData, splitPlainText } from "../crypto"
|
||||||
|
import type { ItemDetails } from "../types"
|
||||||
|
import type { Cipher } from "../crypto"
|
||||||
|
import type { Attachment } from "./attachment"
|
||||||
|
|
||||||
|
export interface EncryptedItem {
|
||||||
|
category: string // "001"
|
||||||
|
/** Unix seconds */
|
||||||
|
created: integer
|
||||||
|
d: string // "b3BkYXRhMbt"
|
||||||
|
folder: string // 32 chars
|
||||||
|
hmac: string // base64
|
||||||
|
k: string // base64
|
||||||
|
o: string // base64
|
||||||
|
tx: integer // Unix seconds
|
||||||
|
updated: integer // Unix seconds
|
||||||
|
uuid: string // 32 chars
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemAttachmentBridge {
|
||||||
|
constructor(public cipher?: Cipher) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Item implements Disposable {
|
||||||
|
#data: EncryptedItem
|
||||||
|
|
||||||
|
itemDetails!: ItemDetails
|
||||||
|
attachments: Attachment[] = []
|
||||||
|
|
||||||
|
get uuid() {
|
||||||
|
return this.#data.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(data: EncryptedItem) {
|
||||||
|
this.#data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.attachments.forEach(a => a.dispose())
|
||||||
|
this.itemDetails = null!
|
||||||
|
}
|
||||||
|
|
||||||
|
#deriveConcreteKey(master: Cipher) {
|
||||||
|
const k = Buffer.from(this.#data.k, "base64")
|
||||||
|
const data = k.slice(0, -32)
|
||||||
|
assertHMac(data, master.hmac, k.slice(-32))
|
||||||
|
const derivedKey = decryptData(master.key, data.slice(0, 16), data.slice(16))
|
||||||
|
return splitPlainText(derivedKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
decryptItemDetail(master: Cipher): ItemDetails {
|
||||||
|
const cipher = this.#deriveConcreteKey(master)
|
||||||
|
const detail = decryptOPData(
|
||||||
|
/* cipherText */ Buffer.from(this.#data.d, "base64"),
|
||||||
|
/* cipher */ cipher
|
||||||
|
)
|
||||||
|
this.itemDetails = JSON.parse(detail.toString("utf-8"))
|
||||||
|
return this.itemDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
decryptAttachments(master: Cipher) {
|
||||||
|
const cipher = this.#deriveConcreteKey(master)
|
||||||
|
this.attachments.forEach(a => a.decrypt(cipher))
|
||||||
|
}
|
||||||
|
}
|
@ -1,24 +1,27 @@
|
|||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
import { HMACAssertionError, invariant } from "./errors"
|
import type { IAdapter } from "../adapters"
|
||||||
import type { IFileSystem } from "./fs"
|
import type { Cipher } from "../crypto"
|
||||||
import { OnePasswordFileManager } from "./fs"
|
import { decryptKeys, splitPlainText, decryptOPData } from "../crypto"
|
||||||
import { i18n } from "./i18n"
|
import { HMACAssertionError, invariant } from "../errors"
|
||||||
|
import { OnePasswordFileManager } from "../fs"
|
||||||
import type {
|
import { i18n } from "../i18n"
|
||||||
Profile,
|
import type { EncryptedItem } from "./item"
|
||||||
Band,
|
import { Item } from "./item"
|
||||||
Overview,
|
import type { Profile, Overview } from "../types"
|
||||||
EncryptedItem,
|
import { WeakValueMap } from "../weakMap"
|
||||||
Item,
|
import { EventEmitter } from "../ee"
|
||||||
AttachmentMetadata,
|
|
||||||
} from "./types"
|
|
||||||
|
|
||||||
|
type Band = { [uuid: string]: Item }
|
||||||
type FoldersMap = { [uuid: string]: Band }
|
type FoldersMap = { [uuid: string]: Band }
|
||||||
|
|
||||||
|
interface VaultEvents {
|
||||||
|
lock: void
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main OnePassword Vault class
|
* Main OnePassword Vault class
|
||||||
*/
|
*/
|
||||||
export class Vault {
|
export class Vault extends EventEmitter<VaultEvents> {
|
||||||
// Ciphers
|
// Ciphers
|
||||||
#master?: Cipher
|
#master?: Cipher
|
||||||
#overview?: Cipher
|
#overview?: Cipher
|
||||||
@ -26,42 +29,50 @@ export class Vault {
|
|||||||
// File system interface
|
// File system interface
|
||||||
#files: OnePasswordFileManager
|
#files: OnePasswordFileManager
|
||||||
|
|
||||||
// Encrypted contents
|
|
||||||
#profile: Profile
|
#profile: Profile
|
||||||
#folders: FoldersMap
|
#folders: FoldersMap
|
||||||
#bands = new Map<string, Band>()
|
|
||||||
|
|
||||||
// Decrypted contents with plain-texts
|
|
||||||
#overviews = new Map<string, Overview>()
|
#overviews = new Map<string, Overview>()
|
||||||
#items = new Map<string, EncryptedItem>()
|
#items: Item[] = []
|
||||||
|
#itemsMap = new WeakValueMap<string, Item>()
|
||||||
|
|
||||||
private constructor(
|
private constructor(
|
||||||
files: OnePasswordFileManager,
|
files: OnePasswordFileManager,
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
folders: FoldersMap,
|
folders: FoldersMap,
|
||||||
bands: Map<string, Band>
|
items: Item[]
|
||||||
) {
|
) {
|
||||||
|
super()
|
||||||
this.#files = files
|
this.#files = files
|
||||||
this.#profile = profile
|
this.#profile = profile
|
||||||
this.#folders = folders
|
this.#folders = folders
|
||||||
this.#bands = bands
|
this.#items = items
|
||||||
|
|
||||||
|
items.forEach(item => {
|
||||||
|
this.#itemsMap.set(item.uuid, item)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new OnePassword Vault instance and read all bands.
|
* Create a new OnePassword Vault instance and read all bands.
|
||||||
|
* @internal
|
||||||
*/
|
*/
|
||||||
static async of(path: string, profileName = "default", fs: IFileSystem) {
|
static async of(path: string, profileName = "default", adapter: IAdapter) {
|
||||||
const files = new OnePasswordFileManager(fs, path, profileName)
|
const files = OnePasswordFileManager(adapter.fs, path, profileName)
|
||||||
const profile = JSON.parse(
|
const profile = JSON.parse(
|
||||||
stripText(await files.getProfile(), /^var profile\s*=/, ";")
|
stripText(await files.getProfile(), /^var profile\s*=/, ";")
|
||||||
)
|
)
|
||||||
const folders = JSON.parse(stripText(await files.getFolders(), "loadFolders(", ");"))
|
const folders = JSON.parse(stripText(await files.getFolders(), "loadFolders(", ");"))
|
||||||
const bands = new Map<string, Band>()
|
const bands: Item[] = []
|
||||||
|
|
||||||
for (let i = 0; i < 36; i++) {
|
for (let i = 0; i < 36; i++) {
|
||||||
const letter = i.toString(36)
|
const letter = i.toString(36).toUpperCase()
|
||||||
const source = await files.getBand(letter)
|
const source = await files.getBand(letter)
|
||||||
bands.set(letter, source ? JSON.parse(stripText(source, "ld(", ");")) : {})
|
if (!source) continue
|
||||||
|
const object = JSON.parse(stripText(source, "ld(", ");"))
|
||||||
|
for (const value of Object.values(object)) {
|
||||||
|
bands.push(new Item(value as EncryptedItem))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Vault(files, profile, folders, bands)
|
return new Vault(files, profile, folders, bands)
|
||||||
@ -120,10 +131,6 @@ export class Vault {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
decryptAttachment(buffer: Buffer) {
|
|
||||||
return decryptAttachment(buffer, this.#master!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove derived keys stored within the class instance.
|
* Remove derived keys stored within the class instance.
|
||||||
*/
|
*/
|
||||||
@ -131,6 +138,7 @@ export class Vault {
|
|||||||
this.#master = null!
|
this.#master = null!
|
||||||
this.#overview = null!
|
this.#overview = null!
|
||||||
this.#overviews.clear()
|
this.#overviews.clear()
|
||||||
|
this.emit("lock")
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,13 +154,25 @@ export class Vault {
|
|||||||
|
|
||||||
getItem(uuid: string) {
|
getItem(uuid: string) {
|
||||||
this.#assertUnlocked()
|
this.#assertUnlocked()
|
||||||
const encrypted = uuid ? this.#bands.get(uuid[0])![uuid] : undefined
|
const item = this.#itemsMap.get(uuid)
|
||||||
return encrypted && decryptItem(encrypted, this.#master!)
|
if (!item) return
|
||||||
|
|
||||||
|
if (!item.itemDetails) {
|
||||||
|
item.decryptItemDetail(this.#master!)
|
||||||
|
}
|
||||||
|
|
||||||
|
const encrypted = uuid ? this.#encryptedItems.get(uuid[0])![uuid] : undefined
|
||||||
|
if (!encrypted) return
|
||||||
|
|
||||||
|
return {
|
||||||
|
original: encrypted,
|
||||||
|
details: decryptItemDetail(encrypted, this.#master!),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#readOverviews() {
|
#readOverviews() {
|
||||||
this.#assertUnlocked()
|
this.#assertUnlocked()
|
||||||
this.#bands.forEach(value => {
|
this.#encryptedItems.forEach(value => {
|
||||||
for (const [uuid, item] of Object.entries(value)) {
|
for (const [uuid, item] of Object.entries(value)) {
|
||||||
const overview = decryptOverview(item, this.#overview!)
|
const overview = decryptOverview(item, this.#overview!)
|
||||||
overview.uuid = uuid
|
overview.uuid = uuid
|
||||||
@ -162,7 +182,7 @@ export class Vault {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decryptOverview(item: EncryptedItem, overviewCipher: Cipher) {
|
function decryptOverview(item: EncryptedItem, overviewCipher: Cipher): Overview {
|
||||||
try {
|
try {
|
||||||
const overview = decryptOPData(toBuffer(item.o), overviewCipher)
|
const overview = decryptOPData(toBuffer(item.o), overviewCipher)
|
||||||
return JSON.parse(overview.toString("utf8")) as Overview
|
return JSON.parse(overview.toString("utf8")) as Overview
|
||||||
@ -172,49 +192,6 @@ function decryptOverview(item: EncryptedItem, overviewCipher: Cipher) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function decryptItem(item: EncryptedItem, master: Cipher): Item {
|
|
||||||
const k = toBuffer(item.k)
|
|
||||||
const data = k.slice(0, -32)
|
|
||||||
assertHMac(data, master.hmac, k.slice(-32))
|
|
||||||
const derivedKey = decryptData(master.key, data.slice(0, 16), data.slice(16))
|
|
||||||
|
|
||||||
const detail = decryptOPData(
|
|
||||||
/* cipherText */ toBuffer(item.d),
|
|
||||||
/* cipher */ splitPlainText(derivedKey)
|
|
||||||
)
|
|
||||||
return JSON.parse(detail.toString("utf-8"))
|
|
||||||
}
|
|
||||||
|
|
||||||
function decryptAttachment(item: Buffer, master: Cipher) {
|
|
||||||
invariant(
|
|
||||||
item.slice(0, 6).toString("utf-8") === "OPCLDA",
|
|
||||||
"Attachment must start with OPCLDA"
|
|
||||||
)
|
|
||||||
invariant(
|
|
||||||
item.readIntLE(7, 1) === 1,
|
|
||||||
"The version for this attachment file format is not supported."
|
|
||||||
)
|
|
||||||
|
|
||||||
const metadataSize = item.readIntLE(8, 2)
|
|
||||||
const iconSize = item.readIntLE(12, 3)
|
|
||||||
|
|
||||||
const metadata: AttachmentMetadata = JSON.parse(
|
|
||||||
item.slice(16, 16 + metadataSize).toString("utf-8")
|
|
||||||
)
|
|
||||||
const icondata = item.slice(16 + metadataSize, 16 + metadataSize + iconSize)
|
|
||||||
console.log(icondata.slice(0, 8).toString())
|
|
||||||
const iconData = decryptOPData(icondata, master)
|
|
||||||
return iconData
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Encryption and MAC */
|
|
||||||
interface Cipher {
|
|
||||||
/** Encryption key */
|
|
||||||
key: Buffer
|
|
||||||
/** HMAC key */
|
|
||||||
hmac: Buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
function stripText(text: string, prefix: string | RegExp, suffix: string | RegExp) {
|
function stripText(text: string, prefix: string | RegExp, suffix: string | RegExp) {
|
||||||
if (typeof prefix === "string") {
|
if (typeof prefix === "string") {
|
||||||
if (text.startsWith(prefix)) {
|
if (text.startsWith(prefix)) {
|
||||||
@ -239,43 +216,6 @@ function stripText(text: string, prefix: string | RegExp, suffix: string | RegEx
|
|||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
const splitPlainText = (derivedKey: Buffer): Cipher => ({
|
|
||||||
key: derivedKey.slice(0, 32),
|
|
||||||
hmac: derivedKey.slice(32, 64),
|
|
||||||
})
|
|
||||||
|
|
||||||
function decryptOPData(cipherText: Buffer, cipher: Cipher) {
|
|
||||||
const key = cipherText.slice(0, -32)
|
|
||||||
assertHMac(key, cipher.hmac, cipherText.slice(-32))
|
|
||||||
|
|
||||||
const plaintext = decryptData(cipher.key, key.slice(16, 32), key.slice(32))
|
|
||||||
const size = readUint16(key.slice(8, 16))
|
|
||||||
return plaintext.slice(-size)
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertHMac(data: Buffer, key: Buffer, expected: Buffer) {
|
|
||||||
const actual = crypto.createHmac("sha256", key).update(data).digest()
|
|
||||||
if (!actual.equals(expected)) {
|
|
||||||
throw new HMACAssertionError()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function decryptKeys(encryptedKey: string, derived: Cipher) {
|
|
||||||
const buffer = toBuffer(encryptedKey)
|
|
||||||
const base = decryptOPData(buffer, derived)
|
|
||||||
const digest = crypto.createHash("sha512").update(base).digest()
|
|
||||||
return splitPlainText(digest)
|
|
||||||
}
|
|
||||||
|
|
||||||
function decryptData(key: Buffer, iv: Buffer, data: Buffer) {
|
|
||||||
const cipher = crypto.createDecipheriv("aes-256-cbc", key, iv).setAutoPadding(false)
|
|
||||||
return Buffer.concat([cipher.update(data), cipher.final()])
|
|
||||||
}
|
|
||||||
|
|
||||||
function readUint16({ buffer, byteOffset, length }: Buffer) {
|
|
||||||
return new DataView(buffer, byteOffset, length).getUint16(0, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
function toBuffer(data: string) {
|
function toBuffer(data: string) {
|
||||||
return Buffer.from(data, "base64")
|
return Buffer.from(data, "base64")
|
||||||
}
|
}
|
32
src/types.ts
32
src/types.ts
@ -15,32 +15,6 @@ export interface Profile {
|
|||||||
createdAt: integer // Unix seconds
|
createdAt: integer // Unix seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EncryptedItem {
|
|
||||||
category: string // "001"
|
|
||||||
/** Unix seconds */
|
|
||||||
created: integer
|
|
||||||
d: string // "b3BkYXRhMbt"
|
|
||||||
folder: string // 32 chars
|
|
||||||
hmac: string // base64
|
|
||||||
k: string // base64
|
|
||||||
o: string // base64
|
|
||||||
tx: integer // Unix seconds
|
|
||||||
updated: integer // Unix seconds
|
|
||||||
uuid: string // 32 chars
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AttachmentMetadata {
|
|
||||||
itemUUID: string
|
|
||||||
contentSize: integer
|
|
||||||
external: boolean
|
|
||||||
updatedAt: integer
|
|
||||||
txTimestamp: integer
|
|
||||||
/** Base64 encoded OPData */
|
|
||||||
overview: string
|
|
||||||
createdAt: integer
|
|
||||||
uuid: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TextField = {
|
export type TextField = {
|
||||||
type: FieldType.Text
|
type: FieldType.Text
|
||||||
value: string
|
value: string
|
||||||
@ -119,7 +93,7 @@ declare namespace ItemSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// One of them is empty?, 0C4F27910A64488BB339AED63565D148
|
// One of them is empty?, 0C4F27910A64488BB339AED63565D148
|
||||||
export interface Item {
|
export interface ItemDetails {
|
||||||
htmlForm?: {
|
htmlForm?: {
|
||||||
htmlAction: string // "/login/"
|
htmlAction: string // "/login/"
|
||||||
htmlMethod: "post" | "get"
|
htmlMethod: "post" | "get"
|
||||||
@ -134,10 +108,6 @@ export interface Item {
|
|||||||
fields?: ItemField[]
|
fields?: ItemField[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Band {
|
|
||||||
[uuid: string]: EncryptedItem
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Folder {
|
export interface Folder {
|
||||||
created: number // 1373754128
|
created: number // 1373754128
|
||||||
overview: string // "b3BkYXRhT/../KBM="
|
overview: string // "b3BkYXRhT/../KBM="
|
||||||
|
39
src/util.ts
Normal file
39
src/util.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { invariant } from "./errors"
|
||||||
|
|
||||||
|
export function asyncMap<T, R>(
|
||||||
|
list: T[],
|
||||||
|
fn: (value: T, index: number, list: T[]) => Promise<R>
|
||||||
|
) {
|
||||||
|
return Promise.all(list.map(fn))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function once<T extends (...args: any[]) => any>(fn: T): T {
|
||||||
|
let result: ReturnType<T>
|
||||||
|
let executed = false
|
||||||
|
const res = function (this: ThisParameterType<T>) {
|
||||||
|
if (executed) {
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
result = fn.apply(this, arguments as any as Parameters<T>)
|
||||||
|
executed = true
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res as any
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cache = (): MethodDecorator => (_, key, descriptor: any) => {
|
||||||
|
if (process.env.NODE_ENV !== "production") {
|
||||||
|
invariant(typeof key === "string")
|
||||||
|
invariant(descriptor.get != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheMap = new WeakMap()
|
||||||
|
const fn = descriptor.get
|
||||||
|
descriptor.get = function () {
|
||||||
|
if (!cacheMap.has(this)) {
|
||||||
|
cacheMap.set(this, fn.call(this))
|
||||||
|
}
|
||||||
|
return cacheMap.get(this)!
|
||||||
|
}
|
||||||
|
}
|
40
src/weakMap.ts
Normal file
40
src/weakMap.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
export class WeakValueMap<K, V extends object> {
|
||||||
|
#map = new Map<K, WeakRef<V>>()
|
||||||
|
|
||||||
|
#delete = (key: K) => {
|
||||||
|
const value = this.#map.get(key)!
|
||||||
|
this.#finalizers.unregister(value)
|
||||||
|
this.#map.delete(key)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
#finalizers = new FinalizationRegistry((key: K) => {
|
||||||
|
this.#map.delete(key)
|
||||||
|
})
|
||||||
|
|
||||||
|
delete(key: K) {
|
||||||
|
return this.#map.has(key) && !this.#delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
has(key: K) {
|
||||||
|
const has = this.#map.has(key)
|
||||||
|
const value = has && !this.#map.get(key)!.deref()
|
||||||
|
return value ? this.#delete(key) : has
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: K) {
|
||||||
|
return this.#map.get(key)?.deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: V) {
|
||||||
|
this.delete(key)
|
||||||
|
const ref = new WeakRef(value)
|
||||||
|
this.#finalizers.register(value, key, ref)
|
||||||
|
this.#map.set(key, ref)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.#map.clear()
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
import chai from "chai";
|
import chai from "chai";
|
||||||
import chaiAsPromised from "chai-as-promised";
|
import chaiAsPromised from "chai-as-promised";
|
||||||
|
import sinonChai from "sinon-chai";
|
||||||
|
|
||||||
process.env.LOCALE = "en";
|
process.env.LOCALE = "en";
|
||||||
|
|
||||||
chai.use(chaiAsPromised);
|
chai.use(chaiAsPromised);
|
||||||
|
chai.use(sinonChai);
|
||||||
|
@ -5,6 +5,7 @@ import { expect } from "chai";
|
|||||||
|
|
||||||
import type { Vault } from "../src/index";
|
import type { Vault } from "../src/index";
|
||||||
import { OnePassword } from "../src/index";
|
import { OnePassword } from "../src/index";
|
||||||
|
// import adapter from "../src/adapters/node";
|
||||||
|
|
||||||
describe("OnePassword", () => {
|
describe("OnePassword", () => {
|
||||||
const freddy = resolve(__dirname, "../freddy-2013-12-04.opvault");
|
const freddy = resolve(__dirname, "../freddy-2013-12-04.opvault");
|
||||||
|
43
test/weakMap.test.ts
Normal file
43
test/weakMap.test.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { describe, it } from "mocha";
|
||||||
|
import { expect } from "chai";
|
||||||
|
|
||||||
|
import { WeakValueMap } from "../src/weakMap";
|
||||||
|
|
||||||
|
declare const gc: () => void;
|
||||||
|
|
||||||
|
describe("WeakValueMap", () => {
|
||||||
|
interface Value {
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("covers base use cases", () => {
|
||||||
|
const map = new WeakValueMap<string, Value>();
|
||||||
|
const object = { value: 1 };
|
||||||
|
map.set("key", object);
|
||||||
|
expect(map.get("key")!.value).to.equal(1);
|
||||||
|
expect(map.delete("key")).to.be.true;
|
||||||
|
expect(!map.delete("key")).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overrides previous value", () => {
|
||||||
|
const map = new WeakValueMap<string, Value>();
|
||||||
|
map.set("key", { value: 2 });
|
||||||
|
map.set("key", { value: 3 });
|
||||||
|
expect(map.get("key")!.value).to.equal(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("deletes garbage collected values", (done) => {
|
||||||
|
const map = new WeakValueMap<string, Value>();
|
||||||
|
map.set("key", { value: 1 });
|
||||||
|
setTimeout(() => {
|
||||||
|
gc();
|
||||||
|
expect(map.has("key")).to.be.false;
|
||||||
|
map.set("key", { value: 2 });
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
gc();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user