From 3b9a6bc85c79b09f2f54df739dadcb7a5245d7a9 Mon Sep 17 00:00:00 2001 From: Codinget Date: Mon, 29 Jun 2026 23:07:14 +0000 Subject: [PATCH] feat: initial commit --- .gitignore | 3 + package-lock.json | 5637 +++++++++++++++++ package.json | 65 + src/bin/abode-migrate.ts | 98 + src/bin/abode-repl.ts | 61 + src/bin/abode-sources.ts | 63 + src/bin/abode-tui.ts | 85 + src/bin/abode-web.ts | 98 + src/db/api/ApiInterface.ts | 289 + src/db/api/getdb.dyn.ts | 14 + src/db/api/getdb.static.ts | 19 + src/db/api/url.ts | 36 + src/db/dbSources.dyn.ts | 24 + src/db/dbSources.shared.ts | 12 + src/db/dbSources.static.ts | 9 + src/db/dbSources.ts | 1 + src/db/index.ts | 16 + src/db/sqlite/SqliteInterface.ts | 481 ++ src/db/sqlite/SqliteMigrator.ts | 108 + src/db/sqlite/cast.ts | 162 + src/db/sqlite/dyn.ts | 0 src/db/sqlite/getdb.dyn.ts | 14 + src/db/sqlite/getdb.static.ts | 16 + src/db/sqlite/impl/better-sqlite3.ts | 89 + src/db/sqlite/impl/implementations.all.ts | 6 + src/db/sqlite/impl/implementations.bs3.ts | 5 + src/db/sqlite/impl/implementations.node.ts | 5 + src/db/sqlite/impl/implementations.ts | 1 + src/db/sqlite/impl/index.ts | 20 + src/db/sqlite/impl/node-sqlite.ts | 76 + src/db/sqlite/impl/types.ts | 18 + .../migrations/1.init/1.users.sqlite.sql | 9 + .../migrations/1.init/2.abodes.sqlite.sql | 8 + .../migrations/1.init/3.residents.sqlite.sql | 11 + src/db/sqlite/migrations/1.init/index.ts | 26 + .../migrations/2.auth/1.sessions.sqlite.sql | 7 + .../migrations/2.auth/2.apikeys.sqlite.sql | 9 + src/db/sqlite/migrations/2.auth/index.ts | 20 + src/db/sqlite/migrations/3.notes/1.notes.sql | 11 + src/db/sqlite/migrations/3.notes/index.ts | 14 + src/db/sqlite/migrations/index.ts | 7 + src/db/sqlite/migrations/init.sqlite.sql | 5 + src/db/sqlite/migrations/types.ts | 19 + src/db/sqlite/pragma.sqlite.sql | 4 + src/db/sqlite/query.ts | 124 + src/db/sqlite/sql.ts | 74 + src/db/sqlite/url.ts | 45 + src/db/stub.ts | 15 + src/db/types/Abode.ts | 13 + src/db/types/Apikey.ts | 25 + src/db/types/DbInterface.ts | 108 + src/db/types/GetDb.ts | 16 + src/db/types/Migrator.ts | 16 + src/db/types/Note.ts | 37 + src/db/types/Resident.ts | 18 + src/db/types/Session.ts | 7 + src/db/types/User.ts | 37 + src/db/types/errors.ts | 7 + src/db/types/utils.ts | 4 + src/globals.d.ts | 6 + src/imports.d.ts | 10 + src/meta/dev/loader.ts | 18 + src/meta/dev/register.ts | 14 + src/meta/dev/restart.ts | 13 + src/meta/dev/silenthot.ts | 8 + src/meta/dev/webhot.ts | 8 + src/meta/pack/natives.ts | 65 + src/meta/pack/sources.ts | 1 + src/meta/pack/valLoader.ts | 4 + src/meta/pack/validators.ts | 36 + src/react/contexts/Db.tsx | 26 + src/react/contexts/PopupManager.tsx | 66 + src/react/hooks/data/abodes.ts | 16 + src/react/hooks/data/residents.ts | 35 + src/react/hooks/data/users.ts | 26 + src/react/hooks/useAction.ts | 20 + src/react/hooks/useLoad.ts | 42 + src/react/store/actions/abodes.ts | 50 + src/react/store/actions/clearAll.ts | 3 + src/react/store/actions/users.ts | 51 + src/react/store/load.ts | 128 + src/react/store/loaders/abodes.ts | 24 + src/react/store/loaders/residents.ts | 39 + src/react/store/loaders/users.ts | 38 + src/react/store/react.tsx | 28 + src/react/store/slices/abodes.ts | 42 + src/react/store/slices/loading.ts | 64 + src/react/store/slices/login.ts | 43 + src/react/store/slices/residents.ts | 42 + src/react/store/slices/users.ts | 46 + src/react/store/store.ts | 15 + src/react/store/utils.ts | 34 + src/schema/abode/abode.schema.json | 48 + src/schema/abode/createabode.schema.json | 16 + src/schema/abode/updateabode.schema.json | 21 + src/schema/ajv.ts | 18 + .../apikey/apikeypermissions.schema.json | 40 + src/schema/apikey/createapikey.schema.json | 29 + src/schema/note/createnote.schema.json | 28 + .../note/partialnoteproperties.schema.json | 15 + src/schema/note/updatenote.schema.json | 28 + src/schema/rawSchemas.ts | 23 + .../resident/createresident.schema.json | 25 + src/schema/resident/resident.schema.json | 53 + src/schema/resident/residentflags.schema.json | 18 + .../resident/updateresident.schema.json | 25 + src/schema/schemas.ts | 76 + src/schema/user/clientuser.schema.json | 40 + src/schema/user/createuser.schema.json | 29 + src/schema/user/loginuser.schema.json | 20 + src/schema/user/partialuser.schema.json | 35 + src/schema/user/updateuser.schema.json | 34 + src/schema/user/user.schema.json | 52 + src/schema/user/userflags.schema.json | 18 + src/schema/validators.ts | 53 + src/tui/App.tsx | 111 + src/tui/components/panels/AbodesPanel.tsx | 63 + src/tui/components/panels/LoginPanel.tsx | 84 + src/tui/components/panels/UsersPanel.tsx | 81 + src/tui/components/popups/AbodePopup.tsx | 135 + .../components/popups/AbodeResidentsPopup.tsx | 79 + .../components/popups/CreateAbodePopup.tsx | 43 + src/tui/components/popups/CreateUserPopup.tsx | 83 + src/tui/components/popups/LoginPopup.tsx | 53 + src/tui/components/popups/UserPopup.tsx | 128 + src/tui/components/ui/AbodeName.tsx | 16 + src/tui/components/ui/Button.tsx | 81 + src/tui/components/ui/EllipsisText.tsx | 29 + src/tui/components/ui/Input.tsx | 39 + src/tui/components/ui/ListBox.tsx | 98 + src/tui/components/ui/ListDisplay.tsx | 91 + src/tui/components/ui/Popup.tsx | 42 + src/tui/components/ui/SearchPanel.tsx | 95 + src/tui/components/ui/UserName.tsx | 16 + src/tui/contexts/BgColor.tsx | 4 + src/tui/contexts/FocusManager.tsx | 66 + src/tui/hooks/size.ts | 17 + src/tui/hooks/useAfterRender.ts | 9 + src/util/error.ts | 32 + src/util/hash.ts | 25 + src/util/length.ts | 1 + src/util/token.ts | 19 + src/util/ts.ts | 4 + src/util/xmlwriter.ts | 209 + src/webapi/apirouter.ts | 200 + src/webapi/middleware/authenticate.ts | 139 + src/webapi/middleware/convertError.ts | 35 + src/webapi/middleware/jsonBody.ts | 64 + src/webapi/middleware/logRequests.ts | 6 + src/webapi/schemarouter.ts | 30 + tsconfig.json | 15 + webpack.config.ts | 284 + 152 files changed, 12558 insertions(+) create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/bin/abode-migrate.ts create mode 100644 src/bin/abode-repl.ts create mode 100644 src/bin/abode-sources.ts create mode 100644 src/bin/abode-tui.ts create mode 100644 src/bin/abode-web.ts create mode 100644 src/db/api/ApiInterface.ts create mode 100644 src/db/api/getdb.dyn.ts create mode 100644 src/db/api/getdb.static.ts create mode 100644 src/db/api/url.ts create mode 100644 src/db/dbSources.dyn.ts create mode 100644 src/db/dbSources.shared.ts create mode 100644 src/db/dbSources.static.ts create mode 100644 src/db/dbSources.ts create mode 100644 src/db/index.ts create mode 100644 src/db/sqlite/SqliteInterface.ts create mode 100644 src/db/sqlite/SqliteMigrator.ts create mode 100644 src/db/sqlite/cast.ts create mode 100644 src/db/sqlite/dyn.ts create mode 100644 src/db/sqlite/getdb.dyn.ts create mode 100644 src/db/sqlite/getdb.static.ts create mode 100644 src/db/sqlite/impl/better-sqlite3.ts create mode 100644 src/db/sqlite/impl/implementations.all.ts create mode 100644 src/db/sqlite/impl/implementations.bs3.ts create mode 100644 src/db/sqlite/impl/implementations.node.ts create mode 100644 src/db/sqlite/impl/implementations.ts create mode 100644 src/db/sqlite/impl/index.ts create mode 100644 src/db/sqlite/impl/node-sqlite.ts create mode 100644 src/db/sqlite/impl/types.ts create mode 100644 src/db/sqlite/migrations/1.init/1.users.sqlite.sql create mode 100644 src/db/sqlite/migrations/1.init/2.abodes.sqlite.sql create mode 100644 src/db/sqlite/migrations/1.init/3.residents.sqlite.sql create mode 100644 src/db/sqlite/migrations/1.init/index.ts create mode 100644 src/db/sqlite/migrations/2.auth/1.sessions.sqlite.sql create mode 100644 src/db/sqlite/migrations/2.auth/2.apikeys.sqlite.sql create mode 100644 src/db/sqlite/migrations/2.auth/index.ts create mode 100644 src/db/sqlite/migrations/3.notes/1.notes.sql create mode 100644 src/db/sqlite/migrations/3.notes/index.ts create mode 100644 src/db/sqlite/migrations/index.ts create mode 100644 src/db/sqlite/migrations/init.sqlite.sql create mode 100644 src/db/sqlite/migrations/types.ts create mode 100644 src/db/sqlite/pragma.sqlite.sql create mode 100644 src/db/sqlite/query.ts create mode 100644 src/db/sqlite/sql.ts create mode 100644 src/db/sqlite/url.ts create mode 100644 src/db/stub.ts create mode 100644 src/db/types/Abode.ts create mode 100644 src/db/types/Apikey.ts create mode 100644 src/db/types/DbInterface.ts create mode 100644 src/db/types/GetDb.ts create mode 100644 src/db/types/Migrator.ts create mode 100644 src/db/types/Note.ts create mode 100644 src/db/types/Resident.ts create mode 100644 src/db/types/Session.ts create mode 100644 src/db/types/User.ts create mode 100644 src/db/types/errors.ts create mode 100644 src/db/types/utils.ts create mode 100644 src/globals.d.ts create mode 100644 src/imports.d.ts create mode 100644 src/meta/dev/loader.ts create mode 100644 src/meta/dev/register.ts create mode 100644 src/meta/dev/restart.ts create mode 100644 src/meta/dev/silenthot.ts create mode 100644 src/meta/dev/webhot.ts create mode 100644 src/meta/pack/natives.ts create mode 100644 src/meta/pack/sources.ts create mode 100644 src/meta/pack/valLoader.ts create mode 100644 src/meta/pack/validators.ts create mode 100644 src/react/contexts/Db.tsx create mode 100644 src/react/contexts/PopupManager.tsx create mode 100644 src/react/hooks/data/abodes.ts create mode 100644 src/react/hooks/data/residents.ts create mode 100644 src/react/hooks/data/users.ts create mode 100644 src/react/hooks/useAction.ts create mode 100644 src/react/hooks/useLoad.ts create mode 100644 src/react/store/actions/abodes.ts create mode 100644 src/react/store/actions/clearAll.ts create mode 100644 src/react/store/actions/users.ts create mode 100644 src/react/store/load.ts create mode 100644 src/react/store/loaders/abodes.ts create mode 100644 src/react/store/loaders/residents.ts create mode 100644 src/react/store/loaders/users.ts create mode 100644 src/react/store/react.tsx create mode 100644 src/react/store/slices/abodes.ts create mode 100644 src/react/store/slices/loading.ts create mode 100644 src/react/store/slices/login.ts create mode 100644 src/react/store/slices/residents.ts create mode 100644 src/react/store/slices/users.ts create mode 100644 src/react/store/store.ts create mode 100644 src/react/store/utils.ts create mode 100644 src/schema/abode/abode.schema.json create mode 100644 src/schema/abode/createabode.schema.json create mode 100644 src/schema/abode/updateabode.schema.json create mode 100644 src/schema/ajv.ts create mode 100644 src/schema/apikey/apikeypermissions.schema.json create mode 100644 src/schema/apikey/createapikey.schema.json create mode 100644 src/schema/note/createnote.schema.json create mode 100644 src/schema/note/partialnoteproperties.schema.json create mode 100644 src/schema/note/updatenote.schema.json create mode 100644 src/schema/rawSchemas.ts create mode 100644 src/schema/resident/createresident.schema.json create mode 100644 src/schema/resident/resident.schema.json create mode 100644 src/schema/resident/residentflags.schema.json create mode 100644 src/schema/resident/updateresident.schema.json create mode 100644 src/schema/schemas.ts create mode 100644 src/schema/user/clientuser.schema.json create mode 100644 src/schema/user/createuser.schema.json create mode 100644 src/schema/user/loginuser.schema.json create mode 100644 src/schema/user/partialuser.schema.json create mode 100644 src/schema/user/updateuser.schema.json create mode 100644 src/schema/user/user.schema.json create mode 100644 src/schema/user/userflags.schema.json create mode 100644 src/schema/validators.ts create mode 100644 src/tui/App.tsx create mode 100644 src/tui/components/panels/AbodesPanel.tsx create mode 100644 src/tui/components/panels/LoginPanel.tsx create mode 100644 src/tui/components/panels/UsersPanel.tsx create mode 100644 src/tui/components/popups/AbodePopup.tsx create mode 100644 src/tui/components/popups/AbodeResidentsPopup.tsx create mode 100644 src/tui/components/popups/CreateAbodePopup.tsx create mode 100644 src/tui/components/popups/CreateUserPopup.tsx create mode 100644 src/tui/components/popups/LoginPopup.tsx create mode 100644 src/tui/components/popups/UserPopup.tsx create mode 100644 src/tui/components/ui/AbodeName.tsx create mode 100644 src/tui/components/ui/Button.tsx create mode 100644 src/tui/components/ui/EllipsisText.tsx create mode 100644 src/tui/components/ui/Input.tsx create mode 100644 src/tui/components/ui/ListBox.tsx create mode 100644 src/tui/components/ui/ListDisplay.tsx create mode 100644 src/tui/components/ui/Popup.tsx create mode 100644 src/tui/components/ui/SearchPanel.tsx create mode 100644 src/tui/components/ui/UserName.tsx create mode 100644 src/tui/contexts/BgColor.tsx create mode 100644 src/tui/contexts/FocusManager.tsx create mode 100644 src/tui/hooks/size.ts create mode 100644 src/tui/hooks/useAfterRender.ts create mode 100644 src/util/error.ts create mode 100644 src/util/hash.ts create mode 100644 src/util/length.ts create mode 100644 src/util/token.ts create mode 100644 src/util/ts.ts create mode 100644 src/util/xmlwriter.ts create mode 100644 src/webapi/apirouter.ts create mode 100644 src/webapi/middleware/authenticate.ts create mode 100644 src/webapi/middleware/convertError.ts create mode 100644 src/webapi/middleware/jsonBody.ts create mode 100644 src/webapi/middleware/logRequests.ts create mode 100644 src/webapi/schemarouter.ts create mode 100644 tsconfig.json create mode 100644 webpack.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a1d7d4c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +/tmp +/dist \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..cf90d16 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5637 @@ +{ + "name": "abode", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "abode", + "version": "0.1.0", + "license": "ISC", + "dependencies": { + "@koa/bodyparser": "^6.0.0", + "@koa/router": "^14.0.0", + "@reduxjs/toolkit": "^2.9.0", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "better-sqlite3": "^12.2.0", + "fullscreen-ink": "^0.1.0", + "hash-wasm": "^4.12.0", + "ink": "^6.3.0", + "ink-text-input": "^6.0.0", + "koa": "^3.0.1", + "pg": "^8.16.3", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-redux": "^9.2.0" + }, + "bin": { + "abode-migrate": "dist/bin/abode-migrate.cjs", + "abode-repl": "dist/bin/abode-repl.cjs", + "abode-tui": "dist/bin/abode-tui.cjs", + "abode-web": "dist/bin/abode-web.cjs" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/koa": "^3.0.0", + "@types/koa__router": "^12.0.4", + "@types/react": "^19.1.12", + "@types/webpack-bundle-analyzer": "^4.7.0", + "copy-webpack-plugin": "^13.0.1", + "css-loader": "^7.1.2", + "dynohot": "^2.1.1", + "mini-css-extract-plugin": "^2.9.4", + "raw-loader": "^4.0.2", + "scss-loader": "^0.0.1", + "ts-loader": "^9.5.4", + "tsx": "^4.20.5", + "typescript": "^5.9.2", + "val-loader": "^6.0.0", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-cli": "^6.0.1" + }, + "engines": { + "node": "^22" + } + }, + "node_modules/@alcalzone/ansi-tokenize": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@alcalzone/ansi-tokenize/-/ansi-tokenize-0.2.0.tgz", + "integrity": "sha512-qI/5TaaaCZE4yeSZ83lu0+xi1r88JSxUjnH4OP/iZF7+KKZ75u3ee5isd0LxX+6N8U0npL61YrpbthILHB6BnA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@alcalzone/ansi-tokenize/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braidai/lang": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@braidai/lang/-/lang-1.1.2.tgz", + "integrity": "sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@koa/bodyparser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@koa/bodyparser/-/bodyparser-6.0.0.tgz", + "integrity": "sha512-CjM/tiisZHBA9iVkOOyj/ocLCNJTbpLsIUjCbVkrx5rkUOZlsFf/qSbXzAbY2SNJTMl0VtKgVen6SZdjaCfVhA==", + "license": "MIT", + "dependencies": { + "@types/co-body": "^6.1.3", + "co-body": "^6.2.0", + "lodash.merge": "^4.6.2", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "koa": ">=2" + } + }, + "node_modules/@koa/router": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-14.0.0.tgz", + "integrity": "sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.1", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "path-to-regexp": "^8.2.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz", + "integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/co-body": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-6.1.3.tgz", + "integrity": "sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-disposition": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-E/DPgzifH4sM1UMadJMWd6mO2jOd4g1Ejwzx8/uRCDpJis1IrlyQEcGAYEomtAqRYmD5ORbNXMeI9U0RiVGZbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-assert": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/koa": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-3.0.0.tgz", + "integrity": "sha512-MOcVYdVYmkSutVHZZPh8j3+dAjLyR5Tl59CN0eKgpkE1h/LBSmPAsQQuWs+bKu7WtGNn+hKfJH9Gzml+PulmDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "^2", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa__router": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/@types/koa__router/-/koa__router-12.0.4.tgz", + "integrity": "sha512-Y7YBbSmfXZpa/m5UGGzb7XadJIRBRnwNY9cdAojZGp65Cpe5MAP3mOZE7e3bImt8dfKS4UFcR16SLH8L/z7PBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.8.tgz", + "integrity": "sha512-4Olc63RY+MKvxMwVknCUDhRQX1pFQoBZ/lXcRLP69PQkEpze/0cr8LNqJQe5NFb/b19DWi2a5bTi2VAlQzhJuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.12", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.12.tgz", + "integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@types/webpack-bundle-analyzer": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@types/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.7.0.tgz", + "integrity": "sha512-c5i2ThslSNSG8W891BRvOd/RoCjI2zwph8maD22b1adtSns20j+0azDDMCK06DiVrzTgnwiDl5Ntmu1YRJw8Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "tapable": "^2.2.0", + "webpack": "^5" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-escapes": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.0.tgz", + "integrity": "sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/auto-bind": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/auto-bind/-/auto-bind-5.0.1.tgz", + "integrity": "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/better-sqlite3": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.2.0.tgz", + "integrity": "sha512-eGbYq2CT+tos1fBwLQ/tkBt9J5M3JEHjku4hbvQUePCckkvVf14xWj+1m7dGoK81M/fOjFT7yM9UMeKT/+vFLQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz", + "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001735", + "electron-to-chromium": "^1.5.204", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001737", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", + "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/co-body": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz", + "integrity": "sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==", + "license": "MIT", + "dependencies": { + "@hapi/bourne": "^3.0.0", + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/co-body/node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/co-body/node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/code-excerpt": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz", + "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==", + "license": "MIT", + "dependencies": { + "convert-to-spaces": "^2.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-to-spaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", + "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz", + "integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-parent": "^6.0.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2", + "tinyglobby": "^0.2.12" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "license": "MIT" + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dynohot": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/dynohot/-/dynohot-2.1.1.tgz", + "integrity": "sha512-cYmzuq9lqzGy5JybcAXQAeLEih2fSSqFqd1/70w4Edv6oeAHwtoS1Aq3q9qne7Rp/1pHWuEb729W2DhqbKa38g==", + "dev": true, + "license": "ISC", + "dependencies": { + "@babel/core": "^7.26.9", + "@babel/generator": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@braidai/lang": "^1.0.0", + "convert-source-map": "^2.0.0", + "tslib": "^2.8.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.209", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.209.tgz", + "integrity": "sha512-Xoz0uMrim9ZETCQt8UgM5FxQF9+imA7PBpokoGcZloA1uw2LeHzTlip5cb5KOAsXZLjh/moN2vReN3ZjJmjI9A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", + "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", + "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.39.10", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.10.tgz", + "integrity": "sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fullscreen-ink": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fullscreen-ink/-/fullscreen-ink-0.1.0.tgz", + "integrity": "sha512-GkyPG5Y8YxRT6i1Q8mZ0BCMSpgQjdBY+C39DnCUMswBpSypTk0G80rAYs6FoEp6Da2gzAwygXbJbju6GahbrFQ==", + "license": "MIT", + "dependencies": { + "ink": ">=4.4.1", + "react": ">=18.2.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.1.tgz", + "integrity": "sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-wasm": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/hash-wasm/-/hash-wasm-4.12.0.tgz", + "integrity": "sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==", + "license": "MIT" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "license": "MIT", + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-assert/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-assert/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/immer": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.3.tgz", + "integrity": "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inflation": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz", + "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "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/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ink": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ink/-/ink-6.3.0.tgz", + "integrity": "sha512-2CbJAa7XeziZYe6pDS5RVLirRY28iSGMQuEV8jRU5NQsONQNfcR/BZHHc9vkMg2lGYTHTM2pskxC1YmY28p6bQ==", + "license": "MIT", + "dependencies": { + "@alcalzone/ansi-tokenize": "^0.2.0", + "ansi-escapes": "^7.0.0", + "ansi-styles": "^6.2.1", + "auto-bind": "^5.0.1", + "chalk": "^5.6.0", + "cli-boxes": "^3.0.0", + "cli-cursor": "^4.0.0", + "cli-truncate": "^4.0.0", + "code-excerpt": "^4.0.0", + "es-toolkit": "^1.39.10", + "indent-string": "^5.0.0", + "is-in-ci": "^2.0.0", + "patch-console": "^2.0.0", + "react-reconciler": "^0.32.0", + "signal-exit": "^3.0.7", + "slice-ansi": "^7.1.0", + "stack-utils": "^2.0.6", + "string-width": "^7.2.0", + "type-fest": "^4.27.0", + "widest-line": "^5.0.0", + "wrap-ansi": "^9.0.0", + "ws": "^8.18.0", + "yoga-layout": "~3.2.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@types/react": ">=19.0.0", + "react": ">=19.0.0", + "react-devtools-core": "^4.19.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react-devtools-core": { + "optional": true + } + } + }, + "node_modules/ink-text-input": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ink-text-input/-/ink-text-input-6.0.0.tgz", + "integrity": "sha512-Fw64n7Yha5deb1rHY137zHTAbSTNelUKuB5Kkk2HACXEtwIHBCf9OH2tP/LQ9fRYTl1F0dZgbW0zPnZk6FA9Lw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "ink": ">=5", + "react": ">=18" + } + }, + "node_modules/ink-text-input/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ink/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ink/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-2.0.0.tgz", + "integrity": "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w==", + "license": "MIT", + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/koa": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/koa/-/koa-3.0.1.tgz", + "integrity": "sha512-oDxVkRwPOHhGlxKIDiDB2h+/l05QPtefD7nSqRgDfZt8P+QVYFWjfeK8jANf5O2YXjk8egd7KntvXKYx82wOag==", + "license": "MIT", + "dependencies": { + "accepts": "^1.3.8", + "content-disposition": "~0.5.4", + "content-type": "^1.0.5", + "cookies": "~0.9.1", + "delegates": "^1.0.0", + "destroy": "^1.2.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.5.0", + "http-errors": "^2.0.0", + "koa-compose": "^4.1.0", + "mime-types": "^3.0.1", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "license": "MIT" + }, + "node_modules/koa/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", + "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/patch-console": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/patch-console/-/patch-console-2.0.0.tgz", + "integrity": "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-loader": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-4.0.2.tgz", + "integrity": "sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/raw-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/raw-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/raw-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/raw-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", + "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", + "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.1" + } + }, + "node_modules/react-reconciler": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.32.0.tgz", + "integrity": "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "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/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/scss-loader": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/scss-loader/-/scss-loader-0.0.1.tgz", + "integrity": "sha512-SbT/smRJjkvvdHSEdAYAplosVkrtaSwwgUlnQCOuDS5sOKNjrS/eYCMvKeV6+YxK5cCOCsOJZd3vltrXatFp+g==", + "dev": true, + "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/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.14", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", + "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/tsx": { + "version": "4.20.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", + "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.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/val-loader": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/val-loader/-/val-loader-6.0.0.tgz", + "integrity": "sha512-NHi81ow+/mVBRuFRNxp8tfTSnAIFsq/wzZGqxv/a82Y722GQSOQi9yP0GuenSBiuw4+zGjmW/H9sLTbP3bewrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/watchpack": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.101.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", + "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.3", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.11", + "watchpack": "^2.4.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", + "colorette": "^2.0.14", + "commander": "^12.1.0", + "cross-spawn": "^7.0.3", + "envinfo": "^7.14.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^6.0.1" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.82.0" + }, + "peerDependenciesMeta": { + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz", + "integrity": "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==", + "license": "MIT", + "dependencies": { + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..958b9f7 --- /dev/null +++ b/package.json @@ -0,0 +1,65 @@ +{ + "name": "abode", + "version": "0.1.0", + "description": "A management interface for household stuff", + "author": "Codinget ", + "license": "ISC", + "type": "module", + "bin": { + "abode-migrate": "dist/bin/abode-migrate.cjs", + "abode-repl": "dist/bin/abode-repl.cjs", + "abode-web": "dist/bin/abode-web.cjs", + "abode-tui": "dist/bin/abode-tui.cjs" + }, + "scripts": { + "repl": "tsx --import ./src/meta/dev/register.ts", + "abode-migrate": "tsx --import ./src/meta/dev/register.ts src/bin/abode-migrate.ts", + "abode-repl": "tsx --import ./src/meta/dev/register.ts src/bin/abode-repl.ts", + "abode-web": "tsx --import ./src/meta/dev/register.ts --import ./src/meta/dev/webhot.ts src/bin/abode-web.ts", + "abode-tui": "tsx --import ./src/meta/dev/register.ts --import ./src/meta/dev/silenthot.ts src/bin/abode-tui.ts", + "abode-sources": "tsx --import ./src/meta/dev/register.ts src/bin/abode-sources.ts", + "build": "NODE_ENV=production npm run build:impl", + "build:impl": "rm -rf dist && tsx node_modules/.bin/webpack && chmod +x dist/bin/* && chmod -x dist/bin/*.*" + }, + "dependencies": { + "@koa/bodyparser": "^6.0.0", + "@koa/router": "^14.0.0", + "@reduxjs/toolkit": "^2.9.0", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "fullscreen-ink": "^0.1.0", + "hash-wasm": "^4.12.0", + "ink": "^6.3.0", + "ink-text-input": "^6.0.0", + "koa": "^3.0.1", + "pg": "^8.16.3", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-redux": "^9.2.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/koa": "^3.0.0", + "@types/koa__router": "^12.0.4", + "@types/react": "^19.1.12", + "@types/webpack-bundle-analyzer": "^4.7.0", + "copy-webpack-plugin": "^13.0.1", + "css-loader": "^7.1.2", + "dynohot": "^2.1.1", + "mini-css-extract-plugin": "^2.9.4", + "raw-loader": "^4.0.2", + "scss-loader": "^0.0.1", + "ts-loader": "^9.5.4", + "tsx": "^4.20.5", + "typescript": "^5.9.2", + "val-loader": "^6.0.0", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-cli": "^6.0.1" + }, + "engines": { + "node": "^22" + }, + "optionalDependencies": { + "better-sqlite3": "^12.2.0" + } +} diff --git a/src/bin/abode-migrate.ts b/src/bin/abode-migrate.ts new file mode 100644 index 0000000..57c789f --- /dev/null +++ b/src/bin/abode-migrate.ts @@ -0,0 +1,98 @@ +import { getMigrator, getDbInterface } from "../db/index.js"; +import { hashPassword } from "../util/hash.js"; + +const args = process.argv.slice(2); + +function printUsage(err: boolean | string = false): never { + const log = (err ? console.error : console.log).bind(console); + if (typeof err === "string") { + log(`Error: ${err}`); + log(""); + } + log("Usage:"); + log("\tabode-migrate --help"); + log("\tabode-migrate current"); + log("\tabode-migrate migrate"); + log("\tabode-migrate available"); + log("\tabode-migrate init"); + process.exit(err ? 1 : 0); +} + +if (!args.length || ["-h", "--help", "help"].some((x) => args.includes(x))) { + printUsage(); +} + +const url = args[0]; +const cmd = args[1]; + +if (!url) printUsage("missing "); +if (!cmd) printUsage("missing command"); +if (!["current", "migrate", "available", "init"].includes(cmd)) + printUsage(`invalid command: ${cmd}`); +if (args.length > 2) printUsage("too many arguments"); + +const migrator = await getMigrator(url); + +switch (cmd) { + case "current": { + const current = await migrator.listAppliedMigrations(); + console.log("Applied migrations:"); + if (!current.length) console.log("(none)"); + for (const migration of current) { + console.log( + `- ${migration.id} (${migration.name}) applied at ${migration.applied_at}` + ); + } + break; + } + + case "migrate": { + const latest = migrator.listAvailableMigrations().at(-1); + if (!latest) throw new Error("No available migration"); + await migrator.migrateTo(latest.id); + break; + } + + case "available": { + const available = migrator.listAvailableMigrations(); + console.log("Available migrations:"); + if (!available.length) console.log("(none)"); + for (const migration of available) { + console.log(`- ${migration.id} (${migration.name})`); + } + } + + case "init": { + const db = await getDbInterface(url); + let users = await db.listUsers(); + if (!users.some((x) => x.flags.admin)) { + const { uid } = await db.createUser({ + email: "admin@codi.moe", + name: "Admin", + flags: { admin: true }, + password: await hashPassword("changeme"), + }); + console.log( + `Created user 'admin@codi.moe' (${uid}) with password 'changeme' and admin flag` + ); + } + users = await db.listUsers(); + const admin = users.find((x) => x.flags.admin && x.name === "Admin"); + if (admin) { + const tokens = await db.listApikeysByUser(admin.uid); + if (!tokens.some((x) => x.permissions.admin && x.permissions.all)) { + const [apikey, token] = await db.createApikey({ + uid: admin.uid, + name: "admin", + permissions: { admin: true, all: true }, + expires_at: null, + }); + console.log( + `Created apikey '${token}' (${apikey.kid}) with permissions admin, all and no expiry` + ); + } + } + } +} + +process.exit(0); diff --git a/src/bin/abode-repl.ts b/src/bin/abode-repl.ts new file mode 100644 index 0000000..e5c7f40 --- /dev/null +++ b/src/bin/abode-repl.ts @@ -0,0 +1,61 @@ +import { getDbInterface, getMigrator } from "../db/index.js"; +import { start } from "node:repl"; +import * as errors from "../db/types/errors.js"; +import * as validators from "../schema/validators.js"; +import { hashPassword, validatePassword } from "../util/hash.js"; + +const args = process.argv.slice(2); + +function printUsage(err: boolean | string = false): never { + const log = (err ? console.error : console.log).bind(console); + if (typeof err === "string") { + log(`Error: ${err}`); + log(""); + } + log("Usage:"); + log("\tabode-repl --help"); + log("\tabode-repl [database]"); + process.exit(err ? 1 : 0); +} + +if (["-h", "--help", "help"].some((x) => args.includes(x))) { + printUsage(); +} +if (args.length > 1) printUsage("too many arguments"); + +const inject = Object.assign({}, errors, { + getDbInterface, + getMigrator, + validators, + errors, + hashPassword, + validatePassword, +}); +Object.assign(inject, { abode: inject }); +Object.assign(globalThis, inject); + +const url: string | undefined = args[0]; +if (url) { + let someSuccess = false; + try { + const db = await getDbInterface(url); + Object.assign(globalThis, { db }); + console.log("`db` set to a DbInterface"); + someSuccess = true; + } catch (e) { + console.error("Failed to obtain DbInterface", e); + } + try { + const migrator = await getMigrator(url); + Object.assign(globalThis, { migrator }); + console.log("`migrator` set to a Migrator"); + someSuccess = true; + } catch (e) { + console.error("Failed to obtain Migrator", e); + } + if (!someSuccess) throw new Error(`Failed to obtain anything for url ${url}`); +} else { + console.log("Pass to inject dbInterface/migrator"); +} + +start({ useGlobal: true }); diff --git a/src/bin/abode-sources.ts b/src/bin/abode-sources.ts new file mode 100644 index 0000000..0ffec96 --- /dev/null +++ b/src/bin/abode-sources.ts @@ -0,0 +1,63 @@ +import { getDbSources } from "../db/dbSources.js"; + +const args = process.argv.slice(2); + +function printUsage(err: boolean | string = false): never { + const log = (err ? console.error : console.log).bind(console); + if (typeof err === "string") { + log(`Error: ${err}`); + log(""); + } + log("Usage:"); + log("\tabode-sources --help"); + log("\tabode-sources [database]"); + process.exit(err ? 1 : 0); +} + +if (["-h", "--help", "help"].some((x) => args.includes(x))) { + printUsage(); +} +if (args.length > 1) printUsage("too many arguments"); + +console.log( + `Compiled with ${compiledSources.length} sources:`, + compiledSources.join(", ") +); + +const url = args[0] ?? "abode://"; +const sources = await getDbSources(url); +console.log(`Found ${sources.length} sources for url ${url}`); +for (const source of sources) { + console.log(`- ${source.name}`); + console.log( + " - protocols:", + source.protocols.map((x) => `'${x}'`).join(" ") + ); + const match = source.checkUrl(url); + console.log(` - matches url: ${match}`); + if (match) { + try { + const db = await source.getDbInterface(url); + console.log( + ` - generates an interface named ${db.name} ${ + db.backend ? "with" : "without" + } backend` + ); + await db.close().catch(console.error); + } catch (e) { + console.error(e); + console.log(" - fails to generate an interface"); + } + try { + const db = await source.getMigrator(url); + console.log( + ` - generates a migrator knowing ${ + db.listAvailableMigrations().length + } migrations` + ); + } catch (e) { + console.error(e); + console.log(" - fails to generate a migrator"); + } + } +} diff --git a/src/bin/abode-tui.ts b/src/bin/abode-tui.ts new file mode 100644 index 0000000..8010c19 --- /dev/null +++ b/src/bin/abode-tui.ts @@ -0,0 +1,85 @@ +import { withFullScreen } from "fullscreen-ink"; +import { app } from "../tui/App.js"; +import { getDbInterface } from "../db/index.js"; +import type {} from "dynohot"; +import { createStore } from "../react/store/store.js"; + +const args = process.argv.slice(2); +function printUsage(err: boolean | string = false): never { + const log = (err ? console.error : console.log).bind(console); + if (typeof err === "string") { + log(`Error: ${err}`); + log(""); + } + log("Usage:"); + log("\tabode-tui --help"); + log("\tabode-tui "); + process.exit(err ? 1 : 0); +} +if (["-h", "--help", "help"].some((x) => args.includes(x))) { + printUsage(); +} +if (!args.length) printUsage("missing database argument"); +if (args.length > 1) printUsage("too many arguments"); + +const db = await getDbInterface(args[0]); + +const raw = process.stdin.isRaw; +const bgColor = await new Promise((ok, ko) => { + process.stdin.setRawMode(true); + process.stdin.once("data", (chunk) => { + const result = chunk.toString("utf8"); + const match = result.match( + /^\u001b]11;rgb:([0-9a-fA-F]+)\/([0-9a-fA-F]+)\/([0-9a-fA-F]+)$/ + ); + if (!match) return ko("Didn't recognize terminal bg color"); + const [r, g, b] = match + .slice(1, 4) + .map((x) => (x.length < 2 ? x.repeat(2) : x.slice(0, 2))) + .map((x) => x.toLowerCase()); + for (const x of [r, g, b]) + if (x.length !== 2) + return ko("Invalid color component in terminal bg color"); + process.stdin.unref(); + return ok(["#", r, g, b].join("")); + }); + process.stdout.write("\x1b]11;?\x1b\\"); + setTimeout(() => { + ko(new Error("Giving up finding terminal bg color")); + }, 1000); +}).finally(() => process.stdin.setRawMode(raw)); + +process.removeAllListeners("warning"); +process.removeAllListeners("multipleResolves"); +process.removeAllListeners("rejectionHandled"); + +const doNothing = () => {}; +for (const method of [ + "log", + "warn", + "error", + "dir", + "dirxml", + "clear", + "count", + "debug", + "info", +] as const) { + console[method] = doNothing; +} + +const store = createStore(); +const fullscreenApp = withFullScreen(app({ db, bgColor, store }), { + exitOnCtrlC: true, +}); +await fullscreenApp.start(); +if (import.meta.hot) { + await import("../meta/dev/restart.js"); + + import.meta.hot.accept("../tui/App.js", (mod) => { + fullscreenApp.instance.rerender( + (mod.app as typeof app)({ db, bgColor, store }) + ); + }); +} +await fullscreenApp.waitUntilExit(); diff --git a/src/bin/abode-web.ts b/src/bin/abode-web.ts new file mode 100644 index 0000000..9151ebc --- /dev/null +++ b/src/bin/abode-web.ts @@ -0,0 +1,98 @@ +import Koa, { type Middleware } from "koa"; +import KoaRouter from "@koa/router"; +import { getDbInterface } from "../db/index.js"; +import { logRequests } from "../webapi/middleware/logRequests.js"; +import { isBackendInterface } from "../db/types/DbInterface.js"; +import { apirouter } from "../webapi/apirouter.js"; +import { schemarouter } from "../webapi/schemarouter.js"; +import type {} from "dynohot"; + +const args = process.argv.slice(2); + +function printUsage(err: boolean | string = false): never { + const log = (err ? console.error : console.log).bind(console); + if (typeof err === "string") { + log(`Error: ${err}`); + log(""); + } + log("Usage:"); + log("\tabode-migrate --help"); + log("\tabode-migrate "); + process.exit(err ? 1 : 0); +} + +if (["-h", "--help", "help"].some((x) => args.includes(x))) { + printUsage(); +} +if (args.length > 1) printUsage("too many arguments"); +if (args.length < 1) printUsage("missing argument"); + +const app = new Koa(); + +const db = await getDbInterface(args[0]); +if (!isBackendInterface(db)) { + throw new Error(`Interface ${db.name} is not a backend interface`); +} + +if (import.meta.hot) { + await import("../meta/dev/restart.js"); + + let middleware: Middleware = async (ctx, next) => { + ctx.status = 500; + ctx.body = { ok: false, err: "still_loading" }; + void next; + }; + app.use((ctx, next) => middleware(ctx, next)); + + let currentDb = db; + let makeApiRouter = apirouter; + + const listenRouter = () => { + console.log("[hot] Creating router"); + const router = new KoaRouter(); + router.use(logRequests); + + const api = makeApiRouter(currentDb); + router.use("/api", api.middleware(), api.allowedMethods()); + + const schema = schemarouter(); + router.use("/schema", schema.middleware(), schema.allowedMethods()); + + const rm = router.middleware(); + const ram = router.allowedMethods(); + middleware = async (ctx, next) => + rm(ctx as any, () => ram(ctx as any, next)); + }; + listenRouter(); + + import.meta.hot.accept("../webapi/apirouter.js", (mod) => { + console.log("[hot] Reloading apirouter"); + makeApiRouter = mod.apirouter as typeof apirouter; + listenRouter(); + }); + import.meta.hot.accept("../db/index.js", async (mod) => { + console.log("[hot] Reloading DbInterface"); + const db = await (mod.getDbInterface as typeof getDbInterface)(args[0]); + if (!isBackendInterface(db)) { + throw new Error(`Interface ${db.name} is not a backend interface`); + } + currentDb = db; + listenRouter(); + }); +} else { + const router = new KoaRouter(); + router.use(logRequests); + app.use(router.middleware()); + app.use(router.allowedMethods()); + + const api = apirouter(db); + router.use("/api", api.middleware(), api.allowedMethods()); + + const schema = schemarouter(); + router.use("/schema", schema.middleware(), schema.allowedMethods()); +} + +const port = +(process.env.PORT ?? "3000"); +if (isNaN(port)) throw new Error(`Invalid port: ${process.env.PORT}`); +app.listen(port); +console.log(`Listening on port ${port}`); diff --git a/src/db/api/ApiInterface.ts b/src/db/api/ApiInterface.ts new file mode 100644 index 0000000..4073a52 --- /dev/null +++ b/src/db/api/ApiInterface.ts @@ -0,0 +1,289 @@ +import type { Abode, CreateAbode, UpdateAbode } from "../types/Abode.js"; +import type { ClientApikey, CreateApikey } from "../types/Apikey.js"; +import type { DbInterface } from "../types/DbInterface.js"; +import { + ConflictAbodeError, + InvalidAbodeError, + NotAuthorizedAbodeError, + NotFoundAbodeError, + ReadonlyAbodeError, +} from "../types/errors.js"; +import type { + CreateNote, + Note, + PartialNote, + UpdateNote, +} from "../types/Note.js"; +import type { + CreateResident, + Resident, + updateResident, +} from "../types/Resident.js"; +import type { + PartialUser, + ClientUser, + CreateUser, + UpdateUser, +} from "../types/User.js"; + +export class ApiInterface implements DbInterface { + #root: string; + #headers: Record; + #readonly: boolean; + + constructor( + root: string, + { + headers = {}, + readonly = false, + }: { + headers?: Record; + readonly?: boolean; + } = {} + ) { + if (root.endsWith("/")) root = root.slice(0, -1); + this.#root = root; + this.#headers = headers; + this.#readonly = readonly; + } + + #url(route: string, params?: Record): string { + if (params) { + const remaining = new Map(Object.entries(params)); + route = route + .split("/") + .map((part) => { + if (part.startsWith(":")) { + const value = remaining.get(part.slice(1)); + remaining.delete(part); + if (value === undefined) + throw new Error(`Missing ${part} in params`); + return encodeURIComponent(value); + } + return part; + }) + .join("/"); + if (remaining.size) { + const sp = new URLSearchParams([...remaining.entries()]); + route += "?" + sp.toString(); + } + } + return this.#root + route; + } + + async #call( + method: string, + route: string, + { + params, + body, + headers, + }: { + params?: Record; + body?: unknown; + headers?: Record; + } = {} + ): Promise { + const resolvedHeaders = { ...this.#headers, ...headers }; + if (body !== undefined) { + body = JSON.stringify(body); + resolvedHeaders["Content-Type"] = "application/json"; + resolvedHeaders["Content-Length"] = "" + (body as string).length; + } + const url = this.#url(route, params); + const res = await fetch(url, { + method, + headers: resolvedHeaders, + body: body as string, + }); + if (!res.ok) { + const text = await res.text(); + switch (res.status) { + case 400: + throw new InvalidAbodeError(); + case 401: + throw new NotAuthorizedAbodeError(); + case 403: + throw new ReadonlyAbodeError(); + case 404: + throw new NotFoundAbodeError(); + case 409: + throw new ConflictAbodeError(); + default: + throw new Error(`${res.status} ${res.statusText} ${text}`); + } + } + return res.json(); + } + + #checkReadonly(): void { + if (this.#readonly) throw new ReadonlyAbodeError(); + } + + get _() { + return { + root: this.#root, + headers: { ...this.#headers }, + url: this.#url.bind(this), + call: this.#call.bind(this), + self: () => this.#call("GET", "/auth/self"), + }; + } + + get readonly(): boolean { + return this.#readonly; + } + get backend(): false { + return false; + } + get name(): "api" { + return "api"; + } + + async close(): Promise { + // do nothing + } + + async listUsers(): Promise<(PartialUser | ClientUser)[]> { + return this.#call("GET", "/users"); + } + async getUserById(uid: string): Promise { + return this.#call("GET", "/users/:uid", { params: { uid } }); + } + async deleteUserById(uid: string): Promise { + this.#checkReadonly(); + await this.#call("DELETE", "/users/:uid", { params: { uid } }); + } + async createUser(user: CreateUser): Promise { + this.#checkReadonly(); + return this.#call("POST", "/users", { body: user }); + } + async updateUser(user: UpdateUser): Promise { + this.#checkReadonly(); + return this.#call("PATCH", "/users/:uid", { + params: { uid: user.uid }, + body: user, + }); + } + + async getUserByEmail(email: string): Promise { + return this.#call("GET", "/users/by-email", { params: { email } }); + } + + async listAbodes(): Promise { + return this.#call("GET", "/abodes"); + } + async getAbodeById(aid: string): Promise { + return this.#call("GET", "/abodes/:aid", { params: { aid } }); + } + async deleteAbodeById(aid: string): Promise { + this.#checkReadonly(); + await this.#call("DELETE", "/abodes/:aid", { params: { aid } }); + } + async createAbode(abode: CreateAbode): Promise { + this.#checkReadonly(); + return this.#call("POST", "/abodes", { body: abode }); + } + async updateAbode(abode: UpdateAbode): Promise { + this.#checkReadonly(); + return this.#call("PATCH", "/abodes/:aid", { + params: { aid: abode.aid }, + body: abode, + }); + } + + async listResidents(): Promise { + return this.#call("GET", "/residents"); + } + async getResidentById(uid: string, aid: string): Promise { + return this.#call("GET", "/residents/:uid/:aid", { params: { uid, aid } }); + } + async deleteResidentById(uid: string, aid: string): Promise { + this.#checkReadonly(); + await this.#call("DELETE", "/residents/:uid/:aid", { + params: { uid, aid }, + }); + } + async createResident(resident: CreateResident): Promise { + this.#checkReadonly(); + return this.#call("POST", "/residents", { body: resident }); + } + async updateResident(resident: updateResident): Promise { + this.#checkReadonly(); + return this.#call("PATCH", "/residents/:uid/:aid", { + params: { uid: resident.uid, aid: resident.aid }, + body: resident, + }); + } + + async listResidentsByUserId(uid: string): Promise { + return this.#call("GET", "/users/:uid/residents", { params: { uid } }); + } + async listResidentsByAbodeId(aid: string): Promise { + return this.#call("GET", "/abodes/:aid/residents", { params: { aid } }); + } + + async listUsersByAbodeId(aid: string): Promise<(PartialUser | ClientUser)[]> { + return this.#call("GET", "/abodes/:aid/users", { params: { aid } }); + } + async listAbodesByUserId(uid: string): Promise { + return this.#call("GET", "/users/:uid/abodes", { params: { uid } }); + } + + async deleteSessionsByUser(uid: string): Promise { + this.#checkReadonly(); + await this.#call("POST", "/users/:uid/auth/clear-sessions", { + params: { uid }, + }); + } + + async listApikeysByUser(uid: string): Promise { + return this.#call("GET", "/users/:uid/apikeys", { params: { uid } }); + } + async getApikeyById(kid: string): Promise { + return this.#call("GET", "/apikeys/:kid", { + params: { kid }, + }); + } + async createApikey( + apikey: CreateApikey + ): Promise<[ClientApikey, `at_${string}`]> { + this.#checkReadonly(); + const { apikey: key, token } = await this.#call<{ + apikey: ClientApikey; + token: `at_${string}`; + }>("POST", "/users/:uid/apikeys", { + params: { uid: apikey.uid }, + body: apikey, + }); + return [key, token]; + } + async deleteApikeyById(kid: string): Promise { + this.#checkReadonly(); + await this.#call("DELETE", "/apikeys/:kid", { + params: { kid }, + }); + } + + async listNotes(): Promise { + throw new Error("Unimplemented"); + } + async getNoteById(nid: string): Promise { + throw new Error("Unimplemented"); + } + async deleteNoteById(nid: string): Promise { + throw new Error("Unimplemented"); + } + async createNote(note: CreateNote): Promise { + throw new Error("Unimplemented"); + } + async updateNote(note: UpdateNote): Promise { + throw new Error("Unimplemented"); + } + async listNotesByAbodeId(aid: string): Promise { + throw new Error("Unimplemented"); + } + async listNotesByUserId(uid: string): Promise { + throw new Error("Unimplemented"); + } +} diff --git a/src/db/api/getdb.dyn.ts b/src/db/api/getdb.dyn.ts new file mode 100644 index 0000000..ba68584 --- /dev/null +++ b/src/db/api/getdb.dyn.ts @@ -0,0 +1,14 @@ +import type { GetDbDynamic } from "../types/GetDb.js"; +import { apiProtocols } from "./url.js"; + +const getApi = () => + import(/* webpackChunkName: 'dbsource-api' */ "./getdb.static.js").then( + (x) => x.default + ); + +const getApiDynamic: GetDbDynamic = { + name: "api", + protocols: apiProtocols, + getSource: getApi, +}; +export default getApiDynamic; diff --git a/src/db/api/getdb.static.ts b/src/db/api/getdb.static.ts new file mode 100644 index 0000000..44c116c --- /dev/null +++ b/src/db/api/getdb.static.ts @@ -0,0 +1,19 @@ +import type { GetDbStatic } from "../types/GetDb.js"; +import { ApiInterface } from "./ApiInterface.js"; +import { apiProtocols, isApiUrl, parseApiUrl } from "./url.js"; + +const getApiStatic: GetDbStatic = { + name: "api", + protocols: apiProtocols, + checkUrl: isApiUrl, + getDbInterface: async (url) => { + const [root, { headers, readonly }] = parseApiUrl(url); + const db = new ApiInterface(root, { headers, readonly }); + await db._.self(); + return db; + }, + getMigrator: async () => { + throw new Error("No migrator for api"); + }, +}; +export default getApiStatic; diff --git a/src/db/api/url.ts b/src/db/api/url.ts new file mode 100644 index 0000000..97372d9 --- /dev/null +++ b/src/db/api/url.ts @@ -0,0 +1,36 @@ +export const apiProtocols = ["abode+https:", "abode+http:", "https:", "http:"]; + +export function isApiUrl(url: string) { + try { + const urlObj = new URL(url); + return apiProtocols.includes(urlObj.protocol); + } catch { + return false; + } +} + +export function parseApiUrl(url: string) { + const urlObj = new URL(url); + if (!apiProtocols.includes(urlObj.protocol)) + throw new Error("Not an {abode+,}http{s,}: protocol"); + // can't replace just the protocol, apparently + urlObj.href = urlObj.href.replace(/^abode\+/, ""); + const readonly = (urlObj.searchParams.get("readonly") ?? "0") !== "0"; + const headers = Object.fromEntries( + [...urlObj.searchParams.entries()].filter(([param]) => param !== "readonly") + ); + if (urlObj.username) { + headers["Authorization"] = + "Basic " + + btoa( + [ + decodeURIComponent(urlObj.username), + decodeURIComponent(urlObj.password), + ].join(":") + ); + urlObj.username = ""; + urlObj.password = ""; + } + urlObj.search = ""; + return [urlObj.href, { headers, readonly }] as const; +} diff --git a/src/db/dbSources.dyn.ts b/src/db/dbSources.dyn.ts new file mode 100644 index 0000000..61c07d2 --- /dev/null +++ b/src/db/dbSources.dyn.ts @@ -0,0 +1,24 @@ +import getApiDynamic from "./api/getdb.dyn.js"; +import getSqliteDynamic from "./sqlite/getdb.dyn.js"; +import type { GetDbDynamic, GetDbStatic } from "./types/GetDb.js"; + +const dynamicSources: GetDbDynamic[] = [getSqliteDynamic, getApiDynamic]; +export async function getDbSources(url: string): Promise { + const urlObj = new URL(url); + const dbSources: GetDbStatic[] = []; + const promises: Promise[] = []; + for (const source of dynamicSources) { + if (source.protocols.includes(urlObj.protocol)) { + promises.push( + source + .getSource(url) + .catch(() => null) + .then((dbSource) => { + if (dbSource) dbSources.push(dbSource); + }) + ); + } + } + await Promise.all(promises); + return dbSources; +} diff --git a/src/db/dbSources.shared.ts b/src/db/dbSources.shared.ts new file mode 100644 index 0000000..3bdef46 --- /dev/null +++ b/src/db/dbSources.shared.ts @@ -0,0 +1,12 @@ +import type { GetDbStatic } from "./types/GetDb.js"; + +let rawGetDbSources: typeof getDbSources | undefined = undefined; +const getGetDbSources = () => + import(/* webpackChunkName: 'dbsources' */ "./dbSources.static.js").then( + (x) => x.getDbSources + ); + +export async function getDbSources(url: string): Promise { + if (!rawGetDbSources) rawGetDbSources = await getGetDbSources(); + return rawGetDbSources(url); +} diff --git a/src/db/dbSources.static.ts b/src/db/dbSources.static.ts new file mode 100644 index 0000000..85bc40b --- /dev/null +++ b/src/db/dbSources.static.ts @@ -0,0 +1,9 @@ +import getApiStatic from "./api/getdb.static.js"; +import getSqliteStatic from "./sqlite/getdb.static.js"; +import type { GetDbStatic } from "./types/GetDb.js"; + +const dbSources: GetDbStatic[] = [getSqliteStatic, getApiStatic]; +export async function getDbSources(url: string): Promise { + void url; + return dbSources; +} diff --git a/src/db/dbSources.ts b/src/db/dbSources.ts new file mode 100644 index 0000000..a9a2c87 --- /dev/null +++ b/src/db/dbSources.ts @@ -0,0 +1 @@ +export * from "./dbSources.static.js"; diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..4fcf1cc --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,16 @@ +import { getDbSources } from "./dbSources.js"; +import type { DbInterface } from "./types/DbInterface.js"; +import type { Migrator } from "./types/Migrator.js"; + +export async function getDbInterface(url: string): Promise { + for (const source of await getDbSources(url)) { + if (source.checkUrl(url)) return source.getDbInterface(url); + } + throw new Error(`No source found for url ${url}`); +} +export async function getMigrator(url: string): Promise { + for (const source of await getDbSources(url)) { + if (source.checkUrl(url)) return source.getMigrator(url); + } + throw new Error(`No source found for url ${url}`); +} diff --git a/src/db/sqlite/SqliteInterface.ts b/src/db/sqlite/SqliteInterface.ts new file mode 100644 index 0000000..8e217b9 --- /dev/null +++ b/src/db/sqlite/SqliteInterface.ts @@ -0,0 +1,481 @@ +import type { BackendDbInterface } from "../types/DbInterface.js"; +import { + isValidUserPassword, + type ClientUser, + type CreateUser, + type LoginUser, + type UpdateUser, + type UserFlags, +} from "../types/User.js"; +import { sqliteToClientUser, sqliteToDate, sqliteToUuid } from "./cast.js"; +import { + ConflictAbodeError, + InvalidAbodeError, + NotAuthorizedAbodeError, + NotFoundAbodeError, + ReadonlyAbodeError, +} from "../types/errors.js"; +import type { Abode, CreateAbode, UpdateAbode } from "../types/Abode.js"; +import type { + CreateResident, + Resident, + ResidentFlags, + updateResident, +} from "../types/Resident.js"; +import { validatePassword } from "../../util/hash.js"; +import { createApikeyToken, createSessionToken } from "../../util/token.js"; +import type { ClientApikey, CreateApikey } from "../types/Apikey.js"; +import { calcUpdates, catSql, joinSql, sql } from "./sql.js"; +import { + selectAbode, + selectAbodes, + selectClientApikey, + selectClientApikeys, + selectClientUser, + selectClientUsers, + selectResident, + selectResidents, +} from "./query.js"; +import type { + CreateNote, + Note, + PartialNote, + UpdateNote, +} from "../types/Note.js"; +import type { WrappedDb } from "./impl/types.js"; + +export class SqliteInterface implements BackendDbInterface { + #db: WrappedDb; + + constructor(db: WrappedDb) { + this.#db = db; + } + + #checkReadonly(): void { + if (this.#db.readonly) throw new ReadonlyAbodeError(); + } + + get _() { + return { + sql, + catSql, + joinSql, + db: this.#db, + }; + } + + get readonly(): boolean { + return this.#db.readonly; + } + get backend(): true { + return true; + } + get name(): "sqlite" { + return "sqlite"; + } + + async close(): Promise { + this.#db.destroy(); + } + + async listUsers(): Promise { + return selectClientUsers(this.#db); + } + #getUserById(uid: string): ClientUser { + const user = selectClientUser(this.#db, sql`"uid" = ${{ uuid: uid }}`); + if (!user) throw new NotFoundAbodeError(); + return user; + } + async getUserById(id: string): Promise { + return this.#getUserById(id); + } + async getUserByEmail(email: string): Promise { + const user = selectClientUser(this.#db, sql`"email" = ${{ text: email }}`); + if (!user) throw new NotFoundAbodeError(); + return user; + } + async getUserByLogin({ email, password }: LoginUser): Promise { + const rawUser = this.#db.get<{ + uid: Buffer; + email: string; + name: string; + flags: string; + created_at: string; + updated_at: string; + password: string; + }>( + sql` + SELECT "uid", "email", "name", json("flags") AS "flags", "created_at", "updated_at", "password" + FROM "users" + WHERE "email" = ${{ text: email }} + ` + ); + if (!rawUser) throw new NotFoundAbodeError(); + if (rawUser.password.startsWith("#")) throw new ConflictAbodeError(); + if (!(await validatePassword(password, rawUser.password))) + throw new NotAuthorizedAbodeError(); + return sqliteToClientUser(rawUser); + } + async deleteUserById(id: string): Promise { + this.#checkReadonly(); + const { changes } = this.#db.run( + sql` + DELETE FROM "users" + WHERE "uid" = ${{ uuid: id }} + ` + ); + if (!changes) throw new NotFoundAbodeError(); + } + async createUser(user: CreateUser): Promise { + this.#checkReadonly(); + if (!isValidUserPassword(user.password)) throw new InvalidAbodeError(); + const uid = crypto.randomUUID(); + return this.#db.rethrow(() => + this.#db.multi(() => { + this.#db.run( + sql` + INSERT INTO "users"("uid", "email", "name", "password", "flags") + VALUES( + ${{ uuid: uid }}, + ${{ text: user.email }}, + ${{ text: user.name }}, + ${{ text: user.password }},${{ jsonb: user.flags }}) + ` + ); + return this.#getUserById(uid); + }) + ); + } + async updateUser(user: UpdateUser): Promise { + this.#checkReadonly(); + return this.#db.rethrow(() => + this.#db.multi(() => { + const updates = calcUpdates({ + email: (value: string) => sql`"email" = ${{ text: value }}`, + name: (value: string) => sql`"name" = ${{ text: value }}`, + password: (value: string) => { + if (!isValidUserPassword(value)) throw new InvalidAbodeError(); + if (value.startsWith("#")) { + this.#db.run(sql` + DELETE FROM "apikeys" + WHERE "uid" = ${{ uuid: user.uid }} + `); + } + this.#db.run(sql` + DELETE FROM "sessions" + WHERE "uid" = ${{ uuid: user.uid }} + `); + return sql`"password" = ${{ text: value }}`; + }, + flags: (value: UserFlags) => sql`"flags" = ${{ jsonb: value }}`, + })(user); + if (!updates.length) throw new InvalidAbodeError(); + const { changes } = this.#db.run( + sql` + UPDATE "users" + SET + "updated_at" = datetime('now', 'localtime', 'subsec'), + ${joinSql(updates, sql`, `)} + WHERE "uid" = ${{ uuid: user.uid }} + ` + ); + if (!changes) throw new NotFoundAbodeError(); + return this.#getUserById(user.uid); + }) + ); + } + + async listAbodes(): Promise { + return selectAbodes(this.#db); + } + #getAbodeById(aid: string): Abode { + const abode = selectAbode(this.#db, sql`"aid" = ${{ uuid: aid }}`); + if (!abode) throw new NotFoundAbodeError(); + return abode; + } + async getAbodeById(id: string): Promise { + return this.#getAbodeById(id); + } + async deleteAbodeById(id: string): Promise { + this.#checkReadonly(); + const { changes } = this.#db.run( + sql` + DELETE FROM "abodes" + WHERE "aid" = ${{ uuid: id }} + ` + ); + if (!changes) throw new NotFoundAbodeError(); + } + async createAbode(abode: CreateAbode, ctx: { uid: string }): Promise { + this.#checkReadonly(); + const aid = crypto.randomUUID(); + return this.#db.rethrow(() => + this.#db.multi(() => { + this.#db.run( + sql` + INSERT INTO "abodes"("aid", "name", "created_by", "updated_by") + VALUES( + ${{ uuid: aid }}, + ${{ text: abode.name }}, + ${{ uuid: ctx.uid }}, + ${{ uuid: ctx.uid }} + ) + ` + ); + return this.#getAbodeById(aid); + }) + ); + } + async updateAbode(abode: UpdateAbode, ctx: { uid: string }): Promise { + this.#checkReadonly(); + const updates = calcUpdates({ + name: (value: string) => sql`"name" = ${{ text: value }}`, + })(abode); + if (!updates.length) throw new InvalidAbodeError(); + return this.#db.rethrow(() => + this.#db.multi(() => { + const { changes } = this.#db.run( + sql` + UPDATE "abodes" + SET + "updated_at" = datetime('now', 'localtime', 'subsec'), + "updated_by" = ${{ uuid: ctx.uid }}, + ${joinSql(updates, sql`, `)} + WHERE "aid" = ${{ uuid: abode.aid }} + ` + ); + if (!changes) throw new NotFoundAbodeError(); + return this.#getAbodeById(abode.aid); + }) + ); + } + + async listResidents(): Promise { + return selectResidents(this.#db); + } + async listResidentsByUserId(uid: string): Promise { + return selectResidents(this.#db, sql`"uid" = ${{ uuid: uid }}`); + } + async listResidentsByAbodeId(aid: string): Promise { + return selectResidents(this.#db, sql`"aid" = ${{ uuid: aid }}`); + } + #getResidentById(uid: string, aid: string): Resident { + const resident = selectResident( + this.#db, + sql`"uid" = ${{ uuid: uid }} AND "aid" = ${{ uuid: aid }}` + ); + if (!resident) throw new NotFoundAbodeError(); + return resident; + } + async getResidentById(uid: string, aid: string): Promise { + return this.#getResidentById(uid, aid); + } + async deleteResidentById(uid: string, aid: string): Promise { + this.#checkReadonly(); + const { changes } = this.#db.run( + sql` + DELETE FROM "residents" + WHERE "uid" = ${{ uuid: uid }} AND "aid" = ${{ uuid: aid }} + ` + ); + if (!changes) throw new NotFoundAbodeError(); + } + async createResident( + resident: CreateResident, + ctx: { uid: string } + ): Promise { + this.#checkReadonly(); + return this.#db.rethrow(() => + this.#db.multi(() => { + this.#db.run( + sql` + INSERT INTO "residents"("uid", "aid", "flags", "created_by", "updated_by") + VALUES( + ${{ uuid: resident.uid }}, + ${{ uuid: resident.aid }}, + ${{ jsonb: resident.flags }}, + ${{ uuid: ctx.uid }}, + ${{ uuid: ctx.uid }} + ) + ` + ); + return this.#getResidentById(resident.uid, resident.aid); + }) + ); + } + async updateResident( + resident: updateResident, + ctx: { uid: string } + ): Promise { + this.#checkReadonly(); + const updates = calcUpdates({ + flags: (value: ResidentFlags) => sql`"flags" = ${{ jsonb: value }}`, + })(resident); + if (!updates.length) throw new InvalidAbodeError(); + return this.#db.rethrow(() => + this.#db.multi(() => { + const { changes } = this.#db.run( + sql` + UPDATE "residents" + SET + "updated_at" = datetime('now', 'localtime', 'subsec'), + "updated_by" = ${{ uuid: ctx.uid }} + ${joinSql(updates, sql`, `)} + WHERE + "uid" = ${{ uuid: resident.uid }} + AND "aid" = ${{ uuid: resident.aid }} + ` + ); + if (!changes) throw new NotFoundAbodeError(); + return this.#getResidentById(resident.uid, resident.aid); + }) + ); + } + + async listUsersByAbodeId(id: string): Promise { + return selectClientUsers( + this.#db, + sql` + JOIN "residents" r ON u."uid" = r."uid" + WHERE r."aid" = ${{ uuid: id }} + ` + ); + } + async listAbodesByUserId(id: string): Promise { + return selectAbodes( + this.#db, + sql` + JOIN "residents" r ON a."aid" = r."aid" + WHERE r."uid" = ${{ uuid: id }} + ` + ); + } + + async getUserBySession(token: `as_${string}`): Promise { + const session = this.#db.get<{ uid: Buffer; expires_at: string }>(sql` + SELECT "uid", "expires_at" + FROM "sessions" + WHERE "token" = ${{ text: token }} + `); + if (!session) throw new NotFoundAbodeError(); + if (new Date(sqliteToDate(session.expires_at)).getTime() < Date.now()) { + if (!this.readonly) { + this.#db.run(sql` + DELETE FROM "sessions" + WHERE "expires_at" < datetime('now', 'localtime', 'subsec') + `); + } + throw new NotFoundAbodeError(); + } + if (!this.readonly) { + this.#db.run(sql` + UPDATE "sessions" + SET "expires_at" = datetime('now', 'localtime', 'subsec', '+7 days') + WHERE "token" = ${{ text: token }} + `); + } + return this.#getUserById(sqliteToUuid(session.uid)); + } + async createSession(uid: string): Promise<`as_${string}`> { + this.#checkReadonly(); + const token = createSessionToken(); + this.#db.run(sql` + INSERT INTO "sessions"("uid", "token") + VALUES(${{ uuid: uid }}, ${{ text: token }}) + `); + return token; + } + async deleteSessionsByUser(uid: string): Promise { + this.#checkReadonly(); + this.#db.run(sql` + DELETE FROM "sessions" + WHERE "uid" = ${{ uuid: uid }} + `); + } + + #getApikeyByToken(token: `at_${string}`): ClientApikey { + const apikey = selectClientApikey( + this.#db, + sql`"token" = ${{ text: token }}` + ); + if (!apikey) throw new NotFoundAbodeError(); + return apikey; + } + async getUserByApikey( + token: `at_${string}` + ): Promise<[ClientUser, ClientApikey]> { + const apikey = this.#getApikeyByToken(token); + if ( + apikey.expires_at && + new Date(apikey.expires_at).getTime() < Date.now() + ) { + throw new NotAuthorizedAbodeError(); + } + return [this.#getUserById(apikey.uid), apikey]; + } + async listApikeysByUser(uid: string): Promise { + return selectClientApikeys(this.#db, sql`"uid" = ${{ uuid: uid }}`); + } + async getApikeyById(kid: string): Promise { + const apikey = selectClientApikey(this.#db, sql`"kid" = ${{ uuid: kid }}`); + if (!apikey) throw new NotFoundAbodeError(); + return apikey; + } + async createApikey( + apikey: CreateApikey + ): Promise<[ClientApikey, `at_${string}`]> { + this.#checkReadonly(); + const token = createApikeyToken(); + const kid = crypto.randomUUID(); + let expires = apikey.expires_at; + if (expires === undefined) + expires = new Date( + new Date().getTime() + 1000 * 60 * 60 * 24 * 365 + ).toISOString(); + if (expires && new Date(expires).getTime() < Date.now()) + throw new InvalidAbodeError(); + + this.#db.run(sql` + INSERT INTO "apikeys"("uid", "kid", "token", "name", "permissions", "expires_at") + VALUES( + ${{ uuid: apikey.uid }}, + ${{ uuid: kid }}, + ${{ text: token }}, + ${{ text: apikey.name }}, + ${{ jsonb: apikey.permissions }}, + ${expires ? { date: expires } : { null: true }} + ) + `); + return [this.#getApikeyByToken(token), token]; + } + async deleteApikeyById(kid: string): Promise { + this.#checkReadonly(); + const { changes } = this.#db.run(sql` + DELETE FROM "apikeys" + WHERE "kid" = ${{ uuid: kid }} + `); + if (!changes) throw new NotFoundAbodeError(); + } + + async listNotes(): Promise { + throw new Error("Unimplemented"); + } + async getNoteById(nid: string): Promise { + throw new Error("Unimplemented"); + } + async deleteNoteById(nid: string): Promise { + throw new Error("Unimplemented"); + } + async createNote(note: CreateNote, ctx: { uid: string }): Promise { + throw new Error("Unimplemented"); + } + async updateNote(note: UpdateNote, ctx: { uid: string }): Promise { + throw new Error("Unimplemented"); + } + async listNotesByAbodeId(aid: string): Promise { + throw new Error("Unimplemented"); + } + async listNotesByUserId(uid: string): Promise { + throw new Error("Unimplemented"); + } +} diff --git a/src/db/sqlite/SqliteMigrator.ts b/src/db/sqlite/SqliteMigrator.ts new file mode 100644 index 0000000..1b56a82 --- /dev/null +++ b/src/db/sqlite/SqliteMigrator.ts @@ -0,0 +1,108 @@ +import type { + AppliedMigration, + AvailableMigration, + Migrator, +} from "../types/Migrator.js"; +import { init, migrations } from "./migrations/index.js"; +import { sqliteToDate } from "./cast.js"; +import type { WrappedDb } from "./impl/types.js"; +import { sql, unsafeSql } from "./sql.js"; + +export class SqliteMigrator implements Migrator { + #db: WrappedDb; + + constructor(db: WrappedDb) { + this.#db = db; + } + + #listAppliedMigrations(): + | { id: number; name: string; applied_at: string }[] + | null { + try { + return this.#db + .all<{ id: number; name: string; applied_at: string }>( + sql`SELECT "id", "name", "applied_at" FROM "_migrations" ORDER BY "id" ASC` + ) + .map((x) => ({ ...x, applied_at: sqliteToDate(x.applied_at) })); + } catch (e) { + if (e instanceof Error && e.message === "no such table: _migrations") { + return null; + } + throw e; + } + } + + async listAppliedMigrations(): Promise { + return this.#listAppliedMigrations() ?? []; + } + + listAvailableMigrations(): AvailableMigration[] { + return migrations.map((migration) => ({ + id: migration.id, + name: migration.name, + })); + } + + async migrateTo(id: number): Promise { + const target = migrations.find((x) => x.id === id); + if (!target) throw new Error(`No known migration with id ${id}`); + + let current = this.#listAppliedMigrations(); + if (!current) { + this.#db.run(unsafeSql(init)); + current = []; + } + + for (const { id, name } of current) { + const migration = migrations.find((x) => x.id === id); + if (!migration) + throw new Error(`Applied migration ${id} (${name}) not known`); + if (migration.name !== name) + throw new Error( + `Applied migration ${id} (${name}) has a different name from expected (${migration.name})` + ); + } + + const start = migrations.findIndex((x) => x.id === current.at(-1)?.id) + 1; + const end = migrations.indexOf(target) + 1; + + if (end < start) { + throw new Error( + `Cannot migrate backward, at ${current.at(-1)?.id ?? 0}, going to ${ + target.id + }` + ); + } + + const toApply = migrations.slice(start, end); + + if (!toApply.length) { + console.log("Nothing to do"); + return; + } + + for (const migration of toApply) { + try { + console.log(`Applying migration ${migration.id} (${migration.name})`); + this.#db.run(sql`BEGIN`); + for (const part of migration.parts) { + console.log(`- Applying part ${part.id} (${part.name})`); + if ("sql" in part) this.#db.run(unsafeSql(part.sql)); + else await part.apply(this.#db); + } + this.#db.run( + sql` + INSERT INTO "_migrations"("id", "name") + VALUES (${{ int: migration.id }}, ${{ text: migration.name }}) + ` + ); + this.#db.run(sql`COMMIT`); + } catch (e) { + this.#db.run(sql`ROLLBACK`); + throw e; + } + } + + console.log("Done migrating database"); + } +} diff --git a/src/db/sqlite/cast.ts b/src/db/sqlite/cast.ts new file mode 100644 index 0000000..129cca9 --- /dev/null +++ b/src/db/sqlite/cast.ts @@ -0,0 +1,162 @@ +import type { Abode } from "../types/Abode.js"; +import type { ApikeyPermissions, ClientApikey } from "../types/Apikey.js"; +import type { Resident, ResidentFlags } from "../types/Resident.js"; +import type { ClientUser, PartialUser, UserFlags } from "../types/User.js"; + +export function dateToSqlite(date: string) { + return new Date(date).getTime() / 1000; +} +export function sqliteToDate(date: string) { + return new Date(date.replace(" ", "T") + "Z").toISOString(); +} + +export function uuidToSqlite(uuid: string) { + return Buffer.from(uuid.replaceAll("-", ""), "hex"); +} +export function sqliteToUuid(uuid: Buffer | Uint8Array) { + const hex = (uuid instanceof Buffer ? uuid : Buffer.from(uuid)).toString( + "hex" + ); + return [ + hex.slice(0, 8), + hex.slice(8, 12), + hex.slice(12, 16), + hex.slice(16, 20), + hex.slice(20), + ].join("-"); +} + +const defaultUserFlags: UserFlags = {}; +export function sqliteToUserFlags(flags: string): UserFlags { + const parsed = JSON.parse(flags); + const out = { ...defaultUserFlags }; + + if (typeof parsed !== "object" || !parsed || Array.isArray(parsed)) + return out; + + if (parsed.admin === true) out.admin = true; + return out; +} + +export function sqliteToPartialUser(user: { + uid: Buffer | Uint8Array; + name: string; + flags: string; + created_at: string; + updated_at: string; +}): PartialUser { + return { + uid: sqliteToUuid(user.uid), + name: user.name, + flags: sqliteToUserFlags(user.flags), + created_at: sqliteToDate(user.created_at), + updated_at: sqliteToDate(user.updated_at), + }; +} +export function sqliteToClientUser(user: { + uid: Buffer | Uint8Array; + email: string; + name: string; + flags: string; + created_at: string; + updated_at: string; +}): ClientUser { + return { + ...sqliteToPartialUser(user), + email: user.email, + }; +} + +export function sqliteToAbode(abode: { + aid: Buffer | Uint8Array; + name: string; + created_at: string; + created_by: Buffer | Uint8Array | null; + updated_at: string; + updated_by: Buffer | Uint8Array | null; +}): Abode { + return { + aid: sqliteToUuid(abode.aid), + name: abode.name, + created_at: sqliteToDate(abode.created_at), + created_by: abode.created_by && sqliteToUuid(abode.created_by), + updated_at: sqliteToDate(abode.updated_at), + updated_by: abode.updated_by && sqliteToUuid(abode.updated_by), + }; +} + +const defaultResidentFlags: ResidentFlags = {}; +export function sqliteToResidentFlags(flags: string): ResidentFlags { + const parsed = JSON.parse(flags); + const out = { ...defaultResidentFlags }; + + if (typeof parsed !== "object" || !parsed || Array.isArray(parsed)) + return out; + + if (parsed.admin === true) out.admin = true; + return out; +} + +export function sqliteToResident(resident: { + uid: Buffer | Uint8Array; + aid: Buffer | Uint8Array; + flags: string; + created_at: string; + created_by: Buffer | Uint8Array | null; + updated_at: string; + updated_by: Buffer | Uint8Array | null; +}): Resident { + return { + uid: sqliteToUuid(resident.uid), + aid: sqliteToUuid(resident.aid), + flags: sqliteToResidentFlags(resident.flags), + created_at: sqliteToDate(resident.created_at), + created_by: resident.created_by && sqliteToUuid(resident.created_by), + updated_at: sqliteToDate(resident.updated_at), + updated_by: resident.updated_by && sqliteToUuid(resident.updated_by), + }; +} + +const defaultApikeyPermissions: ApikeyPermissions = {}; +export function sqliteToApikeyPermissions( + permissions: string +): ApikeyPermissions { + const parsed = JSON.parse(permissions); + const out = { ...defaultApikeyPermissions }; + + if (typeof parsed !== "object" || !parsed || Array.isArray(parsed)) + return out; + + if (parsed.admin === true) out.admin = true; + if (parsed.all === true) out.all = true; + for (const key of ["users", "residents", "abodes"] as const) { + if (parsed[key] === "r" || parsed[key] === "rw") out[key] = parsed[key]; + } + for (const key of ["restrict_users", "restrict_abodes"] as const) { + if ( + Array.isArray(parsed[key]) && + parsed[key].every((x) => typeof x === "string") + ) { + out[key] = parsed[key]; + } + } + return out; +} + +export function sqliteToClientApikey(apikey: { + uid: Buffer | Uint8Array; + kid: Buffer | Uint8Array; + name: string; + permissions: string; + created_at: string; + expires_at: string | null; +}): ClientApikey { + return { + uid: sqliteToUuid(apikey.uid), + kid: sqliteToUuid(apikey.kid), + name: apikey.name, + permissions: sqliteToApikeyPermissions(apikey.permissions), + created_at: sqliteToDate(apikey.created_at), + expires_at: apikey.expires_at ? sqliteToDate(apikey.expires_at) : null, + }; +} diff --git a/src/db/sqlite/dyn.ts b/src/db/sqlite/dyn.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/db/sqlite/getdb.dyn.ts b/src/db/sqlite/getdb.dyn.ts new file mode 100644 index 0000000..98d057a --- /dev/null +++ b/src/db/sqlite/getdb.dyn.ts @@ -0,0 +1,14 @@ +import type { GetDbDynamic } from "../types/GetDb.js"; +import { sqliteProtocols } from "./url.js"; + +const getSqlite = () => + import(/* webpackChunkName: 'dbsource-sqlite' */ "./getdb.static.js").then( + (x) => x.default + ); + +const getSqliteDynamic: GetDbDynamic = { + name: "sqlite", + protocols: sqliteProtocols, + getSource: getSqlite, +}; +export default getSqliteDynamic; diff --git a/src/db/sqlite/getdb.static.ts b/src/db/sqlite/getdb.static.ts new file mode 100644 index 0000000..112a61e --- /dev/null +++ b/src/db/sqlite/getdb.static.ts @@ -0,0 +1,16 @@ +import type { GetDbStatic } from "../types/GetDb.js"; +import { getWrappedDb } from "./impl/index.js"; +import { SqliteInterface } from "./SqliteInterface.js"; +import { SqliteMigrator } from "./SqliteMigrator.js"; +import { isSqliteUrl, parseSqliteUrl, sqliteProtocols } from "./url.js"; + +const getSqliteStatic: GetDbStatic = { + name: "sqlite", + protocols: sqliteProtocols, + checkUrl: isSqliteUrl, + getDbInterface: async (url) => + new SqliteInterface(getWrappedDb(...parseSqliteUrl(url))), + getMigrator: async (url) => + new SqliteMigrator(getWrappedDb(...parseSqliteUrl(url))), +}; +export default getSqliteStatic; diff --git a/src/db/sqlite/impl/better-sqlite3.ts b/src/db/sqlite/impl/better-sqlite3.ts new file mode 100644 index 0000000..2e06876 --- /dev/null +++ b/src/db/sqlite/impl/better-sqlite3.ts @@ -0,0 +1,89 @@ +import { ConflictAbodeError, NotFoundAbodeError } from "../../types/errors.js"; +import type { SqlCode, SqlVar } from "../sql.js"; +import Sqlite, * as sqlite from "better-sqlite3"; +import pragma from "../pragma.sqlite.sql"; +import type { WrappedDb, WrappedDbOptions } from "./types.js"; + +function getDatabase( + path: string, + options?: Omit +): sqlite.Database { + if (!natives.sqlite) throw new Error("No natives found for better-sqlite3"); + options = { ...options }; + if (typeof options.timeout !== "number") delete options.timeout; + const db = new Sqlite(path, { ...options, nativeBinding: natives.sqlite }); + db.exec(pragma); + return db; +} + +function rethrow(fn: () => R): R { + try { + return fn(); + } catch (e) { + if (e instanceof sqlite.SqliteError) { + switch (e.code) { + case "SQLITE_CONSTRAINT_UNIQUE": + case "SQLITE_CONSTRAINT_PRIMARYKEY": + throw new ConflictAbodeError(); + + case "SQLITE_CONSTRAINT_FOREIGNKEY": + throw new NotFoundAbodeError(); + } + } + throw e; + } +} + +export class WrappedBetterSqlite3Db implements WrappedDb { + #db: sqlite.Database; + #statements: Map; + + constructor(path: string, options: WrappedDbOptions = {}) { + this.#db = getDatabase(path, { + readonly: options.readonly, + timeout: options.timeout, + }); + this.#statements = new Map(); + } + + get _db() { + return this.#db; + } + + get readonly(): boolean { + return this.#db.readonly; + } + + destroy(): void { + this.#db.close(); + this.#statements.clear(); + } + + #stmt(stmt: string): sqlite.Statement { + if (!this.#db.open) throw new Error("Db is closed"); + let prep = this.#statements.get(stmt); + if (!prep) { + prep = this.#db.prepare<(string | Buffer)[], R>(stmt); + this.#statements.set(stmt, prep); + } + return prep as sqlite.Statement; + } + + all(stmt: SqlCode): R[] { + return this.#stmt(stmt._sql).all(...stmt._vars); + } + get(stmt: SqlCode): R | null { + const results = this.all(stmt); + if (results.length > 1) throw new Error("Multiple results"); + if (!results.length) return null; + return results[0]; + } + run(stmt: SqlCode): { changes: number } { + return this.#stmt(stmt._sql).run(...stmt._vars); + } + + multi(fn: () => R): R { + return this.#db.transaction(fn)(); + } + rethrow = rethrow; +} diff --git a/src/db/sqlite/impl/implementations.all.ts b/src/db/sqlite/impl/implementations.all.ts new file mode 100644 index 0000000..f3603f3 --- /dev/null +++ b/src/db/sqlite/impl/implementations.all.ts @@ -0,0 +1,6 @@ +import { WrappedBetterSqlite3Db } from "./better-sqlite3.js"; +import { WrappedNodeSqliteDb } from "./node-sqlite.js"; +import type { WrappedDbConstructor } from "./types.js"; + +export const node: WrappedDbConstructor | null = WrappedNodeSqliteDb; +export const bs3: WrappedDbConstructor | null = WrappedBetterSqlite3Db; diff --git a/src/db/sqlite/impl/implementations.bs3.ts b/src/db/sqlite/impl/implementations.bs3.ts new file mode 100644 index 0000000..0106de2 --- /dev/null +++ b/src/db/sqlite/impl/implementations.bs3.ts @@ -0,0 +1,5 @@ +import { WrappedBetterSqlite3Db } from "./better-sqlite3.js"; +import type { WrappedDbConstructor } from "./types.js"; + +export const node: WrappedDbConstructor | null = null; +export const bs3: WrappedDbConstructor | null = WrappedBetterSqlite3Db; diff --git a/src/db/sqlite/impl/implementations.node.ts b/src/db/sqlite/impl/implementations.node.ts new file mode 100644 index 0000000..2686c95 --- /dev/null +++ b/src/db/sqlite/impl/implementations.node.ts @@ -0,0 +1,5 @@ +import { WrappedNodeSqliteDb } from "./node-sqlite.js"; +import type { WrappedDbConstructor } from "./types.js"; + +export const node: WrappedDbConstructor | null = WrappedNodeSqliteDb; +export const bs3: WrappedDbConstructor | null = null; diff --git a/src/db/sqlite/impl/implementations.ts b/src/db/sqlite/impl/implementations.ts new file mode 100644 index 0000000..58b01e8 --- /dev/null +++ b/src/db/sqlite/impl/implementations.ts @@ -0,0 +1 @@ +export * from "./implementations.all.js"; diff --git a/src/db/sqlite/impl/index.ts b/src/db/sqlite/impl/index.ts new file mode 100644 index 0000000..6e99946 --- /dev/null +++ b/src/db/sqlite/impl/index.ts @@ -0,0 +1,20 @@ +import type { WrappedDb, WrappedDbOptions } from "./types.js"; +import { node, bs3 } from "./implementations.js"; + +export function getWrappedDb( + kind: "any" | "node" | "bs3", + path: string, + options: WrappedDbOptions +): WrappedDb { + if (kind === "node") { + if (!node) throw new Error("Requesting unavailable node backend"); + return new node(path, options); + } else if (kind === "bs3") { + if (!bs3) throw new Error("Requesting unavailable better-sqlite3 backend"); + return new bs3(path, options); + } else { + const impl = [node, bs3].find(Boolean); + if (!impl) throw new Error("No available backend"); + return new impl(path, options); + } +} diff --git a/src/db/sqlite/impl/node-sqlite.ts b/src/db/sqlite/impl/node-sqlite.ts new file mode 100644 index 0000000..9c95f2c --- /dev/null +++ b/src/db/sqlite/impl/node-sqlite.ts @@ -0,0 +1,76 @@ +import { DatabaseSync, StatementSync } from "node:sqlite"; +import type { WrappedDb, WrappedDbOptions } from "./types.js"; +import type { SqlCode } from "../sql.js"; +import pragma from "../pragma.sqlite.sql"; + +export class WrappedNodeSqliteDb implements WrappedDb { + #db: DatabaseSync; + #readonly: boolean; + #statements: Map; + + constructor(path: string, options: WrappedDbOptions = {}) { + this.#readonly = !!options.readonly; + this.#db = new DatabaseSync(path, { + readOnly: options.readonly, + timeout: options.timeout, + open: true, + }); + this.#db.exec(pragma); + this.#statements = new Map(); + } + + get _db() { + return this.#db; + } + + get readonly(): boolean { + return this.#readonly; + } + + destroy(): void { + this.#db.close(); + this.#statements.clear(); + } + + #stmt(stmt: string): StatementSync { + if (!this.#db.open) throw new Error("Db is closed"); + let prep = this.#statements.get(stmt); + if (!prep) { + prep = this.#db.prepare(stmt); + prep.setReadBigInts(false); + this.#statements.set(stmt, prep); + } + return prep; + } + + all(stmt: SqlCode): R[] { + return this.#stmt(stmt._sql).all(...stmt._vars) as R[]; + } + get(stmt: SqlCode): R | null { + const results = this.all(stmt); + if (results.length > 1) throw new Error("Multiple results"); + if (!results.length) return null; + return results[0]; + } + run(stmt: SqlCode): { changes: number } { + const { changes } = this.#stmt(stmt._sql).run(...stmt._vars); + return { changes: Number(changes) }; + } + + multi(fn: () => R): R { + if (this.#db.isTransaction) + throw new Error("Nested transactions not supported"); + this.#db.exec("BEGIN"); + try { + const rst = fn(); + this.#db.exec("COMMIT"); + return rst; + } catch (e) { + this.#db.exec("ROLLBACK"); + throw e; + } + } + rethrow(fn: () => R): R { + return fn(); + } +} diff --git a/src/db/sqlite/impl/types.ts b/src/db/sqlite/impl/types.ts new file mode 100644 index 0000000..9b16a59 --- /dev/null +++ b/src/db/sqlite/impl/types.ts @@ -0,0 +1,18 @@ +import type { SqlCode } from "../sql.js"; + +export interface WrappedDb { + get readonly(): boolean; + destroy(): void; + + all(stmt: SqlCode): R[]; + get(stmt: SqlCode): R | null; + run(stmt: SqlCode): { changes: number }; + multi(fn: () => R): R; + rethrow(fn: () => R): R; +} + +export type WrappedDbOptions = { readonly?: boolean; timeout?: number }; + +export interface WrappedDbConstructor { + new (path: string, options?: WrappedDbOptions): WrappedDb; +} diff --git a/src/db/sqlite/migrations/1.init/1.users.sqlite.sql b/src/db/sqlite/migrations/1.init/1.users.sqlite.sql new file mode 100644 index 0000000..4af4e1c --- /dev/null +++ b/src/db/sqlite/migrations/1.init/1.users.sqlite.sql @@ -0,0 +1,9 @@ +CREATE TABLE "users" ( + "uid" BLOB NOT NULL PRIMARY KEY, -- uuid + "email" TEXT NOT NULL UNIQUE, + "name" TEXT NOT NULL, + "password" TEXT NOT NULL DEFAULT '#unset', + "flags" BLOB NOT NULL DEFAULT (jsonb('{}')), -- JSONB + "created_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')), + "updated_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')) +); diff --git a/src/db/sqlite/migrations/1.init/2.abodes.sqlite.sql b/src/db/sqlite/migrations/1.init/2.abodes.sqlite.sql new file mode 100644 index 0000000..2aa1cf8 --- /dev/null +++ b/src/db/sqlite/migrations/1.init/2.abodes.sqlite.sql @@ -0,0 +1,8 @@ +CREATE TABLE "abodes" ( + "aid" BLOB NOT NULL PRIMARY KEY, -- uuid + "name" TEXT NOT NULL, + "created_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')), + "created_by" BLOB REFERENCES "users"("uid") ON DELETE SET NULL, -- uuid + "updated_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')), + "updated_by" BLOB REFERENCES "users"("uid") ON DELETE SET NULL -- uuid +); diff --git a/src/db/sqlite/migrations/1.init/3.residents.sqlite.sql b/src/db/sqlite/migrations/1.init/3.residents.sqlite.sql new file mode 100644 index 0000000..23397c3 --- /dev/null +++ b/src/db/sqlite/migrations/1.init/3.residents.sqlite.sql @@ -0,0 +1,11 @@ +CREATE TABLE "residents" ( + "uid" BLOB NOT NULL REFERENCES "users"("uid") ON DELETE CASCADE, + "aid" BLOB NOT NULL REFERENCES "abodes"("aid") ON DELETE CASCADE, + "flags" BLOB NOT NULL DEFAULT (jsonb('{}')), -- JSONB + "created_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')), + "created_by" BLOB REFERENCES "users"("uid") ON DELETE SET NULL, -- uuid + "updated_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')), + "updated_by" BLOB REFERENCES "users"("uid") ON DELETE SET NULL, -- uuid + + PRIMARY KEY("uid", "aid") +); diff --git a/src/db/sqlite/migrations/1.init/index.ts b/src/db/sqlite/migrations/1.init/index.ts new file mode 100644 index 0000000..044d320 --- /dev/null +++ b/src/db/sqlite/migrations/1.init/index.ts @@ -0,0 +1,26 @@ +import type { SqliteMigration } from "../types.js"; +import p1 from "./1.users.sqlite.sql"; +import p2 from "./2.abodes.sqlite.sql"; +import p3 from "./3.residents.sqlite.sql"; + +export const m1: SqliteMigration = { + id: 1, + name: "init", + parts: [ + { + id: 1, + name: "users", + sql: p1, + }, + { + id: 2, + name: "abodes", + sql: p2, + }, + { + id: 3, + name: "residents", + sql: p3, + }, + ], +}; diff --git a/src/db/sqlite/migrations/2.auth/1.sessions.sqlite.sql b/src/db/sqlite/migrations/2.auth/1.sessions.sqlite.sql new file mode 100644 index 0000000..2a3552a --- /dev/null +++ b/src/db/sqlite/migrations/2.auth/1.sessions.sqlite.sql @@ -0,0 +1,7 @@ +CREATE TABLE "sessions" ( + "uid" BLOB NOT NULL REFERENCES "users"("uid") ON DELETE CASCADE, -- uuid + "token" TEXT NOT NULL PRIMARY KEY, + "created_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')), + "updated_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')), + "expires_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec', '+7 days')) +); diff --git a/src/db/sqlite/migrations/2.auth/2.apikeys.sqlite.sql b/src/db/sqlite/migrations/2.auth/2.apikeys.sqlite.sql new file mode 100644 index 0000000..878b87c --- /dev/null +++ b/src/db/sqlite/migrations/2.auth/2.apikeys.sqlite.sql @@ -0,0 +1,9 @@ +CREATE TABLE "apikeys" ( + "uid" BLOB NOT NULL REFERENCES "users"("uid") ON DELETE CASCADE, -- uuid + "kid" BLOB NOT NULL PRIMARY KEY, -- uuid + "token" TEXT NOT NULL UNIQUE, + "name" TEXT NOT NULL, + "permissions" BLOB NOT NULL DEFAULT (jsonb('{}')), -- JSONB + "created_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')), + "expires_at" TEXT +); diff --git a/src/db/sqlite/migrations/2.auth/index.ts b/src/db/sqlite/migrations/2.auth/index.ts new file mode 100644 index 0000000..5b9219d --- /dev/null +++ b/src/db/sqlite/migrations/2.auth/index.ts @@ -0,0 +1,20 @@ +import type { SqliteMigration } from "../types.js"; +import p1 from "./1.sessions.sqlite.sql"; +import p2 from "./2.apikeys.sqlite.sql"; + +export const m2: SqliteMigration = { + id: 2, + name: "auth", + parts: [ + { + id: 1, + name: "sessions", + sql: p1, + }, + { + id: 2, + name: "apikeys", + sql: p2, + }, + ], +}; diff --git a/src/db/sqlite/migrations/3.notes/1.notes.sql b/src/db/sqlite/migrations/3.notes/1.notes.sql new file mode 100644 index 0000000..d374913 --- /dev/null +++ b/src/db/sqlite/migrations/3.notes/1.notes.sql @@ -0,0 +1,11 @@ +CREATE TABLE "notes" ( + "nid" BLOB NOT NULL PRIMARY KEY, -- uuid + "aid" BLOB NOT NULL REFERENCES "abodes"("aid") ON DELETE CASCADE, -- uuid + "name" TEXT NOT NULL, + "content" TEXT NOT NULL DEFAULT '', -- markdown + "properties" BLOB NOT NULL DEFAULT (jsonb('{}')), -- JSONB + "created_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')), + "created_by" BLOB REFERENCES "users"("uid") ON DELETE SET NULL, -- uuid + "updated_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')), + "updated_by" BLOB REFERENCES "users"("uid") ON DELETE SET NULL -- uuid +); diff --git a/src/db/sqlite/migrations/3.notes/index.ts b/src/db/sqlite/migrations/3.notes/index.ts new file mode 100644 index 0000000..c890439 --- /dev/null +++ b/src/db/sqlite/migrations/3.notes/index.ts @@ -0,0 +1,14 @@ +import type { SqliteMigration } from "../types.js"; +import p1 from "./1.notes.sql"; + +export const m3: SqliteMigration = { + id: 3, + name: "notes", + parts: [ + { + id: 1, + name: "notes", + sql: p1, + }, + ], +}; diff --git a/src/db/sqlite/migrations/index.ts b/src/db/sqlite/migrations/index.ts new file mode 100644 index 0000000..b55fc6a --- /dev/null +++ b/src/db/sqlite/migrations/index.ts @@ -0,0 +1,7 @@ +import { m1 } from "./1.init/index.js"; +import { m2 } from "./2.auth/index.js"; +import { m3 } from "./3.notes/index.js"; +import type { SqliteMigration } from "./types.js"; + +export { default as init } from "./init.sqlite.sql"; +export const migrations: SqliteMigration[] = [m1, m2, m3]; diff --git a/src/db/sqlite/migrations/init.sqlite.sql b/src/db/sqlite/migrations/init.sqlite.sql new file mode 100644 index 0000000..e051577 --- /dev/null +++ b/src/db/sqlite/migrations/init.sqlite.sql @@ -0,0 +1,5 @@ +CREATE TABLE "_migrations" ( + "id" INTEGER NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "applied_at" TEXT NOT NULL DEFAULT (datetime('now', 'localtime', 'subsec')) +); diff --git a/src/db/sqlite/migrations/types.ts b/src/db/sqlite/migrations/types.ts new file mode 100644 index 0000000..8e13507 --- /dev/null +++ b/src/db/sqlite/migrations/types.ts @@ -0,0 +1,19 @@ +import type { WrappedDb } from "../impl/types.js"; + +export type SqliteMigrationPart = { + id: number; + name: string; +} & ( + | { + sql: string; + } + | { + apply: (database: WrappedDb) => Promise; + } +); + +export type SqliteMigration = { + id: number; + name: string; + parts: SqliteMigrationPart[]; +}; diff --git a/src/db/sqlite/pragma.sqlite.sql b/src/db/sqlite/pragma.sqlite.sql new file mode 100644 index 0000000..51dd74f --- /dev/null +++ b/src/db/sqlite/pragma.sqlite.sql @@ -0,0 +1,4 @@ +PRAGMA foreign_keys = ON; +PRAGMA journal_mode = WAL; +PRAGMA synchronous = NORMAL; +PRAGMA temp_store = MEMORY; diff --git a/src/db/sqlite/query.ts b/src/db/sqlite/query.ts new file mode 100644 index 0000000..75fff69 --- /dev/null +++ b/src/db/sqlite/query.ts @@ -0,0 +1,124 @@ +import type { Abode } from "../types/Abode.js"; +import type { ClientApikey } from "../types/Apikey.js"; +import type { Resident } from "../types/Resident.js"; +import type { ClientUser } from "../types/User.js"; +import { + sqliteToAbode, + sqliteToClientApikey, + sqliteToClientUser, + sqliteToResident, +} from "./cast.js"; +import type { WrappedDb } from "./impl/types.js"; +import { sql, type SqlCode } from "./sql.js"; + +type RawClientUser = { + uid: Buffer | Uint8Array; + email: string; + name: string; + flags: string; + created_at: string; + updated_at: string; +}; +const sqlClientUser = sql` + SELECT u."uid", u."email", u."name", json(u."flags") AS "flags", u."created_at", u."updated_at" + FROM "users" u +`; + +export function selectClientUser( + db: WrappedDb, + where: SqlCode +): ClientUser | null { + const rawUser = db.get(sql`${sqlClientUser} WHERE ${where}`); + if (rawUser) return sqliteToClientUser(rawUser); + return null; +} +export function selectClientUsers(db: WrappedDb, rest?: SqlCode): ClientUser[] { + const rawUsers = db.all( + rest ? sql`${sqlClientUser} ${rest}` : sqlClientUser + ); + return rawUsers.map(sqliteToClientUser); +} + +type RawAbode = { + aid: Buffer | Uint8Array; + name: string; + created_at: string; + created_by: Buffer | Uint8Array | null; + updated_at: string; + updated_by: Buffer | Uint8Array | null; +}; +const sqlAbode = sql` + SELECT a."aid", a."name", a."created_at", a."created_by", a."updated_at", a."updated_by" + FROM "abodes" a +`; + +export function selectAbode(db: WrappedDb, where: SqlCode): Abode | null { + const rawAbode = db.get(sql`${sqlAbode} WHERE ${where}`); + if (rawAbode) return sqliteToAbode(rawAbode); + return null; +} +export function selectAbodes(db: WrappedDb, rest?: SqlCode): Abode[] { + const rawAbodes = db.all( + rest ? sql`${sqlAbode} ${rest}` : sqlAbode + ); + return rawAbodes.map(sqliteToAbode); +} + +type RawResident = { + uid: Buffer | Uint8Array; + aid: Buffer | Uint8Array; + flags: string; + created_at: string; + created_by: Buffer | Uint8Array | null; + updated_at: string; + updated_by: Buffer | Uint8Array | null; +}; +const sqlResident = sql` + SELECT "uid", "aid", json("flags") AS "flags", "created_at", "created_by", "updated_at", "updated_by" + FROM "residents" +`; + +export function selectResident(db: WrappedDb, where: SqlCode): Resident | null { + const rawResident = db.get(sql`${sqlResident} WHERE ${where}`); + if (rawResident) return sqliteToResident(rawResident); + return null; +} +export function selectResidents(db: WrappedDb, where?: SqlCode): Resident[] { + const rawResidents = db.all( + where ? sql`${sqlResident} WHERE ${where}` : sqlResident + ); + return rawResidents.map(sqliteToResident); +} + +type RawClientApikey = { + uid: Buffer | Uint8Array; + kid: Buffer | Uint8Array; + name: string; + permissions: string; + created_at: string; + expires_at: string | null; +}; +const sqlClientApikey = sql` + SELECT k."uid", k."kid", k."name", json(k."permissions") AS "permissions", k."created_at", k."expires_at" + FROM "apikeys" k +`; + +export function selectClientApikey( + db: WrappedDb, + where: SqlCode +): ClientApikey | null { + const rawApikey = db.get( + sql`${sqlClientApikey} WHERE ${where}` + ); + if (rawApikey) return sqliteToClientApikey(rawApikey); + return null; +} +export function selectClientApikeys( + db: WrappedDb, + where: SqlCode +): ClientApikey[] { + const rawApikeys = db.all( + sql`${sqlClientApikey} WHERE ${where}` + ); + return rawApikeys.map(sqliteToClientApikey); +} diff --git a/src/db/sqlite/sql.ts b/src/db/sqlite/sql.ts new file mode 100644 index 0000000..f3e43b7 --- /dev/null +++ b/src/db/sqlite/sql.ts @@ -0,0 +1,74 @@ +import { uuidToSqlite } from "./cast.js"; + +export type SqlVar = string | Buffer | number; +export type SqlCode = { _sql: string; _vars: SqlVar[] }; +type SqlArg = + | { uuid: string } + | { text: string } + | { jsonb: unknown } + | { date: string } + | { int: number } + | { null: true } + | SqlCode; +export function sql(text: TemplateStringsArray, ...args: SqlArg[]): SqlCode { + let code = ""; + const vars: SqlVar[] = []; + for (const [i, part] of text.entries()) { + code += part; + if (i < args.length) { + const arg = args[i]; + if ("uuid" in arg) { + code += "?"; + vars.push(uuidToSqlite(arg.uuid)); + } else if ("text" in arg) { + code += "?"; + vars.push(arg.text); + } else if ("jsonb" in arg) { + code += "jsonb(?)"; + vars.push(JSON.stringify(arg.jsonb)); + } else if ("date" in arg) { + code += "datetime(?, 'unixepoch', 'subsec')"; + vars.push(new Date(arg.date).getTime() / 1000 + ""); + } else if ("int" in arg) { + if (arg.int % 1) throw new Error("Not an integer"); + code += "?"; + vars.push(arg.int); + } else if ("null" in arg) { + code += "NULL"; + } else { + code += arg._sql; + for (const v of arg._vars) vars.push(v); + } + } + } + return { _sql: code, _vars: vars }; +} +export function catSql(a: SqlCode, b: SqlCode): SqlCode { + return { + _sql: a._sql + b._sql, + _vars: [...a._vars, ...b._vars], + }; +} +export function joinSql(parts: SqlCode[], joiner: SqlCode): SqlCode { + return parts.reduce((a, b) => catSql(catSql(a, joiner), b)); +} + +export function unsafeSql(sql: string): SqlCode { + return { _sql: sql, _vars: [] }; +} + +export function calcUpdates(updater: { + [K in keyof T]: (value: NonNullable) => SqlCode; +}): (obj: Partial) => SqlCode[] { + return (obj) => { + const updates: SqlCode[] = []; + for (const [prop, update] of Object.entries(updater)) { + if (prop in obj) { + updates.push( + (update as (value: unknown) => SqlCode)(obj[prop as keyof T]!) + ); + } + } + return updates; + }; +} diff --git a/src/db/sqlite/url.ts b/src/db/sqlite/url.ts new file mode 100644 index 0000000..acbf3a6 --- /dev/null +++ b/src/db/sqlite/url.ts @@ -0,0 +1,45 @@ +import type { WrappedDbOptions } from "./impl/types.js"; + +function checkBooleanParam(url: URL, param: string): boolean { + return (url.searchParams.get(param) ?? "0") !== "0"; +} + +function checkNumberParam(url: URL, param: string): number | undefined { + const value = url.searchParams.get(param); + if (!value || isNaN(+value)) return; + return +value; +} + +export const sqliteProtocols = [ + "sqlite:", + "sqlite3:", + "node+sqlite:", + "bs3+sqlite:", +]; + +export function isSqliteUrl(url: string) { + try { + const urlObj = new URL(url); + return sqliteProtocols.includes(urlObj.protocol); + } catch { + return false; + } +} + +export function parseSqliteUrl( + url: string +): ["any" | "node" | "bs3", string, WrappedDbOptions] { + if (!isSqliteUrl(url)) throw new Error("Not sqlite: protocol"); + const urlObj = new URL(url); + const options: WrappedDbOptions = { + readonly: checkBooleanParam(urlObj, "readonly"), + timeout: checkNumberParam(urlObj, "timeout"), + }; + const kind = + urlObj.protocol === "node+sqlite:" + ? "node" + : urlObj.protocol === "bs3+sqlite:" + ? "bs3" + : "any"; + return [kind, urlObj.pathname, options]; +} diff --git a/src/db/stub.ts b/src/db/stub.ts new file mode 100644 index 0000000..9b3dd27 --- /dev/null +++ b/src/db/stub.ts @@ -0,0 +1,15 @@ +import type { GetDbDynamic, GetDbStatic } from "./types/GetDb.js"; + +const getStub: GetDbStatic & GetDbDynamic = { + name: "stub", + protocols: [], + checkUrl: () => false, + getDbInterface: async () => { + throw new Error("Stub db interface"); + }, + getMigrator: async () => { + throw new Error("Stub db interface"); + }, + getSource: async () => null, +}; +export default getStub; diff --git a/src/db/types/Abode.ts b/src/db/types/Abode.ts new file mode 100644 index 0000000..598f7e9 --- /dev/null +++ b/src/db/types/Abode.ts @@ -0,0 +1,13 @@ +import type { Create, Update } from "./utils.js"; + +export type Abode = { + aid: string; // PK, uuid + name: string; + created_at: string; // ISO datetime + created_by: string | null; // uuid, FK Users.uid + updated_at: string; // ISO datetime + updated_by: string | null; // uuid, FK Users.uid +}; + +export type CreateAbode = Create; +export type UpdateAbode = Update; diff --git a/src/db/types/Apikey.ts b/src/db/types/Apikey.ts new file mode 100644 index 0000000..8478efc --- /dev/null +++ b/src/db/types/Apikey.ts @@ -0,0 +1,25 @@ +import type { Create } from "./utils.js"; + +export type ApikeyPermissions = { + admin?: boolean; + all?: boolean; + users?: "r" | "rw"; + residents?: "r" | "rw"; + abodes?: "r" | "rw"; + restrict_users?: string[]; // uuid, FK users.uid + restrict_abodes?: string[]; // uuid, FK abodes.aid +}; + +export type Apikey = { + uid: string; // uuid, FK users.uid + kid: string; // uuid, PK + token: string; // unique + name: string; + permissions: ApikeyPermissions; + created_at: string; // ISO datetime + expires_at: string | null; // ISO datetime +}; + +export type ClientApikey = Omit; +export type CreateApikey = Create & + Partial>; diff --git a/src/db/types/DbInterface.ts b/src/db/types/DbInterface.ts new file mode 100644 index 0000000..44db796 --- /dev/null +++ b/src/db/types/DbInterface.ts @@ -0,0 +1,108 @@ +import type { Abode, CreateAbode, UpdateAbode } from "./Abode.js"; +import type { ClientApikey, CreateApikey } from "./Apikey.js"; +import type { CreateNote, Note, PartialNote, UpdateNote } from "./Note.js"; +import type { CreateResident, Resident, updateResident } from "./Resident.js"; +import type { + ClientUser, + CreateUser, + LoginUser, + PartialUser, + UpdateUser, +} from "./User.js"; + +export interface DbInterface { + readonly: boolean; + backend: boolean; + name: string; + + close(): Promise; + + // CRUD users + listUsers(): Promise<(PartialUser | ClientUser)[]>; + getUserById(uid: string): Promise; + deleteUserById(uid: string): Promise; + createUser(user: CreateUser): Promise; + updateUser(user: UpdateUser): Promise; + + // get user by other properties + getUserByEmail(email: string): Promise; + + // CRUD abodes + listAbodes(): Promise; + getAbodeById(aid: string): Promise; + deleteAbodeById(aid: string): Promise; + createAbode(abode: CreateAbode, ctx: { uid: string }): Promise; + updateAbode(abode: UpdateAbode, ctx: { uid: string }): Promise; + + // CRUD residents + listResidents(): Promise; + getResidentById(uid: string, aid: string): Promise; + deleteResidentById(uid: string, aid: string): Promise; + createResident( + resident: CreateResident, + ctx: { uid: string } + ): Promise; + updateResident( + resident: updateResident, + ctx: { uid: string } + ): Promise; + + // list residents by member + listResidentsByUserId(uid: string): Promise; + listResidentsByAbodeId(aid: string): Promise; + + // list users/abodes through residents + listUsersByAbodeId(aid: string): Promise<(PartialUser | ClientUser)[]>; + listAbodesByUserId(uid: string): Promise; + + // CRUD notes + listNotes(): Promise; + getNoteById(nid: string): Promise; + deleteNoteById(nid: string): Promise; + createNote(note: CreateNote, ctx: { uid: string }): Promise; + updateNote(note: UpdateNote, ctx: { uid: string }): Promise; + + // list notes by access + listNotesByAbodeId(aid: string): Promise; + listNotesByUserId(uid: string): Promise; + + // auth by session + deleteSessionsByUser(uid: string): Promise; + + // auth by apikey + listApikeysByUser(uid: string): Promise; + getApikeyById(kid: string): Promise; + createApikey(apikey: CreateApikey): Promise<[ClientApikey, `at_${string}`]>; + deleteApikeyById(kid: string): Promise; +} + +export interface BackendDbInterface extends DbInterface { + backend: true; + + // auth by email/password + getUserByLogin(login: LoginUser): Promise; + + // auth by session + getUserBySession(token: `as_${string}`): Promise; + createSession(uid: string): Promise<`as_${string}`>; + + // auth by apikey + getUserByApikey(token: `at_${string}`): Promise<[ClientUser, ClientApikey]>; +} + +export function isBackendInterface(db: DbInterface): db is BackendDbInterface { + return ( + db.backend && + ( + [ + "getUserByLogin", + "getUserBySession", + "createSession", + "getUserByApikey", + ] as const + ).every( + (x) => + x in db && typeof (db as Partial)[x] === "function" + ) + ); +} diff --git a/src/db/types/GetDb.ts b/src/db/types/GetDb.ts new file mode 100644 index 0000000..6676f92 --- /dev/null +++ b/src/db/types/GetDb.ts @@ -0,0 +1,16 @@ +import type { DbInterface } from "./DbInterface.js"; +import type { Migrator } from "./Migrator.js"; + +export interface GetDbStatic { + name: string; + protocols: string[]; + checkUrl(url: string): boolean; + getDbInterface(url: string): Promise; + getMigrator(url: string): Promise; +} + +export interface GetDbDynamic { + name: string; + protocols: string[]; + getSource(url: string): Promise; +} diff --git a/src/db/types/Migrator.ts b/src/db/types/Migrator.ts new file mode 100644 index 0000000..31f7057 --- /dev/null +++ b/src/db/types/Migrator.ts @@ -0,0 +1,16 @@ +export type AppliedMigration = { + id: number; // PK + name: string; + applied_at: string; // isodatetime +}; + +export type AvailableMigration = { + id: number; + name: string; +}; + +export interface Migrator { + listAppliedMigrations(): Promise; + listAvailableMigrations(): AvailableMigration[]; + migrateTo(id: number): Promise; +} diff --git a/src/db/types/Note.ts b/src/db/types/Note.ts new file mode 100644 index 0000000..57ece9f --- /dev/null +++ b/src/db/types/Note.ts @@ -0,0 +1,37 @@ +import type { Create, Update } from "./utils.js"; + +export type NoteType = "note"; + +export type NoteProperties = { + /** + * @default 'note' + */ + type?: NoteType; +}; + +export type PartialNoteProperties = Required>; + +export type Note = { + nid: string; // PK, uuid + aid: string; // uuid, FK Abodes.aid + name: string; + content: string; // markdown + properties: NoteProperties; + created_at: string; // ISO datetime + created_by: string | null; // uuid, FK Users.uid + updated_at: string; // ISO datetime + updated_by: string | null; // uuid, FK Users.uid +}; + +export type PartialNote = Omit & { + properties: PartialNoteProperties; +}; + +export type CreateNote = Create< + Omit & { properties: PartialNoteProperties }, + "nid" +>; +export type UpdateNote = Update< + Omit & { properties: PartialNoteProperties }, + "nid" +>; diff --git a/src/db/types/Resident.ts b/src/db/types/Resident.ts new file mode 100644 index 0000000..e14af3f --- /dev/null +++ b/src/db/types/Resident.ts @@ -0,0 +1,18 @@ +import type { Create, Update } from "./utils.js"; + +export type ResidentFlags = { + admin?: boolean; +}; + +export type Resident = { + uid: string; // PK, uuid, FK User.uid + aid: string; // PK, uuid, FK Abode.aid + flags: ResidentFlags; + created_at: string; // ISO datetime + created_by: string | null; // uuid, FK Users.uid + updated_at: string; // ISO datetime + updated_by: string | null; // uuid, FK Users.uid +}; + +export type CreateResident = Create; +export type updateResident = Update; diff --git a/src/db/types/Session.ts b/src/db/types/Session.ts new file mode 100644 index 0000000..f3da6a2 --- /dev/null +++ b/src/db/types/Session.ts @@ -0,0 +1,7 @@ +export type Session = { + uid: string; // uuid, FK users.uid + token: string; // PK + created_at: string; // ISO datetime + updated_at: string; // ISO datetime + expires_at: string; // ISO datetime +}; diff --git a/src/db/types/User.ts b/src/db/types/User.ts new file mode 100644 index 0000000..8e035f6 --- /dev/null +++ b/src/db/types/User.ts @@ -0,0 +1,37 @@ +import type { Create, Update } from "./utils.js"; + +export type UserFlags = { + admin?: boolean; +}; + +export type User = { + uid: string; // PK, uuid + email: string; // email + name: string; + password: + | `#${"unset"}` // special state + | `$${string}$${string}`; // hashed password + flags: UserFlags; + created_at: string; // ISO datetime + updated_at: string; // ISO datetime +}; + +export type PartialUser = Omit; +export type ClientUser = Omit; +export type CreateUser = Create; +export type UpdateUser = Update; + +export type LoginUser = { + email: string; + password: string; +}; + +export function isValidUserPassword( + password: string +): password is User["password"] { + if (password.startsWith("#")) { + return ["unset"].includes(password.slice(1)); + } else { + return !!password.match(/^\$.+\$.+$/); + } +} diff --git a/src/db/types/errors.ts b/src/db/types/errors.ts new file mode 100644 index 0000000..bd4bb05 --- /dev/null +++ b/src/db/types/errors.ts @@ -0,0 +1,7 @@ +export class AbodeError extends Error {} + +export class NotFoundAbodeError extends AbodeError {} +export class NotAuthorizedAbodeError extends AbodeError {} +export class ConflictAbodeError extends AbodeError {} +export class ReadonlyAbodeError extends AbodeError {} +export class InvalidAbodeError extends AbodeError {} diff --git a/src/db/types/utils.ts b/src/db/types/utils.ts new file mode 100644 index 0000000..a9d110e --- /dev/null +++ b/src/db/types/utils.ts @@ -0,0 +1,4 @@ +export type WithoutMetadata = Omit; +export type Update = Partial> & + Pick; +export type Create = Omit, K>; diff --git a/src/globals.d.ts b/src/globals.d.ts new file mode 100644 index 0000000..b24c4ad --- /dev/null +++ b/src/globals.d.ts @@ -0,0 +1,6 @@ +declare const natives: { + sqlite: string | null; +}; + +declare const compiledSources: string[] & + Omit>, keyof string[]>; diff --git a/src/imports.d.ts b/src/imports.d.ts new file mode 100644 index 0000000..e805d8c --- /dev/null +++ b/src/imports.d.ts @@ -0,0 +1,10 @@ +declare module "*.sql" { + const data: string; + export default data; +} + +declare module "*.schema.json" { + import type { Schema } from "ajv"; + const schema: Schema; + export default schema; +} diff --git a/src/meta/dev/loader.ts b/src/meta/dev/loader.ts new file mode 100644 index 0000000..268c329 --- /dev/null +++ b/src/meta/dev/loader.ts @@ -0,0 +1,18 @@ +import type { LoadHook } from "node:module"; +import { readFile } from "node:fs/promises"; + +export const load: LoadHook = async (url, context, nextLoad) => { + const urlObj = new URL(url); + text: { + if (urlObj.protocol !== "file:") break text; + if (!urlObj.pathname.endsWith(".sql")) break text; + const text = await readFile(urlObj.pathname, "utf8"); + return { + format: "json", + shortCircuit: true, + source: JSON.stringify(text), + }; + } + + return nextLoad(url, context); +}; diff --git a/src/meta/dev/register.ts b/src/meta/dev/register.ts new file mode 100644 index 0000000..9afa3db --- /dev/null +++ b/src/meta/dev/register.ts @@ -0,0 +1,14 @@ +import { register } from "node:module"; +import { findNatives } from "../pack/natives.js"; +import { existingSources } from "../pack/sources.js"; + +register(new URL("./loader.ts", import.meta.url)); + +Object.assign(globalThis, { + natives: await findNatives(), + compiledSources: ["api", "sqlite"].sort(), +}); +for (const source of existingSources) + Object.assign(compiledSources, { + [source]: compiledSources.includes(source), + }); diff --git a/src/meta/dev/restart.ts b/src/meta/dev/restart.ts new file mode 100644 index 0000000..101415d --- /dev/null +++ b/src/meta/dev/restart.ts @@ -0,0 +1,13 @@ +if (import.meta.hot) { + import.meta.hot.on("message", (msg) => { + if ( + msg.includes( + "A pending update was not accepted, and reached the root module:" + ) + ) { + throw new Error("[hot] Restarting due to unaccepted pending update"); + } + }); +} else { + throw new Error("What are you doing?"); +} diff --git a/src/meta/dev/silenthot.ts b/src/meta/dev/silenthot.ts new file mode 100644 index 0000000..ba02762 --- /dev/null +++ b/src/meta/dev/silenthot.ts @@ -0,0 +1,8 @@ +import { register } from "node:module"; + +register("dynohot/loader", { + parentURL: import.meta.url, + data: { + silent: true, + }, +}); diff --git a/src/meta/dev/webhot.ts b/src/meta/dev/webhot.ts new file mode 100644 index 0000000..db577f1 --- /dev/null +++ b/src/meta/dev/webhot.ts @@ -0,0 +1,8 @@ +import { register } from "node:module"; + +register("dynohot/loader", { + parentURL: import.meta.url, + data: { + ignore: /\/node_modules\/|\/schema\//, // it doesn't really seem to enjoy us loading raw schema data + }, +}); diff --git a/src/meta/pack/natives.ts b/src/meta/pack/natives.ts new file mode 100644 index 0000000..19670fd --- /dev/null +++ b/src/meta/pack/natives.ts @@ -0,0 +1,65 @@ +import { readdir, stat } from "node:fs/promises"; +import { join, dirname } from "node:path"; +import { createRequire } from "node:module"; + +async function find(path: string, name: string): Promise { + for (const file of await readdir(path, { withFileTypes: true })) { + if (file.isDirectory()) { + const found = await find(join(file.parentPath, file.name), name); + if (found) return found; + } else if (file.isFile()) { + if (file.name === name) return join(file.parentPath, file.name); + } + } + return null; +} + +async function isFile(path: string): Promise { + try { + const st = await stat(path); + return st.isFile(); + } catch { + return false; + } +} + +async function getPackageJsonDir(path: string): Promise { + if (await isFile(join(path, "package.json"))) return path; + const next = dirname(path); + if (next === path) return null; + return getPackageJsonDir(next); +} + +export async function findNative( + module: string, + native: string +): Promise { + const path = await getPackageJsonDir( + createRequire(import.meta.url).resolve(module) + ); + if (!path) throw new Error(`Cannot find module directory for ${module}`); + const file = await find(path, native); + if (!file) + throw new Error( + `Cannot find native ${native} of package ${module} in ${path}` + ); + return file; +} + +export async function tryFindNative( + module: string, + native: string +): Promise { + try { + return await findNative(module, native); + } catch (e) { + console.warn(e); + return null; + } +} + +export async function findNatives(): Promise { + return { + sqlite: await tryFindNative("better-sqlite3", "better_sqlite3.node"), + }; +} diff --git a/src/meta/pack/sources.ts b/src/meta/pack/sources.ts new file mode 100644 index 0000000..b73a410 --- /dev/null +++ b/src/meta/pack/sources.ts @@ -0,0 +1 @@ +export const existingSources: string[] = ["api", "sqlite"]; diff --git a/src/meta/pack/valLoader.ts b/src/meta/pack/valLoader.ts new file mode 100644 index 0000000..02286d9 --- /dev/null +++ b/src/meta/pack/valLoader.ts @@ -0,0 +1,4 @@ +export default async function loader({ loader }: { loader: string }) { + const { webpack } = await import(loader); + return await webpack(); +} diff --git a/src/meta/pack/validators.ts b/src/meta/pack/validators.ts new file mode 100644 index 0000000..0baccf8 --- /dev/null +++ b/src/meta/pack/validators.ts @@ -0,0 +1,36 @@ +import * as schemas from "../../schema/schemas.js"; +import { createAjv, loadSchemas } from "../../schema/ajv.js"; +// @ts-expect-error no type for us apparently +import standaloneCode from "ajv/dist/standalone"; + +// this code generator overrides the code from @/schema/validators.ts +// runs at build time when compiling with webpack + +export async function webpack(): Promise<{ code: string }> { + const validator = createAjv({ + code: { + source: true, + esm: true, + // we want to optimize as much as possible and don't care how long it takes + optimize: 2, + }, + }); + loadSchemas(validator); + + // this is halfway to magical because it's not typed for some reason + // it seems to output schema code but not all of it is used, and it doesn't assign .schema + let code: string = standaloneCode( + validator, + Object.fromEntries( + Object.entries(schemas).map(([id, schema]) => [id, schema.$id]) + ) + ); + + // assign the .schema ourselves to the validation functions + // this is potentially wasteful, some of it is already in the output but we can't access it + for (const [name, schema] of Object.entries(schemas)) { + code += `;Object.assign(${name},{schema:${JSON.stringify(schema)}})`; + } + + return { code }; +} diff --git a/src/react/contexts/Db.tsx b/src/react/contexts/Db.tsx new file mode 100644 index 0000000..9bce78d --- /dev/null +++ b/src/react/contexts/Db.tsx @@ -0,0 +1,26 @@ +import { createContext, useState, type ReactNode } from "react"; +import type { DbInterface } from "../../db/types/DbInterface.js"; + +export const DbContext = createContext(null); +DbContext.displayName = "DbContext"; + +export const SetDbContext = createContext< + ((db: DbInterface | null) => void) | null +>(null); +SetDbContext.displayName = "SetDbContext"; + +export function DbProvider({ + children, + db: initialDb = null, +}: { + children: ReactNode; + db?: DbInterface | null; +}) { + const [db, setDb] = useState(initialDb); + + return ( + + {children} + + ); +} diff --git a/src/react/contexts/PopupManager.tsx b/src/react/contexts/PopupManager.tsx new file mode 100644 index 0000000..d87d953 --- /dev/null +++ b/src/react/contexts/PopupManager.tsx @@ -0,0 +1,66 @@ +import { + createContext, + useCallback, + useMemo, + useState, + type ComponentType, + type ReactNode, +} from "react"; +import { idAssert } from "../../util/ts.js"; + +export interface PopupManagerContextData { + openPopup(popup: ComponentType<{ id: string; onClose: () => void }>): string; + openPopup( + popup: ComponentType<{ id: string; onClose: () => void } & T>, + props: T + ): string; + + closePopup(id: string): void; +} + +export const PopupManagerContext = + createContext(null); +PopupManagerContext.displayName = "PopupManagerContext"; + +type Popup = { + id: string; + Component: ComponentType<{ id: string; onClose: () => void }>; + props: { id: string; onClose: () => void }; +}; + +export function PopupManager({ children }: { children: ReactNode }) { + const [popups, setPopups] = useState([]); + + const openPopup = useCallback( + ( + Component: ComponentType<{ id: string; onClose: () => void }>, + props = {} + ) => { + const id = crypto.randomUUID(); + Object.assign(props, { + id, + onClose: () => setPopups((prev) => prev.filter((x) => x.id !== id)), + }); + idAssert<{ id: string; onClose: () => void }>(props); + setPopups((prev) => [...prev, { id, Component, props }]); + return id; + }, + [] + ); + const closePopup = useCallback((id: string) => { + setPopups((prev) => prev.filter((x) => x.id !== id)); + }, []); + const ctx = useMemo( + () => ({ openPopup, closePopup }), + [] + ); + + return ( + + {children} + {popups.map((popup) => ( + + ))} + + ); +} diff --git a/src/react/hooks/data/abodes.ts b/src/react/hooks/data/abodes.ts new file mode 100644 index 0000000..79f0a74 --- /dev/null +++ b/src/react/hooks/data/abodes.ts @@ -0,0 +1,16 @@ +import { loadAbodeById, loadAllAbodes } from "../../store/loaders/abodes.js"; +import { useSelector } from "../../store/react.js"; +import { getAbode, getAbodes } from "../../store/slices/abodes.js"; +import { useLoad } from "../useLoad.js"; + +export function useDataAllAbodes() { + const abodes = useSelector(getAbodes); + const status = useLoad(loadAllAbodes); + return { ...status, abodes }; +} + +export function useDataAbodeById(aid: string) { + const abode = useSelector((state) => getAbode(state, aid)); + const status = useLoad(loadAbodeById, { aid }); + return { ...status, abode }; +} diff --git a/src/react/hooks/data/residents.ts b/src/react/hooks/data/residents.ts new file mode 100644 index 0000000..d58a3cb --- /dev/null +++ b/src/react/hooks/data/residents.ts @@ -0,0 +1,35 @@ +import { useMemo } from "react"; +import { + loadAllResidents, + loadResidentsByAbodeId, + loadResidentsByUserId, +} from "../../store/loaders/residents.js"; +import { useSelector } from "../../store/react.js"; +import { getResidents } from "../../store/slices/residents.js"; +import { useLoad } from "../useLoad.js"; + +export function useDataAllResidents() { + const residents = useSelector(getResidents); + const status = useLoad(loadAllResidents); + return { ...status, residents }; +} + +export function useDataResidentsByAbodeId(aid: string) { + const allResidents = useSelector(getResidents); + const status = useLoad(loadResidentsByAbodeId, { aid }); + const residents = useMemo( + () => Object.values(allResidents).filter((x) => x.aid === aid), + [allResidents, aid] + ); + return { ...status, residents }; +} + +export function useDataResidentsByUserId(uid: string) { + const allResidents = useSelector(getResidents); + const status = useLoad(loadResidentsByUserId, { uid }); + const residents = useMemo( + () => Object.values(allResidents).filter((x) => x.uid === uid), + [allResidents, uid] + ); + return { ...status, residents }; +} diff --git a/src/react/hooks/data/users.ts b/src/react/hooks/data/users.ts new file mode 100644 index 0000000..a726077 --- /dev/null +++ b/src/react/hooks/data/users.ts @@ -0,0 +1,26 @@ +import { + loadAllUsers, + loadUserByEmail, + loadUserById, +} from "../../store/loaders/users.js"; +import { useSelector } from "../../store/react.js"; +import { getUser, getUserByEmail, getUsers } from "../../store/slices/users.js"; +import { useLoad } from "../useLoad.js"; + +export function useDataAllUsers() { + const users = useSelector(getUsers); + const status = useLoad(loadAllUsers); + return { ...status, users }; +} + +export function useDataUserById(uid: string) { + const user = useSelector((state) => getUser(state, uid)); + const status = useLoad(loadUserById, { uid }); + return { ...status, user }; +} + +export function useDataUserByEmail(email: string) { + const user = useSelector((state) => getUserByEmail(state, email)); + const status = useLoad(loadUserByEmail, { email }); + return { ...status, user }; +} diff --git a/src/react/hooks/useAction.ts b/src/react/hooks/useAction.ts new file mode 100644 index 0000000..d5dde48 --- /dev/null +++ b/src/react/hooks/useAction.ts @@ -0,0 +1,20 @@ +import { use, useCallback } from "react"; +import { DbContext } from "../contexts/Db.js"; +import type { DbInterface } from "../../db/types/DbInterface.js"; +import type { Store } from "../store/store.js"; +import { useStore } from "../store/react.js"; + +export function useAction

( + action: (...params: [...P, { db: DbInterface; store: Store }]) => Promise +): (...args: P) => Promise { + const db = use(DbContext); + const store = useStore(); + + return useCallback( + async (...params: P) => { + if (!db) throw new Error("DB not present"); + return action(...params, { db, store }); + }, + [action, db] + ); +} diff --git a/src/react/hooks/useLoad.ts b/src/react/hooks/useLoad.ts new file mode 100644 index 0000000..b7fcb52 --- /dev/null +++ b/src/react/hooks/useLoad.ts @@ -0,0 +1,42 @@ +import { use, useCallback, useEffect } from "react"; +import { load, type Loader } from "../store/load.js"; +import { useSelector, useStore } from "../store/react.js"; +import { getLoading, type LoadingState } from "../store/slices/loading.js"; +import { DbContext } from "../contexts/Db.js"; + +function selectTrue() { + return true; +} + +export type UseLoadResult = (LoadingState | { status: "pending" }) & { + refresh: () => Promise; +}; + +export function useLoad

(loader: Loader

, params: P): UseLoadResult; +export function useLoad(loader: Loader): UseLoadResult; +export function useLoad

(loader: Loader

, params?: P): UseLoadResult { + const id = loader.id(params!); + const state = useSelector((state) => getLoading(state, id)); + + const db = use(DbContext); + const store = useStore(); + + const condition = loader.condition ?? selectTrue; + const meetsCondition = useSelector((state) => condition(params!, state)); + const meetsDb = !!db || !loader.requiresDb; + + useEffect(() => { + if (!meetsCondition || !meetsDb) return; + void load({ loader, params: params!, store, db }); + }, [loader, params, db, meetsCondition, meetsDb]); + + const refresh = useCallback( + () => load({ loader, params: params!, store, db, refresh: true }), + [loader, params, db] + ); + + return { + ...(state ?? { status: "pending" }), + refresh, + }; +} diff --git a/src/react/store/actions/abodes.ts b/src/react/store/actions/abodes.ts new file mode 100644 index 0000000..a25b228 --- /dev/null +++ b/src/react/store/actions/abodes.ts @@ -0,0 +1,50 @@ +import type { CreateAbode, UpdateAbode } from "../../../db/types/Abode.js"; +import type { DbInterface } from "../../../db/types/DbInterface.js"; +import { waitForLoadIfLoading } from "../load.js"; +import { delAbode, getAbode, setAbode } from "../slices/abodes.js"; +import { clearLoading } from "../slices/loading.js"; +import { getLoginUser } from "../slices/login.js"; +import type { Store } from "../store.js"; + +export async function deleteAbodeById( + aid: string, + { store, db }: { store: Store; db: DbInterface } +): Promise { + await Promise.all([ + waitForLoadIfLoading(store, "loadAllAbodes"), + waitForLoadIfLoading(store, `loadAbodeById:${aid}`), + ]); + + await db.deleteAbodeById(aid); + store.dispatch(delAbode(aid)); + store.dispatch(clearLoading(`loadAbodeById:${aid}`)); +} + +export async function updateAbode( + abode: UpdateAbode, + { store, db }: { store: Store; db: DbInterface } +): Promise { + await Promise.all([ + waitForLoadIfLoading(store, "loadAllAbodes"), + waitForLoadIfLoading(store, `loadAbodeById:${abode.aid}`), + ]); + + const user = getLoginUser(store.getState()); + if (!user) throw new Error("Not logged in"); + const next = await db.updateAbode(abode, { uid: user.uid }); + store.dispatch(setAbode(next)); + store.dispatch(clearLoading(`loadAbodeById:${abode.aid}`)); +} + +export async function createAbode( + abode: CreateAbode, + { store, db }: { store: Store; db: DbInterface } +): Promise { + await waitForLoadIfLoading(store, "loadAllAbodes"); + + const user = getLoginUser(store.getState()); + if (!user) throw new Error("Not logged in"); + const next = await db.createAbode(abode, { uid: user.uid }); + store.dispatch(setAbode(next)); + return next.aid; +} diff --git a/src/react/store/actions/clearAll.ts b/src/react/store/actions/clearAll.ts new file mode 100644 index 0000000..0e78bf8 --- /dev/null +++ b/src/react/store/actions/clearAll.ts @@ -0,0 +1,3 @@ +import { createAction } from "@reduxjs/toolkit"; + +export const clearAll = createAction("store/clear"); diff --git a/src/react/store/actions/users.ts b/src/react/store/actions/users.ts new file mode 100644 index 0000000..ab5c22a --- /dev/null +++ b/src/react/store/actions/users.ts @@ -0,0 +1,51 @@ +import type { DbInterface } from "../../../db/types/DbInterface.js"; +import type { CreateUser, UpdateUser } from "../../../db/types/User.js"; +import { waitForLoadIfLoading } from "../load.js"; +import { clearLoading } from "../slices/loading.js"; +import { delUser, getUser, setUser } from "../slices/users.js"; +import type { Store } from "../store.js"; + +export async function deleteUserById( + uid: string, + { store, db }: { store: Store; db: DbInterface } +): Promise { + await Promise.all([ + waitForLoadIfLoading(store, "loadAllUsers"), + waitForLoadIfLoading(store, `loadUserById:${uid}`), + ]); + + const user = getUser(store.getState(), uid); + await db.deleteUserById(uid); + store.dispatch(delUser(uid)); + store.dispatch(clearLoading(`loadUserById:${uid}`)); + if (user && "email" in user) + store.dispatch(clearLoading(`loadUserByEmail:${user.email}`)); +} + +export async function updateUser( + user: UpdateUser, + { store, db }: { store: Store; db: DbInterface } +): Promise { + await Promise.all([ + waitForLoadIfLoading(store, "loadAllUsers"), + waitForLoadIfLoading(store, `loadUserById:${user.uid}`), + ]); + + const current = getUser(store.getState(), user.uid); + const next = await db.updateUser(user); + store.dispatch(setUser(next)); + store.dispatch(clearLoading(`loadUserById:${user.uid}`)); + if (current && "email" in current) + store.dispatch(clearLoading(`loadUserByEmail:${current.email}`)); +} + +export async function createUser( + user: CreateUser, + { store, db }: { store: Store; db: DbInterface } +): Promise { + await waitForLoadIfLoading(store, "loadAllUsers"); + + const next = await db.createUser(user); + store.dispatch(setUser(next)); + return next.uid; +} diff --git a/src/react/store/load.ts b/src/react/store/load.ts new file mode 100644 index 0000000..c898bd2 --- /dev/null +++ b/src/react/store/load.ts @@ -0,0 +1,128 @@ +import type { DbInterface } from "../../db/types/DbInterface.js"; +import { objectError } from "../../util/error.js"; +import { getLoading, getLoadingStatus, setLoading } from "./slices/loading.js"; +import type { Action, State, Store } from "./store.js"; +import { waitFor } from "./utils.js"; + +export type LoadApi = { + signal: AbortSignal; + store: Store; + db: DbInterface | null; + waitFor: (id: string) => Promise; +}; + +export type Loader

= { + type: string; + id: (params: P) => string; + load: (params: P, api: LoadApi) => Promise; + requiresDb?: boolean; + condition?: (params: P, state: State) => boolean; +}; + +async function loadImpl

({ + loader, + params, + store, + db, + refresh = false, +}: { + loader: Loader

; + params: P; + store: Store; + db: DbInterface | null; + refresh?: boolean; +}): Promise { + const id = loader.id(params); + const { type } = loader; + const state = store.getState(); + const status = getLoadingStatus(state, id); + if (status === "loaded" && !refresh) return; + if (status === "loading") { + return waitFor(store, (state) => { + const status = getLoadingStatus(state, id); + return status === "loaded" || status === "error"; + }); + } + + store.dispatch( + setLoading([ + id, + { status: refresh ? "refreshing" : "loading", type, params }, + ]) + ); + const controller = new AbortController(); + try { + if (loader.requiresDb && !db) + throw new Error("Loader requires DB and it was not ready"); + if (loader.condition && !loader.condition(params, state)) + throw new Error("Loader has an unmet condition"); + + const result = await loader.load(params, { + db, + store, + signal: controller.signal, + waitFor: (id) => + waitForLoadIfLoading(store, id, { signal: controller.signal }), + }); + const actions = Array.isArray(result) ? result : result ? [result] : []; + for (const action of actions) store.dispatch(action); + store.dispatch(setLoading([id, { status: "loaded", type, params }])); + } catch (e) { + store.dispatch( + setLoading([id, { status: "error", type, params, error: objectError(e) }]) + ); + throw e; + } +} + +export const loaders = new Map>(); + +export function load

(props: { + loader: Loader

; + params: P; + store: Store; + db: DbInterface | null; + refresh?: boolean; +}): Promise { + const { loader } = props; + if (!loaders.has(loader.type)) loaders.set(loader.type, loader); + return loadImpl(props).catch(() => void 0); +} + +export async function refresh({ + id, + store, + db, +}: { + id: string; + store: Store; + db: DbInterface | null; +}): Promise { + const state = getLoading(store.getState(), id); + if (state?.status !== "loaded" && state?.status !== "error") + throw new Error(`Cannot refresh while in ${state?.status ?? "none"} state`); + const loader = loaders.get(state.type); + if (!loader) + throw new Error(`Loader ${state.type} not known, cannot refresh`); + return loadImpl({ loader, params: state.params, store, db, refresh: true }); +} + +export function loader

(loader: Loader

): Loader

{ + return loader; +} + +export async function waitForLoadIfLoading( + store: Store, + id: string, + { signal }: { signal?: AbortSignal } = {} +) { + if (!getLoadingStatus(store.getState(), id)) return; + return waitFor( + store, + (state) => { + const status = getLoadingStatus(state, id); + return status === "loaded" || status === "error"; + }, + { signal } + ); +} diff --git a/src/react/store/loaders/abodes.ts b/src/react/store/loaders/abodes.ts new file mode 100644 index 0000000..b428bc6 --- /dev/null +++ b/src/react/store/loaders/abodes.ts @@ -0,0 +1,24 @@ +import { loader } from "../load.js"; +import { addAbodes, getAbode, setAbode } from "../slices/abodes.js"; + +export const loadAllAbodes = loader({ + type: "loadAllAbodes", + id: () => "loadAllAbodes", + requiresDb: true, + async load(_, { db }) { + const abodes = await db!.listAbodes(); + return addAbodes(abodes); + }, +}); + +export const loadAbodeById = loader({ + type: "loadAbodeById", + id: ({ aid }: { aid: string }) => `loadAbodeById:${aid}`, + requiresDb: true, + async load({ aid }, { db, store, waitFor }) { + await waitFor("loadAllAbodes"); + if (getAbode(store.getState(), aid)) return; + const abode = await db!.getAbodeById(aid); + return setAbode(abode); + }, +}); diff --git a/src/react/store/loaders/residents.ts b/src/react/store/loaders/residents.ts new file mode 100644 index 0000000..53c1e27 --- /dev/null +++ b/src/react/store/loaders/residents.ts @@ -0,0 +1,39 @@ +import { loader } from "../load.js"; +import { getLoadingStatus } from "../slices/loading.js"; +import { addResidents } from "../slices/residents.js"; + +export const loadAllResidents = loader({ + type: "loadAllResidents", + id: () => "loadAllResidents", + requiresDb: true, + async load(_, { db }) { + const residents = await db!.listResidents(); + return addResidents(residents); + }, +}); + +export const loadResidentsByUserId = loader<{ uid: string }>({ + type: "loadResidentsByUserId", + id: ({ uid }) => `loadResidentsByUserId:${uid}`, + requiresDb: true, + async load({ uid }, { db, store, waitFor }) { + await waitFor("loadAllResidents"); + if (getLoadingStatus(store.getState(), "loadAllResidents") === "loaded") + return; + const residents = await db!.listResidentsByUserId(uid); + return addResidents(residents); + }, +}); + +export const loadResidentsByAbodeId = loader<{ aid: string }>({ + type: "loadResidentsByAbodeId", + id: ({ aid }) => `loadResidentsByAbodeId:${aid}`, + requiresDb: true, + async load({ aid }, { db, store, waitFor }) { + await waitFor("loadAllResidents"); + if (getLoadingStatus(store.getState(), "loadAllResidents") === "loaded") + return; + const residents = await db!.listResidentsByAbodeId(aid); + return addResidents(residents); + }, +}); diff --git a/src/react/store/loaders/users.ts b/src/react/store/loaders/users.ts new file mode 100644 index 0000000..5a2749f --- /dev/null +++ b/src/react/store/loaders/users.ts @@ -0,0 +1,38 @@ +import { loader } from "../load.js"; +import { addUsers, getUser, getUsers, setUser } from "../slices/users.js"; + +export const loadAllUsers = loader({ + type: "loadAllUsers", + id: () => "loadAllUsers", + requiresDb: true, + async load(_, { db }) { + const users = await db!.listUsers(); + return addUsers(users); + }, +}); + +export const loadUserById = loader({ + type: "loadUserById", + id: ({ uid }: { uid: string }) => `loadUserById:${uid}`, + requiresDb: true, + async load({ uid }, { db, store, waitFor }) { + await waitFor("loadAllUsers"); + if (getUser(store.getState(), uid)) return; + const user = await db!.getUserById(uid); + return setUser(user); + }, +}); + +export const loadUserByEmail = loader({ + type: "loadUserByEmail", + id: ({ email }: { email: string }) => `loadUserByEmail:${email}`, + requiresDb: true, + async load({ email }, { db, store, waitFor }) { + await waitFor("loadAllUsers"); + const users = getUsers(store.getState()); + if (Object.values(users).some((x) => "email" in x && x.email === email)) + return; + const user = await db!.getUserByEmail(email); + return setUser({ email, ...user }); + }, +}); diff --git a/src/react/store/react.tsx b/src/react/store/react.tsx new file mode 100644 index 0000000..abfc715 --- /dev/null +++ b/src/react/store/react.tsx @@ -0,0 +1,28 @@ +import { + createDispatchHook, + createSelectorHook, + createStoreHook, + Provider as RawProvider, + type ProviderProps, + type ReactReduxContextValue, +} from "react-redux"; +import type { Store, State } from "./store.js"; +import { createContext } from "react"; + +const AbodeStoreContext = createContext(null); +AbodeStoreContext.displayName = "AbodeStoreContext"; + +export function Provider( + props: Omit & { + store: Store; + serverState?: State; + } +) { + return ; +} +Object.assign(Provider, { displayName: "AbodeStoreProvider" }); +export const useSelector = + createSelectorHook(AbodeStoreContext).withTypes(); +export const useStore = createStoreHook(AbodeStoreContext).withTypes(); +export const useDispatch = + createDispatchHook(AbodeStoreContext).withTypes(); diff --git a/src/react/store/slices/abodes.ts b/src/react/store/slices/abodes.ts new file mode 100644 index 0000000..722411a --- /dev/null +++ b/src/react/store/slices/abodes.ts @@ -0,0 +1,42 @@ +import { + createEntityAdapter, + createSlice, + type WithSlice, +} from "@reduxjs/toolkit"; +import { reducer } from "../store.js"; +import { clearAll } from "../actions/clearAll.js"; +import type { Abode } from "../../../db/types/Abode.js"; + +const abodesAdapter = createEntityAdapter({ + selectId: (abode: Abode) => abode.aid, +}); +const abodesSelectors = abodesAdapter.getSelectors(); + +const abodesSlice = createSlice({ + name: "abodes", + initialState: abodesAdapter.getInitialState(), + selectors: { + getAbode: abodesSelectors.selectById, + getAbodes: abodesSelectors.selectEntities, + getAbodeIds: abodesSelectors.selectIds, + }, + reducers: { + setAbode: abodesAdapter.setOne, + addAbodes: abodesAdapter.addMany, + delAbode: abodesAdapter.removeOne, + clearAbodes: abodesAdapter.removeAll, + }, + extraReducers(builder) { + builder.addCase(clearAll, () => abodesAdapter.getInitialState()); + }, +}); + +declare module "../store.js" { + export interface LazySlices extends WithSlice {} +} + +export const { + selectors: { getAbode, getAbodes, getAbodeIds }, + actions: { setAbode, addAbodes, delAbode, clearAbodes }, + selectSlice: selectAbodes, +} = abodesSlice.injectInto(reducer); diff --git a/src/react/store/slices/loading.ts b/src/react/store/slices/loading.ts new file mode 100644 index 0000000..65f7e4f --- /dev/null +++ b/src/react/store/slices/loading.ts @@ -0,0 +1,64 @@ +import { + createSlice, + type PayloadAction, + type WithSlice, +} from "@reduxjs/toolkit"; +import { clearAll } from "../actions/clearAll.js"; +import { reducer } from "../store.js"; +import type { ObjectError } from "../../../util/error.js"; + +export type LoadingSlice = Record; +export type LoadingState = ( + | { + status: "loading"; + } + | { + status: "refreshing"; + } + | { + status: "error"; + error: ObjectError; + } + | { + status: "loaded"; + } +) & { + type: string; + params: unknown; +}; + +const initialLoadingSlice: LoadingSlice = {}; + +const loadingSlice = createSlice({ + name: "loading", + initialState: initialLoadingSlice, + selectors: { + getLoading: (state, id: string): LoadingState | null => state[id] ?? null, + getLoadingStatus: (state, id: string): LoadingState["status"] | null => + state[id]?.status ?? null, + }, + reducers: { + setLoading: ( + state, + action: PayloadAction<[id: string, state: LoadingState]> + ) => { + state[action.payload[0]] = action.payload[1]; + }, + clearLoading: (state, action: PayloadAction) => { + delete state[action.payload]; + }, + }, + extraReducers(builder) { + builder.addCase(clearAll, () => initialLoadingSlice); + }, +}); + +declare module "../store.js" { + export interface LazySlices extends WithSlice {} +} + +export const { + selectors: { getLoading, getLoadingStatus }, + actions: { setLoading, clearLoading }, + selectSlice: selectLoading, +} = loadingSlice.injectInto(reducer); diff --git a/src/react/store/slices/login.ts b/src/react/store/slices/login.ts new file mode 100644 index 0000000..5b32bea --- /dev/null +++ b/src/react/store/slices/login.ts @@ -0,0 +1,43 @@ +import { + createSlice, + type PayloadAction, + type WithSlice, +} from "@reduxjs/toolkit"; +import type { ClientUser } from "../../../db/types/User.js"; +import { clearAll } from "../actions/clearAll.js"; +import { reducer } from "../store.js"; + +export type LoginSlice = { + user?: ClientUser; +}; + +const initialLoginSlice: LoginSlice = {}; + +const loginSlice = createSlice({ + name: "login", + initialState: initialLoginSlice, + selectors: { + getLoginUser: (state) => state.user, + isLoggedIn: (state) => !!state.user, + }, + reducers: { + setLoginUser: (state, action: PayloadAction) => { + state.user = action.payload; + }, + logOut: (state) => { + delete state.user; + }, + }, + extraReducers(builder) { + builder.addCase(clearAll, () => initialLoginSlice); + }, +}); + +declare module "../store.js" { + export interface LazySlices extends WithSlice {} +} + +export const { + selectors: { getLoginUser, isLoggedIn }, + actions: { setLoginUser, logOut }, +} = loginSlice.injectInto(reducer); diff --git a/src/react/store/slices/residents.ts b/src/react/store/slices/residents.ts new file mode 100644 index 0000000..ad2d4e8 --- /dev/null +++ b/src/react/store/slices/residents.ts @@ -0,0 +1,42 @@ +import { + createEntityAdapter, + createSlice, + type WithSlice, +} from "@reduxjs/toolkit"; +import { reducer } from "../store.js"; +import { clearAll } from "../actions/clearAll.js"; +import type { Resident } from "../../../db/types/Resident.js"; + +const residentsAdapter = createEntityAdapter({ + selectId: (resident: Resident) => resident.uid + "/" + resident.aid, +}); +const residentsSelectors = residentsAdapter.getSelectors(); + +const residentsSlice = createSlice({ + name: "residents", + initialState: residentsAdapter.getInitialState(), + selectors: { + getResident: (state, uid: string, aid: string) => + residentsSelectors.selectById(state, uid + "/" + aid), + getResidents: residentsSelectors.selectEntities, + }, + reducers: { + setResident: residentsAdapter.setOne, + addResidents: residentsAdapter.addMany, + delResident: residentsAdapter.removeOne, + clearResidents: residentsAdapter.removeAll, + }, + extraReducers(builder) { + builder.addCase(clearAll, () => residentsAdapter.getInitialState()); + }, +}); + +declare module "../store.js" { + export interface LazySlices extends WithSlice {} +} + +export const { + selectors: { getResident, getResidents }, + actions: { setResident, addResidents, delResident, clearResidents }, + selectSlice: selectResidents, +} = residentsSlice.injectInto(reducer); diff --git a/src/react/store/slices/users.ts b/src/react/store/slices/users.ts new file mode 100644 index 0000000..ee36e2d --- /dev/null +++ b/src/react/store/slices/users.ts @@ -0,0 +1,46 @@ +import { + createEntityAdapter, + createSlice, + type WithSlice, +} from "@reduxjs/toolkit"; +import type { ClientUser, PartialUser } from "../../../db/types/User.js"; +import { reducer } from "../store.js"; +import { clearAll } from "../actions/clearAll.js"; + +const usersAdapter = createEntityAdapter({ + selectId: (user: ClientUser | PartialUser) => user.uid, +}); +const usersSelectors = usersAdapter.getSelectors(); + +const usersSlice = createSlice({ + name: "users", + initialState: usersAdapter.getInitialState(), + selectors: { + getUser: usersSelectors.selectById, + getUserByEmail: (state, email: string) => + Object.values(state.entities).find( + (x): x is ClientUser => "email" in x && x.email === email + ), + getUsers: usersSelectors.selectEntities, + getUserIds: usersSelectors.selectIds, + }, + reducers: { + setUser: usersAdapter.setOne, + addUsers: usersAdapter.addMany, + delUser: usersAdapter.removeOne, + clearUsers: usersAdapter.removeAll, + }, + extraReducers(builder) { + builder.addCase(clearAll, () => usersAdapter.getInitialState()); + }, +}); + +declare module "../store.js" { + export interface LazySlices extends WithSlice {} +} + +export const { + selectors: { getUser, getUserByEmail, getUsers, getUserIds }, + actions: { setUser, addUsers, delUser, clearUsers }, + selectSlice: selectUsers, +} = usersSlice.injectInto(reducer); diff --git a/src/react/store/store.ts b/src/react/store/store.ts new file mode 100644 index 0000000..8bef15b --- /dev/null +++ b/src/react/store/store.ts @@ -0,0 +1,15 @@ +import { combineSlices, configureStore } from "@reduxjs/toolkit"; + +export interface LazySlices {} + +export const reducer = combineSlices().withLazyLoadedSlices(); +export type State = ReturnType; + +export function createStore({ + preloadedState, +}: { preloadedState?: State } = {}) { + return configureStore({ reducer, preloadedState, devTools: true }); +} +export type Store = ReturnType; +export type Dispatch = Store["dispatch"]; +export type Action = Parameters[0]; diff --git a/src/react/store/utils.ts b/src/react/store/utils.ts new file mode 100644 index 0000000..7bcc1c2 --- /dev/null +++ b/src/react/store/utils.ts @@ -0,0 +1,34 @@ +import type { State, Store } from "./store.js"; + +export async function waitFor( + store: Store, + cond: (state: State) => boolean, + { signal }: { signal?: AbortSignal } = {} +): Promise { + return new Promise((ok, ko) => { + signal?.throwIfAborted(); + + const controller = new AbortController(); + controller.signal.addEventListener("abort", () => { + ko(new Error("Aborted waitFor")); + }); + + const check = () => { + const state = store.getState(); + if (!cond(state)) return false; + ok(); + controller.abort(); + return true; + }; + if (check()) return; + controller.signal.addEventListener("abort", store.subscribe(check)); + + signal?.addEventListener( + "abort", + () => { + controller.abort(); + }, + { signal: controller.signal } + ); + }); +} diff --git a/src/schema/abode/abode.schema.json b/src/schema/abode/abode.schema.json new file mode 100644 index 0000000..11cc3b1 --- /dev/null +++ b/src/schema/abode/abode.schema.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Abode", + "description": "Abode object", + "$id": "https://abode.codi.moe/schema/abode.schema.json", + "type": "object", + "required": [ + "aid", + "name", + "created_at", + "created_by", + "updated_at", + "updated_by" + ], + "additionalProperties": false, + "properties": { + "aid": { + "description": "Abode ID (UUID)", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "Abode name", + "type": "string", + "minLength": 1 + }, + "created_at": { + "description": "Abode creation date (ISO 8601 datetime with TZ)", + "type": "string", + "format": "date-time" + }, + "created_by": { + "description": "Abode creating user (UUID)", + "type": ["string", "null"], + "format": "uuid" + }, + "updated_at": { + "description": "Abode modification date (ISO 8601 datetime with TZ)", + "type": "string", + "format": "date-time" + }, + "updated_by": { + "description": "Abode modifying user (UUID)", + "type": ["string", "null"], + "format": "uuid" + } + } +} diff --git a/src/schema/abode/createabode.schema.json b/src/schema/abode/createabode.schema.json new file mode 100644 index 0000000..a8977fb --- /dev/null +++ b/src/schema/abode/createabode.schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Create", + "description": "Create Abode object", + "$id": "https://abode.codi.moe/schema/createabode.schema.json", + "type": "object", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "description": "Abode name", + "type": "string", + "minLength": 1 + } + } +} diff --git a/src/schema/abode/updateabode.schema.json b/src/schema/abode/updateabode.schema.json new file mode 100644 index 0000000..07f8bf4 --- /dev/null +++ b/src/schema/abode/updateabode.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Update", + "description": "Update Abode object", + "$id": "https://abode.codi.moe/schema/updateabode.schema.json", + "type": "object", + "required": ["aid"], + "additionalProperties": false, + "properties": { + "aid": { + "description": "Abode ID (UUID)", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "Abode name", + "type": "string", + "minLength": 1 + } + } +} diff --git a/src/schema/ajv.ts b/src/schema/ajv.ts new file mode 100644 index 0000000..df7e3f1 --- /dev/null +++ b/src/schema/ajv.ts @@ -0,0 +1,18 @@ +import { Ajv } from "ajv"; +import ajvFormats from "ajv-formats"; +import * as schemas from "./schemas.js"; + +export function createAjv(options: ConstructorParameters[0]): Ajv { + const ajv = new Ajv({ strict: true, ...options }); + (ajvFormats as unknown as (ajv: Ajv) => void)(ajv); + return ajv; +} + +export function loadSchemas(ajv: Ajv): void { + for (const schema of Object.values(schemas)) { + ajv.addSchema(schema); + } + for (const schema of Object.values(schemas)) { + ajv.validateSchema(schema); + } +} diff --git a/src/schema/apikey/apikeypermissions.schema.json b/src/schema/apikey/apikeypermissions.schema.json new file mode 100644 index 0000000..7817fd6 --- /dev/null +++ b/src/schema/apikey/apikeypermissions.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "ApikeyPermissions", + "description": "Apikey permissions", + "$id": "https://abode.codi.moe/schema/apikeypermissions.schema.json", + "type": "object", + "required": [], + "additionalProperties": false, + "properties": { + "admin": { + "type": "boolean" + }, + "all": { + "type": "boolean" + }, + "users": { + "enum": ["r", "rw"] + }, + "residents": { + "enum": ["r", "rw"] + }, + "abodes": { + "enum": ["r", "rw"] + }, + "restrict_users": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "restrict_abodes": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + } + } +} diff --git a/src/schema/apikey/createapikey.schema.json b/src/schema/apikey/createapikey.schema.json new file mode 100644 index 0000000..1683b02 --- /dev/null +++ b/src/schema/apikey/createapikey.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Create", + "description": "Create Apikey", + "$id": "https://abode.codi.moe/schema/createapikey.schema.json", + "type": "object", + "required": ["uid", "name", "permissions"], + "additionalProperties": false, + "properties": { + "uid": { + "description": "User ID (UUID)", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "Apikey name", + "type": "string" + }, + "permissions": { + "description": "Apikey permissions", + "$ref": "https://abode.codi.moe/schema/apikeypermissions.schema.json" + }, + "expires_at": { + "description": "Apikey expiry", + "type": ["string", "null"], + "format": "date-time" + } + } +} diff --git a/src/schema/note/createnote.schema.json b/src/schema/note/createnote.schema.json new file mode 100644 index 0000000..fb42a02 --- /dev/null +++ b/src/schema/note/createnote.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "CreateNote", + "description": "Create Note object", + "$id": "https://abode.codi.moe/schema/createnote.schema.json", + "type": "object", + "required": ["aid", "name", "content", "properties"], + "additionalProperties": false, + "properties": { + "aid": { + "type": "string", + "description": "Abode ID (uuid)", + "format": "uuid" + }, + "name": { + "type": "string", + "description": "Note name" + }, + "content": { + "type": "string", + "description": "Note content (Markdown)" + }, + "properties": { + "$ref": "https://abode.codi.moe/schema/partialnoteproperties.schema.json", + "description": "Note properties" + } + } +} diff --git a/src/schema/note/partialnoteproperties.schema.json b/src/schema/note/partialnoteproperties.schema.json new file mode 100644 index 0000000..e3b7ab8 --- /dev/null +++ b/src/schema/note/partialnoteproperties.schema.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "PartialNoteProperties", + "description": "Partial Note properties", + "$id": "https://abode.codi.moe/schema/partialnoteproperties.schema.json", + "type": "object", + "required": ["type"], + "additionalProperties": false, + "properties": { + "type": { + "enum": ["note"], + "description": "Note type" + } + } +} diff --git a/src/schema/note/updatenote.schema.json b/src/schema/note/updatenote.schema.json new file mode 100644 index 0000000..bd66bc2 --- /dev/null +++ b/src/schema/note/updatenote.schema.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "UpdateNote", + "description": "Update Note object", + "$id": "https://abode.codi.moe/schema/updatenote.schema.json", + "type": "object", + "required": ["nid"], + "additionalProperties": false, + "properties": { + "nid": { + "type": "string", + "description": "Note ID (uuid)", + "format": "uuid" + }, + "name": { + "type": "string", + "description": "Note name" + }, + "content": { + "type": "string", + "description": "Note content (Markdown)" + }, + "properties": { + "$ref": "https://abode.codi.moe/schema/partialnoteproperties.schema.json", + "description": "Note properties" + } + } +} diff --git a/src/schema/rawSchemas.ts b/src/schema/rawSchemas.ts new file mode 100644 index 0000000..4644ac1 --- /dev/null +++ b/src/schema/rawSchemas.ts @@ -0,0 +1,23 @@ +export { default as user } from "./user/user.schema.json" with {type: 'json'}; +export { default as createuser } from "./user/createuser.schema.json" with {type: 'json'}; +export { default as updateuser } from "./user/updateuser.schema.json" with {type: 'json'}; +export { default as partialuser } from "./user/partialuser.schema.json" with {type: 'json'}; +export { default as clientuser } from "./user/clientuser.schema.json" with {type: 'json'}; +export { default as userflags } from "./user/userflags.schema.json" with {type: 'json'}; +export { default as loginuser } from "./user/loginuser.schema.json" with {type: 'json'}; + +export { default as abode } from './abode/abode.schema.json' with {type: 'json'}; +export { default as createabode } from './abode/createabode.schema.json' with {type: 'json'}; +export { default as updateabode } from './abode/updateabode.schema.json' with {type: 'json'}; + +export { default as resident } from './resident/resident.schema.json' with {type: 'json'}; +export { default as createresident } from './resident/createresident.schema.json' with {type: 'json'}; +export { default as updateresident } from './resident/updateresident.schema.json' with {type: 'json'}; +export { default as residentflags } from './resident/residentflags.schema.json' with {type: 'json'}; + +export { default as createapikey } from './apikey/createapikey.schema.json' with {type: 'json'}; +export { default as apikeypermissions } from './apikey/apikeypermissions.schema.json' with {type: 'json'}; + +export { default as createnote } from './note/createnote.schema.json' with {type: 'json'}; +export { default as updatenote } from './note/updatenote.schema.json' with {type: 'json'}; +export { default as partialnoteproperties } from './note/partialnoteproperties.schema.json' with {type: 'json'}; diff --git a/src/schema/resident/createresident.schema.json b/src/schema/resident/createresident.schema.json new file mode 100644 index 0000000..ddabf5b --- /dev/null +++ b/src/schema/resident/createresident.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Create", + "description": "Create Resident object", + "$id": "https://abode.codi.moe/schema/createresident.schema.json", + "type": "object", + "required": ["uid", "aid", "flags"], + "additionalProperties": false, + "properties": { + "uid": { + "description": "Resident user (UUID)", + "type": "string", + "format": "uuid" + }, + "aid": { + "description": "Resident abode (UUID)", + "type": "string", + "format": "uuid" + }, + "flags": { + "description": "Resident flags", + "$ref": "https://abode.codi.moe/schema/residentflags.schema.json" + } + } +} diff --git a/src/schema/resident/resident.schema.json b/src/schema/resident/resident.schema.json new file mode 100644 index 0000000..9126f08 --- /dev/null +++ b/src/schema/resident/resident.schema.json @@ -0,0 +1,53 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Resident", + "description": "Resident object", + "$id": "https://abode.codi.moe/schema/resident.schema.json", + "type": "object", + "required": [ + "uid", + "aid", + "flags", + "created_at", + "created_by", + "updated_at", + "updated_by" + ], + "additionalProperties": false, + "properties": { + "uid": { + "description": "Resident user (UUID)", + "type": "string", + "format": "uuid" + }, + "aid": { + "description": "Resident abode (UUID)", + "type": "string", + "format": "uuid" + }, + "flags": { + "description": "Resident flags", + "$ref": "https://abode.codi.moe/schema/residentflags.schema.json" + }, + "created_at": { + "description": "Resident creation date (ISO 8601 datetime with TZ)", + "type": "string", + "format": "date-time" + }, + "created_by": { + "description": "Resident creating user (UUID)", + "type": ["string", "null"], + "format": "uuid" + }, + "updated_at": { + "description": "Resident modification date (ISO 8601 datetime with TZ)", + "type": "string", + "format": "date-time" + }, + "updated_by": { + "description": "Resident modifying user (UUID)", + "type": ["string", "null"], + "format": "uuid" + } + } +} diff --git a/src/schema/resident/residentflags.schema.json b/src/schema/resident/residentflags.schema.json new file mode 100644 index 0000000..2ae55a8 --- /dev/null +++ b/src/schema/resident/residentflags.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "ResidentFlags", + "description": "Resident flags", + "$id": "https://abode.codi.moe/schema/residentflags.schema.json", + "type": "object", + "required": [], + "additionalProperties": false, + "properties": { + "admin": { + "description": "Admin flag, true if the user is an administrator of the abode", + "type": "boolean" + } + }, + "patternProperties": { + "^_": true + } +} diff --git a/src/schema/resident/updateresident.schema.json b/src/schema/resident/updateresident.schema.json new file mode 100644 index 0000000..bdaa0a2 --- /dev/null +++ b/src/schema/resident/updateresident.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Update", + "description": "Update Resident object", + "$id": "https://abode.codi.moe/schema/updateresident.schema.json", + "type": "object", + "required": ["uid", "aid"], + "additionalProperties": false, + "properties": { + "uid": { + "description": "Resident user (UUID)", + "type": "string", + "format": "uuid" + }, + "aid": { + "description": "Resident abode (UUID)", + "type": "string", + "format": "uuid" + }, + "flags": { + "description": "Resident flags", + "$ref": "https://abode.codi.moe/schema/residentflags.schema.json" + } + } +} diff --git a/src/schema/schemas.ts b/src/schema/schemas.ts new file mode 100644 index 0000000..fced54b --- /dev/null +++ b/src/schema/schemas.ts @@ -0,0 +1,76 @@ +import type { AnySchema } from "ajv"; +import type { Abode, CreateAbode, UpdateAbode } from "../db/types/Abode.js"; +import type { + CreateResident, + Resident, + updateResident, +} from "../db/types/Resident.js"; +import type { + ClientUser, + CreateUser, + LoginUser, + PartialUser, + UpdateUser, + User, + UserFlags, +} from "../db/types/User.js"; +import * as schemas from "./rawSchemas.js"; +import type { ApikeyPermissions, CreateApikey } from "../db/types/Apikey.js"; +import type { + CreateNote, + PartialNoteProperties, + UpdateNote, +} from "../db/types/Note.js"; +export * from "./rawSchemas.js"; + +function checkSchema(name: string, schema: AnySchema) { + if (typeof schema !== "object" || Array.isArray(schema) || !schema) + throw new Error(`Unexpected shape for schema ${name}`); + if (schema.$schema !== "https://json-schema.org/draft-07/schema") + throw new Error(`Unexpected $schema for schema ${name}`); + if (!("title" in schema) || typeof schema.title !== "string" || !schema.title) + throw new Error(`Missing title for schema ${name}`); + if ( + !("description" in schema) || + typeof schema.description !== "string" || + !schema.description + ) + throw new Error(`Missing description for schema ${name}`); + if (!schema.$id) throw new Error(`Missing $id for schema ${name}`); + if ( + !schema.$id.match( + /^https:\/\/abode\.codi\.moe\/schema\/[a-zA-Z0_9_-]+\.schema\.json$/ + ) + ) + throw new Error(`Unexpected $id for schema ${name}`); + + schema.$schema = "http://json-schema.org/draft-07/schema"; +} +for (const [name, schema] of Object.entries(schemas)) { + checkSchema(name, schema); +} + +export type Types = { + user: User; + createuser: CreateUser; + updateuser: UpdateUser; + partialuser: PartialUser; + clientuser: ClientUser; + userflags: UserFlags; + loginuser: LoginUser; + + abode: Abode; + createabode: CreateAbode; + updateabode: UpdateAbode; + + resident: Resident; + createresident: CreateResident; + updateresident: updateResident; + + createapikey: CreateApikey; + apikeypermissions: ApikeyPermissions; + + createnote: CreateNote; + updatenote: UpdateNote; + partialnoteproperties: PartialNoteProperties; +}; diff --git a/src/schema/user/clientuser.schema.json b/src/schema/user/clientuser.schema.json new file mode 100644 index 0000000..e1542f4 --- /dev/null +++ b/src/schema/user/clientuser.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "ClientUser", + "description": "client User object", + "$id": "https://abode.codi.moe/schema/clientuser.schema.json", + "type": "object", + "required": ["uid", "email", "name", "flags", "created_at", "updated_at"], + "additionalProperties": false, + "properties": { + "uid": { + "description": "User ID (UUID)", + "type": "string", + "format": "uuid" + }, + "email": { + "description": "User email", + "type": "string", + "format": "email" + }, + "name": { + "description": "User name", + "type": "string", + "minLength": 1 + }, + "flags": { + "description": "User flags", + "$ref": "https://abode.codi.moe/schema/userflags.schema.json" + }, + "created_at": { + "description": "User creation date (ISO 8601 datetime with TZ)", + "type": "string", + "format": "date-time" + }, + "updated_at": { + "description": "User modification date (ISO 8601 datetime with TZ)", + "type": "string", + "format": "date-time" + } + } +} diff --git a/src/schema/user/createuser.schema.json b/src/schema/user/createuser.schema.json new file mode 100644 index 0000000..2d4e5af --- /dev/null +++ b/src/schema/user/createuser.schema.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Create", + "description": "Create User object", + "$id": "https://abode.codi.moe/schema/createuser.schema.json", + "type": "object", + "required": ["email", "name", "password", "flags"], + "additionalProperties": false, + "properties": { + "email": { + "description": "User email", + "type": "string", + "format": "email" + }, + "name": { + "description": "User name", + "type": "string", + "minLength": 1 + }, + "password": { + "description": "Hashed user password (PHC string) or special state", + "type": "string" + }, + "flags": { + "description": "User flags", + "$ref": "https://abode.codi.moe/schema/userflags.schema.json" + } + } +} diff --git a/src/schema/user/loginuser.schema.json b/src/schema/user/loginuser.schema.json new file mode 100644 index 0000000..0f36d68 --- /dev/null +++ b/src/schema/user/loginuser.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "LoginUser", + "description": "Login as user object", + "$id": "https://abode.codi.moe/schema/loginuser.schema.json", + "type": "object", + "required": ["email", "password"], + "additionalProperties": false, + "properties": { + "email": { + "description": "User email", + "type": "string", + "format": "email" + }, + "password": { + "description": "User password", + "type": "string" + } + } +} diff --git a/src/schema/user/partialuser.schema.json b/src/schema/user/partialuser.schema.json new file mode 100644 index 0000000..41c304c --- /dev/null +++ b/src/schema/user/partialuser.schema.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "PartialUser", + "description": "Partial User object", + "$id": "https://abode.codi.moe/schema/partialuser.schema.json", + "type": "object", + "required": ["uid", "name", "flags", "created_at", "updated_at"], + "additionalProperties": false, + "properties": { + "uid": { + "description": "User ID (UUID)", + "type": "string", + "format": "uuid" + }, + "name": { + "description": "User name", + "type": "string", + "minLength": 1 + }, + "flags": { + "description": "User flags", + "$ref": "https://abode.codi.moe/schema/userflags.schema.json" + }, + "created_at": { + "description": "User creation date (ISO 8601 datetime with TZ)", + "type": "string", + "format": "date-time" + }, + "updated_at": { + "description": "User modification date (ISO 8601 datetime with TZ)", + "type": "string", + "format": "date-time" + } + } +} diff --git a/src/schema/user/updateuser.schema.json b/src/schema/user/updateuser.schema.json new file mode 100644 index 0000000..9af2cb6 --- /dev/null +++ b/src/schema/user/updateuser.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "Update", + "description": "Update User object", + "$id": "https://abode.codi.moe/schema/updateuser.schema.json", + "type": "object", + "required": ["uid"], + "additionalProperties": false, + "properties": { + "uid": { + "description": "User ID (UUID)", + "type": "string", + "format": "uuid" + }, + "email": { + "description": "User email", + "type": "string", + "format": "email" + }, + "name": { + "description": "User name", + "type": "string", + "minLength": 1 + }, + "password": { + "description": "Hashed user password (PHC string) or special state", + "type": "string" + }, + "flags": { + "description": "User flags", + "$ref": "https://abode.codi.moe/schema/userflags.schema.json" + } + } +} diff --git a/src/schema/user/user.schema.json b/src/schema/user/user.schema.json new file mode 100644 index 0000000..1d1f7b4 --- /dev/null +++ b/src/schema/user/user.schema.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "User", + "description": "User object", + "$id": "https://abode.codi.moe/schema/user.schema.json", + "type": "object", + "required": [ + "uid", + "email", + "name", + "password", + "flags", + "created_at", + "updated_at" + ], + "additionalProperties": false, + "properties": { + "uid": { + "description": "User ID (UUID)", + "type": "string", + "format": "uuid" + }, + "email": { + "description": "User email", + "type": "string", + "format": "email" + }, + "name": { + "description": "User name", + "type": "string", + "minLength": 1 + }, + "password": { + "description": "Hashed user password (PHC string) or special state", + "type": "string" + }, + "flags": { + "description": "User flags", + "$ref": "https://abode.codi.moe/schema/userflags.schema.json" + }, + "created_at": { + "description": "User creation date (ISO 8601 datetime with TZ)", + "type": "string", + "format": "date-time" + }, + "updated_at": { + "description": "User modification date (ISO 8601 datetime with TZ)", + "type": "string", + "format": "date-time" + } + } +} diff --git a/src/schema/user/userflags.schema.json b/src/schema/user/userflags.schema.json new file mode 100644 index 0000000..1a33ef1 --- /dev/null +++ b/src/schema/user/userflags.schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema", + "title": "UserFlags", + "description": "User flags", + "$id": "https://abode.codi.moe/schema/userflags.schema.json", + "type": "object", + "required": [], + "additionalProperties": false, + "properties": { + "admin": { + "description": "Admin flag, true if the user is an administrator", + "type": "boolean" + } + }, + "patternProperties": { + "^_": true + } +} diff --git a/src/schema/validators.ts b/src/schema/validators.ts new file mode 100644 index 0000000..5b4bdc2 --- /dev/null +++ b/src/schema/validators.ts @@ -0,0 +1,53 @@ +import { type AnySchema, type ErrorObject } from "ajv"; +import * as schemas from "./schemas.js"; +import type { Types } from "./schemas.js"; +import { createAjv, loadSchemas } from "./ajv.js"; + +// this file is overridden by the generated code from @/meta/pack/validators.ts when building with webpack +// only runs when in dev mode (with `tsx` / `npm run abode-xxx`) + +const validator = createAjv({ + code: { + // we're running in dev mode here, we don't want to take forever to start up + optimize: false, + }, +}); +loadSchemas(validator); + +const validators = Object.fromEntries( + Object.entries(schemas).map(([name, schema]) => [ + name, + validator.compile(schema), + ]) +) as unknown as { + [T in keyof Types]: { + (obj: unknown): obj is Types[T]; + errors?: null | Partial[]; + schema: AnySchema & { title: string; description: string; $id: string }; + }; +}; + +export const { + user, + createuser, + updateuser, + partialuser, + clientuser, + userflags, + loginuser, + + abode, + createabode, + updateabode, + + resident, + createresident, + updateresident, + + createapikey, + apikeypermissions, + + createnote, + updatenote, + partialnoteproperties, +} = validators; diff --git a/src/tui/App.tsx b/src/tui/App.tsx new file mode 100644 index 0000000..2d2925a --- /dev/null +++ b/src/tui/App.tsx @@ -0,0 +1,111 @@ +import { Box, useApp, useInput } from "ink"; +import { use, useState, type ComponentType, type ReactNode } from "react"; +import { ListBox } from "./components/ui/ListBox.js"; +import type { DbInterface } from "../db/types/DbInterface.js"; +import { DbProvider } from "../react/contexts/Db.js"; +import { UsersPanel } from "./components/panels/UsersPanel.js"; +import { BgColorContext } from "./contexts/BgColor.js"; +import { FocusManager, useActive } from "./contexts/FocusManager.js"; +import { Provider } from "../react/store/react.js"; +import { createStore, type Store } from "../react/store/store.js"; +import { AbodesPanel } from "./components/panels/AbodesPanel.js"; +import { LoginPanel } from "./components/panels/LoginPanel.js"; +import { PopupManager } from "../react/contexts/PopupManager.js"; + +function AppWrapper({ + children, + db, + store, + bgColor, +}: { + children: ReactNode; + db: DbInterface; + store: Store; + bgColor?: string; +}) { + return ( + + + + + + + + {children} + + + + + + + ); +} + +export type CollectionType = "users" | "abodes" | "apikeys" | "notes"; +const collectionTypes: CollectionType[] = [ + "users", + "abodes", + "apikeys", + "notes", +]; +const collectionTypeDisplay: Record = { + users: "Users", + abodes: "Abodes", + apikeys: "API Keys", + notes: "Notes", +}; + +const collections: Record = { + users: UsersPanel, + abodes: AbodesPanel, + apikeys: () => null, + notes: () => null, +}; + +function App() { + const app = useApp(); + const isActive = useActive(); + useInput( + (input, key) => { + if (input === "q" || key.escape) app.exit(); + }, + { isActive } + ); + + const [activeCollection, setActiveCollection] = + useState("users"); + const CollectionPanel = collections[activeCollection]; + return ( + + + + + + + + ); +} + +export function app({ + db, + bgColor, + store = createStore(), +}: { + db: DbInterface; + bgColor?: string; + store?: Store; +}) { + return ( + + + + ); +} diff --git a/src/tui/components/panels/AbodesPanel.tsx b/src/tui/components/panels/AbodesPanel.tsx new file mode 100644 index 0000000..33dd248 --- /dev/null +++ b/src/tui/components/panels/AbodesPanel.tsx @@ -0,0 +1,63 @@ +import { Box, Text } from "ink"; +import { usePanelSize } from "../../hooks/size.js"; +import { EllipsisText } from "../ui/EllipsisText.js"; +import { use, useCallback, useMemo } from "react"; +import { type ButtonListItem } from "../ui/Button.js"; +import type { Abode } from "../../../db/types/Abode.js"; +import { useDataAllAbodes } from "../../../react/hooks/data/abodes.js"; +import { lengthOfUuid } from "../../../util/length.js"; +import { PopupManagerContext } from "../../../react/contexts/PopupManager.js"; +import { SearchPanel } from "../ui/SearchPanel.js"; +import { AbodePopup } from "../popups/AbodePopup.js"; +import { CreateAbodePopup } from "../popups/CreateAbodePopup.js"; + +function AbodeComponent({ + item, + selected, +}: { + item: Abode; + selected: boolean; +}) { + const { width } = usePanelSize(); + let maxNameLen = width - 2 - lengthOfUuid - 1; + + return ( + + + {" "} + {item.aid} + + + ); +} + +const match = (filter: string, abode: Abode) => + abode.name.toLowerCase().includes(filter.toLowerCase()); +const sort = (a: Abode, b: Abode) => (a.name < b.name ? -1 : 1); + +export function AbodesPanel() { + const { status, abodes, refresh } = useDataAllAbodes(); + const { openPopup } = use(PopupManagerContext)!; + + const onSelect = useCallback( + (abode: Abode) => openPopup(AbodePopup, { aid: abode.aid }), + [openPopup] + ); + const buttons = useMemo( + () => [{ children: "New", onClick: () => openPopup(CreateAbodePopup) }], + [openPopup] + ); + + return ( + + ); +} diff --git a/src/tui/components/panels/LoginPanel.tsx b/src/tui/components/panels/LoginPanel.tsx new file mode 100644 index 0000000..0079181 --- /dev/null +++ b/src/tui/components/panels/LoginPanel.tsx @@ -0,0 +1,84 @@ +import { Box, Text } from "ink"; +import { useManagedFocus } from "../../contexts/FocusManager.js"; +import { useDispatch, useSelector } from "../../../react/store/react.js"; +import { + getLoginUser, + logOut, + setLoginUser, +} from "../../../react/store/slices/login.js"; +import { Button } from "../ui/Button.js"; +import { use, useEffect } from "react"; +import { DbContext } from "../../../react/contexts/Db.js"; +import { Popup } from "../ui/Popup.js"; +import { PopupManagerContext } from "../../../react/contexts/PopupManager.js"; +import type { DbInterface } from "../../../db/types/DbInterface.js"; +import { idAssert } from "../../../util/ts.js"; +import type { ApiInterface } from "../../../db/api/ApiInterface.js"; +import type { Dispatch } from "../../../react/store/store.js"; +import { LoginPopup } from "../popups/LoginPopup.js"; + +function ConnectPopup({ onClose }: { onClose: () => void }) { + return ( + + Currently unimplemented + + ); +} + +// toplevel if's inside are build time +// gets completely removed at build time if empty +function useAutoLogin(db: DbInterface | null, dispatch: Dispatch) { + if (compiledSources.api) { + useEffect(() => { + if (db?.name === "api") { + idAssert(db); + db._.self() + .then((user) => dispatch(setLoginUser(user))) + .catch(() => {}); + } + }, [db]); + } +} + +export function LoginPanel() { + const { isFocused } = useManagedFocus(); + const user = useSelector(getLoginUser); + const db = use(DbContext); + const { openPopup } = use(PopupManagerContext)!; + const dispatch = useDispatch(); + useAutoLogin(db, dispatch); + + return ( + <> + + {user ? ( + <> + + Logged in as{" "} + + {user.name} + + + + + ) : db ? ( + <> + Not logged in + + + ) : ( + <> + No db interface + + + )} + + + ); +} diff --git a/src/tui/components/panels/UsersPanel.tsx b/src/tui/components/panels/UsersPanel.tsx new file mode 100644 index 0000000..eff7fee --- /dev/null +++ b/src/tui/components/panels/UsersPanel.tsx @@ -0,0 +1,81 @@ +import { Box, Text } from "ink"; +import { usePanelSize } from "../../hooks/size.js"; +import { lengthOfUuid } from "../../../util/length.js"; +import { EllipsisText } from "../ui/EllipsisText.js"; +import { use, useCallback, useMemo } from "react"; +import { type ButtonListItem } from "../ui/Button.js"; +import { useDataAllUsers } from "../../../react/hooks/data/users.js"; +import type { ClientUser, PartialUser } from "../../../db/types/User.js"; +import { PopupManagerContext } from "../../../react/contexts/PopupManager.js"; +import { SearchPanel } from "../ui/SearchPanel.js"; +import { UserPopup } from "../popups/UserPopup.js"; +import { CreateUserPopup } from "../popups/CreateUserPopup.js"; + +function UserComponent({ + item, + selected, +}: { + item: ClientUser | PartialUser; + selected: boolean; +}) { + const { width } = usePanelSize(); + let maxNameLen = width - 2 - lengthOfUuid - 1; + let maxEmailLen = 0; + + if (maxNameLen > 40) { + maxEmailLen = Math.floor((maxNameLen * 2) / 3); + maxNameLen -= maxEmailLen; + maxEmailLen--; + } + + return ( + + + + {!!maxEmailLen && ( + <> + {" "} + + + )}{" "} + {item.uid} + + + ); +} + +const match = (filter: string, user: ClientUser | PartialUser) => + user.name.toLowerCase().includes(filter.toLowerCase()); +const sort = (a: ClientUser | PartialUser, b: ClientUser | PartialUser) => + a.name < b.name ? -1 : 1; + +export function UsersPanel() { + const { status, users, refresh } = useDataAllUsers(); + const { openPopup } = use(PopupManagerContext)!; + + const onSelect = useCallback( + (user: ClientUser | PartialUser) => openPopup(UserPopup, { uid: user.uid }), + [openPopup] + ); + const buttons = useMemo( + () => [{ children: "New", onClick: () => openPopup(CreateUserPopup) }], + [openPopup] + ); + + return ( + + ); +} diff --git a/src/tui/components/popups/AbodePopup.tsx b/src/tui/components/popups/AbodePopup.tsx new file mode 100644 index 0000000..7a5af06 --- /dev/null +++ b/src/tui/components/popups/AbodePopup.tsx @@ -0,0 +1,135 @@ +import { use, useState } from "react"; +import { useDataAbodeById } from "../../../react/hooks/data/abodes.js"; +import { useAction } from "../../../react/hooks/useAction.js"; +import { + deleteAbodeById, + updateAbode, +} from "../../../react/store/actions/abodes.js"; +import type { UpdateAbode } from "../../../db/types/Abode.js"; +import { useFreeSize } from "../../hooks/size.js"; +import { useDataResidentsByAbodeId } from "../../../react/hooks/data/residents.js"; +import { Popup } from "../ui/Popup.js"; +import { Box, Text } from "ink"; +import { Input } from "../ui/Input.js"; +import { UserName } from "../ui/UserName.js"; +import { Button, ButtonList } from "../ui/Button.js"; +import { PopupManagerContext } from "../../../react/contexts/PopupManager.js"; +import { AbodeResidentsPopup } from "./AbodeResidentsPopup.js"; + +export function AbodePopup({ + aid, + onClose, +}: { + aid: string; + onClose: () => void; +}) { + const { abode, status } = useDataAbodeById(aid); + const del = useAction(deleteAbodeById); + const upd = useAction(updateAbode); + const [changes, setChanges] = useState(null); + const { width } = useFreeSize(); + const { residents, status: residentStatus } = useDataResidentsByAbodeId(aid); + const { openPopup } = use(PopupManagerContext)!; + + return ( + + + {abode ? ( + <> + + Name:{" "} + {changes ? ( + setChanges((e) => ({ ...e!, name: v }))} + /> + ) : ( + <>{abode.name} + )} + + + aid:{" "} + {abode.aid} + + + Created:{" "} + {abode.created_at} ( + {abode.created_by ? ( + + ) : ( + N/A + )} + ) + + + Updated:{" "} + {abode.updated_at} ( + {abode.updated_by ? ( + + ) : ( + N/A + )} + ) + + + Residents: + {residentStatus === "loaded" ? ( + residents.length + ) : ( + {residentStatus} + )} + + + {changes ? ( + upd(changes).then(() => setChanges(null)), + }, + { + children: "Discard", + onClick: () => setChanges(null), + }, + ]} + /> + ) : ( + setChanges({ aid }), + }, + { + children: "Residents", + onClick: () => openPopup(AbodeResidentsPopup, { aid }), + }, + { + children: "Delete", + onClick: () => del(aid).then(onClose), + }, + ]} + /> + )} + + ) : ( + <> + {status} + + + + )} + + + ); +} diff --git a/src/tui/components/popups/AbodeResidentsPopup.tsx b/src/tui/components/popups/AbodeResidentsPopup.tsx new file mode 100644 index 0000000..b023c06 --- /dev/null +++ b/src/tui/components/popups/AbodeResidentsPopup.tsx @@ -0,0 +1,79 @@ +import { Box, Text } from "ink"; +import { Popup } from "../ui/Popup.js"; +import { useFreeSize } from "../../hooks/size.js"; +import { useDataResidentsByAbodeId } from "../../../react/hooks/data/residents.js"; +import { AbodeName } from "../ui/AbodeName.js"; +import { SearchPanel } from "../ui/SearchPanel.js"; +import type { Resident } from "../../../db/types/Resident.js"; +import { UserName } from "../ui/UserName.js"; +import { ButtonList } from "../ui/Button.js"; + +function AbodeResidentComponent({ + item, + selected, +}: { + item: Resident; + selected: boolean; +}) { + return ( + + + + + {selected && ( + {}, + }, + { + children: "Delete", + onClick: () => {}, + }, + { + children: "Delete", + onClick: () => {}, + }, + ]} + /> + )} + + ); +} + +export function AbodeResidentsPopup({ + aid, + onClose, +}: { + aid: string; + onClose: () => void; +}) { + const { width, height } = useFreeSize(); + const { residents, status, refresh } = useDataResidentsByAbodeId(aid); + + return ( + + + + + Residents of{" "} + + + + + + + + + + ); +} diff --git a/src/tui/components/popups/CreateAbodePopup.tsx b/src/tui/components/popups/CreateAbodePopup.tsx new file mode 100644 index 0000000..10e14bc --- /dev/null +++ b/src/tui/components/popups/CreateAbodePopup.tsx @@ -0,0 +1,43 @@ +import { useState } from "react"; +import type { CreateAbode } from "../../../db/types/Abode.js"; +import { useFreeSize } from "../../hooks/size.js"; +import { createAbode } from "../../../react/store/actions/abodes.js"; +import { useAction } from "../../../react/hooks/useAction.js"; +import { Popup } from "../ui/Popup.js"; +import { Box, Text } from "ink"; +import { Input } from "../ui/Input.js"; +import { ButtonList } from "../ui/Button.js"; + +export function CreateAbodePopup({ onClose }: { onClose: () => void }) { + const [create, setCreate] = useState({ + name: "", + }); + const { width } = useFreeSize(); + const add = useAction(createAbode); + + return ( + + + + Name: + setCreate((u) => ({ ...u, name: v }))} + /> + + + add(create).then(onClose), + }, + { children: "Cancel", onClick: onClose }, + ]} + /> + + + ); +} diff --git a/src/tui/components/popups/CreateUserPopup.tsx b/src/tui/components/popups/CreateUserPopup.tsx new file mode 100644 index 0000000..5544ce9 --- /dev/null +++ b/src/tui/components/popups/CreateUserPopup.tsx @@ -0,0 +1,83 @@ +import { useState } from "react"; +import type { CreateUser } from "../../../db/types/User.js"; +import { useFreeSize } from "../../hooks/size.js"; +import { createUser } from "../../../react/store/actions/users.js"; +import { useAction } from "../../../react/hooks/useAction.js"; +import { Popup } from "../ui/Popup.js"; +import { Box, Text } from "ink"; +import { Input } from "../ui/Input.js"; +import { Button, ButtonList } from "../ui/Button.js"; +import { hashPassword } from "../../../util/hash.js"; + +export function CreateUserPopup({ onClose }: { onClose: () => void }) { + const [create, setCreate] = useState>({ + name: "", + email: "", + flags: {}, + }); + const [password, setPassword] = useState(""); + const { width } = useFreeSize(); + const add = useAction(createUser); + + return ( + + + + Name:{" "} + setCreate((u) => ({ ...u, name: v }))} + /> + + + Email:{" "} + setCreate((u) => ({ ...u, email: v }))} + /> + + + Password: + + + + Admin:{" "} + + + + + (password + ? hashPassword(password) + : Promise.resolve("#unset" as const) + ) + .then((password) => ({ ...create, password })) + .then(add) + .then(onClose), + }, + { children: "Cancel", onClick: onClose }, + ]} + /> + + + ); +} diff --git a/src/tui/components/popups/LoginPopup.tsx b/src/tui/components/popups/LoginPopup.tsx new file mode 100644 index 0000000..7ddb75e --- /dev/null +++ b/src/tui/components/popups/LoginPopup.tsx @@ -0,0 +1,53 @@ +import { useEffect, useState } from "react"; +import { useFreeSize } from "../../hooks/size.js"; +import { Popup } from "../ui/Popup.js"; +import { Box, Text } from "ink"; +import { Input } from "../ui/Input.js"; +import { Button } from "../ui/Button.js"; +import { useDataUserByEmail } from "../../../react/hooks/data/users.js"; +import { useDispatch } from "../../../react/store/react.js"; +import { setLoginUser } from "../../../react/store/slices/login.js"; + +function LoginPopupCheck({ + email, + onClose, +}: { + email: string; + onClose: () => void; +}) { + const { status, user } = useDataUserByEmail(email); + const dispatch = useDispatch(); + useEffect(() => { + if (user) { + dispatch(setLoginUser(user)); + onClose(); + } + }, [user, onClose]); + + return {status}; +} + +export function LoginPopup({ onClose }: { onClose: () => void }) { + const [email, setEmail] = useState(""); + const [confirmedEmail, setConfirmedEmail] = useState(email); + const { width } = useFreeSize(); + + return ( + + + + Email: + + + {confirmedEmail ? ( + + ) : ( + + )} + + + + + + ); +} diff --git a/src/tui/components/popups/UserPopup.tsx b/src/tui/components/popups/UserPopup.tsx new file mode 100644 index 0000000..8db0ba2 --- /dev/null +++ b/src/tui/components/popups/UserPopup.tsx @@ -0,0 +1,128 @@ +import { Box, Text } from "ink"; +import { Button, ButtonList } from "../ui/Button.js"; +import { Input } from "../ui/Input.js"; +import { Popup } from "../ui/Popup.js"; +import { useFreeSize } from "../../hooks/size.js"; +import { useState } from "react"; +import type { UpdateUser } from "../../../db/types/User.js"; +import { + deleteUserById, + updateUser, +} from "../../../react/store/actions/users.js"; +import { useDataUserById } from "../../../react/hooks/data/users.js"; +import { useAction } from "../../../react/hooks/useAction.js"; + +export function UserPopup({ + uid, + onClose, +}: { + uid: string; + onClose: () => void; +}) { + const { user, status } = useDataUserById(uid); + const del = useAction(deleteUserById); + const upd = useAction(updateUser); + const [changes, setChanges] = useState(null); + const { width } = useFreeSize(); + + return ( + + + {user ? ( + <> + + Name:{" "} + {changes ? ( + setChanges((e) => ({ ...e!, name: v }))} + /> + ) : ( + <>{user.name} + )} + + + Uid:{" "} + {user.uid} + + + Email:{" "} + {changes ? ( + setChanges((e) => ({ ...e!, email: v }))} + /> + ) : ( + <> + {"email" in user ? ( + <>{user.email} + ) : ( + N/A + )} + + )} + + + Flags:{" "} + {(user.flags.admin ?? false) && admin} + + + Created: + {user.created_at} + + + Updated: + {user.updated_at} + + + {changes ? ( + upd(changes).then(() => setChanges(null)), + }, + { + children: "Discard", + onClick: () => setChanges(null), + }, + ]} + /> + ) : ( + { + setChanges({ uid }); + }, + }, + { + children: "Delete", + onClick: () => del(uid).then(onClose), + }, + ]} + /> + )} + + ) : ( + <> + {status} + + + + )} + + + ); +} diff --git a/src/tui/components/ui/AbodeName.tsx b/src/tui/components/ui/AbodeName.tsx new file mode 100644 index 0000000..5456f65 --- /dev/null +++ b/src/tui/components/ui/AbodeName.tsx @@ -0,0 +1,16 @@ +import type { ComponentProps } from "react"; +import { Text } from "ink"; +import { useDataAbodeById } from "../../../react/hooks/data/abodes.js"; + +export function AbodeName({ + aid, + ...props +}: { aid: string } & ComponentProps) { + const { status, abode } = useDataAbodeById(aid); + + return abode ? ( + {abode.name} + ) : ( + {status} + ); +} diff --git a/src/tui/components/ui/Button.tsx b/src/tui/components/ui/Button.tsx new file mode 100644 index 0000000..fe7ac32 --- /dev/null +++ b/src/tui/components/ui/Button.tsx @@ -0,0 +1,81 @@ +import { useState, type ComponentProps, type ReactNode } from "react"; +import { useManagedFocus } from "../../contexts/FocusManager.js"; +import { Box, Text, useInput } from "ink"; + +export function Button({ + children, + onClick, + autoFocus, + focusId, +}: { + children: ReactNode; + onClick: () => void; + autoFocus?: boolean; + focusId?: string; +}) { + const { isFocused } = useManagedFocus({ autoFocus, id: focusId }); + useInput( + (input, key) => { + if (input === " " || key.return) onClick(); + }, + { isActive: isFocused } + ); + + return [{children}]; +} + +export type ButtonListItem = { + children: ReactNode; + onClick: () => void; +}; + +export function ButtonList({ + buttons, + autoFocus, + focusId, + isFocused: forceFocus, + horizontal = false, + vertical = false, + ...props +}: { + buttons: ButtonListItem[]; + autoFocus?: boolean; + focusId?: string; + isFocused?: boolean; + horizontal?: boolean; + vertical?: boolean; +} & Omit, "children">) { + if (!horizontal && !vertical) horizontal = true; + + const [selected, setSelected] = useState(0); + const { isFocused } = useManagedFocus({ + autoFocus, + id: focusId, + isActive: forceFocus === undefined, + }); + useInput( + (input, key) => { + if (input === " " || key.return) { + buttons[selected].onClick(); + } else if ( + (vertical && key.downArrow) || + (horizontal && key.rightArrow) + ) { + setSelected((prev) => (prev + 1) % buttons.length); + } else if ((vertical && key.upArrow) || (horizontal && key.leftArrow)) { + setSelected((prev) => (prev - 1 + buttons.length) % buttons.length); + } + }, + { isActive: isFocused || forceFocus || false } + ); + + return ( + + {buttons.map((button, i) => ( + + [{button.children}] + + ))} + + ); +} diff --git a/src/tui/components/ui/EllipsisText.tsx b/src/tui/components/ui/EllipsisText.tsx new file mode 100644 index 0000000..5dd45a0 --- /dev/null +++ b/src/tui/components/ui/EllipsisText.tsx @@ -0,0 +1,29 @@ +import { Text } from "ink"; + +export function EllipsisText({ + text, + maxLen, + fill = "", +}: { + text: string; + maxLen: number; + fill?: string; +}) { + if (text.length <= maxLen) + return ( + + {text} + {fill && text.length < maxLen && ( + + {fill.repeat(maxLen - text.length).slice(0, maxLen - text.length)} + + )} + + ); + return ( + + {text.slice(0, maxLen - 3)} + ... + + ); +} diff --git a/src/tui/components/ui/Input.tsx b/src/tui/components/ui/Input.tsx new file mode 100644 index 0000000..36ffba8 --- /dev/null +++ b/src/tui/components/ui/Input.tsx @@ -0,0 +1,39 @@ +import TextInput from "ink-text-input"; +import { useManagedFocus } from "../../contexts/FocusManager.js"; + +export function Input({ + focus, + value, + onChange, + onSubmit, + mask, + placeholder, + autoFocus, + focusId, +}: { + focus?: boolean; + value: string; + onChange: (value: string) => void; + onSubmit?: () => void; + mask?: string; + placeholder?: string; + autoFocus?: boolean; + focusId?: string; +}) { + const { isFocused } = useManagedFocus({ + autoFocus, + id: focusId, + isActive: typeof focus !== "boolean", + }); + return ( + + ); +} diff --git a/src/tui/components/ui/ListBox.tsx b/src/tui/components/ui/ListBox.tsx new file mode 100644 index 0000000..28b48fa --- /dev/null +++ b/src/tui/components/ui/ListBox.tsx @@ -0,0 +1,98 @@ +import { Box, Text, useInput } from "ink"; +import type { ReactNode } from "react"; +import { useManagedFocus } from "../../contexts/FocusManager.js"; + +function Item({ + text, + focused, + checked, +}: { + text: string; + focused: boolean; + checked: boolean; +}) { + return ( + + {text} + + ); +} + +export function ListBox({ + title, + items, + display, + selected, + setSelected, + autoFocus, + focusId, + width, + height, +}: { + title?: ReactNode | string; + items: T[]; + display?: Record; + selected: T; + setSelected: (item: T) => void; + autoFocus?: boolean; + focusId?: string; + width?: number; + height?: number; +}) { + const { isFocused } = useManagedFocus({ + autoFocus, + id: focusId, + }); + useInput( + (_, key) => { + if (key.downArrow) { + setSelected(items[(items.indexOf(selected) + 1) % items.length]); + } else if (key.upArrow) { + setSelected( + items[(items.indexOf(selected) - 1 + items.length) % items.length] + ); + } else if (key.pageUp) { + setSelected(items[0]); + } else if (key.pageDown) { + setSelected(items[items.length - 1]); + } + }, + { isActive: isFocused } + ); + + return ( + + {title && ( + + {typeof title === "string" ? ( + + {title} + + ) : ( + title + )} + + )} + {items.map((item, i) => ( + + ))} + + ); +} diff --git a/src/tui/components/ui/ListDisplay.tsx b/src/tui/components/ui/ListDisplay.tsx new file mode 100644 index 0000000..6b01c1e --- /dev/null +++ b/src/tui/components/ui/ListDisplay.tsx @@ -0,0 +1,91 @@ +import { Box, Spacer, Text, useInput } from "ink"; +import { + useEffect, + useMemo, + useState, + type ComponentType, + type ReactNode, +} from "react"; + +export function ListDisplay({ + items, + Component, + height, + isFocused, + status, + onSelect, +}: { + items: T[]; + Component: ComponentType<{ item: T; selected: boolean }>; + height: number; + isFocused?: boolean; + status?: ReactNode; + onSelect?: (item: T) => void; +}) { + const [start, setStart] = useState(0); + const slice = Math.min(start + height - 1, items.length); + const [selected, setSelected] = useState(0); + + useInput( + (input, key) => { + if (key.upArrow) { + setSelected((prev) => (prev > 0 ? prev - 1 : items.length - 1)); + } else if (key.downArrow) { + setSelected((prev) => (prev + 1) % items.length); + } else if (input === " " || key.return) { + onSelect?.(items[selected]); + } + }, + { isActive: isFocused } + ); + + useEffect(() => { + if (selected < start) setStart(selected); + else if (selected >= slice) + setStart( + Math.max(Math.min(selected - slice + start + 1, items.length - 1), 0) + ); + }, [selected, start, slice, items.length]); + useEffect(() => { + if (selected < 0) setSelected(Math.max(items.length - 1, 0)); + if (selected >= items.length) setSelected(0); + }, [selected, items.length]); + useEffect(() => { + setSelected(0); + }, [items]); + + const indexed = useMemo( + () => items.map((item, index) => ({ item, index })), + [items] + ); + + return ( + + + {indexed.slice(start, slice).map(({ item, index }) => ( + + ))} + {items.length < height - 1 && ( + + )} + + + {status} + + + {items.length ? ( + <> + {start + 1}-{slice}/{items.length} + + ) : ( + <>(empty) + )} + + + + ); +} diff --git a/src/tui/components/ui/Popup.tsx b/src/tui/components/ui/Popup.tsx new file mode 100644 index 0000000..9b7726c --- /dev/null +++ b/src/tui/components/ui/Popup.tsx @@ -0,0 +1,42 @@ +import { Box, useInput } from "ink"; +import { use, type ReactNode } from "react"; +import { BgColorContext } from "../../contexts/BgColor.js"; +import { PopupContext, usePopup } from "../../contexts/FocusManager.js"; + +export function Popup({ + children, + onClose, +}: { + children: ReactNode; + onClose?: () => void; +}) { + const { id, active } = usePopup(); + + useInput( + (_, key) => { + if (key.escape) onClose?.(); + }, + { isActive: active && !!onClose } + ); + + return ( + + + + {children} + + + + ); +} diff --git a/src/tui/components/ui/SearchPanel.tsx b/src/tui/components/ui/SearchPanel.tsx new file mode 100644 index 0000000..56b0475 --- /dev/null +++ b/src/tui/components/ui/SearchPanel.tsx @@ -0,0 +1,95 @@ +import { useMemo, useState, type ComponentType } from "react"; +import type { UseLoadResult } from "../../../react/hooks/useLoad.js"; +import { ButtonList, type ButtonListItem } from "./Button.js"; +import { useAfterRender } from "../../hooks/useAfterRender.js"; +import { useManagedFocus } from "../../contexts/FocusManager.js"; +import { usePanelSize } from "../../hooks/size.js"; +import { Box, Text, useInput } from "ink"; +import { Input } from "./Input.js"; +import { ListDisplay } from "./ListDisplay.js"; + +export function SearchPanel({ + sub = false, + status, + refresh, + items, + match, + sort, + onSelect, + buttons, + ItemComponent, + height: forceHeight, +}: { + sub?: boolean; + status: UseLoadResult["status"]; + refresh?: UseLoadResult["refresh"]; + items: T[] | Record; + match?: (filter: string, item: T) => boolean; + sort?: (a: T, b: T) => number; + onSelect?: (item: T) => void; + buttons?: ButtonListItem[]; + ItemComponent: ComponentType<{ + item: T; + selected: boolean; + }>; + height?: number; +}) { + const [filter, setFilter] = useState(""); + const orderedItems = useMemo(() => { + let ordered = Array.isArray(items) ? items : Object.values(items); + if (match) ordered = ordered.filter((x) => !filter || match(filter, x)); + if (sort) ordered.sort(sort); + return ordered; + }, [items, filter, match, sort]); + + const afterRender = useAfterRender(); + const { isFocused } = useManagedFocus(); + const { height } = usePanelSize(); + + useInput( + (input, _) => { + if (input === "r") { + refresh?.(); + } + }, + { isActive: isFocused && !!refresh } + ); + + const topbar = !!match || !!buttons?.length; + + return ( + + {topbar && ( + + {!!match && ( + + Search: + { + if (orderedItems.length === 1) onSelect?.(orderedItems[0]); + }} + /> + + )} + {afterRender && !!buttons?.length && } + + )} + {status}} + isFocused={isFocused} + onSelect={onSelect} + /> + + ); +} diff --git a/src/tui/components/ui/UserName.tsx b/src/tui/components/ui/UserName.tsx new file mode 100644 index 0000000..4776d95 --- /dev/null +++ b/src/tui/components/ui/UserName.tsx @@ -0,0 +1,16 @@ +import type { ComponentProps } from "react"; +import { useDataUserById } from "../../../react/hooks/data/users.js"; +import { Text } from "ink"; + +export function UserName({ + uid, + ...props +}: { uid: string } & ComponentProps) { + const { status, user } = useDataUserById(uid); + + return user ? ( + {user.name} + ) : ( + {status} + ); +} diff --git a/src/tui/contexts/BgColor.tsx b/src/tui/contexts/BgColor.tsx new file mode 100644 index 0000000..c94ffd1 --- /dev/null +++ b/src/tui/contexts/BgColor.tsx @@ -0,0 +1,4 @@ +import { createContext } from "react"; + +export const BgColorContext = createContext("black"); +BgColorContext.displayName = "BgColorContext"; diff --git a/src/tui/contexts/FocusManager.tsx b/src/tui/contexts/FocusManager.tsx new file mode 100644 index 0000000..74e44d3 --- /dev/null +++ b/src/tui/contexts/FocusManager.tsx @@ -0,0 +1,66 @@ +import { useFocus } from "ink"; +import { + createContext, + use, + useEffect, + useId, + useState, + type ReactNode, +} from "react"; + +const FocusManagerWriteContext = createContext< + ((fn: (prev: string[]) => string[]) => void) | null +>(null); +FocusManagerWriteContext.displayName = "FocusManagerWriteContext"; +const FocusManagerReadContext = createContext([]); +FocusManagerReadContext.displayName = "FocusManagerReadContext"; + +export const PopupContext = createContext(null); +PopupContext.displayName = "PopupContext"; + +export function FocusManager({ children }: { children: ReactNode }) { + const [popupStack, setPopupStack] = useState([]); + + return ( + + + {children} + + + ); +} + +export function usePopup() { + const id = useId(); + const setPopupStack = use(FocusManagerWriteContext)!; + const popupStack = use(FocusManagerReadContext); + useEffect(() => { + setPopupStack((prev) => [...prev, id]); + return () => setPopupStack((prev) => prev.filter((x) => x !== id)); + }, [id]); + return { id, active: id === (popupStack.at(-1) ?? null) }; +} +export function useActive() { + const id = use(PopupContext); + const popupStack = use(FocusManagerReadContext); + return id === (popupStack.at(-1) ?? null); +} + +export function useManagedFocus({ + isActive = true, + autoFocus = false, + id, +}: Parameters[0] = {}): ReturnType { + const active = useActive(); + const computedId = useId(); + id ??= computedId; + const { focus, isFocused } = useFocus({ + isActive: active && isActive, + autoFocus: active && autoFocus, + id, + }); + useEffect(() => { + if (active && autoFocus) focus(id); + }, [active, autoFocus, id]); + return { focus, isFocused: isFocused && active }; +} diff --git a/src/tui/hooks/size.ts b/src/tui/hooks/size.ts new file mode 100644 index 0000000..54190fa --- /dev/null +++ b/src/tui/hooks/size.ts @@ -0,0 +1,17 @@ +import { useScreenSize } from "fullscreen-ink"; + +export function usePanelSize() { + const { width, height } = useScreenSize(); + return { + width: width - 13, + height: height - 4, + }; +} + +export function useFreeSize() { + const { width, height } = useScreenSize(); + return { + width, + height: height - 4, + }; +} diff --git a/src/tui/hooks/useAfterRender.ts b/src/tui/hooks/useAfterRender.ts new file mode 100644 index 0000000..73b23e3 --- /dev/null +++ b/src/tui/hooks/useAfterRender.ts @@ -0,0 +1,9 @@ +import { useEffect, useState } from "react"; + +export function useAfterRender(): boolean { + const [afterRender, setAfterRender] = useState(false); + useEffect(() => { + setAfterRender(true); + }, []); + return afterRender; +} diff --git a/src/util/error.ts b/src/util/error.ts new file mode 100644 index 0000000..6c1018a --- /dev/null +++ b/src/util/error.ts @@ -0,0 +1,32 @@ +export function stringError(error: unknown): string { + if (error instanceof Error) return error.message; + if ( + typeof error === "string" || + typeof error === "number" || + typeof error === "boolean" + ) + return "" + error; + if ( + error && + typeof error === "object" && + "message" in error && + typeof error.message === "string" + ) + return error.message; + return "unknown_error"; +} + +export type ObjectError = { + message: string; + stack?: string; + cause?: ObjectError; +}; +export function objectError(error: unknown): ObjectError { + const message = stringError(error); + const object: ObjectError = { message }; + if (error instanceof Error) { + if (error.stack) object.stack = error.stack; + if (error.cause) object.cause = objectError(error.cause); + } + return object; +} diff --git a/src/util/hash.ts b/src/util/hash.ts new file mode 100644 index 0000000..918cd15 --- /dev/null +++ b/src/util/hash.ts @@ -0,0 +1,25 @@ +import { argon2id, argon2Verify } from "hash-wasm"; + +export async function validatePassword( + password: string, + hash: string +): Promise { + return await argon2Verify({ password, hash }); +} + +export async function hashPassword( + password: string +): Promise<`$${string}$${string}`> { + const salt = new Uint8Array(16); + crypto.getRandomValues(salt); + const hash = await argon2id({ + password, + outputType: "encoded", + hashLength: 32, + iterations: 3, + memorySize: 65536, + parallelism: 4, + salt, + }); + return hash as `$${string}$${string}`; +} diff --git a/src/util/length.ts b/src/util/length.ts new file mode 100644 index 0000000..4422358 --- /dev/null +++ b/src/util/length.ts @@ -0,0 +1 @@ +export const lengthOfUuid = 36; diff --git a/src/util/token.ts b/src/util/token.ts new file mode 100644 index 0000000..f0bbf33 --- /dev/null +++ b/src/util/token.ts @@ -0,0 +1,19 @@ +function createTokenPart(): string { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + return [...bytes].map((x) => x.toString(16).padStart(2, "0")).join(""); +} + +export function createSessionToken(): `as_${string}` { + return `as_${createTokenPart()}`; +} +export function createApikeyToken(): `at_${string}` { + return `at_${createTokenPart()}`; +} + +export function isSessionToken(token: string): token is `as_${string}` { + return token.startsWith("as_"); +} +export function isApikeyToken(token: string): token is `at_${string}` { + return token.startsWith("at_"); +} diff --git a/src/util/ts.ts b/src/util/ts.ts new file mode 100644 index 0000000..710f112 --- /dev/null +++ b/src/util/ts.ts @@ -0,0 +1,4 @@ +export function idAssert(a: unknown): asserts a is T {} +export function id(a: T): T { + return a; +} diff --git a/src/util/xmlwriter.ts b/src/util/xmlwriter.ts new file mode 100644 index 0000000..61383a4 --- /dev/null +++ b/src/util/xmlwriter.ts @@ -0,0 +1,209 @@ +const escapes = { + '"': """, + "'": "'", + "<": "<", + ">": ">", + "&": "&", +}; + +function parseAdd( + rest: (Record | string | ((writer: XmlWriter) => T))[] +): [ + props?: Record, + content?: string, + children?: (writer: XmlWriter) => T +] { + let props: Record | undefined; + let children: ((writer: XmlWriter) => T) | undefined; + let content: string | undefined; + for (const item of rest) { + if (typeof item === "function") children = item; + else if (typeof item === "object") props = item; + else content = item; + } + if (children && content) + throw new Error("XML cannot have both children and text content"); + return [props, content, children]; +} + +export function escapeXml(text: string): string { + return text.replaceAll(/["'<>&]/g, (x) => escapes[x as keyof typeof escapes]); +} + +export class XmlWriter { + #content: string; + #indent: string | null; + #stack: { tag: string; children: boolean }[]; + + constructor({ + indent = false, + header = true, + }: { indent?: string | boolean; header?: string | boolean } = {}) { + if (indent === true) indent = "\t"; + this.#indent = indent || null; + if (header === true) header = ''; + this.#content = (header || "") + (this.#indent ? "\n" : ""); + this.#stack = []; + } + + get content(): string { + if (this.#stack.length) throw new Error("In the middle of building"); + return this.#content; + } + + #addPre( + tag: string, + props?: Record, + content?: string, + children?: NonNullable + ) { + const top = this.#stack.at(-1); + if (top && !top.children) { + top.children = true; + this.#content += ">" + (this.#indent ? "\n" : ""); + } + + if (this.#indent) this.#content += this.#indent.repeat(this.#stack.length); + this.#content += "<" + tag; + if (props) { + for (const [key, value] of Object.entries(props)) { + this.#content += " " + key + '="' + escapeXml(value) + '"'; + } + } + + if (content) { + this.#content += + ">" + + escapeXml(content) + + "" + + (this.#indent ? "\n" : ""); + } else if (children) { + this.#stack.push({ tag, children: false }); + } else { + this.#content += "/>" + (this.#indent ? "\n" : ""); + } + } + #addPost(tag: string) { + const top = this.#stack.pop()!; + if (top.children) { + if (this.#indent) + this.#content += this.#indent.repeat(this.#stack.length); + this.#content += "" + (this.#indent ? "\n" : ""); + } else { + this.#content += "/>" + (this.#indent ? "\n" : ""); + } + } + + add(tag: string): XmlWriter; + add(tag: string, content: string): XmlWriter; + add(tag: string, props: Record): XmlWriter; + add(tag: string, props: Record, content: string): XmlWriter; + add(tag: string, children: (writer: XmlWriter) => void): XmlWriter; + add( + tag: string, + props: Record, + children: (writer: XmlWriter) => void + ): XmlWriter; + add( + tag: string, + ...rest: (Record | string | ((writer: XmlWriter) => void))[] + ): XmlWriter { + const [props, content, children] = parseAdd(rest); + this.#addPre(tag, props, content, children); + if (children) { + children(this); + this.#addPost(tag); + } + return this; + } + + addAsync(tag: string): Promise; + addAsync(tag: string, content: string): Promise; + addAsync(tag: string, props: Record): Promise; + addAsync( + tag: string, + props: Record, + content: string + ): Promise; + addAsync( + tag: string, + children: (writer: XmlWriter) => Promise + ): Promise; + addAsync( + tag: string, + props: Record, + children: (writer: XmlWriter) => Promise + ): Promise; + async addAsync( + tag: string, + ...rest: ( + | Record + | string + | ((writer: XmlWriter) => Promise) + )[] + ): Promise { + const [props, content, children] = parseAdd(rest); + this.#addPre(tag, props, content, children); + if (children) { + await children(this); + this.#addPost(tag); + } + } + + static build( + options: NonNullable[0]>, + ...rest: Parameters["add"]> + ): string; + static build( + ...rest: Parameters["add"]> + ): string; + static build( + ...rest: + | Parameters["add"]> + | [ + NonNullable[0]>, + ...Parameters["add"]> + ] + ): string { + let options: ConstructorParameters[0]; + if (typeof rest[0] === "object") { + options = rest.shift()! as ConstructorParameters[0]; + } + return new XmlWriter(options).add( + ...(rest as Parameters["add"]>) + ).content; + } + + static buildAsync( + options: NonNullable[0]>, + ...rest: Parameters["addAsync"]> + ): Promise; + static buildAsync( + ...rest: Parameters["addAsync"]> + ): Promise; + static async buildAsync( + ...rest: + | Parameters["addAsync"]> + | [ + NonNullable[0]>, + ...Parameters["addAsync"]> + ] + ): Promise { + let options: ConstructorParameters[0]; + if (typeof rest[0] === "object") { + options = rest.shift()! as ConstructorParameters[0]; + } + const writer = new XmlWriter(options); + await writer.addAsync( + ...(rest as Parameters["addAsync"]>) + ); + return writer.content; + } + + static escape = escapeXml; +} + +export const buildXml = XmlWriter.build; +export const buildXmlAsync = XmlWriter.buildAsync; diff --git a/src/webapi/apirouter.ts b/src/webapi/apirouter.ts new file mode 100644 index 0000000..be970cb --- /dev/null +++ b/src/webapi/apirouter.ts @@ -0,0 +1,200 @@ +import KoaRouter from "@koa/router"; +import type { BackendDbInterface } from "../db/types/DbInterface.js"; +import { convertError } from "./middleware/convertError.js"; +import { jsonBody } from "./middleware/jsonBody.js"; +import { + createabode, + createapikey, + createnote, + createresident, + createuser, + loginuser, + updateabode, + updateresident, + updateuser, +} from "../schema/validators.js"; +import { authenticate } from "./middleware/authenticate.js"; +import { InvalidAbodeError, NotFoundAbodeError } from "../db/types/errors.js"; + +export function apirouter(db: BackendDbInterface): KoaRouter { + const router = new KoaRouter(); + + router.use(convertError); + router.post("/auth/login", jsonBody({ validate: loginuser }), async (ctx) => { + const user = await db.getUserByLogin(ctx.request.body); + const token = await db.createSession(user.uid); + ctx.cookies.set("abode_session", token); + ctx.body = user; + }); + router.post("/auth/logout", authenticate(db), async (ctx) => { + if (ctx.session!.source !== "session") throw new InvalidAbodeError(); + ctx.cookies.set("abode_session", "", { expires: new Date("1970-01-01") }); + ctx.status = 204; + }); + router.get("/auth/self", authenticate(db), async (ctx) => { + ctx.body = ctx.user!; + }); + router.post("/auth/clear-sessions", authenticate(db), async (ctx) => { + await db.deleteSessionsByUser(ctx.user!.uid); + ctx.status = 204; + }); + + router.use("/users", authenticate(db)); + router.get("/users", async (ctx) => { + ctx.body = await db.listUsers(); + }); + router.post("/users", jsonBody({ validate: createuser }), async (ctx) => { + ctx.body = await db.createUser(ctx.request.body); + }); + router.get("/users/by-email", async (ctx) => { + if (typeof ctx.query.email !== "string") throw new InvalidAbodeError(); + ctx.body = await db.getUserByEmail(ctx.query.email); + }); + router.get("/users/:uid", async (ctx) => { + ctx.body = await db.getUserById(ctx.params.uid); + }); + router.patch( + "/users/:uid", + jsonBody({ validate: updateuser, includeParams: ["uid"] }), + async (ctx) => { + ctx.body = await db.updateUser(ctx.request.body); + } + ); + router.delete("/users/:uid", async (ctx) => { + await db.deleteUserById(ctx.params.uid); + ctx.status = 204; + }); + router.get("/users/:uid/residents", async (ctx) => { + ctx.body = await db.listResidentsByUserId(ctx.params.uid); + }); + router.get("/users/:uid/abodes", async (ctx) => { + ctx.body = await db.listAbodesByUserId(ctx.params.uid); + }); + router.get("/users/:uid/apikeys", async (ctx) => { + ctx.body = await db.listApikeysByUser(ctx.params.uid); + }); + router.post( + "/users/:uid/apikeys", + jsonBody({ validate: createapikey, includeParams: ["uid"] }), + async (ctx) => { + const [apikey, token] = await db.createApikey(ctx.request.body); + ctx.body = { apikey, token }; + } + ); + router.get("/users/:uid/apikeys/:kid", async (ctx) => { + const apikey = await db.getApikeyById(ctx.params.kid); + if (apikey.uid !== ctx.params.uid) throw new NotFoundAbodeError(); + ctx.body = apikey; + }); + router.delete("/users/:uid/apikeys/:kid", async (ctx) => { + const apikey = await db.getApikeyById(ctx.params.kid); + if (apikey.uid !== ctx.params.uid) throw new NotFoundAbodeError(); + await db.deleteApikeyById(ctx.params.kid); + ctx.status = 204; + }); + router.post("/user/:uid/auth/clear-sessions", async (ctx) => { + await db.deleteSessionsByUser(ctx.params.uid); + ctx.status = 204; + }); + router.get("/user/:uid/notes", async (ctx) => { + ctx.body = await db.listNotesByUserId(ctx.params.uid); + }); + + router.use("/abodes", authenticate(db)); + router.get("/abodes", async (ctx) => { + ctx.body = await db.listAbodes(); + }); + router.post("/abodes", jsonBody({ validate: createabode }), async (ctx) => { + ctx.body = await db.createAbode(ctx.request.body, { uid: ctx.user!.uid }); + }); + router.get("/abodes/:aid", async (ctx) => { + ctx.body = await db.getAbodeById(ctx.params.aid); + }); + router.patch( + "/abodes/:aid", + jsonBody({ validate: updateabode, includeParams: ["aid"] }), + async (ctx) => { + ctx.body = await db.updateAbode(ctx.request.body, { uid: ctx.user!.uid }); + } + ); + router.delete("/abodes/:aid", async (ctx) => { + await db.deleteAbodeById(ctx.params.aid); + ctx.status = 204; + }); + router.get("/abodes/:aid/residents", async (ctx) => { + ctx.body = await db.listResidentsByAbodeId(ctx.params.aid); + }); + router.get("/abodes/:aid/users", async (ctx) => { + ctx.body = await db.listUsersByAbodeId(ctx.params.aid); + }); + router.get("/abodes/:aid/notes", async (ctx) => { + ctx.body = await db.listNotesByAbodeId(ctx.params.aid); + }); + router.post( + "/abodes/:aid/notes", + jsonBody({ validate: createnote, includeParams: ["aid"] }), + async (ctx) => { + ctx.body = await db.createNote(ctx.request.body, { uid: ctx.user!.uid }); + } + ); + + router.use("/residents", authenticate(db)); + router.get("/residents", async (ctx) => { + ctx.body = await db.listResidents(); + }); + router.post( + "/residents", + jsonBody({ validate: createresident }), + async (ctx) => { + ctx.body = await db.createResident(ctx.request.body, { + uid: ctx.user!.uid, + }); + } + ); + router.get("/residents/:uid/:aid", async (ctx) => { + ctx.body = await db.getResidentById(ctx.params.uid, ctx.params.aid); + }); + router.patch( + "/residents/:uid/:aid", + jsonBody({ validate: updateresident, includeParams: ["uid", "aid"] }), + async (ctx) => { + ctx.body = await db.updateResident(ctx.request.body, { + uid: ctx.user!.uid, + }); + } + ); + router.delete("/residents/:uid/:aid", async (ctx) => { + await db.deleteResidentById(ctx.params.uid, ctx.params.aid); + ctx.status = 204; + }); + + router.use("/apikeys", authenticate(db)); + router.get("/apikeys/:kid", async (ctx) => { + ctx.body = await db.getApikeyById(ctx.params.kid); + }); + router.delete("/apikeys/:kid", async (ctx) => { + await db.deleteApikeyById(ctx.params.kid); + ctx.status = 204; + }); + + router.use("/notes", authenticate(db)); + router.get("/notes", async (ctx) => { + ctx.body = await db.listNotes(); + }); + router.get("/notes/:nid", async (ctx) => { + ctx.body = await db.getNoteById(ctx.params.nid); + }); + router.patch( + "/notes/:nid", + jsonBody({ includeParams: ["nid"] }), + async (ctx) => { + ctx.body = await db.updateNote(ctx.request.body, { uid: ctx.user!.uid }); + } + ); + router.delete("/notes/:nid", async (ctx) => { + await db.deleteNoteById(ctx.params.nid); + ctx.status = 204; + }); + + return router; +} diff --git a/src/webapi/middleware/authenticate.ts b/src/webapi/middleware/authenticate.ts new file mode 100644 index 0000000..25fb0f9 --- /dev/null +++ b/src/webapi/middleware/authenticate.ts @@ -0,0 +1,139 @@ +import type { Middleware } from "@koa/router"; +import type { ClientUser } from "../../db/types/User.js"; +import type { BackendDbInterface } from "../../db/types/DbInterface.js"; +import { + ConflictAbodeError, + NotAuthorizedAbodeError, + NotFoundAbodeError, +} from "../../db/types/errors.js"; +import type { ClientApikey } from "../../db/types/Apikey.js"; +import { isApikeyToken, isSessionToken } from "../../util/token.js"; + +declare module "koa" { + export interface ExtendableContext { + user?: ClientUser; + session?: + | { source: "basic" } + | { source: "session" } + | { source: "apikey"; key: ClientApikey }; + } +} + +export function authenticate(db: BackendDbInterface): Middleware { + return async (ctx, next) => { + if (ctx.user) return next(); + + const authorization = + ctx.get("X-Abode-Authorization") || ctx.get("Authorization"); + if (authorization) { + if (authorization.startsWith("Basic ")) { + const basic = authorization.slice("Basic ".length); + let email: string, password: string; + try { + const unb64 = atob(basic); + const parts = unb64.split(":"); + if (parts.length < 2) throw new Error(); + email = parts[0]; + password = parts.slice(1).join(":"); + } catch (e) { + ctx.status = 400; + ctx.body = { + ok: false, + error: "invalid_basic_auth", + }; + return; + } + try { + ctx.user = await db.getUserByLogin({ email, password }); + ctx.session = { source: "basic" }; + } catch (e) { + if (e instanceof NotFoundAbodeError) { + ctx.status = 401; + ctx.body = { + ok: false, + error: "unknown_user", + }; + return; + } else if (e instanceof ConflictAbodeError) { + ctx.status = 401; + ctx.body = { + ok: false, + error: "user_not_loggable", + }; + return; + } else if (e instanceof NotAuthorizedAbodeError) { + ctx.status = 401; + ctx.body = { + ok: false, + error: "invalid_password", + }; + return; + } + throw e; + } + } else if (authorization.startsWith("Bearer ")) { + const bearer = authorization.slice("Bearer ".length); + if (isApikeyToken(bearer)) { + try { + const [user, apikey] = await db.getUserByApikey(bearer); + ctx.user = user; + ctx.session = { source: "apikey", key: apikey }; + } catch (e) { + if ( + e instanceof NotFoundAbodeError || + e instanceof NotAuthorizedAbodeError + ) { + ctx.status = 401; + ctx.body = { + ok: false, + error: "invalid_apikey", + }; + return; + } + throw e; + } + } else { + ctx.status = 401; + ctx.body = { + ok: false, + error: "unrecognized_bearer", + }; + return; + } + } + } + if (ctx.user) return next(); + + const cookie = ctx.cookies.get("abode_session"); + if (cookie) { + let invalidate = false; + if (isSessionToken(cookie)) { + try { + const user = await db.getUserBySession(cookie); + ctx.user = user; + ctx.session = { source: "session" }; + } catch (e) { + invalidate = true; + } + } else { + invalidate = true; + } + if (invalidate) { + ctx.cookies.set("abode_session", "", { + expires: new Date("1970-01-01"), + }); + } + } + if (ctx.user) return next(); + + if (!ctx.user) { + ctx.status = 401; + ctx.body = { + ok: false, + error: "not_authenticated", + }; + return; + } + return next(); + }; +} diff --git a/src/webapi/middleware/convertError.ts b/src/webapi/middleware/convertError.ts new file mode 100644 index 0000000..90dafa7 --- /dev/null +++ b/src/webapi/middleware/convertError.ts @@ -0,0 +1,35 @@ +import type { Middleware } from "@koa/router"; +import { + ConflictAbodeError, + InvalidAbodeError, + NotAuthorizedAbodeError, + NotFoundAbodeError, + ReadonlyAbodeError, +} from "../../db/types/errors.js"; + +export const convertError: Middleware = async (ctx, next) => { + try { + await next(); + } catch (e) { + if (e instanceof NotFoundAbodeError) { + ctx.status = 404; + ctx.body = { ok: false, error: "not_found" }; + } else if (e instanceof NotAuthorizedAbodeError) { + ctx.status = 401; + ctx.body = { ok: false, error: "not_authorized" }; + } else if (e instanceof ConflictAbodeError) { + ctx.status = 409; + ctx.body = { ok: false, error: "conflict" }; + } else if (e instanceof InvalidAbodeError) { + ctx.status = 400; + ctx.body = { ok: false, error: "invalid" }; + } else if (e instanceof ReadonlyAbodeError) { + ctx.status = 403; + ctx.body = { ok: false, error: "readonly" }; + } else { + ctx.status = 500; + ctx.body = { ok: false, error: "unknown" }; + console.error(ctx.method, ctx.path, e); + } + } +}; diff --git a/src/webapi/middleware/jsonBody.ts b/src/webapi/middleware/jsonBody.ts new file mode 100644 index 0000000..8b64f0e --- /dev/null +++ b/src/webapi/middleware/jsonBody.ts @@ -0,0 +1,64 @@ +import { bodyParser } from "@koa/bodyparser"; +import type { Middleware } from "@koa/router"; +import type { AnySchema, ErrorObject } from "ajv"; +import type { Context } from "koa"; + +export function jsonBody({ + validate, + patchBody, + includeParams, +}: { + validate?: { + (body: unknown): boolean; + errors?: null | Partial[]; + schema: AnySchema; + }; + patchBody?: (ctx: Context) => void; + includeParams?: string[]; +} = {}): Middleware { + const parseBody = bodyParser({ enableTypes: ["json"], encoding: "utf8" }); + return async (ctx, next) => { + await parseBody(ctx, async () => { + if (typeof ctx.request.body === "undefined") { + ctx.status = 400; + ctx.body = { + ok: false, + error: "missing_body", + }; + return; + } + if (patchBody) patchBody(ctx); + if ( + includeParams && + typeof ctx.request.body === "object" && + !Array.isArray(ctx.request.body) && + ctx.request.body + ) { + for (const param of includeParams) { + if (param in ctx.request.body) { + if (ctx.request.body[param] !== ctx.params[param]) { + ctx.status = 400; + ctx.body = { + ok: false, + error: "mismatch_params", + param, + }; + return; + } + } else ctx.request.body[param] = ctx.params[param]; + } + } + if (validate && !validate(ctx.request.body)) { + ctx.status = 400; + ctx.body = { + ok: false, + error: "jsonchema_validation_failed", + schema: validate.schema, + errors: validate.errors!, + }; + return; + } + await next(); + }); + }; +} diff --git a/src/webapi/middleware/logRequests.ts b/src/webapi/middleware/logRequests.ts new file mode 100644 index 0000000..ec90dc4 --- /dev/null +++ b/src/webapi/middleware/logRequests.ts @@ -0,0 +1,6 @@ +import type { Middleware } from "@koa/router"; + +export const logRequests: Middleware = async (ctx, next) => { + await next(); + console.log(`${ctx.status} - ${ctx.method.toUpperCase()} ${ctx.path}`); +}; diff --git a/src/webapi/schemarouter.ts b/src/webapi/schemarouter.ts new file mode 100644 index 0000000..c696c82 --- /dev/null +++ b/src/webapi/schemarouter.ts @@ -0,0 +1,30 @@ +import KoaRouter from "@koa/router"; +import * as validators from "../schema/validators.js"; + +export function schemarouter(): KoaRouter { + const router = new KoaRouter(); + + router.get("/", (ctx) => { + ctx.body = Object.fromEntries( + Object.entries(validators).map(([name, { schema }]) => { + return [ + name, + { + $id: schema.$id, + title: schema.title, + description: schema.description, + url: `${ctx.URL}/${name}.schema.json`, + }, + ]; + }) + ); + }); + + for (const [name, { schema }] of Object.entries(validators)) { + router.get(`/${name}.schema.json`, (ctx) => { + ctx.body = schema; + }); + } + + return router; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f33ae09 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "rootDir": "src", + "strict": true, + "verbatimModuleSyntax": true, + "moduleResolution": "nodenext", + "module": "nodenext", + "target": "esnext", + "allowImportingTsExtensions": false, + "noEmit": true, + "sourceMap": true, + "jsx": "react-jsxdev" + }, + "include": ["src/**/*.ts","src/**/*.tsx"] +} \ No newline at end of file diff --git a/webpack.config.ts b/webpack.config.ts new file mode 100644 index 0000000..70e257d --- /dev/null +++ b/webpack.config.ts @@ -0,0 +1,284 @@ +import { DefinePlugin, IgnorePlugin, type Configuration } from "webpack"; +import { findNatives } from "./src/meta/pack/natives.ts"; +import { existingSources } from "./src/meta/pack/sources.ts"; +import CopyPlugin from "copy-webpack-plugin"; +import { BundleAnalyzerPlugin } from "webpack-bundle-analyzer"; +import { readdir } from "node:fs/promises"; + +const self = new URL(import.meta.url).pathname; +function file(path: string): string { + return new URL(path, import.meta.url).pathname; +} + +export default async (): Promise => { + // global webpack config options + const defines: Record = {}; + const copies: CopyPlugin.Pattern[] = []; + const aliases: Record = {}; + const compiledSources: string[] = []; + + // read environment to configure the build + const env = + process.env.NODE_ENV === "production" ? "production" : "development"; + + const dbSources = + process.env.DB_SOURCES === "dynamic" + ? "dynamic" + : process.env.DB_SOURCES === "static" + ? "static" + : "shared"; + + let disableDbSqlite = process.env.DISABLE_DB_SQLITE === "1"; + let disableBs3 = disableDbSqlite || process.env.DISABLE_BS3 === "1"; + let disableNodeSqlite = + disableDbSqlite || process.env.DISABLE_NODE_SQLITE === "1"; + + const disableDbApi = process.env.DISABLE_DB_API === "1"; + + // find natives and add them to the build + const natives: Record = await findNatives(); + if (disableBs3) natives.sqlite = null; + if (!natives.sqlite) disableBs3 = true; + for (const [k, v] of Object.entries(natives)) { + if (!v) { + defines[`natives.${k}`] = "null"; + continue; + } + defines[`natives.${k}`] = + "(__dirname+" + + JSON.stringify("/../natives/" + v.split("/").at(-1)!) + + ")"; + copies.push({ from: v, to: "./natives" }); + } + + // find binaries and add them to the build + const binaries = Object.fromEntries( + (await readdir(file("./src/bin"))).map((bin) => [ + bin.replace(/\..+$/, ""), + file(`./src/bin/${bin}`), + ]) + ); + for (const bin of Object.keys(binaries)) { + copies.push({ + from: self, + to: `./bin/${bin}`, + toType: "file", + transform: () => + `#!/bin/sh\nexec node --enable-source-maps "$(dirname "$0")/${bin}.cjs" "$@"\n`, + }); + } + + // find schemas and add them to the build + const schemas: string[] = []; + { + async function impl(dir: string) { + for (const file of await readdir(dir, { withFileTypes: true })) { + if (file.isDirectory()) { + await impl(dir + "/" + file.name); + } else if (file.isFile() && file.name.endsWith(".schema.json")) { + schemas.push(dir + "/" + file.name); + } + } + } + await impl(file("./src/schema")); + for (const schema of schemas) { + copies.push({ from: schema, to: "./schema" }); + } + } + + // resolve db sources statically or dynamically + if (dbSources === "dynamic") { + console.log("Resolving db interfaces dynamically"); + aliases[file("./src/db/dbSources.ts")] = file("./src/db/dbSources.dyn.ts"); + } else if (dbSources === "static") { + console.log("Resolving db interfaces statically"); + aliases[file("./src/db/dbSources.ts")] = file( + "./src/db/dbSources.static.ts" + ); + } else { + console.log("Resolving db interfaces shared"); + aliases[file("./src/db/dbSources.ts")] = file( + "./src/db/dbSources.shared.ts" + ); + } + + // disable parts or all of the db interface + if (disableBs3 && disableNodeSqlite) disableDbSqlite = true; + if (disableDbSqlite) { + console.log("Disabling sqlite db interface completely"); + aliases[file("./src/db/sqlite/getdb.static.ts")] = file("./src/db/stub.ts"); + aliases[file("./src/db/sqlite/getdb.dyn.ts")] = file("./src/db/stub.ts"); + } else if (disableBs3) { + console.log("Disabling better-sqlite3 sqlite db backend"); + aliases[file("./src/db/sqlite/impl/implementations.ts")] = file( + "./src/db/sqlite/impl/implementations.node.ts" + ); + compiledSources.push("sqlite"); + } else if (disableNodeSqlite) { + console.log("Disabling node:sqlite sqlite db backend"); + aliases[file("./src/db/sqlite/impl/implementations.ts")] = file( + "./src/db/sqlite/impl/implementations.bs3.ts" + ); + compiledSources.push("sqlite"); + } else { + console.log( + "Enabling sqlite db interface with better-sqlite3 and node:sqlite backends" + ); + aliases[file("./src/db/sqlite/impl/implementations.ts")] = file( + "./src/db/sqlite/impl/implementations.all.ts" + ); + compiledSources.push("sqlite"); + } + + // disable the api db interface + if (disableDbApi) { + console.log("Disabling api db interface completely"); + aliases[file("./src/db/api/getdb.static.ts")] = file("./src/db/stub.ts"); + aliases[file("./src/db/api/getdb.dyn.ts")] = file("./src/db/stub.ts"); + } else { + console.log("Enabling api db interface"); + compiledSources.push("api"); + } + + // check the compiled sources and add it to the defines + compiledSources.sort(); + defines.compiledSources = JSON.stringify(compiledSources); + for (const source of existingSources) + defines[`compiledSources.${source}`] = JSON.stringify( + compiledSources.includes(source) + ); + if (!compiledSources.length) { + console.warn( + "No db interface enabled, the builds will be completely useless" + ); + process.exit(1); + } + + // log about the natives we have + console.log( + `Using ${Object.values(natives).filter(Boolean).length} natives:` + ); + for (const [key, path] of Object.entries(natives)) { + if (path) console.log(`- ${key}: ${path}`); + else console.warn(`- ${key}: (not found)`); + } + + // log about the binaries we're making + console.log(`Generating ${Object.entries(binaries).length} binaries:`); + for (const [key, path] of Object.entries(binaries)) { + console.log(`- ${key}: ${path}`); + } + + // generate a json report for the stuff included in the build + { + const report = { + binaries: Object.keys(binaries), + natives: Object.entries(natives) + .filter((x) => x[1]) + .map((x) => x[0]), + db: { mode: dbSources, sources: compiledSources }, + schemas: schemas.map((x) => x.split("/").at(-1)!), + }; + if (!disableDbSqlite) + Object.assign(report.db, { + sqlite: { bs3: !disableBs3, node: !disableNodeSqlite }, + }); + copies.push({ + from: self, + to: "./report/build.json", + toType: "file", + transform: () => JSON.stringify(report, null, 2), + }); + } + + const config: Configuration[] = []; + + config.push({ + mode: env, + devtool: "source-map", + target: "node", + entry: binaries, + output: { + path: file("./dist"), + filename: "bin/[name].cjs", + chunkFilename: "chunk/bin.[name].chunk.cjs", + }, + resolve: { + extensionAlias: { + ".js": [".js", ".ts", ".tsx"], + }, + extensions: [".js", ".ts"], + alias: { + ...aliases, + }, + }, + module: { + rules: [ + { + test: /\.sql$/, + include: file("./src"), + loader: "raw-loader", + }, + { + type: "javascript/esm", + include: file("./src/schema/validators.ts"), + use: [ + { + loader: "val-loader", + options: { + executableFile: file("./src/meta/pack/valLoader.ts"), + loader: file("./src/meta/pack/validators.ts"), + }, + }, + ], + }, + { + test: /\.tsx?$/, + type: "javascript/esm", + include: file("./src"), + use: [ + { + loader: "ts-loader", + options: { + ignoreDiagnostics: true, + compilerOptions: { + noEmit: false, + jsx: "react-jsx", + diagnostics: false, + noEmitOnError: false, + }, + }, + }, + ], + }, + ], + }, + plugins: [ + new DefinePlugin({ + ...defines, + "import.meta.hot": "undefined", + }), + new IgnorePlugin({ + resourceRegExp: /^react-devtools-core$|^\.\/devtools\.js$|^bindings$/, + }), + new CopyPlugin({ + patterns: copies, + }), + new BundleAnalyzerPlugin({ + openAnalyzer: false, + analyzerMode: "static", + reportFilename: "report/bin.html", + }), + ], + ignoreWarnings: [ + { + // webpack doesn't like the `require` used by better-sqlite3 + // it's an indirect call to a runtime string, so it can't be analyzed + // the way we use it however, we force the argument to be constant at build time + module: /\/node_modules\/better-sqlite3\/lib\/database\.js/, + }, + ], + }); + + return config; +};