First working version
Some checks failed
CI / build (20.x) (push) Has been cancelled
CI / build (22.x) (push) Has been cancelled

This commit is contained in:
2025-06-21 01:01:55 -05:00
parent d961cbb51d
commit 5ab210f2bf
7 changed files with 1232 additions and 15 deletions

File diff suppressed because one or more lines are too long

578
package-lock.json generated
View File

@ -8,6 +8,9 @@
"name": "wiki-plugin-useraccesstokens",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"bcrypt": "^5.1.1"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
"esbuild": "^0.25.5",
@ -677,6 +680,26 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz",
"integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==",
"license": "BSD-3-Clause",
"dependencies": {
"detect-libc": "^2.0.0",
"https-proxy-agent": "^5.0.0",
"make-dir": "^3.1.0",
"node-fetch": "^2.6.7",
"nopt": "^5.0.0",
"npmlog": "^5.0.1",
"rimraf": "^3.0.2",
"semver": "^7.3.5",
"tar": "^6.1.11"
},
"bin": {
"node-pre-gyp": "bin/node-pre-gyp"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@ -691,6 +714,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"license": "ISC"
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@ -714,6 +743,18 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"license": "MIT",
"dependencies": {
"debug": "4"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -731,6 +772,15 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@ -747,6 +797,26 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/aproba": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz",
"integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==",
"license": "ISC"
},
"node_modules/are-we-there-yet": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"delegates": "^1.0.0",
"readable-stream": "^3.6.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@ -758,14 +828,26 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/bcrypt": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
"integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@mapbox/node-pre-gyp": "^1.0.11",
"node-addon-api": "^5.0.0"
},
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@ -799,6 +881,15 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
"integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -819,13 +910,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==",
"license": "ISC",
"bin": {
"color-support": "bin.js"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==",
"license": "ISC"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@ -845,7 +950,6 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@ -866,6 +970,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
"integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==",
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
@ -1147,6 +1272,78 @@
"dev": true,
"license": "ISC"
},
"node_modules/fs-minipass": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
"integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
"license": "ISC",
"dependencies": {
"minipass": "^3.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/fs-minipass/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
"node_modules/gauge": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
"color-support": "^1.1.2",
"console-control-strings": "^1.0.0",
"has-unicode": "^2.0.1",
"object-assign": "^4.1.1",
"signal-exit": "^3.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1",
"wide-align": "^1.1.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@ -1183,6 +1380,25 @@
"node": ">=8"
}
},
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
"integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==",
"license": "ISC"
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
"license": "MIT",
"dependencies": {
"agent-base": "6",
"debug": "4"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@ -1220,6 +1436,23 @@
"node": ">=0.8.19"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@ -1230,6 +1463,15 @@
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@ -1331,11 +1573,34 @@
"dev": true,
"license": "MIT"
},
"node_modules/make-dir": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
"integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
"license": "MIT",
"dependencies": {
"semver": "^6.0.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@ -1344,11 +1609,56 @@
"node": "*"
}
},
"node_modules/minipass": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
"integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
"license": "ISC",
"engines": {
"node": ">=8"
}
},
"node_modules/minizlib": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
"integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
"license": "MIT",
"dependencies": {
"minipass": "^3.0.0",
"yallist": "^4.0.0"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/minizlib/node_modules/minipass": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
"integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"license": "MIT",
"bin": {
"mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/natural-compare": {
@ -1358,6 +1668,78 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
"integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==",
"license": "MIT"
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/nopt": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
"integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==",
"license": "ISC",
"dependencies": {
"abbrev": "1"
},
"bin": {
"nopt": "bin/nopt.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"license": "ISC",
"dependencies": {
"are-we-there-yet": "^2.0.0",
"console-control-strings": "^1.1.0",
"gauge": "^3.0.0",
"set-blocking": "^2.0.0"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -1431,6 +1813,15 @@
"node": ">=8"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@ -1477,6 +1868,20 @@
"node": ">=6"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@ -1487,6 +1892,60 @@
"node": ">=4"
}
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
"deprecated": "Rimraf versions prior to v4 are no longer supported",
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -1510,6 +1969,47 @@
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@ -1536,6 +2036,29 @@
"node": ">=8"
}
},
"node_modules/tar": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
"license": "ISC",
"dependencies": {
"chownr": "^2.0.0",
"fs-minipass": "^2.0.0",
"minipass": "^5.0.0",
"minizlib": "^2.1.1",
"mkdirp": "^1.0.3",
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -1559,6 +2082,28 @@
"punycode": "^2.1.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -1575,6 +2120,15 @@
"node": ">= 8"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
"integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==",
"license": "ISC",
"dependencies": {
"string-width": "^1.0.2 || 2 || 3 || 4"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@ -1585,6 +2139,18 @@
"node": ">=0.10.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View File

@ -1,7 +1,7 @@
{
"name": "wiki-plugin-useraccesstokens",
"version": "0.1.0",
"description": "Add description here.",
"description": "Federated Wiki plugin for managing User Access Tokens (API tokens) for programmatic authentication",
"keywords": [
"wiki",
"federated wiki",
@ -22,6 +22,9 @@
"engines": {
"node": ">=20.x"
},
"dependencies": {
"bcrypt": "^5.1.1"
},
"devDependencies": {
"@eslint/js": "^9.29.0",
"esbuild": "^0.25.5",

View File

@ -1,8 +1,21 @@
{
"title": "About UserAccessTokens Plugin",
"story": [
{ "type": "paragraph", "id": "03770af2dea78190", "text": "Describe the plugin here." },
{ "type": "useraccesstokens", "id": "76e9b31c71a6a853", "text": "SAMPLE Markup" }
{
"type": "paragraph",
"id": "03770af2dea78190",
"text": "The UserAccessTokens plugin allows users to create, manage, and revoke User Access Tokens for programmatic authentication with Federated Wiki. These tokens provide a secure way to authenticate API requests without requiring interactive login."
},
{
"type": "useraccesstokens",
"id": "76e9b31c71a6a853",
"text": "Create tokens for scripts, apps, or automated tools that need to interact with your wiki."
},
{
"type": "markdown",
"id": "263207b563ef6712",
"text": "**Using Your Tokens:**\n\nOnce you create a token, you can use it in API requests by including it in the Authorization header:\n\n```\nAuthorization: Bearer fwuat-your-token-here\n```\n\n**Security Notes:**\n\n• Store tokens securely - they won't be shown again after creation\n• Tokens are prefixed with `fwuat-` to identify them as User Access Tokens\n• Revoke tokens you no longer need\n• Set expiration dates for temporary access\n• Each token shows only the last 4 characters for identification"
}
],
"journal": [
{

271
server/server.js Normal file
View File

@ -0,0 +1,271 @@
// useraccesstokens plugin, server-side component
// These handlers are launched with the wiki server.
import fs from 'node:fs'
import fsp from 'node:fs/promises'
import path from 'node:path'
import crypto from 'node:crypto'
import bcrypt from 'bcrypt'
const SALT_ROUNDS = 12
const TOKEN_PREFIX = 'fwuat-'
const TOKEN_BYTES = 32
class TokenManager {
constructor(statusPath) {
this.statusPath = statusPath
this.tokensFile = path.join(statusPath, 'user-access-tokens.json')
}
async ensureTokensFile() {
try {
await fsp.access(this.tokensFile)
} catch {
await fsp.writeFile(this.tokensFile, JSON.stringify([], null, 2))
}
}
async loadTokens() {
await this.ensureTokensFile()
const data = await fsp.readFile(this.tokensFile, 'utf8')
return JSON.parse(data)
}
async saveTokens(tokens) {
await fsp.writeFile(this.tokensFile, JSON.stringify(tokens, null, 2))
}
generateToken() {
const tokenBytes = crypto.randomBytes(TOKEN_BYTES)
const token = TOKEN_PREFIX + tokenBytes.toString('base64url')
return token
}
async hashToken(token) {
return await bcrypt.hash(token, SALT_ROUNDS)
}
async verifyToken(token, hash) {
return await bcrypt.compare(token, hash)
}
getDisplayHint(token) {
return token.slice(-4)
}
async createToken(user, name, expiresInDays = null) {
const tokens = await this.loadTokens()
// Check if token name already exists for this user
const existingToken = tokens.find(t => t.user === user && t.name === name && !t.revoked)
if (existingToken) {
throw new Error(`Token with name "${name}" already exists`)
}
const token = this.generateToken()
const tokenHash = await this.hashToken(token)
const created = new Date().toISOString()
const expires = expiresInDays ? new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000).toISOString() : null
const tokenRecord = {
name,
user,
tokenHash,
displayHint: this.getDisplayHint(token),
created,
expires,
lastUsed: null,
revoked: false,
scopes: ['site:read', 'site:write'] // Default scopes
}
tokens.push(tokenRecord)
await this.saveTokens(tokens)
return { token, record: tokenRecord }
}
async listTokens(user) {
const tokens = await this.loadTokens()
return tokens
.filter(t => t.user === user)
.map(t => {
const { tokenHash, ...safeToken } = t
return safeToken
})
}
async revokeToken(user, name) {
const tokens = await this.loadTokens()
const tokenIndex = tokens.findIndex(t => t.user === user && t.name === name)
if (tokenIndex === -1) {
throw new Error(`Token "${name}" not found`)
}
tokens[tokenIndex].revoked = true
await this.saveTokens(tokens)
return tokens[tokenIndex]
}
async deleteToken(user, name) {
const tokens = await this.loadTokens()
const tokenIndex = tokens.findIndex(t => t.user === user && t.name === name)
if (tokenIndex === -1) {
throw new Error(`Token "${name}" not found`)
}
const deletedToken = tokens.splice(tokenIndex, 1)[0]
await this.saveTokens(tokens)
return deletedToken
}
async validateToken(token) {
if (!token.startsWith(TOKEN_PREFIX)) {
return null
}
const tokens = await this.loadTokens()
const now = new Date()
for (const tokenRecord of tokens) {
if (tokenRecord.revoked) continue
if (tokenRecord.expires && new Date(tokenRecord.expires) < now) continue
const isValid = await this.verifyToken(token, tokenRecord.tokenHash)
if (isValid) {
// Update last used timestamp
tokenRecord.lastUsed = now.toISOString()
await this.saveTokens(tokens)
return tokenRecord
}
}
return null
}
}
const startServer = async function (params) {
const { app, argv } = params
// Initialize token manager
const tokenManager = new TokenManager(argv.status)
// Middleware to check if user is authenticated
const authenticated = function (req, res, next) {
if (!app.securityhandler.isAuthorized(req)) {
return res.status(401).json({ error: 'Must be authenticated' })
}
return next()
}
// Middleware to get current user
const getCurrentUser = function (req) {
return app.securityhandler.getUser(req)
}
// API Routes
// POST /plugin/useraccesstokens/create - Create a new token
app.post('/plugin/useraccesstokens/create', authenticated, async (req, res) => {
try {
const user = getCurrentUser(req)
const { name, expiresInDays } = req.body
if (!name || typeof name !== 'string' || name.trim() === '') {
return res.status(400).json({ error: 'Token name is required' })
}
const result = await tokenManager.createToken(user, name.trim(), expiresInDays)
res.json({
token: result.token,
name: result.record.name,
displayHint: result.record.displayHint,
created: result.record.created,
expires: result.record.expires,
scopes: result.record.scopes
})
} catch (error) {
res.status(400).json({ error: error.message })
}
})
// GET /plugin/useraccesstokens/list - List user's tokens
app.get('/plugin/useraccesstokens/list', authenticated, async (req, res) => {
try {
const user = getCurrentUser(req)
const tokens = await tokenManager.listTokens(user)
res.json(tokens)
} catch (error) {
res.status(500).json({ error: error.message })
}
})
// POST /plugin/useraccesstokens/revoke - Revoke a token
app.post('/plugin/useraccesstokens/revoke', authenticated, async (req, res) => {
try {
const user = getCurrentUser(req)
const { name } = req.body
if (!name) {
return res.status(400).json({ error: 'Token name is required' })
}
await tokenManager.revokeToken(user, name)
res.json({ message: `Token "${name}" revoked successfully` })
} catch (error) {
res.status(400).json({ error: error.message })
}
})
// DELETE /plugin/useraccesstokens/delete - Delete a token
app.delete('/plugin/useraccesstokens/delete', authenticated, async (req, res) => {
try {
const user = getCurrentUser(req)
const { name } = req.body
if (!name) {
return res.status(400).json({ error: 'Token name is required' })
}
await tokenManager.deleteToken(user, name)
res.json({ message: `Token "${name}" deleted successfully` })
} catch (error) {
res.status(400).json({ error: error.message })
}
})
// Middleware for token-based authentication
app.use('/api', async (req, res, next) => {
const authHeader = req.headers.authorization
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7)
try {
const tokenRecord = await tokenManager.validateToken(token)
if (tokenRecord) {
// Set user context for token-based requests
req.tokenAuth = {
user: tokenRecord.user,
scopes: tokenRecord.scopes,
tokenName: tokenRecord.name
}
return next()
}
} catch (error) {
console.error('Token validation error:', error)
}
}
next()
})
console.log('UserAccessTokens plugin server started')
}
export { startServer, TokenManager }

View File

@ -6,15 +6,231 @@ const expand = text => {
.replace(/\*(.+?)\*/g, '<i>$1</i>')
}
const error = text => {
return `<div class="error" style="color:#F88; margin: 10px 0;">${text}</div>`
}
const success = text => {
return `<div class="success" style="color:#4A4; margin: 10px 0;">${text}</div>`
}
const tokenForm = () => {
return `
<div class=fields>
<input type=text name=tokenName size=30 placeholder="token name" required>
<select name=expiresInDays>
<option value="">never expires</option>
<option value="7">7 days</option>
<option value="30">30 days</option>
<option value="90">90 days</option>
<option value="365">1 year</option>
</select>
</div>
<p><button class=create-token>Create Token</button></p>
<span class=create-result></span>
<hr>
`
}
const tokenList = (tokens) => {
if (!tokens || tokens.length === 0) {
return '<p>No tokens created yet.</p>'
}
const tokenRows = tokens.map(token => {
const status = token.revoked ? 'revoked' :
(token.expires && new Date(token.expires) < new Date()) ? 'expired' :
'active'
const lastUsed = token.lastUsed ? new Date(token.lastUsed).toLocaleDateString() : 'never'
const expires = token.expires ? new Date(token.expires).toLocaleDateString() : 'never'
return `
<p><strong>${expand(token.name)}</strong> (...${token.displayHint}) - ${status}
${!token.revoked ? `<button class=revoke-token data-token-name="${token.name}">revoke</button>` : ''}
<button class=delete-token data-token-name="${token.name}">delete</button>
<br><small>created: ${new Date(token.created).toLocaleDateString()}, expires: ${expires}, last used: ${lastUsed}</small>
</p>
`
}).join('')
return `
<div class=token-list>
${tokenRows}
<span class=list-result></span>
</div>
`
}
const emit = ($item, item) => {
return $item.append(`
<p style="background-color:#eee;padding:15px;">
${expand(item.text)}
</p>`)
const content = `
<div style="background-color:#eee; padding:15px;">
<center>
<p><img src='/favicon.png' width=16> <span style='color:gray;'>${window.location.host}</span></p>
<p>${expand(item.text)}</p>
${tokenForm()}
</center>
<div class=tokens-container>
<p>Loading tokens...</p>
</div>
<center>
<p><button class=refresh-tokens>Refresh Tokens</button></p>
</center>
</div>
`
$item.html(content)
// Load tokens on initial render
loadTokens($item)
}
const loadTokens = ($item) => {
fetch('/plugin/useraccesstokens/list')
.then(res => {
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`)
}
return res.json()
})
.then(tokens => {
$item.find('.tokens-container').html(tokenList(tokens))
})
.catch(err => {
$item.find('.tokens-container').html(error(`Failed to load tokens: ${err.message}`))
})
}
const createToken = ($item) => {
const $form = $item.find('.fields')
const name = $form.find('input[name="tokenName"]').val().trim()
const expiresInDays = $form.find('select[name="expiresInDays"]').val()
$item.find('.error, .success').remove()
if (!name) {
$item.find('.create-result').html(error('Token name is required'))
return
}
const data = { name }
if (expiresInDays) {
data.expiresInDays = parseInt(expiresInDays)
}
fetch('/plugin/useraccesstokens/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(res => {
if (!res.ok) {
return res.json().then(err => Promise.reject(err))
}
return res.json()
})
.then(result => {
$item.find('.create-result').html(`
${success('Token created successfully!')}
<p><strong>Save this token now - you won't see it again:</strong><br>
<code>${result.token}</code></p>
`)
// Clear form
$form.find('input[name="tokenName"]').val('')
$form.find('select[name="expiresInDays"]').val('')
// Refresh token list
loadTokens($item)
})
.catch(err => {
$item.find('.create-result').html(error(err.error || 'Failed to create token'))
})
}
const revokeToken = ($item, tokenName) => {
if (!confirm(`Are you sure you want to revoke the token "${tokenName}"?`)) {
return
}
fetch('/plugin/useraccesstokens/revoke', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: tokenName })
})
.then(res => {
if (!res.ok) {
return res.json().then(err => Promise.reject(err))
}
return res.json()
})
.then(result => {
$item.find('.list-result').html(success(result.message))
loadTokens($item)
})
.catch(err => {
$item.find('.list-result').html(error(err.error || 'Failed to revoke token'))
})
}
const deleteToken = ($item, tokenName) => {
if (!confirm(`Are you sure you want to permanently delete the token "${tokenName}"? This cannot be undone.`)) {
return
}
fetch('/plugin/useraccesstokens/delete', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: tokenName })
})
.then(res => {
if (!res.ok) {
return res.json().then(err => Promise.reject(err))
}
return res.json()
})
.then(result => {
$item.find('.list-result').html(success(result.message))
loadTokens($item)
})
.catch(err => {
$item.find('.list-result').html(error(err.error || 'Failed to delete token'))
})
}
const bind = ($item, item) => {
return $item.dblclick(() => {
// Handle create token button
$item.on('click', '.create-token', () => {
createToken($item)
})
// Handle refresh tokens button
$item.on('click', '.refresh-tokens', () => {
loadTokens($item)
})
// Handle revoke token buttons
$item.on('click', '.revoke-token', (e) => {
const tokenName = $(e.target).data('token-name')
revokeToken($item, tokenName)
})
// Handle delete token buttons
$item.on('click', '.delete-token', (e) => {
const tokenName = $(e.target).data('token-name')
deleteToken($item, tokenName)
})
// Handle double-click for text editing
$item.dblclick(() => {
return wiki.textEditor($item, item)
})
}

View File

@ -1,12 +1,153 @@
import { useraccesstokens } from '../src/client/useraccesstokens.js'
import { suite, test } from 'node:test'
import assert from 'node:assert'
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { TokenManager } from '../server/server.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
suite('useraccesstokens plugin', () => {
suite('expand', () => {
suite('client expand', () => {
test('can escape html markup characters', () => {
const result = useraccesstokens.expand('try < & >')
assert.equal(result, 'try &lt; &amp; &gt;')
})
test('can format italic text', () => {
const result = useraccesstokens.expand('This is *italic* text')
assert.equal(result, 'This is <i>italic</i> text')
})
})
suite('server TokenManager', async () => {
let tempDir
let tokenManager
// Setup before each test
const setup = async () => {
tempDir = path.join(__dirname, 'temp-' + Date.now())
await fs.mkdir(tempDir, { recursive: true })
tokenManager = new (class extends TokenManager {
constructor(statusPath) {
super(statusPath)
}
})(tempDir)
}
// Cleanup after each test
const cleanup = async () => {
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true })
}
}
test('can generate tokens with correct prefix', async () => {
await setup()
try {
const token = tokenManager.generateToken()
assert(token.startsWith('fwuat-'))
assert(token.length > 20) // Should be reasonably long
} finally {
await cleanup()
}
})
test('can create and validate tokens', async () => {
await setup()
try {
const result = await tokenManager.createToken('testuser', 'test-token')
assert(result.token.startsWith('fwuat-'))
assert.equal(result.record.name, 'test-token')
assert.equal(result.record.user, 'testuser')
assert.equal(result.record.revoked, false)
assert(result.record.created)
assert(result.record.displayHint)
const validation = await tokenManager.validateToken(result.token)
assert(validation)
assert.equal(validation.user, 'testuser')
assert.equal(validation.name, 'test-token')
} finally {
await cleanup()
}
})
test('can list user tokens safely', async () => {
await setup()
try {
await tokenManager.createToken('user1', 'token1')
await tokenManager.createToken('user1', 'token2')
await tokenManager.createToken('user2', 'token3')
const user1Tokens = await tokenManager.listTokens('user1')
assert.equal(user1Tokens.length, 2)
// Verify tokenHash is not included in the response
user1Tokens.forEach(token => {
assert(!token.tokenHash)
assert(token.displayHint)
assert.equal(token.user, 'user1')
})
const user2Tokens = await tokenManager.listTokens('user2')
assert.equal(user2Tokens.length, 1)
} finally {
await cleanup()
}
})
test('can revoke tokens', async () => {
await setup()
try {
const result = await tokenManager.createToken('testuser', 'test-token')
// Token should work before revocation
let validation = await tokenManager.validateToken(result.token)
assert(validation)
// Revoke the token
await tokenManager.revokeToken('testuser', 'test-token')
// Token should not work after revocation
validation = await tokenManager.validateToken(result.token)
assert.equal(validation, null)
} finally {
await cleanup()
}
})
test('rejects duplicate token names for same user', async () => {
await setup()
try {
await tokenManager.createToken('testuser', 'duplicate-name')
try {
await tokenManager.createToken('testuser', 'duplicate-name')
assert.fail('Should have thrown an error for duplicate name')
} catch (error) {
assert(error.message.includes('already exists'))
}
} finally {
await cleanup()
}
})
test('handles token expiration', async () => {
await setup()
try {
// Create token that expires immediately (for testing)
const result = await tokenManager.createToken('testuser', 'expired-token', -1)
// Token should be expired and not validate
const validation = await tokenManager.validateToken(result.token)
assert.equal(validation, null)
} finally {
await cleanup()
}
})
})
})