From a0100ceade63c55c83419d9788cb0284cf8b2fca Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sat, 14 Dec 2024 22:04:51 -0300 Subject: [PATCH 001/243] chore(scripts): improve script names --- package.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index ea838d0c..d60a4aa6 100644 --- a/package.json +++ b/package.json @@ -43,20 +43,20 @@ "private": true, "scripts": { "build": "turbo run build", - "build:android": "pnpm --filter @deepnotes/client run build:android", - "build:electron": "pnpm --filter @deepnotes/client run build:electron", - "build:electron:publish": "pnpm --filter @deepnotes/client run build:electron:publish", - "build:ios": "pnpm --filter @deepnotes/client run build:ios", - "build:spa": "pnpm --filter @deepnotes/client run build:spa", - "build:ssr": "pnpm --filter @deepnotes/client run build:ssr", + "android:build": "pnpm --filter @deepnotes/client run build:android", + "electron:build": "pnpm --filter @deepnotes/client run build:electron", + "electron:build:publish": "pnpm --filter @deepnotes/client run build:electron:publish", + "ios:build": "pnpm --filter @deepnotes/client run build:ios", + "spa:build": "pnpm --filter @deepnotes/client run build:spa", + "ssr:build": "pnpm --filter @deepnotes/client run build:ssr", "build:watch": "turbo run build:watch", "clean": "turbo run clean", "dev": "turbo run dev --parallel", - "dev:android": "pnpm --filter @deepnotes/client run dev:android", - "dev:electron": "pnpm --filter @deepnotes/client run dev:electron", - "dev:ios": "pnpm --filter @deepnotes/client run dev:ios", - "dev:spa": "pnpm --filter @deepnotes/client run dev:spa", - "dev:ssr": "pnpm --filter @deepnotes/client run dev:ssr", + "android:dev": "pnpm --filter @deepnotes/client run dev:android", + "electron:dev": "pnpm --filter @deepnotes/client run dev:electron", + "ios:dev": "pnpm --filter @deepnotes/client run dev:ios", + "spa:dev": "pnpm --filter @deepnotes/client run dev:spa", + "ssr:dev": "pnpm --filter @deepnotes/client run dev:ssr", "fix": "turbo run fix --parallel", "format": "prettier --write \"**/*.{ts,tsx,md}\"", "lint": "turbo run lint", From a4c75195c657c8e49d3d0dd06d1aa78d6449d0ca Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sat, 14 Dec 2024 22:18:29 -0300 Subject: [PATCH 002/243] chore: update lockfile --- pnpm-lock.yaml | 16218 ++++++++++++++++++++++++++--------------------- 1 file changed, 9074 insertions(+), 7144 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68112024..1fb189ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true @@ -174,7 +174,7 @@ importers: version: 2.3.0 ioredis: specifier: npm:@deepnotes/ioredis@^5.3.1 - version: /@deepnotes/ioredis@5.3.1 + version: '@deepnotes/ioredis@5.3.1' jws: specifier: ^4.0.0 version: 4.0.0 @@ -216,7 +216,7 @@ importers: version: 14.3.0 superjson: specifier: npm:@deepnotes/superjson@^1.12.4 - version: /@deepnotes/superjson@1.12.4 + version: '@deepnotes/superjson@1.12.4' unilogr: specifier: ^0.0.27 version: 0.0.27 @@ -292,7 +292,7 @@ importers: version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(y-prosemirror@1.0.20) '@tiptap/extension-collaboration-cursor': specifier: npm:@deepnotes/tiptap-extension-collaboration-cursor@^2.0.0-beta.202 - version: /@deepnotes/tiptap-extension-collaboration-cursor@2.0.0-beta.202(@tiptap/core@2.1.12)(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8) + version: '@deepnotes/tiptap-extension-collaboration-cursor@2.0.0-beta.202(@tiptap/core@2.1.12)(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8)' '@tiptap/extension-highlight': specifier: ^2.1.12 version: 2.1.12(@tiptap/core@2.1.12) @@ -409,10 +409,10 @@ importers: version: 11.9.0 html2canvas: specifier: npm:@deepnotes/html2canvas@^1.4.2 - version: /@deepnotes/html2canvas@1.4.2 + version: '@deepnotes/html2canvas@1.4.2' ioredis: specifier: npm:@deepnotes/ioredis@^5.3.1 - version: /@deepnotes/ioredis@5.3.1 + version: '@deepnotes/ioredis@5.3.1' js-base64: specifier: ^3.7.5 version: 3.7.5 @@ -472,7 +472,7 @@ importers: version: 1.5.3 quasar: specifier: npm:@deepnotes/quasar@^2.13.2 - version: /@deepnotes/quasar@2.13.2 + version: '@deepnotes/quasar@2.13.2' serialize-javascript: specifier: ^6.0.1 version: 6.0.1 @@ -481,7 +481,7 @@ importers: version: 2.1.0 superjson: specifier: npm:@deepnotes/superjson@^1.12.4 - version: /@deepnotes/superjson@1.12.4 + version: '@deepnotes/superjson@1.12.4' turndown: specifier: ^7.1.2 version: 7.1.2 @@ -518,7 +518,7 @@ importers: version: 3.4.0(vite@2.9.16)(vue-i18n@9.6.5) '@quasar/app-vite': specifier: npm:@deepnotes/quasar-app-vite@^2.0.0-alpha.42 - version: /@deepnotes/quasar-app-vite@2.0.0-alpha.42(@deepnotes/quasar@2.13.2)(electron-builder@24.4.0)(electron-packager@17.1.1)(eslint@8.53.0)(pinia@2.0.36)(vue-router@4.2.5)(vue@3.2.47) + version: '@deepnotes/quasar-app-vite@2.0.0-alpha.42(@deepnotes/quasar@2.13.2)(electron-builder@24.4.0)(electron-packager@17.1.1)(eslint@8.53.0)(pinia@2.0.36)(vue-router@4.2.5)(vue@3.2.47)' '@types/argon2-browser': specifier: ^1.18.3 version: 1.18.3 @@ -650,7 +650,7 @@ importers: version: 9.0.0(patch_hash=adqtadvolhkco6cfvcsvmd42ua) ioredis: specifier: npm:@deepnotes/ioredis@^5.3.1 - version: /@deepnotes/ioredis@5.3.1 + version: '@deepnotes/ioredis@5.3.1' jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -729,7 +729,7 @@ importers: version: 9.0.0(patch_hash=adqtadvolhkco6cfvcsvmd42ua) ioredis: specifier: npm:@deepnotes/ioredis@^5.3.1 - version: /@deepnotes/ioredis@5.3.1 + version: '@deepnotes/ioredis@5.3.1' knex: specifier: 2.3.0 version: 2.3.0(pg@8.11.3) @@ -778,7 +778,7 @@ importers: version: 9.0.0(patch_hash=adqtadvolhkco6cfvcsvmd42ua) ioredis: specifier: npm:@deepnotes/ioredis@^5.3.1 - version: /@deepnotes/ioredis@5.3.1 + version: '@deepnotes/ioredis@5.3.1' jsonwebtoken: specifier: ^9.0.2 version: 9.0.2 @@ -845,7 +845,7 @@ importers: version: 9.0.0(patch_hash=adqtadvolhkco6cfvcsvmd42ua) ioredis: specifier: npm:@deepnotes/ioredis@^5.3.1 - version: /@deepnotes/ioredis@5.3.1 + version: '@deepnotes/ioredis@5.3.1' knex: specifier: 2.3.0 version: 2.3.0(pg@8.11.3) @@ -888,7 +888,7 @@ importers: version: 4.2.0 ioredis: specifier: npm:@deepnotes/ioredis@^5.3.1 - version: /@deepnotes/ioredis@5.3.1 + version: '@deepnotes/ioredis@5.3.1' lodash: specifier: ^4.17.21 version: 4.17.21 @@ -1030,7 +1030,7 @@ importers: version: link:../misc ioredis: specifier: npm:@deepnotes/ioredis@^5.3.1 - version: /@deepnotes/ioredis@5.3.1 + version: '@deepnotes/ioredis@5.3.1' lodash: specifier: ^4.17.21 version: 4.17.21 @@ -1116,96 +1116,7298 @@ importers: packages: - /7zip-bin@5.1.1: + 7zip-bin@5.1.1: resolution: {integrity: sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ==} - dev: true - /@_ueberdosis/prosemirror-tables@1.1.3: + '@_ueberdosis/prosemirror-tables@1.1.3': resolution: {integrity: sha512-su3pbFi1DT89g6Cuh72TE0MWWKHmWgHcQJ3ODRkm6XfIppWaGpU49t02ur3sgJc7hUhfQXjB93aSkDgOmIii2w==} + + '@aashutoshrathi/word-wrap@1.2.6': + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + + '@antfu/utils@0.7.6': + resolution: {integrity: sha512-pvFiLP2BeOKA/ZOS6jxx4XhKzdVLHDhGlFEaZ2flWWYf2xOqVniqpk38I04DFRyz+L0ASggl7SkItTc+ZLju4w==} + + '@babel/code-frame@7.22.13': + resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.22.5': + resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.22.20': + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + + '@babel/highlight@7.22.20': + resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.23.0': + resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.23.2': + resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.23.0': + resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} + engines: {node: '>=6.9.0'} + + '@capacitor/android@5.5.1': + resolution: {integrity: sha512-WTnPnpaEvTtaEtTNRbh06Y1afF7A4plY/4uajAL0WW8tdR1FxieadF357yKGiAT6CudI/B+eOu6rxn6qWuphKg==} + peerDependencies: + '@capacitor/core': ^5.5.0 + + '@capacitor/app@5.0.6': + resolution: {integrity: sha512-6ZXVdnNmaYILasC/RjQw+yfTmq2ZO7Q3v5lFcDVfq3PFGnybyYQh+RstBrYri+376OmXOXxBD7E6UxBhrMzXGA==} + peerDependencies: + '@capacitor/core': ^5.0.0 + + '@capacitor/cli@5.5.1': + resolution: {integrity: sha512-/oGd2IIc+k1H/fc7tUzP7vqMtZi0gNcJ4/4wUE2kzAnETxxxHXMM/2V62KfjCby/OOAzJbtI7n5OPlnWE9un1A==} + engines: {node: '>=16.0.0'} + hasBin: true + + '@capacitor/clipboard@5.0.6': + resolution: {integrity: sha512-VsokRAn+0HVWj6riSRdspczEfqFoHbrhS/XRhGoEPsj0uvYPSufy0Kb2dpnSqkeeElhh2Jvn8jmVAzII2XeR9w==} + peerDependencies: + '@capacitor/core': ^5.0.0 + + '@capacitor/core@5.5.1': + resolution: {integrity: sha512-VG6Iv8Q7ZAbvjodxpvjcSe0jfxUwZXnvjbi93ehuJ6eYP8U926qLSXyrT/DToZq+F6v/HyGyVgn3mrE/9jW2Tg==} + + '@capacitor/ios@5.5.1': + resolution: {integrity: sha512-h00qt8u32t8eEbIkuG4IjR0r34YZC0sIXglDH8fRDdA84xDkTybmz3WtdpRWDzh6ukE2RIY7rmD7p410WSJ2yA==} + peerDependencies: + '@capacitor/core': ^5.5.0 + + '@capacitor/splash-screen@5.0.6': + resolution: {integrity: sha512-9B8wSm89D+LlshFw8B+mjPU8pJNf1WOx2mkMjMvcH0/EqxNaE+ZaO8lPCX+9WvWSEZs3O3l11qiSnOFHeK0t9A==} + peerDependencies: + '@capacitor/core': ^5.0.0 + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@commitlint/cli@18.2.0': + resolution: {integrity: sha512-F/DCG791kMFmWg5eIdogakuGeg4OiI2kD430ed1a1Hh3epvrJdeIAgcGADAMIOmF+m0S1+VlIYUKG2dvQQ1Izw==} + engines: {node: '>=v18'} + hasBin: true + + '@commitlint/config-conventional@18.1.0': + resolution: {integrity: sha512-8vvvtV3GOLEMHeKc8PjRL1lfP1Y4B6BG0WroFd9PJeRiOc3nFX1J0wlJenLURzl9Qus6YXVGWf+a/ZlbCKT3AA==} + engines: {node: '>=v18'} + + '@commitlint/config-validator@18.1.0': + resolution: {integrity: sha512-kbHkIuItXn93o2NmTdwi5Mk1ujyuSIysRE/XHtrcps/27GuUKEIqBJp6TdJ4Sq+ze59RlzYSHMKuDKZbfg9+uQ==} + engines: {node: '>=v18'} + + '@commitlint/ensure@18.1.0': + resolution: {integrity: sha512-CkPzJ9UBumIo54VDcpmBlaVX81J++wzEhN3DJH9+6PaLeiIG+gkSx8t7C2gfwG7PaiW4HzQtdQlBN5ab+c4vFQ==} + engines: {node: '>=v18'} + + '@commitlint/execute-rule@18.1.0': + resolution: {integrity: sha512-w3Vt4K+O7+nSr9/gFSEfZ1exKUOPSlJaRpnk7Y+XowEhvwT7AIk1HNANH+gETf0zGZ020+hfiMW/Ome+SNCUsg==} + engines: {node: '>=v18'} + + '@commitlint/format@18.1.0': + resolution: {integrity: sha512-So/w217tGWMZZb1yXcUFNF2qFLyYtSVqbnGoMbX8a+JKcG4oB11Gc1adS0ssUOMivtiNpaLtkSHFynyiwtJtiQ==} + engines: {node: '>=v18'} + + '@commitlint/is-ignored@18.1.0': + resolution: {integrity: sha512-fa1fY93J/Nx2GH6r6WOLdBOiL7x9Uc1N7wcpmaJ1C5Qs6P+rPSUTkofe2IOhSJIJoboHfAH6W0ru4xtK689t0Q==} + engines: {node: '>=v18'} + + '@commitlint/lint@18.1.0': + resolution: {integrity: sha512-LGB3eI5UYu5LLayibNrRM4bSbowr1z9uyqvp0c7+0KaSJi+xHxy/QEhb6fy4bMAtbXEvygY0sUu9HxSWg41rVQ==} + engines: {node: '>=v18'} + + '@commitlint/load@18.2.0': + resolution: {integrity: sha512-xjX3d3CRlOALwImhOsmLYZh14/+gW/KxsY7+bPKrzmGuFailf9K7ckhB071oYZVJdACnpY4hDYiosFyOC+MpAA==} + engines: {node: '>=v18'} + + '@commitlint/message@18.1.0': + resolution: {integrity: sha512-8dT/jJg73wf3o2Mut/fqEDTpBYSIEVtX5PWyuY/0uviEYeheZAczFo/VMIkeGzhJJn1IrcvAwWsvJ1lVGY2I/w==} + engines: {node: '>=v18'} + + '@commitlint/parse@18.1.0': + resolution: {integrity: sha512-23yv8uBweXWYn8bXk4PjHIsmVA+RkbqPh2h7irupBo2LthVlzMRc4LM6UStasScJ4OlXYYaWOmuP7jcExUF50Q==} + engines: {node: '>=v18'} + + '@commitlint/read@18.1.0': + resolution: {integrity: sha512-rzfzoKUwxmvYO81tI5o1371Nwt3vhcQR36oTNfupPdU1jgSL3nzBIS3B93LcZh3IYKbCIMyMPN5WZ10BXdeoUg==} + engines: {node: '>=v18'} + + '@commitlint/resolve-extends@18.1.0': + resolution: {integrity: sha512-3mZpzOEJkELt7BbaZp6+bofJyxViyObebagFn0A7IHaLARhPkWTivXdjvZHS12nAORftv88Yhbh8eCPKfSvB7g==} + engines: {node: '>=v18'} + + '@commitlint/rules@18.1.0': + resolution: {integrity: sha512-VJNQ674CRv4znI0DbsjZLVnn647J+BTxHGcrDIsYv7c99gW7TUGeIe5kL80G7l8+5+N0se8v9yn+Prr8xEy6Yw==} + engines: {node: '>=v18'} + + '@commitlint/to-lines@18.1.0': + resolution: {integrity: sha512-aHIoSDjG0ckxPLYDpODUeSLbEKmF6Jrs1B5JIssbbE9eemBtXtjm9yzdiAx9ZXcwoHlhbTp2fbndDb3YjlvJag==} + engines: {node: '>=v18'} + + '@commitlint/top-level@18.1.0': + resolution: {integrity: sha512-1/USHlolIxJlsfLKecSXH+6PDojIvnzaJGPYwF7MtnTuuXCNQ4izkeqDsRuNMe9nU2VIKpK9OT8Q412kGNmgGw==} + engines: {node: '>=v18'} + + '@commitlint/types@18.1.0': + resolution: {integrity: sha512-65vGxZmbs+2OVwEItxhp3Ul7X2m2LyLfifYI/NdPwRqblmuES2w2aIRhIjb7cwUIBHHSTT8WXj4ixVHQibmvLQ==} + engines: {node: '>=v18'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@dabh/diagnostics@2.0.3': + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + + '@deepnotes/html2canvas@1.4.2': + resolution: {integrity: sha512-JXDQkAZ8CJs6feHad9tTSyalZVd74MedZFFcgCAkNo227daCtNvE9cv50qUh1zAJaHvF//G7Nmky7h3G3fYFkw==} + engines: {node: '>=8.0.0'} + + '@deepnotes/ioredis@5.3.1': + resolution: {integrity: sha512-ny3cufMR9HqgKNtjBg1GDUmoWD7hYPyxhiOi5Jm1TCOkbYU5WcGsiamX0w7YCo62BRo76VCYiYXtbpXOzP6rCA==} + engines: {node: '>=12.22.0'} + + '@deepnotes/quasar-app-vite@2.0.0-alpha.42': + resolution: {integrity: sha512-MoBwXbOSqveO++KnfQ8oVzktZ8b0uNuJUvOxpdA7fFiyfK9+JqwgbAOiNtuFzQSODLuwwmGsv5EAtalrqi7hww==} + engines: {node: ^24 || ^22 || ^20 || ^18 || ^16 || ^14.19, npm: '>= 6.14.12', yarn: '>= 1.17.3'} + hasBin: true + peerDependencies: + electron-builder: '>= 22' + electron-packager: '>= 15' + eslint: ^8.11.0 + pinia: ^2.0.0 + quasar: ^2.8.0 + vue: ^3.2.29 + vue-router: ^4.0.12 + vuex: ^4.0.0 + workbox-build: '>= 6' + peerDependenciesMeta: + electron-builder: + optional: true + electron-packager: + optional: true + eslint: + optional: true + pinia: + optional: true + vuex: + optional: true + workbox-build: + optional: true + + '@deepnotes/quasar@2.13.2': + resolution: {integrity: sha512-5WsHR7QcVQ1S5ye8QYNkxbVBYKoUocr4wYmANexmyLXzar2waN3PdTflGLaB8+fKY8ngRBl9jRLnZgAaRalXpg==} + engines: {node: '>= 10.18.1', npm: '>= 6.13.4', yarn: '>= 1.21.1'} + + '@deepnotes/simple-lru-cache@0.0.2': + resolution: {integrity: sha512-jD3J8lvxoq2xKgsTXD7GwWU02fBqrdpIXtIAqdb/yxbmGxByt7BrDGnaUTRF6STVq9lTri7bQmlWq+qqF+zHog==} + + '@deepnotes/superjson@1.12.4': + resolution: {integrity: sha512-5qT4by0h8kzPy59adyvlp6y9MQri9jAm5rCNuB9boMpauibceeetbUoMU59fPJZhdArKEndhiMvGss5sp/+6SA==} + engines: {node: '>=10'} + + '@deepnotes/tiptap-extension-collaboration-cursor@2.0.0-beta.202': + resolution: {integrity: sha512-PUor3vgCHaQd3SpVN4v7bShryUx+xVJUaSvyoZd2CndiT5+Gnn1cvoma7Q9AQEiQgAUBK8kgy3ZWLL5bt/JMwg==} + peerDependencies: + '@tiptap/core': ^2.0.0-beta.193 + + '@develar/schema-utils@2.6.5': + resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} + engines: {node: '>= 8.9.0'} + + '@effect/data@0.17.1': + resolution: {integrity: sha512-QCYkLE5Y5Dm5Yax5R3GmW4ZIgTx7W+kSZ7yq5eqQ/mFWa8i4yxbLuu8cudqzdeZtRtTGZKlhDxfFfgVtMywXJg==} + + '@effect/io@0.38.0': + resolution: {integrity: sha512-qlVC9ASxNC+L2NKX5qOV9672CE5wWizfwBSFaX2XLI7CC118WRvohCTIPQ52n50Bj5TmR20+na+U9C7e4VkqzA==} + peerDependencies: + '@effect/data': ^0.17.1 + + '@effect/match@0.32.0': + resolution: {integrity: sha512-04QfnIgCcMnnNbGxTv2xa9/7q1c5kgpsBodqTUZ8eX86A/EdE8Czz+JkVarG00/xE+nYhQLXOXCN9Zj+dtqVkQ==} + peerDependencies: + '@effect/data': ^0.17.1 + '@effect/schema': ^0.33.0 + + '@effect/schema@0.33.1': + resolution: {integrity: sha512-h+fQInui4q3we8fegAygL0Cs5B2DD/+oC3JWthOh8eLcbKkbYM9smCD/PsHuyQ+BaeWiSP5JdvREGlP4Sg+Ysw==} + peerDependencies: + '@effect/data': ^0.17.1 + '@effect/io': ^0.38.0 + + '@electron/asar@3.2.7': + resolution: {integrity: sha512-8FaSCAIiZGYFWyjeevPQt+0e9xCK9YmJ2Rjg5SXgdsXon6cRnU0Yxnbe6CvJbQn26baifur2Y2G5EBayRIsjyg==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/get@1.14.1': + resolution: {integrity: sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw==} + engines: {node: '>=8.6'} + + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + + '@electron/notarize@1.2.4': + resolution: {integrity: sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.0.5': + resolution: {integrity: sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/rebuild@3.3.0': + resolution: {integrity: sha512-S1vgpzIOS1wCJmsYjdLz97MTUV6UTLcMk/HE3w90HYtVxvW+PQdwxLbgsrECX2bysqcnmM5a0K6mXj/gwVgYtQ==} + engines: {node: '>=12.13.0'} + hasBin: true + + '@electron/universal@1.3.4': + resolution: {integrity: sha512-BdhBgm2ZBnYyYRLRgOjM5VHkyFItsbggJ0MHycOjKWdFGYwK97ZFXH54dTvUWEfha81vfvwr5On6XBjt99uDcg==} + engines: {node: '>=8.6'} + + '@electron/universal@1.4.5': + resolution: {integrity: sha512-3vE9WBQnvlulKylrPbyc+9M4xnD7t1JxuCOF0nrFz00XrrkgbqeqxDf90PNcjLiuB4hAZKr1JooVA6KwsXj94w==} + engines: {node: '>=8.6'} + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.14.54': + resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.0': + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.10.0': + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.3': + resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.53.0': + resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@fastify/ajv-compiler@3.5.0': + resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==} + + '@fastify/cookie@9.1.0': + resolution: {integrity: sha512-w/LlQjj7cmYlQNhEKNm4jQoLkFXCL73kFu1Jy3aL7IFbYEojEKur0f7ieCKUxBBaU65tpaWC83UM8xW7AzY6uw==} + + '@fastify/cors@8.4.1': + resolution: {integrity: sha512-iYQJtrY3pFiDS5mo5zRaudzg2OcUdJ96PD6xfkKOOEilly5nnrFZx/W6Sce2T79xxlEn2qpU3t5+qS2phS369w==} + + '@fastify/deepmerge@1.3.0': + resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} + + '@fastify/error@3.4.1': + resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} + + '@fastify/fast-json-stringify-compiler@4.3.0': + resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + + '@fastify/helmet@11.1.1': + resolution: {integrity: sha512-pjJxjk6SLEimITWadtYIXt6wBMfFC1I6OQyH/jYVCqSAn36sgAIFjeNiibHtifjCd+e25442pObis3Rjtame6A==} + + '@fastify/rate-limit@8.0.3': + resolution: {integrity: sha512-7wbSKXGKKLI1VkpW2XvS7SFg4n4/uzYt0YA5O2pfCcM6PYaBSV3VhSKGJ9/hJceCSH+zNEDRrWpufqxbcDkTZg==} + + '@fastify/websocket@8.2.0': + resolution: {integrity: sha512-B4tlHFBKCX7tenEG9aUcQEpksW2e0+dgRTaH/05+cro1Xsq1+kSj+9IB9Gep7a0KbHZGrat+zBsOas6lRs5dFQ==} + + '@gar/promisify@1.1.3': + resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} + + '@getbrevo/brevo@1.0.1': + resolution: {integrity: sha512-NwUOlkft6NwLSKTph9FWQujMM5ysSGWOa9Wdf0Bc/RezejOW5VG5KXvTmXrF1Q+MHmjzTh6GDWjq5EukQsDdnA==} + + '@hapi/address@4.1.0': + resolution: {integrity: sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==} + deprecated: Moved to 'npm install @sideway/address' + + '@hapi/formula@2.0.0': + resolution: {integrity: sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==} + deprecated: Moved to 'npm install @sideway/formula' + + '@hapi/hoek@9.3.0': + resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} + + '@hapi/joi@17.1.1': + resolution: {integrity: sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg==} + deprecated: Switch to 'npm install joi' + + '@hapi/pinpoint@2.0.1': + resolution: {integrity: sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==} + + '@hapi/topo@5.1.0': + resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + + '@humanwhocodes/config-array@0.11.13': + resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} + engines: {node: '>=10.10.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.1': + resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + + '@hutson/parse-repository-url@3.0.2': + resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} + engines: {node: '>=6.9.0'} + + '@intlify/bundle-utils@2.2.2': + resolution: {integrity: sha512-vngkvlIVV8ZJoyC5VqMvqJd2nvsx+qMN7pQjPiPjOrVndeiR7Dlue0k86Q8FsFUzyksW3HJZZi833ldxwbFzTA==} + engines: {node: '>= 12'} + peerDependencies: + petite-vue-i18n: '*' + vue-i18n: '*' + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + + '@intlify/core-base@9.6.5': + resolution: {integrity: sha512-LzbGXiZkMWPIHnHI0g6q554S87Cmh2mmCmjytK/3pDQfjI84l+dgGoeQuKj02q7EbULRuUUgYVZVqAwEUawXGg==} + engines: {node: '>= 16'} + + '@intlify/message-compiler@9.6.5': + resolution: {integrity: sha512-WeJ499thIj0p7JaIO1V3JaJbqdqfBykS5R8fElFs5hNeotHtPAMBs4IiA+8/KGFkAbjJusgFefCq6ajP7F7+4Q==} + engines: {node: '>= 16'} + + '@intlify/shared@9.6.5': + resolution: {integrity: sha512-gD7Ey47Xi4h/t6P+S04ymMSoA3wVRxGqjxuIMglwRO8POki9h164Epu2N8wk/GHXM/hR6ZGcsx2HArCCENjqSQ==} + engines: {node: '>= 16'} + + '@intlify/vite-plugin-vue-i18n@3.4.0': + resolution: {integrity: sha512-XXcZBgwJ+3FRu11c4ARoY9N00kElPii0/jNZ49qR045Ka7/YGCwb1Ku14BBlMSEHiHDSjLQknLwrJKSQGVZLyA==} + engines: {node: '>= 12'} + peerDependencies: + petite-vue-i18n: ^9.1.0 + vite: ^2.0.0 + vue-i18n: ^9.1.0 + peerDependenciesMeta: + petite-vue-i18n: + optional: true + vue-i18n: + optional: true + + '@ionic/cli-framework-output@2.2.7': + resolution: {integrity: sha512-/BXeclqu3y+bsBF7VFRS9xtNbrXf2JYCj/LeJoyLpWA9PeXNfvFrn91W2lwS2HVDjEDWKl4Ye6edJDdtn76EnA==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-array@2.1.6': + resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-fs@3.1.7': + resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-object@2.1.6': + resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-process@2.1.11': + resolution: {integrity: sha512-Uavxn+x8j3rDlZEk1X7YnaN6wCgbCwYQOeIjv/m94i1dzslqWhqIHEqxEyeE8HsT5Negboagg7GtQiABy+BLbA==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-stream@3.1.6': + resolution: {integrity: sha512-4+Kitey1lTA1yGtnigeYNhV/0tggI3lWBMjC7tBs1K9GXa/q7q4CtOISppdh8QgtOhrhAXS2Igp8rbko/Cj+lA==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-subprocess@2.1.14': + resolution: {integrity: sha512-nGYvyGVjU0kjPUcSRFr4ROTraT3w/7r502f5QJEsMRKTqa4eEzCshtwRk+/mpASm0kgBN5rrjYA5A/OZg8ahqg==} + engines: {node: '>=16.0.0'} + + '@ionic/utils-terminal@2.3.4': + resolution: {integrity: sha512-cEiMFl3jklE0sW60r8JHH3ijFTwh/jkdEKWbylSyExQwZ8pPuwoXz7gpkWoJRLuoRHHSvg+wzNYyPJazIHfoJA==} + engines: {node: '>=16.0.0'} + + '@ioredis/commands@1.2.0': + resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jimp/bmp@0.14.0': + resolution: {integrity: sha512-5RkX6tSS7K3K3xNEb2ygPuvyL9whjanhoaB/WmmXlJS6ub4DjTqrapu8j4qnIWmO4YYtFeTbDTXV6v9P1yMA5A==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/core@0.14.0': + resolution: {integrity: sha512-S62FcKdtLtj3yWsGfJRdFXSutjvHg7aQNiFogMbwq19RP4XJWqS2nOphu7ScB8KrSlyy5nPF2hkWNhLRLyD82w==} + + '@jimp/custom@0.14.0': + resolution: {integrity: sha512-kQJMeH87+kWJdVw8F9GQhtsageqqxrvzg7yyOw3Tx/s7v5RToe8RnKyMM+kVtBJtNAG+Xyv/z01uYQ2jiZ3GwA==} + + '@jimp/gif@0.14.0': + resolution: {integrity: sha512-DHjoOSfCaCz72+oGGEh8qH0zE6pUBaBxPxxmpYJjkNyDZP7RkbBkZJScIYeQ7BmJxmGN4/dZn+MxamoQlr+UYg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/jpeg@0.14.0': + resolution: {integrity: sha512-561neGbr+87S/YVQYnZSTyjWTHBm9F6F1obYHiyU3wVmF+1CLbxY3FQzt4YolwyQHIBv36Bo0PY2KkkU8BEeeQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-blit@0.14.0': + resolution: {integrity: sha512-YoYOrnVHeX3InfgbJawAU601iTZMwEBZkyqcP1V/S33Qnz9uzH1Uj1NtC6fNgWzvX6I4XbCWwtr4RrGFb5CFrw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-blur@0.14.0': + resolution: {integrity: sha512-9WhZcofLrT0hgI7t0chf7iBQZib//0gJh9WcQMUt5+Q1Bk04dWs8vTgLNj61GBqZXgHSPzE4OpCrrLDBG8zlhQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-circle@0.14.0': + resolution: {integrity: sha512-o5L+wf6QA44tvTum5HeLyLSc5eVfIUd5ZDVi5iRfO4o6GT/zux9AxuTSkKwnjhsG8bn1dDmywAOQGAx7BjrQVA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-color@0.14.0': + resolution: {integrity: sha512-JJz512SAILYV0M5LzBb9sbOm/XEj2fGElMiHAxb7aLI6jx+n0agxtHpfpV/AePTLm1vzzDxx6AJxXbKv355hBQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-contain@0.14.0': + resolution: {integrity: sha512-RX2q233lGyaxiMY6kAgnm9ScmEkNSof0hdlaJAVDS1OgXphGAYAeSIAwzESZN4x3ORaWvkFefeVH9O9/698Evg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blit': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + '@jimp/plugin-scale': '>=0.3.5' + + '@jimp/plugin-cover@0.14.0': + resolution: {integrity: sha512-0P/5XhzWES4uMdvbi3beUgfvhn4YuQ/ny8ijs5kkYIw6K8mHcl820HahuGpwWMx56DJLHRl1hFhJwo9CeTRJtQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-crop': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + '@jimp/plugin-scale': '>=0.3.5' + + '@jimp/plugin-crop@0.14.0': + resolution: {integrity: sha512-Ojtih+XIe6/XSGtpWtbAXBozhCdsDMmy+THUJAGu2x7ZgKrMS0JotN+vN2YC3nwDpYkM+yOJImQeptSfZb2Sug==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-displace@0.14.0': + resolution: {integrity: sha512-c75uQUzMgrHa8vegkgUvgRL/PRvD7paFbFJvzW0Ugs8Wl+CDMGIPYQ3j7IVaQkIS+cAxv+NJ3TIRBQyBrfVEOg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-dither@0.14.0': + resolution: {integrity: sha512-g8SJqFLyYexXQQsoh4dc1VP87TwyOgeTElBcxSXX2LaaMZezypmxQfLTzOFzZoK8m39NuaoH21Ou1Ftsq7LzVQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-fisheye@0.14.0': + resolution: {integrity: sha512-BFfUZ64EikCaABhCA6mR3bsltWhPpS321jpeIQfJyrILdpFsZ/OccNwCgpW1XlbldDHIoNtXTDGn3E+vCE7vDg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-flip@0.14.0': + resolution: {integrity: sha512-WtL1hj6ryqHhApih+9qZQYA6Ye8a4HAmdTzLbYdTMrrrSUgIzFdiZsD0WeDHpgS/+QMsWwF+NFmTZmxNWqKfXw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-rotate': '>=0.3.5' + + '@jimp/plugin-gaussian@0.14.0': + resolution: {integrity: sha512-uaLwQ0XAQoydDlF9tlfc7iD9drYPriFe+jgYnWm8fbw5cN+eOIcnneEX9XCOOzwgLPkNCxGox6Kxjn8zY6GxtQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-invert@0.14.0': + resolution: {integrity: sha512-UaQW9X9vx8orQXYSjT5VcITkJPwDaHwrBbxxPoDG+F/Zgv4oV9fP+udDD6qmkgI9taU+44Fy+zm/J/gGcMWrdg==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-mask@0.14.0': + resolution: {integrity: sha512-tdiGM69OBaKtSPfYSQeflzFhEpoRZ+BvKfDEoivyTjauynbjpRiwB1CaiS8En1INTDwzLXTT0Be9SpI3LkJoEA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-normalize@0.14.0': + resolution: {integrity: sha512-AfY8sqlsbbdVwFGcyIPy5JH/7fnBzlmuweb+Qtx2vn29okq6+HelLjw2b+VT2btgGUmWWHGEHd86oRGSoWGyEQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-print@0.14.0': + resolution: {integrity: sha512-MwP3sH+VS5AhhSTXk7pui+tEJFsxnTKFY3TraFJb8WFbA2Vo2qsRCZseEGwpTLhENB7p/JSsLvWoSSbpmxhFAQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blit': '>=0.3.5' + + '@jimp/plugin-resize@0.14.0': + resolution: {integrity: sha512-qFeMOyXE/Bk6QXN0GQo89+CB2dQcXqoxUcDb2Ah8wdYlKqpi53skABkgVy5pW3EpiprDnzNDboMltdvDslNgLQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/plugin-rotate@0.14.0': + resolution: {integrity: sha512-aGaicts44bvpTcq5Dtf93/8TZFu5pMo/61lWWnYmwJJU1RqtQlxbCLEQpMyRhKDNSfPbuP8nyGmaqXlM/82J0Q==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blit': '>=0.3.5' + '@jimp/plugin-crop': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + + '@jimp/plugin-scale@0.14.0': + resolution: {integrity: sha512-ZcJk0hxY5ZKZDDwflqQNHEGRblgaR+piePZm7dPwPUOSeYEH31P0AwZ1ziceR74zd8N80M0TMft+e3Td6KGBHw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + + '@jimp/plugin-shadow@0.14.0': + resolution: {integrity: sha512-p2igcEr/iGrLiTu0YePNHyby0WYAXM14c5cECZIVnq/UTOOIQ7xIcWZJ1lRbAEPxVVXPN1UibhZAbr3HAb5BjQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blur': '>=0.3.5' + '@jimp/plugin-resize': '>=0.3.5' + + '@jimp/plugin-threshold@0.14.0': + resolution: {integrity: sha512-N4BlDgm/FoOMV/DQM2rSpzsgqAzkP0DXkWZoqaQrlRxQBo4zizQLzhEL00T/YCCMKnddzgEhnByaocgaaa0fKw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + '@jimp/plugin-color': '>=0.8.0' + '@jimp/plugin-resize': '>=0.8.0' + + '@jimp/plugins@0.14.0': + resolution: {integrity: sha512-vDO3XT/YQlFlFLq5TqNjQkISqjBHT8VMhpWhAfJVwuXIpilxz5Glu4IDLK6jp4IjPR6Yg2WO8TmRY/HI8vLrOw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/png@0.14.0': + resolution: {integrity: sha512-0RV/mEIDOrPCcNfXSPmPBqqSZYwGADNRVUTyMt47RuZh7sugbYdv/uvKmQSiqRdR0L1sfbCBMWUEa5G/8MSbdA==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/tiff@0.14.0': + resolution: {integrity: sha512-zBYDTlutc7j88G/7FBCn3kmQwWr0rmm1e0FKB4C3uJ5oYfT8645lftUsvosKVUEfkdmOaMAnhrf4ekaHcb5gQw==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/types@0.14.0': + resolution: {integrity: sha512-hx3cXAW1KZm+b+XCrY3LXtdWy2U+hNtq0rPyJ7NuXCjU7lZR3vIkpz1DLJ3yDdS70hTi5QDXY3Cd9kd6DtloHQ==} + peerDependencies: + '@jimp/custom': '>=0.3.5' + + '@jimp/utils@0.14.0': + resolution: {integrity: sha512-MY5KFYUru0y74IsgM/9asDwb3ERxWxXEu3CRCZEvE7DtT86y1bR1XgtlSliMrptjz4qbivNGMQSvUBpEFJDp1A==} + + '@jridgewell/gen-mapping@0.3.3': + resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.1': + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.1.2': + resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.4.15': + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + '@jridgewell/trace-mapping@0.3.20': + resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@lukeed/ms@2.0.1': + resolution: {integrity: sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==} + engines: {node: '>=8'} + + '@malept/cross-spawn-promise@1.1.1': + resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==} + engines: {node: '>= 10'} + + '@malept/cross-spawn-promise@2.0.0': + resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} + engines: {node: '>= 12.13.0'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2': + resolution: {integrity: sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2': + resolution: {integrity: sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2': + resolution: {integrity: sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2': + resolution: {integrity: sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2': + resolution: {integrity: sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2': + resolution: {integrity: sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==} + cpu: [x64] + os: [win32] + + '@nestjs/common@10.2.8': + resolution: {integrity: sha512-rmpwcdvq2IWMmsUVP8rsdKub6uDWk7dwCYo0aif50JTwcvcxzaP3iKVFKoSgvp0RKYu8h15+/AEOfaInmPpl0Q==} + peerDependencies: + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/core@10.2.8': + resolution: {integrity: sha512-9+MZ2s8ixfY9Bl/M9ofChiyYymcwdK9ZWNH4GDMF7Am7XRAQ1oqde6MYGG05rhQwiVXuTwaYLlXciJKfsrg5qg==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + '@nestjs/websockets': ^10.0.0 + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + + '@nestjs/jwt@10.2.0': + resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + + '@nestjs/testing@10.2.8': + resolution: {integrity: sha512-9Kj5IQhM67/nj/MT6Wi2OmWr5YQnCMptwKVFrX1TDaikpY12196v7frk0jVjdT7wms7rV07GZle9I2z0aSjqtQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 + '@nestjs/core': ^10.0.0 + '@nestjs/microservices': ^10.0.0 + '@nestjs/platform-express': ^10.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@npmcli/fs@2.1.2': + resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + '@npmcli/move-file@2.0.1': + resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This functionality has been moved to @npmcli/fs + + '@nuxtjs/opencollective@0.3.2': + resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + '@opentelemetry/api@1.6.0': + resolution: {integrity: sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g==} + engines: {node: '>=8.0.0'} + + '@otplib/core@12.0.1': + resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} + + '@otplib/plugin-crypto@12.0.1': + resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==} + + '@otplib/plugin-thirty-two@12.0.1': + resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==} + + '@otplib/preset-default@12.0.1': + resolution: {integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==} + + '@otplib/preset-v11@12.0.1': + resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/utils@2.4.2': + resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@pnpm/config.env-replace@1.1.0': + resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} + engines: {node: '>=12.22.0'} + + '@pnpm/network.ca-file@1.0.2': + resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} + engines: {node: '>=12.22.0'} + + '@pnpm/npm-conf@2.2.2': + resolution: {integrity: sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==} + engines: {node: '>=12'} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@quasar/extras@1.16.7': + resolution: {integrity: sha512-nYF3gVE/si1YJ/D4qmAiHGwxoJIDCvTT8NI6ZmbTMPrur4J8xBKhfhfhyLoQ4k2jJZP6Rx0rUcB71FBNC2C8vQ==} + + '@quasar/icongenie@3.1.1': + resolution: {integrity: sha512-FreTVI4udcmdAssLN7e70BFGpyCXOTxur/cXJLTnuu42oTfPUVbQ9LZATksutBRppIAbPL4RmAuNML043Erj6w==} + engines: {node: '>= 14.19.0'} + hasBin: true + + '@quasar/render-ssr-error@1.0.2': + resolution: {integrity: sha512-Y0wyqYHVxc1IOBH6pRiKMSWDqO1mwQu11Zo8rw4cBdclPOQqFb7f65UuRbk5LfbqlXV2hYvklNcy0SBAOiAQnw==} + engines: {node: '>= 16'} + + '@quasar/vite-plugin@1.6.0': + resolution: {integrity: sha512-LmbV76G1CwWZbrEQhqyZpkRQTJyO3xpW55aXY1zWN+JhyUeG77CcMCEWteBVnJ6I6ehUPFDC9ONd2+WlwH6rNQ==} + engines: {node: '>=12'} + peerDependencies: + '@vitejs/plugin-vue': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-beta.0 + quasar: ^2.8.0 + vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-beta.0 + vue: ^3.0.0 + + '@reactivedata/reactive@0.2.2': + resolution: {integrity: sha512-KnINM/Sng25QAv6sHkJO9q/XyslLegCF5jTsTSVu+AouY3uZDVf4Am99xNCqsfqFZFvnTBBDvCsHNdvTVGvPEA==} + + '@remirror/core-constants@2.0.2': + resolution: {integrity: sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==} + + '@remirror/core-helpers@3.0.0': + resolution: {integrity: sha512-tusEgQJIqg4qKj6HSBUFcyRnWnziw3neh4T9wOmsPGHFC3w9kl5KSrDb9UAgE8uX6y32FnS7vJ955mWOl3n50A==} + + '@remirror/types@1.0.1': + resolution: {integrity: sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA==} + + '@revenuecat/purchases-capacitor@7.1.1': + resolution: {integrity: sha512-m7fTzFtGq0YaK2M3U0ygkzk53uHeY/JLoy6+gpJtgmrctQPWOkRTEQhCJ74l0LmWpai6j72XFTPPt6GMkV0bBw==} + peerDependencies: + '@capacitor/core': ^5.0.0 + + '@revenuecat/purchases-typescript-internal-esm@7.3.3': + resolution: {integrity: sha512-Vfwj49iQunqxiuy/EDMVdDfqR2LM5BKfAOfFyzV3oDCHpXIsBjSUZT2/zdsoPVreeylVwf4alHf0U2ivA9OHPQ==} + + '@rollup/pluginutils@4.2.1': + resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} + engines: {node: '>= 8.0.0'} + + '@rollup/pluginutils@5.0.5': + resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@sendgrid/client@7.7.0': + resolution: {integrity: sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==} + engines: {node: 6.* || 8.* || >=10.*} + + '@sendgrid/helpers@7.7.0': + resolution: {integrity: sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==} + engines: {node: '>= 6.0.0'} + + '@sendgrid/mail@7.7.0': + resolution: {integrity: sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==} + engines: {node: 6.* || 8.* || >=10.*} + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@sindresorhus/is@0.14.0': + resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} + engines: {node: '>=6'} + + '@sindresorhus/is@0.7.0': + resolution: {integrity: sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==} + engines: {node: '>=4'} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@sindresorhus/is@5.6.0': + resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} + engines: {node: '>=14.16'} + + '@socket.io/component-emitter@3.1.0': + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + + '@stripe/stripe-js@2.1.11': + resolution: {integrity: sha512-GRyInO+VPMjjgUzVPKpDtz+5s8JKssJ99uhWBGo09yxDQBb+bhkm6PxmVa8C+qsSd30JFO1Z+pgIJ0AMmmZJKg==} + + '@syncedstore/core@0.6.0': + resolution: {integrity: sha512-6TtjEoYJsceYi8u1oRecXwbbLmjHaU0S7HvVfOaEdDfphZLGm/faVuA2fpazqc28F0yIFGvYzvPEBUJn9vqRNw==} + peerDependencies: + yjs: ^13.5.13 + + '@syncedstore/yjs-reactive-bindings@0.6.0': + resolution: {integrity: sha512-VF78h0J4iOt79YU9d6j5E6bFKu7WXYuiI2ue9ZnA+T4SNVn8viRvg0AHm3NqHzudZZUgYT3dpnbv1/ZmU7yPZQ==} + peerDependencies: + yjs: ^13.5.13 + + '@szmarczak/http-timer@1.1.2': + resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} + engines: {node: '>=6'} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@szmarczak/http-timer@5.0.1': + resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} + engines: {node: '>=14.16'} + + '@tiptap/core@2.1.12': + resolution: {integrity: sha512-ZGc3xrBJA9KY8kln5AYTj8y+GDrKxi7u95xIl2eccrqTY5CQeRu6HRNM1yT4mAjuSaG9jmazyjGRlQuhyxCKxQ==} + peerDependencies: + '@tiptap/pm': ^2.0.0 + + '@tiptap/extension-blockquote@2.1.12': + resolution: {integrity: sha512-Qb3YRlCfugx9pw7VgLTb+jY37OY4aBJeZnqHzx4QThSm13edNYjasokbX0nTwL1Up4NPTcY19JUeHt6fVaVVGg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-bold@2.1.12': + resolution: {integrity: sha512-AZGxIxcGU1/y6V2YEbKsq6BAibL8yQrbRm6EdcBnby41vj1WziewEKswhLGmZx5IKM2r2ldxld03KlfSIlKQZg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-bubble-menu@2.1.12': + resolution: {integrity: sha512-gAGi21EQ4wvLmT7klgariAc2Hf+cIjaNU2NWze3ut6Ku9gUo5ZLqj1t9SKHmNf4d5JG63O8GxpErqpA7lHlRtw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + + '@tiptap/extension-bullet-list@2.1.12': + resolution: {integrity: sha512-vtD8vWtNlmAZX8LYqt2yU9w3mU9rPCiHmbp4hDXJs2kBnI0Ju/qAyXFx6iJ3C3XyuMnMbJdDI9ee0spAvFz7cQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-code-block-lowlight@2.1.12': + resolution: {integrity: sha512-dtIbpI9QrWa9TzNO4v5q/zf7+d83wpy5i9PEccdJAVtRZ0yOI8JIZAWzG5ex3zAoCA0CnQFdsPSVykYSDdxtDA==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/extension-code-block': ^2.0.0 + '@tiptap/pm': ^2.0.0 + + '@tiptap/extension-code-block@2.1.12': + resolution: {integrity: sha512-RXtSYCVsnk8D+K80uNZShClfZjvv1EgO42JlXLVGWQdIgaNyuOv/6I/Jdf+ZzhnpsBnHufW+6TJjwP5vJPSPHA==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + + '@tiptap/extension-code@2.1.12': + resolution: {integrity: sha512-CRiRq5OTC1lFgSx6IMrECqmtb93a0ZZKujEnaRhzWliPBjLIi66va05f/P1vnV6/tHaC3yfXys6dxB5A4J8jxw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-collaboration@2.1.12': + resolution: {integrity: sha512-U/2Vo1RyFIhi2oMW371wO145PU8mjQp7shCDdio/hNF+GasaNN9mrqkMBj8JBhH7UOTJcEqLPP4df1nEDo1BBQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + y-prosemirror: 1.0.20 + + '@tiptap/extension-document@2.1.12': + resolution: {integrity: sha512-0QNfAkCcFlB9O8cUNSwTSIQMV9TmoEhfEaLz/GvbjwEq4skXK3bU+OQX7Ih07waCDVXIGAZ7YAZogbvrn/WbOw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-dropcursor@2.1.12': + resolution: {integrity: sha512-0tT/q8nL4NBCYPxr9T0Brck+RQbWuczm9nV0bnxgt0IiQXoRHutfPWdS7GA65PTuVRBS/3LOco30fbjFhkfz/A==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + + '@tiptap/extension-floating-menu@2.1.12': + resolution: {integrity: sha512-uo0ydCJNg6AWwLT6cMUJYVChfvw2PY9ZfvKRhh9YJlGfM02jS4RUG/bJBts6R37f+a5FsOvAVwg8EvqPlNND1A==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + + '@tiptap/extension-gapcursor@2.1.12': + resolution: {integrity: sha512-zFYdZCqPgpwoB7whyuwpc8EYLYjUE5QYKb8vICvc+FraBUDM51ujYhFSgJC3rhs8EjI+8GcK8ShLbSMIn49YOQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + + '@tiptap/extension-hard-break@2.1.12': + resolution: {integrity: sha512-nqKcAYGEOafg9D+2cy1E4gHNGuL12LerVa0eS2SQOb+PT8vSel9OTKU1RyZldsWSQJ5rq/w4uIjmLnrSR2w6Yw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-heading@2.1.12': + resolution: {integrity: sha512-MoANP3POAP68Ko9YXarfDKLM/kXtscgp6m+xRagPAghRNujVY88nK1qBMZ3JdvTVN6b/ATJhp8UdrZX96TLV2w==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-highlight@2.1.12': + resolution: {integrity: sha512-buen31cYPyiiHA2i0o2i/UcjRTg/42mNDCizGr1OJwvv3AELG3qOFc4Y58WJWIvWNv+1Dr4ZxHA3GNVn0ANWyg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-history@2.1.12': + resolution: {integrity: sha512-6b7UFVkvPjq3LVoCTrYZAczt5sQrQUaoDWAieVClVZoFLfjga2Fwjcfgcie8IjdPt8YO2hG/sar/c07i9vM0Sg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + + '@tiptap/extension-horizontal-rule@2.1.12': + resolution: {integrity: sha512-RRuoK4KxrXRrZNAjJW5rpaxjiP0FJIaqpi7nFbAua2oHXgsCsG8qbW2Y0WkbIoS8AJsvLZ3fNGsQ8gpdliuq3A==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + + '@tiptap/extension-image@2.1.12': + resolution: {integrity: sha512-VCgOTeNLuoR89WoCESLverpdZpPamOd7IprQbDIeG14sUySt7RHNgf2AEfyTYJEHij12rduvAwFzerPldVAIJg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-italic@2.1.12': + resolution: {integrity: sha512-/XYrW4ZEWyqDvnXVKbgTXItpJOp2ycswk+fJ3vuexyolO6NSs0UuYC6X4f+FbHYL5VuWqVBv7EavGa+tB6sl3A==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-link@2.1.12': + resolution: {integrity: sha512-Sti5hhlkCqi5vzdQjU/gbmr8kb578p+u0J4kWS+SSz3BknNThEm/7Id67qdjBTOQbwuN07lHjDaabJL0hSkzGQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + + '@tiptap/extension-list-item@2.1.12': + resolution: {integrity: sha512-Gk7hBFofAPmNQ8+uw8w5QSsZOMEGf7KQXJnx5B022YAUJTYYxO3jYVuzp34Drk9p+zNNIcXD4kc7ff5+nFOTrg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-ordered-list@2.1.12': + resolution: {integrity: sha512-tF6VGl+D2avCgn9U/2YLJ8qVmV6sPE/iEzVAFZuOSe6L0Pj7SQw4K6AO640QBob/d8VrqqJFHCb6l10amJOnXA==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-paragraph@2.1.12': + resolution: {integrity: sha512-hoH/uWPX+KKnNAZagudlsrr4Xu57nusGekkJWBcrb5MCDE91BS+DN2xifuhwXiTHxnwOMVFjluc0bPzQbkArsw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-strike@2.1.12': + resolution: {integrity: sha512-HlhrzIjYUT8oCH9nYzEL2QTTn8d1ECnVhKvzAe6x41xk31PjLMHTUy8aYjeQEkWZOWZ34tiTmslV1ce6R3Dt8g==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-subscript@2.1.12': + resolution: {integrity: sha512-tb1jysEvf4SIiXwEOgDTXiyrG39RVNHvn/zsGMg5wy5t9qUp9m1k7kKYTH084ktuKDAPQonCcpn3hwc+ngTFzg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-superscript@2.1.12': + resolution: {integrity: sha512-ek6L+DNsrjiJieArlgTvQt1VfJ56d8V19WAPW/ciRhq88YRlTEY9nSO3QuUCSUO1nGmE5OWQpgrsiW/XZbONVw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-table-cell@2.0.0-beta.202': + resolution: {integrity: sha512-Ypmcq7zaMSZ0VNKwDPINOsSzyuH+gSIw+FrXy6O1dzVHAo1gNFJ2pEG/ZhQ2RqpDTpGfJFD8tNDx8wjCCAVlxA==} + peerDependencies: + '@tiptap/core': ^2.0.0-beta.193 + + '@tiptap/extension-table-header@2.0.0-beta.202': + resolution: {integrity: sha512-/l0lz3Hmc+hikj+RfSW7F6B/jYV2dROGQnK1/EYjgbvOK0158ml1mB6/Dhm+BhldV73MI7eU8+3YLB9uhsPR4w==} + peerDependencies: + '@tiptap/core': ^2.0.0-beta.193 + + '@tiptap/extension-table-row@2.0.0-beta.202': + resolution: {integrity: sha512-IsHBT3lp//XSqcAWPIGWjPIKQ4okVaDJbwcElehlOo/rcRBeK0orT+c10T08PoOsozi4BeMYRo0nfA5tvrJMEw==} + peerDependencies: + '@tiptap/core': ^2.0.0-beta.193 + + '@tiptap/extension-table@2.0.0-beta.202': + resolution: {integrity: sha512-WMfXtDfx45FgU81WnfxGOSJbVoaDpe8hjuBJSGbwJj+Qj4HGhbK7/RbTtDrM8oqseHRzHuGWgNX+EfOUQppjdA==} + peerDependencies: + '@tiptap/core': ^2.0.0-beta.193 + + '@tiptap/extension-task-item@2.1.12': + resolution: {integrity: sha512-uqrDTO4JwukZUt40GQdvB6S+oDhdp4cKNPMi0sbteWziQugkSMLlkYvxU0Hfb/YeziaWWwFI7ssPu/hahyk6dQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + + '@tiptap/extension-task-list@2.1.12': + resolution: {integrity: sha512-BUpYlEWK+Q3kw9KIiOqvhd0tUPhMcOf1+fJmCkluJok+okAxMbP1umAtCEQ3QkoCwLr+vpHJov7h3yi9+dwgeQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-text-align@2.1.12': + resolution: {integrity: sha512-siMlwrkgVrAxxgmZn8GOc75J7UZi2CVrP9vDHkUPPyKm/fjssYekXwGCEk4Vswii1BbOh2gt+MDsRkeYRGyDlQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-text@2.1.12': + resolution: {integrity: sha512-rCNUd505p/PXwU9Jgxo4ZJv4A3cIBAyAqlx/dtcY6cjztCQuXJhuQILPhjGhBTOLEEL4kW2wQtqzCmb7O8i2jg==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-underline@2.1.12': + resolution: {integrity: sha512-NwwdhFT8gDD0VUNLQx85yFBhP9a8qg8GPuxlGzAP/lPTV8Ubh3vSeQ5N9k2ZF/vHlEvnugzeVCbmYn7wf8vn1g==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/extension-youtube@2.1.12': + resolution: {integrity: sha512-Kr/sGESmWDNCKHEgbpAFCZJgvGYBuFNGUUY1eIxXRSz3w7sxS+cQ8xQ0+7jD2f2BVaJgdy7kf/V0wQ6ocGdHbw==} + peerDependencies: + '@tiptap/core': ^2.0.0 + + '@tiptap/pm@2.1.12': + resolution: {integrity: sha512-Q3MXXQABG4CZBesSp82yV84uhJh/W0Gag6KPm2HRWPimSFELM09Z9/5WK9RItAYE0aLhe4Krnyiczn9AAa1tQQ==} + + '@tiptap/starter-kit@2.1.12': + resolution: {integrity: sha512-+RoP1rWV7rSCit2+3wl2bjvSRiePRJE/7YNKbvH8Faz/+AMO23AFegHoUFynR7U0ouGgYDljGkkj35e0asbSDA==} + + '@tiptap/vue-3@2.1.12': + resolution: {integrity: sha512-yAcfmWw/9jtIUbhb0uGQVI9NoPYgHRasX2sAGWnm9Al+0aJktgmQ3mLCifXfXfjyEbeMF0p2L6Ul8tO7eho7aQ==} + peerDependencies: + '@tiptap/core': ^2.0.0 + '@tiptap/pm': ^2.0.0 + vue: ^3.0.0 + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tootallnate/once@2.0.0': + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + + '@trpc/client@10.43.1': + resolution: {integrity: sha512-pkPtbDS0ck/2WZo2cWaPV11NFMII2I/nzse1Ggs5Cr0YczsZk3Z0DM77Sfb9FTSjmccYfkEtumHqxfTj6fRbbg==} + peerDependencies: + '@trpc/server': 10.43.1 + + '@trpc/server@10.43.1': + resolution: {integrity: sha512-rKOSCpJOb1MdTyJFqdf3QNNESDfPkbP+yBOZBM2x6iIOS4VlCfJqxsaSrb3uLPR6s8Ni7DhTu+cu/q1r0xOGcw==} + engines: {node: '>=18.0.0'} + + '@trysound/sax@0.2.0': + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + + '@tsconfig/node10@1.0.9': + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/argon2-browser@1.18.3': + resolution: {integrity: sha512-WmFgbCKUDqwVbidRRf+JbdvKlt8ptAUX4vND0BkV/vGcU9Zcw6tSb3aRY3UMDJj7yJxhjjkUOPk0Gt/F+WPFRA==} + + '@types/body-parser@1.19.4': + resolution: {integrity: sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/chai-subset@1.3.4': + resolution: {integrity: sha512-CCWNXrJYSUIojZ1149ksLl3AN9cmZ5djf+yUoVVV+NuYrtydItQVlL2ZDqyC6M6O9LWRnVf8yYDxbXHO2TfQZg==} + + '@types/chai@4.3.9': + resolution: {integrity: sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==} + + '@types/chrome@0.0.208': + resolution: {integrity: sha512-VDU/JnXkF5qaI7WBz14Azpa2VseZTgML0ia/g/B1sr9OfdOnHiH/zZ7P7qCDqxSlkqJh76/bPc8jLFcx8rHJmw==} + + '@types/color-convert@2.0.2': + resolution: {integrity: sha512-KGRIgCxwcgazts4MXRCikPbIMzBpjfdgEZSy8TRHU/gtg+f9sOfHdtK8unPfxIoBtyd2aTTwINVLSNENlC8U8A==} + + '@types/color-name@1.1.2': + resolution: {integrity: sha512-JWO/ZyxTKk0bLuOhAavGjnwLR73rUE7qzACnU7gMeyA/gdrSHm2xJwqNPipw2MtaZUaqQ2UG/q7pP6AQiZ8mqw==} + + '@types/color@3.0.5': + resolution: {integrity: sha512-T9yHCNtd8ap9L/r8KEESu5RDMLkoWXHo7dTureNoI1dbp25NsCN054vOu09iniIjR21MXUL+LU9bkIWrbyg8gg==} + + '@types/compression@1.7.4': + resolution: {integrity: sha512-sdFVnQJRkQBX83ydsLCBm4A39p45y0QkxdAR689yOtAFNbbS9Acrp86RZWJj6BHRXyZH9tX4t1dU7XDiGdY3nA==} + + '@types/connect@3.4.37': + resolution: {integrity: sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==} + + '@types/cookie@0.4.1': + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + + '@types/cookie@0.5.3': + resolution: {integrity: sha512-SLg07AS9z1Ab2LU+QxzU8RCmzsja80ywjf/t5oqw+4NSH20gIGlhLOrBDm1L3PBWzPa4+wkgFQVZAjE6Ioj2ug==} + + '@types/cordova@0.0.34': + resolution: {integrity: sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA==} + + '@types/cors@2.8.15': + resolution: {integrity: sha512-n91JxbNLD8eQIuXDIChAN1tCKNWCEgpceU9b7ZMbFA+P+Q4yIeh80jizFLEvolRPc1ES0VdwFlGv+kJTSirogw==} + + '@types/crypto-js@4.2.0': + resolution: {integrity: sha512-LW9TlhpMeoQKOu6I6HvOp7eInNNnvd7B+ndAfZb826HUl7eHJJApofbHnlAxrIVS/uiCjkkHGNEaKHoGxB5IHw==} + + '@types/debug@4.1.10': + resolution: {integrity: sha512-tOSCru6s732pofZ+sMv9o4o3Zc+Sa8l3bxd/tweTQudFn06vAzb13ZX46Zi6m6EJ+RUbRTHvgQJ1gBtSgkaUYA==} + + '@types/downloadjs@1.4.5': + resolution: {integrity: sha512-pr3zSKY0QwP+1tQumJGNfziGNi1xLVHtGsrclGsgjVpfWSPoW/9He42w6X+GTp4PO5I2wuHDU2wJdxI7dqjW+w==} + + '@types/estree@1.0.4': + resolution: {integrity: sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==} + + '@types/express-serve-static-core@4.17.39': + resolution: {integrity: sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==} + + '@types/express@4.17.20': + resolution: {integrity: sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==} + + '@types/file-saver@2.0.6': + resolution: {integrity: sha512-Mw671DVqoMHbjw0w4v2iiOro01dlT/WhWp5uwecBa0Wg8c+bcZOjgF1ndBnlaxhtvFCgTRBtsGivSVhrK/vnag==} + + '@types/filesystem@0.0.34': + resolution: {integrity: sha512-La4bGrgck8/rosDUA1DJJP8hrFcKq0BV6JaaVlNnOo1rJdJDcft3//slEbAmsWNUJwXRCc0DXpeO40yuATlexw==} + + '@types/filewriter@0.0.31': + resolution: {integrity: sha512-12df1utOvPC80+UaVoOO1d81X8pa5MefHNS+gWX9R2ucSESpMz9K5QwlTWDGKASrzCpSFwj7NPYh+nTsolgEGA==} + + '@types/fs-extra@8.1.5': + resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/har-format@1.2.14': + resolution: {integrity: sha512-pEmBAoccWvO6XbSI8A7KvIDGEoKtlLWtdqVCKoVBcCDSFvR4Ijd7zGLu7MWGEqk2r8D54uWlMRt+VZuSrfFMzQ==} + + '@types/hast@2.3.7': + resolution: {integrity: sha512-EVLigw5zInURhzfXUM65eixfadfsHKomGKUakToXo84t8gGIJuTcD2xooM2See7GyQ7DRtYjhCHnSUQez8JaLw==} + + '@types/http-cache-semantics@4.0.3': + resolution: {integrity: sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA==} + + '@types/http-errors@2.0.3': + resolution: {integrity: sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==} + + '@types/json-schema@7.0.14': + resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} + + '@types/jsonwebtoken@9.0.4': + resolution: {integrity: sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==} + + '@types/jsonwebtoken@9.0.5': + resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + + '@types/jws@3.2.8': + resolution: {integrity: sha512-xDIvCuI7hEicqUa+dSc2TnrC92yvJtzbLghMrMcwroWzU9RWlF64cqrWW8QGikq4f7fN0V7edDIWuMLeICk+2A==} + + '@types/katex@0.16.5': + resolution: {integrity: sha512-DD2Y3xMlTQvAnN6d8803xdgnOeYZ+HwMglb7/9YCf49J9RkJL53azf9qKa40MkEYhqVwxZ1GS2+VlShnz4Z1Bw==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/libsodium-wrappers-sumo@0.7.7': + resolution: {integrity: sha512-L5KaYOEJqPlMZjP2kUaKjr0vQyv8LRR/QkwAKUazl3JrcEt/VXDdCAi2+Z5mSHOUjan7PEPRSxEPvwsIyXDLDA==} + + '@types/libsodium-wrappers@0.7.12': + resolution: {integrity: sha512-NNUV6W5KFMYSazUh7bofvIqjHunu1ia24Q4gygXrhluXvvdPtkV6fXuciidYU7eBc9XTltAc6k727wYlrpo9Jg==} + + '@types/lodash@4.14.200': + resolution: {integrity: sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==} + + '@types/mime@1.3.4': + resolution: {integrity: sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==} + + '@types/mime@3.0.3': + resolution: {integrity: sha512-i8MBln35l856k5iOhKk2XJ4SeAWg75mLIpZB4v6imOagKL6twsukBZGDMNhdOVk7yRFTMPpfILocMos59Q1otQ==} + + '@types/minimist@1.2.4': + resolution: {integrity: sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==} + + '@types/ms@0.7.33': + resolution: {integrity: sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ==} + + '@types/node-fetch@2.6.8': + resolution: {integrity: sha512-nnH5lV9QCMPsbEVdTb5Y+F3GQxLSw1xQgIydrb2gSfEavRPs50FnMr+KUaa+LoPSqibm2N+ZZxH7lavZlAT4GA==} + + '@types/node@16.18.60': + resolution: {integrity: sha512-ZUGPWx5vKfN+G2/yN7pcSNLkIkXEvlwNaJEd4e0ppX7W2S8XAkdc/37hM4OUNJB9sa0p12AOvGvxL4JCPiz9DA==} + + '@types/node@16.9.1': + resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} + + '@types/node@18.18.8': + resolution: {integrity: sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ==} + + '@types/node@20.8.10': + resolution: {integrity: sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==} + + '@types/node@20.9.1': + resolution: {integrity: sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==} + + '@types/normalize-package-data@2.4.3': + resolution: {integrity: sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==} + + '@types/object.omit@3.0.3': + resolution: {integrity: sha512-xrq4bQTBGYY2cw+gV4PzoG2Lv3L0pjZ1uXStRRDQoATOYW1lCsFQHhQ+OkPhIcQoqLjAq7gYif7D14Qaa6Zbew==} + + '@types/object.pick@1.3.4': + resolution: {integrity: sha512-5PjwB0uP2XDp3nt5u5NJAG2DORHIRClPzWT/TTZhJ2Ekwe8M5bA9tvPdi9NO/n2uvu2/ictat8kgqvLfcIE1SA==} + + '@types/plist@3.0.5': + resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} + + '@types/qrcode@1.5.4': + resolution: {integrity: sha512-ufYqUO7wUBq49hugJry+oIYKscvxIQerJSmXeny215aJKfrepN04DDZP8FCgxvV82kOqKPULCE4PIW3qUmZrRA==} + + '@types/qs@6.9.9': + resolution: {integrity: sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==} + + '@types/range-parser@1.2.6': + resolution: {integrity: sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==} + + '@types/responselike@1.0.2': + resolution: {integrity: sha512-/4YQT5Kp6HxUDb4yhRkm0bJ7TbjvTddqX7PZ5hz6qV3pxSo72f/6YPRo+Mu2DU307tm9IioO69l7uAwn5XNcFA==} + + '@types/semver@7.5.4': + resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==} + + '@types/send@0.17.3': + resolution: {integrity: sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==} + + '@types/serve-static@1.15.4': + resolution: {integrity: sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==} + + '@types/showdown@2.0.3': + resolution: {integrity: sha512-cFuAcA3p2YPq8HR8KxvDXnOdccOZ74ypANB3kb3AL5Srji0QnteVw6vf4o7GJ8hMyz+uZ+nSQHVgXSgjYD1a5g==} + + '@types/slice-ansi@4.0.0': + resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==} + + '@types/strip-bom@3.0.0': + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + + '@types/strip-json-comments@0.0.30': + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + + '@types/throttle-debounce@2.1.0': + resolution: {integrity: sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==} + + '@types/triple-beam@1.3.4': + resolution: {integrity: sha512-HlJjF3wxV4R2VQkFpKe0YqJLilYNgtRtsqqZtby7RkVsSs+i+vbyzjtUwpFEdUCKcrGzCiEJE7F/0mKjh0sunA==} + + '@types/turndown@5.0.3': + resolution: {integrity: sha512-2PCZA9g/dkeHIGTf6ESMOD3Gz5RMpDzODtvlBbkLAdtKa/yTQDAFudDEVolHjaBUnu8ugd8BeTCWk4x0STnqkA==} + + '@types/unist@2.0.9': + resolution: {integrity: sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ==} + + '@types/verror@1.10.9': + resolution: {integrity: sha512-MLx9Z+9lGzwEuW16ubGeNkpBDE84RpB/NyGgg6z2BTpWzKkGU451cAY3UkUzZEp72RHF585oJ3V8JVNqIplcAQ==} + + '@types/web-bluetooth@0.0.18': + resolution: {integrity: sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw==} + + '@types/ws@8.5.3': + resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@types/zxcvbn@4.4.3': + resolution: {integrity: sha512-AxZBi8J3V3lm+f2Vgg06D8y0womXpLf3ZEDYeLPGGK0ydR724sQH83T5tYgN+CN6VTRnAlevFKJkWTecCnk8ug==} + + '@typescript-eslint/eslint-plugin@6.10.0': + resolution: {integrity: sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@6.10.0': + resolution: {integrity: sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@6.10.0': + resolution: {integrity: sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/type-utils@6.10.0': + resolution: {integrity: sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@6.10.0': + resolution: {integrity: sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@typescript-eslint/typescript-estree@6.10.0': + resolution: {integrity: sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@6.10.0': + resolution: {integrity: sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + + '@typescript-eslint/visitor-keys@6.10.0': + resolution: {integrity: sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==} + engines: {node: ^16.0.0 || >=18.0.0} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + '@vitejs/plugin-vue@2.3.4': + resolution: {integrity: sha512-IfFNbtkbIm36O9KB8QodlwwYvTEsJb4Lll4c2IwB3VHc2gie2mSPtSzL0eYay7X2jd/2WX02FjSGTWR6OPr/zg==} + engines: {node: '>=12.0.0'} + peerDependencies: + vite: ^2.5.10 + vue: ^3.2.25 + + '@vitest/expect@0.34.6': + resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} + + '@vitest/runner@0.34.6': + resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} + + '@vitest/snapshot@0.34.6': + resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} + + '@vitest/spy@0.34.6': + resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} + + '@vitest/utils@0.34.6': + resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} + + '@vue/compiler-core@3.2.47': + resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==} + + '@vue/compiler-dom@3.2.47': + resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==} + + '@vue/compiler-sfc@3.2.47': + resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==} + + '@vue/compiler-ssr@3.2.47': + resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==} + + '@vue/devtools-api@6.5.1': + resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==} + + '@vue/devtools@6.5.1': + resolution: {integrity: sha512-3xSNDzebOTUHoCPFNsyklY8tC8RZNg6gy63zXAppdz9FV4gUG/hlWkOZd9xcuotaZ1HcurmLLfHckfUfbTheXw==} + hasBin: true + + '@vue/reactivity-transform@3.2.47': + resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==} + + '@vue/reactivity@3.2.47': + resolution: {integrity: sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==} + + '@vue/runtime-core@3.2.47': + resolution: {integrity: sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==} + + '@vue/runtime-dom@3.2.47': + resolution: {integrity: sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==} + + '@vue/server-renderer@3.2.47': + resolution: {integrity: sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==} + peerDependencies: + vue: 3.2.47 + + '@vue/shared@3.2.47': + resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==} + + '@vueuse/core@10.5.0': + resolution: {integrity: sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==} + + '@vueuse/metadata@10.5.0': + resolution: {integrity: sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==} + + '@vueuse/shared@10.5.0': + resolution: {integrity: sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==} + + '@xmldom/xmldom@0.8.10': + resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} + engines: {node: '>=10.0.0'} + + '@zxcvbn-ts/core@3.0.4': + resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==} + + '@zxcvbn-ts/language-common@3.0.4': + resolution: {integrity: sha512-viSNNnRYtc7ULXzxrQIVUNwHAPSXRtoIwy/Tq4XQQdIknBzw4vz36lQLF6mvhMlTIlpjoN/Z1GFu/fwiAlUSsw==} + + '@zxcvbn-ts/language-en@3.0.2': + resolution: {integrity: sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg==} + + JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true + + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.0: + resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} + engines: {node: '>=0.4.0'} + + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@8.11.2: + resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} + engines: {node: '>=0.4.0'} + hasBin: true + + add-stream@1.0.0: + resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.12.0: + resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + any-base@1.1.0: + resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + app-builder-bin@4.0.0: + resolution: {integrity: sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==} + + app-builder-lib@24.4.0: + resolution: {integrity: sha512-EcdqtWvg1LAApKCfyRBukcVkmsa94s2e1VKHjZLpvA9/D14QEt8rHhffYeaA+cH/pVeoNVn2ob735KnfJKEEow==} + engines: {node: '>=14.0.0'} + + aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + + archive-type@4.0.0: + resolution: {integrity: sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==} + engines: {node: '>=4'} + + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + + archy@1.0.0: + resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} + + are-we-there-yet@3.0.1: + resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argon2-browser@1.18.0: + resolution: {integrity: sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-ify@1.0.0: + resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + array-union@3.0.1: + resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==} + engines: {node: '>=12'} + + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async@3.2.5: + resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + author-regex@1.0.0: + resolution: {integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==} + engines: {node: '>=0.8'} + + autoprefixer@10.4.16: + resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + avvio@8.2.1: + resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==} + + axios@0.26.1: + resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} + + axios@0.27.2: + resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + + axios@1.6.0: + resolution: {integrity: sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==} + + b4a@1.6.4: + resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + big-integer@1.6.51: + resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} + engines: {node: '>=0.6'} + + bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + + bin-build@3.0.0: + resolution: {integrity: sha512-jcUOof71/TNAI2uM5uoUaDq2ePcVBQ3R/qhxAz1rX7UfvduAL/RXD3jXzvn8cVcDJdGVkiR1shal3OH0ImpuhA==} + engines: {node: '>=4'} + + bin-check@4.1.0: + resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==} + engines: {node: '>=4'} + + bin-version-check@4.0.0: + resolution: {integrity: sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==} + engines: {node: '>=6'} + + bin-version@3.1.0: + resolution: {integrity: sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==} + engines: {node: '>=6'} + + bin-wrapper@4.1.0: + resolution: {integrity: sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q==} + engines: {node: '>=6'} + + binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + + bintrees@1.0.2: + resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} + + bl@1.2.3: + resolution: {integrity: sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird-lst@1.0.9: + resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + bmp-js@0.1.0: + resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} + + bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + + body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + + boxen@7.1.1: + resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} + engines: {node: '>=14.16'} + + bplist-parser@0.2.0: + resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} + engines: {node: '>= 5.10.0'} + + bplist-parser@0.3.2: + resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} + engines: {node: '>= 5.10.0'} + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + + browserslist@4.22.1: + resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-alloc-unsafe@1.1.0: + resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} + + buffer-alloc@1.2.0: + resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-equal@0.0.1: + resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==} + engines: {node: '>=0.4.0'} + + buffer-equal@1.0.1: + resolution: {integrity: sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==} + engines: {node: '>=0.4'} + + buffer-fill@1.0.0: + resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer-writer@2.0.0: + resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} + engines: {node: '>=4'} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + builder-util-runtime@8.7.0: + resolution: {integrity: sha512-G1AqqVM2vYTrSFR982c1NNzwXKrGLQjVjaZaWQdn4O6Z3YKjdMDofw88aD9jpyK9ZXkrCxR0tI3Qe9wNbyTlXg==} + engines: {node: '>=8.2.0'} + + builder-util-runtime@9.2.1: + resolution: {integrity: sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==} + engines: {node: '>=12.0.0'} + + builder-util@24.4.0: + resolution: {integrity: sha512-tONb/GIK1MKa1BcOPHE1naId3o5nj6gdka5kP7yUJh2DOfF+jMq3laiu+UOZH6A7ZtkMtnGNMYFKFTIv408n/A==} + + builtins@5.0.1: + resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} + + bundle-name@3.0.0: + resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} + engines: {node: '>=12'} + + bundle-require@4.0.2: + resolution: {integrity: sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.17' + + bytes@3.0.0: + resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} + engines: {node: '>= 0.8'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + cacache@16.1.3: + resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-lookup@7.0.0: + resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} + engines: {node: '>=14.16'} + + cacheable-request@10.2.14: + resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} + engines: {node: '>=14.16'} + + cacheable-request@2.1.4: + resolution: {integrity: sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==} + + cacheable-request@6.1.0: + resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} + engines: {node: '>=8'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + call-bind@1.0.5: + resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camel-case@3.0.0: + resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + + camelcase-keys@6.2.2: + resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} + engines: {node: '>=8'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@7.0.1: + resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} + engines: {node: '>=14.16'} + + caniuse-lite@1.0.30001561: + resolution: {integrity: sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==} + + case-anything@2.1.13: + resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} + engines: {node: '>=12.13'} + + caw@2.0.1: + resolution: {integrity: sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==} + engines: {node: '>=4'} + + chai@4.3.10: + resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} + engines: {node: '>=4'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + + chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + clean-css@4.2.4: + resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} + engines: {node: '>= 4.0'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.1: + resolution: {integrity: sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==} + engines: {node: '>=6'} + + cli-truncate@2.1.0: + resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} + engines: {node: '>=8'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + + clone-response@1.0.2: + resolution: {integrity: sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + + color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colorette@2.0.19: + resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} + + colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@11.0.0: + resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} + engines: {node: '>=16'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + compare-func@2.0.0: + resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + + component-emitter@1.3.0: + resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} + + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + + compressible@2.0.18: + resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} + engines: {node: '>= 0.6'} + + compression@1.7.4: + resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} + engines: {node: '>= 0.8.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + concurrently@8.2.2: + resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} + engines: {node: ^14.13.0 || >=16.0.0} + hasBin: true + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + config-file-ts@0.2.4: + resolution: {integrity: sha512-cKSW0BfrSaAUnxpgvpXPLaaW/umg4bqg4k3GO1JqlRfpx+d5W0GDXznCMkWotJQek5Mmz1MJVChQnz3IVaeMZQ==} + + configstore@6.0.0: + resolution: {integrity: sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==} + engines: {node: '>=12'} + + consola@2.15.3: + resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} + + console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + conventional-changelog-angular@5.0.13: + resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} + engines: {node: '>=10'} + + conventional-changelog-angular@6.0.0: + resolution: {integrity: sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==} + engines: {node: '>=14'} + + conventional-changelog-atom@2.0.8: + resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==} + engines: {node: '>=10'} + + conventional-changelog-codemirror@2.0.8: + resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==} + engines: {node: '>=10'} + + conventional-changelog-config-spec@2.1.0: + resolution: {integrity: sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==} + + conventional-changelog-conventionalcommits@4.6.3: + resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} + engines: {node: '>=10'} + + conventional-changelog-conventionalcommits@7.0.2: + resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} + engines: {node: '>=16'} + + conventional-changelog-core@4.2.4: + resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} + engines: {node: '>=10'} + + conventional-changelog-ember@2.0.9: + resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==} + engines: {node: '>=10'} + + conventional-changelog-eslint@3.0.9: + resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==} + engines: {node: '>=10'} + + conventional-changelog-express@2.0.6: + resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==} + engines: {node: '>=10'} + + conventional-changelog-jquery@3.0.11: + resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==} + engines: {node: '>=10'} + + conventional-changelog-jshint@2.0.9: + resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==} + engines: {node: '>=10'} + + conventional-changelog-preset-loader@2.3.4: + resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==} + engines: {node: '>=10'} + + conventional-changelog-writer@5.0.1: + resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==} + engines: {node: '>=10'} + hasBin: true + + conventional-changelog@3.1.25: + resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==} + engines: {node: '>=10'} + + conventional-commits-filter@2.0.7: + resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==} + engines: {node: '>=10'} + + conventional-commits-parser@3.2.4: + resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==} + engines: {node: '>=10'} + hasBin: true + + conventional-commits-parser@5.0.0: + resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} + engines: {node: '>=16'} + hasBin: true + + conventional-recommended-bump@6.1.0: + resolution: {integrity: sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw==} + engines: {node: '>=10'} + hasBin: true + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + + cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cosmiconfig-typescript-loader@5.0.0: + resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} + engines: {node: '>=v16'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=8.2' + typescript: '>=4' + + cosmiconfig@8.2.0: + resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} + engines: {node: '>=14'} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + + crc@3.8.0: + resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn-windows-exe@1.2.0: + resolution: {integrity: sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==} + engines: {node: '>= 10'} + + cross-spawn@5.1.0: + resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + + cross-spawn@6.0.5: + resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} + engines: {node: '>=4.8'} + + cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + + crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + + css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + + csstype@2.6.21: + resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} + + dargs@7.0.0: + resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} + engines: {node: '>=8'} + + dash-get@1.0.2: + resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==} + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + dateformat@3.0.3: + resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} + + db-errors@0.2.3: + resolution: {integrity: sha512-OOgqgDuCavHXjYSJoV2yGhv6SeG8nk42aoCSoyXLZUH7VwFG27rxbavU1z+VrZbZjphw5UkDQwUlD21MwZpUng==} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize-keys@1.1.1: + resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} + engines: {node: '>=0.10.0'} + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + decompress-response@3.3.0: + resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} + engines: {node: '>=4'} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + decompress-tar@4.1.1: + resolution: {integrity: sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==} + engines: {node: '>=4'} + + decompress-tarbz2@4.1.1: + resolution: {integrity: sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==} + engines: {node: '>=4'} + + decompress-targz@4.1.1: + resolution: {integrity: sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==} + engines: {node: '>=4'} + + decompress-unzip@4.0.1: + resolution: {integrity: sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==} + engines: {node: '>=4'} + + decompress@4.2.1: + resolution: {integrity: sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==} + engines: {node: '>=4'} + + deep-eql@4.1.3: + resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} + engines: {node: '>=6'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@3.0.0: + resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} + engines: {node: '>=12'} + + default-browser@4.0.0: + resolution: {integrity: sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==} + engines: {node: '>=14.16'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defer-to-connect@1.1.3: + resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.1: + resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} + engines: {node: '>= 0.4'} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + detect-libc@2.0.2: + resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + + dir-compare@3.3.0: + resolution: {integrity: sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dmg-builder@24.4.0: + resolution: {integrity: sha512-p5z9Cx539GSBYb+b09Z+hMhuBTh/BrI71VRg4rgF6f2xtIRK/YlTGVS/O08k5OojoyhZcpS7JXxDVSmQoWgiiQ==} + + dmg-license@1.0.11: + resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} + engines: {node: '>=8'} + os: [darwin] + hasBin: true + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domino@2.1.6: + resolution: {integrity: sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + + dot-prop@5.3.0: + resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} + engines: {node: '>=8'} + + dot-prop@6.0.1: + resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} + engines: {node: '>=10'} + + dotenv-expand@5.1.0: + resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} + + dotenv-expand@9.0.0: + resolution: {integrity: sha512-uW8Hrhp5ammm9x7kBLR6jDfujgaDarNA02tprvZdyrJ7MpdzD1KyrIHG4l+YoC2fJ2UcdFdNWNWIjt+sexBHJw==} + engines: {node: '>=12'} + + dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + + dotenv@9.0.2: + resolution: {integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==} + engines: {node: '>=10'} + + dotgitignore@2.1.0: + resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} + engines: {node: '>=6'} + + download@6.2.5: + resolution: {integrity: sha512-DpO9K1sXAST8Cpzb7kmEhogJxymyVUd5qz/vCOSyvwtp2Klj2XcDt5YUuasgxka44SxF0q5RriKIwJmQHG2AuA==} + engines: {node: '>=4'} + + download@7.1.0: + resolution: {integrity: sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==} + engines: {node: '>=6'} + + downloadjs@1.4.7: + resolution: {integrity: sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==} + + duplexer3@0.1.5: + resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} + + dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + ejs@3.1.9: + resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder@24.4.0: + resolution: {integrity: sha512-D5INxodxaUIJgEX6p/fqBd8wQNS8XRAToNIJ9SQC+taNS5D73ZsjLuXiRraFGCB0cVk9KeKhEkdEOH5AaVya4g==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-context-menu@3.6.1: + resolution: {integrity: sha512-lcpO6tzzKUROeirhzBjdBWNqayEThmdW+2I2s6H6QMrwqTVyT3EK47jW3Nxm60KTxl5/bWfEoIruoUNn57/QkQ==} + + electron-dl@3.5.1: + resolution: {integrity: sha512-5Yb9s/iPVJ5mW5x3j6XkKxt7WEqREr/AhYxZmtEfW1ffQHs1+aGoiQ2fXCAU6UIXMnWog2MXK82vrxJsjA3nbQ==} + engines: {node: '>=12'} + + electron-is-dev@2.0.0: + resolution: {integrity: sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA==} + + electron-log@4.4.8: + resolution: {integrity: sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA==} + + electron-packager@17.1.1: + resolution: {integrity: sha512-r1NDtlajsq7gf2EXgjRfblCVPquvD2yeg+6XGErOKblvxOpDi0iulZLVhgYDP4AEF1P5/HgbX/vwjlkEv7PEIQ==} + engines: {node: '>= 14.17.5'} + hasBin: true + + electron-publish@24.4.0: + resolution: {integrity: sha512-U3mnVSxIfNrLW7ZnwiedFhcLf6ExPFXgAsx89WpfQFsV4gFAt/LG+H74p0m9NSvsLXiZuF82yXoxi7Ou8GHq4Q==} + + electron-to-chromium@1.4.576: + resolution: {integrity: sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==} + + electron-updater@4.3.1: + resolution: {integrity: sha512-UDC5AHCgeiHJYDYWZG/rsl1vdAFKqI/Lm7whN57LKAk8EfhTewhcEHzheRcncLgikMcQL8gFo1KeX51tf5a5Wg==} + + electron@21.4.4: + resolution: {integrity: sha512-N5O7y7Gtt7mDgkJLkW49ETiT8M3myZ9tNIEvGTKhpBduX4WdgMj6c3hYeYBD6XW7SvbRkWEQaTl25RNday8Xpw==} + engines: {node: '>= 10.17.0'} + hasBin: true + + electron@25.2.0: + resolution: {integrity: sha512-I/rhcW2sV2fyiveVSBr2N7v5ZiCtdGY0UiNCDZgk2fpSC+irQjbeh7JT2b4vWmJ2ogOXBjqesrN9XszTIG6DHg==} + engines: {node: '>= 12.20.55'} + hasBin: true + + elementtree@0.1.7: + resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==} + engines: {node: '>= 0.4.0'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + + encode-utf8@1.0.3: + resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + + end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + + engine.io-client@6.5.2: + resolution: {integrity: sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==} + + engine.io-parser@5.2.1: + resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==} + engines: {node: '>=10.0.0'} + + engine.io@6.5.3: + resolution: {integrity: sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw==} + engines: {node: '>=10.2.0'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@3.0.1: + resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + engines: {node: '>=0.12'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild-android-64@0.14.51: + resolution: {integrity: sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + esbuild-android-64@0.14.54: + resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + esbuild-android-arm64@0.14.51: + resolution: {integrity: sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + esbuild-android-arm64@0.14.54: + resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + esbuild-darwin-64@0.14.51: + resolution: {integrity: sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + esbuild-darwin-64@0.14.54: + resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + esbuild-darwin-arm64@0.14.51: + resolution: {integrity: sha512-juYD0QnSKwAMfzwKdIF6YbueXzS6N7y4GXPDeDkApz/1RzlT42mvX9jgNmyOlWKN7YzQAYbcUEJmZJYQGdf2ow==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + esbuild-darwin-arm64@0.14.54: + resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + esbuild-freebsd-64@0.14.51: + resolution: {integrity: sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + esbuild-freebsd-64@0.14.54: + resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + esbuild-freebsd-arm64@0.14.51: + resolution: {integrity: sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + esbuild-freebsd-arm64@0.14.54: + resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + esbuild-linux-32@0.14.51: + resolution: {integrity: sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + esbuild-linux-32@0.14.54: + resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + esbuild-linux-64@0.14.51: + resolution: {integrity: sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + esbuild-linux-64@0.14.54: + resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + esbuild-linux-arm64@0.14.51: + resolution: {integrity: sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + esbuild-linux-arm64@0.14.54: + resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + esbuild-linux-arm@0.14.51: + resolution: {integrity: sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + esbuild-linux-arm@0.14.54: + resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + esbuild-linux-mips64le@0.14.51: + resolution: {integrity: sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + esbuild-linux-mips64le@0.14.54: + resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + esbuild-linux-ppc64le@0.14.51: + resolution: {integrity: sha512-xcdd62Y3VfGoyphNP/aIV9LP+RzFw5M5Z7ja+zdpQHHvokJM7d0rlDRMN+iSSwvUymQkqZO+G/xjb4/75du8BQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + esbuild-linux-ppc64le@0.14.54: + resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + esbuild-linux-riscv64@0.14.51: + resolution: {integrity: sha512-syXHGak9wkAnFz0gMmRBoy44JV0rp4kVCEA36P5MCeZcxFq8+fllBC2t6sKI23w3qd8Vwo9pTADCgjTSf3L3rA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + esbuild-linux-riscv64@0.14.54: + resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + esbuild-linux-s390x@0.14.51: + resolution: {integrity: sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + esbuild-linux-s390x@0.14.54: + resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + esbuild-netbsd-64@0.14.51: + resolution: {integrity: sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + esbuild-netbsd-64@0.14.54: + resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + esbuild-openbsd-64@0.14.51: + resolution: {integrity: sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + esbuild-openbsd-64@0.14.54: + resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + esbuild-sunos-64@0.14.51: + resolution: {integrity: sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + esbuild-sunos-64@0.14.54: + resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + esbuild-windows-32@0.14.51: + resolution: {integrity: sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + esbuild-windows-32@0.14.54: + resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + esbuild-windows-64@0.14.51: + resolution: {integrity: sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + esbuild-windows-64@0.14.54: + resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + esbuild-windows-arm64@0.14.51: + resolution: {integrity: sha512-JQDqPjuOH7o+BsKMSddMfmVJXrnYZxXDHsoLHc0xgmAZkOOCflRmC43q31pk79F9xuyWY45jDBPolb5ZgGOf9g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + esbuild-windows-arm64@0.14.54: + resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + esbuild@0.14.51: + resolution: {integrity: sha512-+CvnDitD7Q5sT7F+FM65sWkF8wJRf+j9fPcprxYV4j+ohmzVj2W7caUqH2s5kCaCJAfcAICjSlKhDCcvDpU7nw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.14.54: + resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + + escape-goat@2.1.1: + resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==} + engines: {node: '>=8'} + + escape-goat@4.0.0: + resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} + engines: {node: '>=12'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-config-prettier@9.0.0: + resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.0.1: + resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '*' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-simple-import-sort@10.0.0: + resolution: {integrity: sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==} + peerDependencies: + eslint: '>=5.0.0' + + eslint-plugin-unused-imports@3.0.0: + resolution: {integrity: sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/eslint-plugin': ^6.0.0 + eslint: ^8.0.0 + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + + eslint-plugin-vue@9.18.1: + resolution: {integrity: sha512-7hZFlrEgg9NIzuVik2I9xSnJA5RsmOfueYgsUGUokEDLJ1LHtxO0Pl4duje1BriZ/jDWb+44tcIlC3yi0tdlZg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 + + eslint-rule-composer@0.3.0: + resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} + engines: {node: '>=4.0.0'} + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-utils@2.1.0: + resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} + engines: {node: '>=6'} + + eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.53.0: + resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + + esm@3.2.25: + resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} + engines: {node: '>=6'} + + espree@6.2.1: + resolution: {integrity: sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==} + engines: {node: '>=6.0.0'} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@0.7.0: + resolution: {integrity: sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==} + engines: {node: '>=4'} + + execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + execa@7.2.0: + resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} + engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + + executable@4.1.1: + resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} + engines: {node: '>=4'} + + exif-parser@0.1.12: + resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + exponential-backoff@3.1.1: + resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} + + express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + + ext-list@2.2.2: + resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} + engines: {node: '>=0.10.0'} + + ext-name@5.0.0: + resolution: {integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==} + engines: {node: '>=4'} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + extsprintf@1.4.1: + resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} + engines: {'0': node >=0.6.0} + + fast-check@3.13.2: + resolution: {integrity: sha512-ouTiFyeMoqmNg253xqy4NSacr5sHxH6pZpLOaHgaAdgZxFWdtsfxExwolpveoAE9CJdV+WYjqErNGef6SqA5Mg==} + engines: {node: '>=8.0.0'} + + fast-content-type-parse@1.1.0: + resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-json-stringify@5.9.1: + resolution: {integrity: sha512-NMrf+uU9UJnTzfxaumMDXK1NWqtPCfGoM9DYIE+ESlaTQqjlANFBy0VAbsm6FB88Mx0nceyi18zTo5kIEUlzxg==} + + fast-jwt@3.3.1: + resolution: {integrity: sha512-1YuuIJeh1hEvfcYDe89P2oGACWI5hd2GadRDKHalSxkc1Z0z8I6yzuVK6SF15sW09QZngTV6d7g4+TFL9bvs5A==} + engines: {node: '>=16 <22'} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-redact@3.3.0: + resolution: {integrity: sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==} + engines: {node: '>=6'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@2.3.0: + resolution: {integrity: sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==} + + fastest-levenshtein@1.0.16: + resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} + engines: {node: '>= 4.9.1'} + + fastify-plugin@4.5.1: + resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} + + fastify-raw-body@4.2.2: + resolution: {integrity: sha512-6l4fXtxNn7WOQiylu5fv9/JfUTvWCg1ED4gF44hqnVesgttOXEUMnNkdV8ZxwufCstRyUYaYSBIN4VuRHDbJkw==} + engines: {node: '>= 10'} + + fastify@4.24.3: + resolution: {integrity: sha512-6HHJ+R2x2LS3y1PqxnwEIjOTZxFl+8h4kSC/TuDPXtA+v2JnV9yEtOsNSKK1RMD7sIR2y1ZsA4BEFaid/cK5pg==} + + fastq@1.15.0: + resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + + file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + + file-type@3.9.0: + resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==} + engines: {node: '>=0.10.0'} + + file-type@4.4.0: + resolution: {integrity: sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==} + engines: {node: '>=4'} + + file-type@5.2.0: + resolution: {integrity: sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==} + engines: {node: '>=4'} + + file-type@6.2.0: + resolution: {integrity: sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==} + engines: {node: '>=4'} + + file-type@8.1.0: + resolution: {integrity: sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==} + engines: {node: '>=6'} + + file-type@9.0.0: + resolution: {integrity: sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw==} + engines: {node: '>=6'} + + filelist@1.0.4: + resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + + filename-reserved-regex@2.0.0: + resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} + engines: {node: '>=4'} + + filenamify@2.1.0: + resolution: {integrity: sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==} + engines: {node: '>=4'} + + filenamify@4.3.0: + resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==} + engines: {node: '>=8'} + + fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + + finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + + find-my-way@7.7.0: + resolution: {integrity: sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==} + engines: {node: '>=14'} + + find-up@2.1.0: + resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} + engines: {node: '>=4'} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + find-versions@3.2.0: + resolution: {integrity: sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==} + engines: {node: '>=6'} + + flat-cache@3.1.1: + resolution: {integrity: sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==} + engines: {node: '>=12.0.0'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.2.9: + resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} + + flora-colossus@1.0.1: + resolution: {integrity: sha512-d+9na7t9FyH8gBJoNDSi28mE4NgQVGGvxQ4aHtFRetjyh5SXjuus+V5EZaxFmFdXVemSOrx0lsgEl/ZMjnOWJA==} + engines: {node: '>= 6.0.0'} + + fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + + follow-redirects@1.15.3: + resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + + form-data-encoder@2.1.4: + resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} + engines: {node: '>= 14.17'} + + form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + formidable@2.1.2: + resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + + fs-extra@4.0.3: + resolution: {integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + galactus@0.2.1: + resolution: {integrity: sha512-mDc8EQJKtxjp9PMYS3PbpjjbX3oXhBTxoGaPahw620XZBIHJ4+nvw5KN/tRtmmSDR9dypstGNvqQ3C29QGoGHQ==} + + gauge@4.0.4: + resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-intrinsic@1.2.2: + resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + + get-package-info@1.0.0: + resolution: {integrity: sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==} + engines: {node: '>= 4.0'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-pkg-repo@4.2.1: + resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} + engines: {node: '>=6.9.0'} + hasBin: true + + get-proxy@2.1.0: + resolution: {integrity: sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==} + engines: {node: '>=4'} + + get-stream@2.3.1: + resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==} + engines: {node: '>=0.10.0'} + + get-stream@3.0.0: + resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} + engines: {node: '>=4'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + getopts@2.3.0: + resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} + + gifwrap@0.9.4: + resolution: {integrity: sha512-MDMwbhASQuVeD4JKd1fKgNgCRL3fGqMM4WaqpNhWO0JiMOAjbQdumbs4BbBZEy9/M00EHEjKN3HieVhCUlwjeQ==} + + git-raw-commits@2.0.11: + resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} + engines: {node: '>=10'} + hasBin: true + + git-remote-origin-url@2.0.0: + resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==} + engines: {node: '>=4'} + + git-semver-tags@4.1.1: + resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==} + engines: {node: '>=10'} + hasBin: true + + gitconfiglocal@1.0.0: + resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + + glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + global-dirs@0.1.1: + resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} + engines: {node: '>=4'} + + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + + global-tunnel-ng@2.7.1: + resolution: {integrity: sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==} + engines: {node: '>=0.10'} + + global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + + globals@13.23.0: + resolution: {integrity: sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==} + engines: {node: '>=8'} + + globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + globby@12.2.0: + resolution: {integrity: sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + got@12.6.1: + resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} + engines: {node: '>=14.16'} + + got@7.1.0: + resolution: {integrity: sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==} + engines: {node: '>=4'} + + got@8.3.2: + resolution: {integrity: sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==} + engines: {node: '>=4'} + + got@9.6.0: + resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} + engines: {node: '>=8.6'} + + graceful-fs@4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + hard-rejection@2.1.0: + resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} + engines: {node: '>=6'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.1: + resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + + has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + + has-symbol-support-x@1.4.2: + resolution: {integrity: sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==} + + has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + has-to-string-tag-x@1.4.1: + resolution: {integrity: sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==} + + has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + + has-yarn@3.0.0: + resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + hasown@2.0.0: + resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + helmet@7.0.0: + resolution: {integrity: sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ==} + engines: {node: '>=16.0.0'} + + hexoid@1.0.0: + resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} + engines: {node: '>=8'} + + highlight.js@11.8.0: + resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==} + engines: {node: '>=12.0.0'} + + highlight.js@11.9.0: + resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==} + engines: {node: '>=12.0.0'} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + hosted-git-info@6.1.1: + resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + html-minifier@4.0.0: + resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==} + engines: {node: '>=6'} + hasBin: true + + html-to-text@7.1.1: + resolution: {integrity: sha512-c9QWysrfnRZevVpS8MlE7PyOdSuIOjg8Bt8ZE10jMU/BEngA6j3llj4GRfAmtQzcd1FjKE0sWu5IHXRUH9YxIQ==} + engines: {node: '>=10.23.2'} + hasBin: true + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + + http-cache-semantics@3.8.1: + resolution: {integrity: sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + http2-wrapper@2.2.0: + resolution: {integrity: sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + human-signals@4.3.1: + resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} + engines: {node: '>=14.18.0'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + husky@8.0.3: + resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} + engines: {node: '>=14'} + hasBin: true + + iconv-corefoundation@1.1.7: + resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} + engines: {node: ^8.11.2 || >=10} + os: [darwin] + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.2.4: + resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} + engines: {node: '>= 4'} + + image-q@4.0.0: + resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} + + imagemin-pngquant@9.0.2: + resolution: {integrity: sha512-cj//bKo8+Frd/DM8l6Pg9pws1pnDUjgb7ae++sUX1kUVdv2nrngPykhiUOgFeE0LGY/LmUbCf4egCHC4YUcZSg==} + engines: {node: '>=10'} + + imagemin@8.0.1: + resolution: {integrity: sha512-Q/QaPi+5HuwbZNtQRqUVk6hKacI6z9iWiCSQBisAv7uBynZwO7t1svkryKl7+iSQbkU/6t9DWnHz04cFs2WY7w==} + engines: {node: '>=12'} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-lazy@3.1.0: + resolution: {integrity: sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==} + engines: {node: '>=6'} + + import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + infer-owner@1.0.4: + resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + ini@3.0.1: + resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + inquirer@8.2.6: + resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} + engines: {node: '>=12.0.0'} + + interpret@2.2.0: + resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} + engines: {node: '>= 0.10'} + + into-stream@3.1.0: + resolution: {integrity: sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==} + engines: {node: '>=4'} + + ip@1.1.8: + resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} + + ip@2.0.0: + resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-ci@3.0.1: + resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} + hasBin: true + + is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-function@1.0.2: + resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-lambda@1.0.1: + resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} + + is-natural-number@4.0.1: + resolution: {integrity: sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==} + + is-npm@6.0.0: + resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@2.0.0: + resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} + engines: {node: '>=8'} + + is-object@1.0.2: + resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-plain-obj@1.1.0: + resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} + engines: {node: '>=0.10.0'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-png@2.0.0: + resolution: {integrity: sha512-4KPGizaVGj2LK7xwJIz8o5B2ubu1D/vcQsgOGFEDlpcvgZHto4gBnyd0ig7Ws+67ixmwKoNmu0hYnpo6AaKb5g==} + engines: {node: '>=8'} + + is-png@3.0.1: + resolution: {integrity: sha512-8TqC8+bdsm3YkpI2aECCDycFDl1hTB0HMVRnP3xRRa3Tqx2oVE7sBi1G6CuO9IqEyWSzbBZr1mGqdb3it9h/pg==} + engines: {node: '>=12'} + + is-retry-allowed@1.2.0: + resolution: {integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==} + engines: {node: '>=0.10.0'} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + is-text-path@1.0.1: + resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} + engines: {node: '>=0.10.0'} + + is-text-path@2.0.0: + resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} + engines: {node: '>=8'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + is-yarn-global@0.4.1: + resolution: {integrity: sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==} + engines: {node: '>=12'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.0: + resolution: {integrity: sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==} + engines: {node: '>= 14.0.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + + isurl@1.0.0: + resolution: {integrity: sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==} + engines: {node: '>= 4'} + + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + + jake@10.8.7: + resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} + engines: {node: '>=10'} + hasBin: true + + jimp@0.14.0: + resolution: {integrity: sha512-8BXU+J8+SPmwwyq9ELihpSV4dWPTiOKBWCEgtkbnxxAVMjXdf3yGmyaLSshBfXc8sP/JQ9OZj5R8nZzz2wPXgA==} + + jiti@1.21.0: + resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + hasBin: true + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + jpeg-js@0.4.4: + resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} + + js-base64@3.7.5: + resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-buffer@3.0.0: + resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-ref-resolver@1.0.1: + resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-eslint-parser@1.4.1: + resolution: {integrity: sha512-hXBrvsR1rdjmB2kQmUjf1rEIa+TqHBGMge8pwi++C+Si1ad7EjZrJcpgwym+QGK/pqTx+K7keFAtLlVNdLRJOg==} + engines: {node: '>=8.10.0'} + + jsonc-parser@3.2.0: + resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + junk@3.1.0: + resolution: {integrity: sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==} + engines: {node: '>=8'} + + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jwa@2.0.0: + resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + + jws@4.0.0: + resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + + katex@0.16.9: + resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==} + hasBin: true + + keyv@3.0.0: + resolution: {integrity: sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==} + + keyv@3.1.0: + resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + knex@2.3.0: + resolution: {integrity: sha512-WMizPaq9wRMkfnwKXKXgBZeZFOSHGdtoSz5SaLAVNs3WRDfawt9O89T4XyH52PETxjV8/kRk0Yf+8WBEP/zbYw==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + better-sqlite3: '*' + mysql: '*' + mysql2: '*' + pg: '*' + pg-native: '*' + sqlite3: '*' + tedious: '*' + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + + latest-version@7.0.0: + resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} + engines: {node: '>=14.16'} + + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lib0@0.2.87: + resolution: {integrity: sha512-TbB63XJixvNToW2IHWAFsCJj9tVnajmwjE14p69i51Rx8byOQd2IP4ourE8v4d7vhyO++nVm1sQk3ePslfbucg==} + engines: {node: '>=16'} + hasBin: true + + libsodium-sumo@0.7.13: + resolution: {integrity: sha512-zTGdLu4b9zSNLfovImpBCbdAA4xkpkZbMnSQjP8HShyOutnGjRHmSOKlsylh1okao6QhLiz7nG98EGn+04cZjQ==} + + libsodium-wrappers-sumo@0.7.13: + resolution: {integrity: sha512-lz4YdplzDRh6AhnLGF2Dj2IUj94xRN6Bh8T0HLNwzYGwPehQJX6c7iYVrFUPZ3QqxE0bqC+K0IIqqZJYWumwSQ==} + + light-my-request@5.11.0: + resolution: {integrity: sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + linkify-it@4.0.1: + resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} + + linkifyjs@4.1.1: + resolution: {integrity: sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA==} + + load-bmfont@1.4.1: + resolution: {integrity: sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA==} + + load-json-file@2.0.0: + resolution: {integrity: sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==} + engines: {node: '>=4'} + + load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + local-pkg@0.4.3: + resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} + engines: {node: '>=14'} + + locate-path@2.0.0: + resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} + engines: {node: '>=4'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.ismatch@4.4.0: + resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.kebabcase@4.1.1: + resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + + lodash.upperfirst@4.3.1: + resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + logform@2.6.0: + resolution: {integrity: sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==} + engines: {node: '>= 12.0.0'} + + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + + lower-case@1.1.4: + resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} + + lowercase-keys@1.0.0: + resolution: {integrity: sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==} + engines: {node: '>=0.10.0'} + + lowercase-keys@1.0.1: + resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} + engines: {node: '>=0.10.0'} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lowercase-keys@3.0.0: + resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lowlight@2.9.0: + resolution: {integrity: sha512-OpcaUTCLmHuVuBcyNckKfH5B0oA4JUavb/M/8n9iAvanJYNQkrVm4pvyX0SUaqkBG4dnWHKt7p50B3ngAG2Rfw==} + + lru-cache@10.0.1: + resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} + engines: {node: 14 || >=16.14} + + lru-cache@4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + magic-string@0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + + magic-string@0.26.7: + resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==} + engines: {node: '>=12'} + + magic-string@0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + + magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} + engines: {node: '>=12'} + + make-dir@1.3.0: + resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} + engines: {node: '>=4'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + make-fetch-happen@10.2.1: + resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + map-obj@1.0.1: + resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} + engines: {node: '>=0.10.0'} + + map-obj@4.3.0: + resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} + engines: {node: '>=8'} + + markdown-it@13.0.2: + resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==} + hasBin: true + + marked-gfm-heading-id@3.1.1: + resolution: {integrity: sha512-PATvg4bpYxYY7SiTkknZWNiuKtfgpIctCHsbCHZiEUB+7eZ6SjGMlpL//X0JzE3/Z9B9aqLgQS9UTMFfYs6CEg==} + peerDependencies: + marked: '>=4 <11' + + marked@9.1.5: + resolution: {integrity: sha512-14QG3shv8Kg/xc0Yh6TNkMj90wXH9mmldi5941I2OevfJ/FQAFLEwtwU2/FfgSAOMlWHrEukWSGQf8MiVYNG2A==} + engines: {node: '>= 16'} + hasBin: true + + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + + mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + + mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + meow@8.1.2: + resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} + engines: {node: '>=10'} + + merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + mimic-response@4.0.0: + resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + min-document@2.19.0: + resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + + minimatch@8.0.4: + resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} + engines: {node: '>=16 || 14 >=14.17'} + + minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist-options@4.1.0: + resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} + engines: {node: '>= 6'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass-collect@1.0.2: + resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} + engines: {node: '>= 8'} + + minipass-fetch@2.1.2: + resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + minipass-flush@1.0.5: + resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} + engines: {node: '>= 8'} + + minipass-pipeline@1.2.4: + resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} + engines: {node: '>=8'} + + minipass-sized@1.0.3: + resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} + engines: {node: '>=8'} + + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + + minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.4.2: + resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} + + mnemonist@0.39.5: + resolution: {integrity: sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==} + + modify-filename@1.1.0: + resolution: {integrity: sha512-EickqnKq3kVVaZisYuCxhtKbZjInCuwgwZWyAmRIp1NTMhri7r3380/uqwrUHfaDiPzLVTuoNy4whX66bxPVog==} + engines: {node: '>=0.10.0'} + + modify-values@1.0.1: + resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} + engines: {node: '>=0.10.0'} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msgpackr-extract@3.0.2: + resolution: {integrity: sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==} + hasBin: true + + msgpackr@1.9.9: + resolution: {integrity: sha512-sbn6mioS2w0lq1O6PpGtsv6Gy8roWM+o3o4Sqjd6DudrL/nOugY+KyJUimoWzHnf9OkO0T6broHFnYE/R05t9A==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + mylas@2.1.13: + resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} + engines: {node: '>=12.0.0'} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@1.0.2: + resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + + native-run@1.7.4: + resolution: {integrity: sha512-yDEwTp66vmXpqFiSQzz4sVQgyq5U58gGRovglY4GHh12ITyWa6mh6Lbpm2gViVOVD1JYFtYnwcgr7GTFBinXNA==} + engines: {node: '>=12.13.0'} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + no-case@2.3.2: + resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + + node-abi@3.51.0: + resolution: {integrity: sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==} + engines: {node: '>=10'} + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-addon-api@1.7.2: + resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} + + node-addon-api@6.1.0: + resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + + node-api-version@0.1.4: + resolution: {integrity: sha512-KGXihXdUChwJAOHO53bv9/vXcLmdUsZ6jIptbvYvkpKfth+r7jw44JkVxQFA3kX5nQjzjmGu1uAu/xNNLNlI5g==} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build-optional-packages@5.0.7: + resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==} + hasBin: true + + node-gyp-build@4.6.1: + resolution: {integrity: sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==} + hasBin: true + + node-gyp@9.4.1: + resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==} + engines: {node: ^12.13 || ^14.13 || >=16} + hasBin: true + + node-mailjet@6.0.4: + resolution: {integrity: sha512-gNWfbVnsH+KxkhfDLPA8OrQ2Q25OgyKp19C7DSJYmN2zNfqTKIXzhB9BZwgxZtErmPxz2Fp1NR18WPCmrJDuwg==} + engines: {node: '>= 12.0.0', npm: '>= 6.9.0'} + + node-releases@2.0.13: + resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + + nodemailer-html-to-text@3.2.0: + resolution: {integrity: sha512-RJUC6640QV1PzTHHapOrc6IzrAJUZtk2BdVdINZ9VTLm+mcQNyBO9LYyhrnufkzqiD9l8hPLJ97rSyK4WanPNg==} + engines: {node: '>= 10.23.0'} + + nodemailer@6.9.7: + resolution: {integrity: sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==} + engines: {node: '>=6.0.0'} + + nopt@6.0.0: + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-package-data@3.0.3: + resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} + engines: {node: '>=10'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + normalize-url@2.0.1: + resolution: {integrity: sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==} + engines: {node: '>=4'} + + normalize-url@4.5.1: + resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + engines: {node: '>=8'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + normalize-url@8.0.0: + resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} + engines: {node: '>=14.16'} + + npm-conf@1.1.3: + resolution: {integrity: sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==} + engines: {node: '>=4'} + + npm-package-arg@10.1.0: + resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + npm-run-path@5.1.0: + resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + npmlog@6.0.2: + resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.omit@3.0.0: + resolution: {integrity: sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==} + engines: {node: '>=0.10.0'} + + object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} + + objection@3.0.1: + resolution: {integrity: sha512-rqNnyQE+C55UHjdpTOJEKQHJGZ/BGtBBtgxdUpKG4DQXRUmqxfmgS/MhPWxB9Pw0mLSVLEltr6soD4c0Sddy0Q==} + engines: {node: '>=12.0.0'} + peerDependencies: + knex: '>=0.95.0' + + obliterator@2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + + omggif@1.0.10: + resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + open@9.1.0: + resolution: {integrity: sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==} + engines: {node: '>=14.16'} + + optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + + os-filter-obj@2.0.0: + resolution: {integrity: sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==} + engines: {node: '>=4'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + otplib@12.0.1: + resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} + + ow@0.17.0: + resolution: {integrity: sha512-i3keDzDQP5lWIe4oODyDFey1qVrq2hXKTuTH2VpqwpYtzPiKZt2ziRI4NBQmgW40AnV5Euz17OyWweCb+bNEQA==} + engines: {node: '>=10'} + + p-cancelable@0.3.0: + resolution: {integrity: sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==} + engines: {node: '>=4'} + + p-cancelable@0.4.1: + resolution: {integrity: sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==} + engines: {node: '>=4'} + + p-cancelable@1.1.0: + resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} + engines: {node: '>=6'} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-cancelable@3.0.0: + resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} + engines: {node: '>=12.20'} + + p-event@1.3.0: + resolution: {integrity: sha512-hV1zbA7gwqPVFcapfeATaNjQ3J0NuzorHPyG8GPL9g/Y/TplWVBVoCKCXL6Ej2zscrCEv195QNWJXuBH6XZuzA==} + engines: {node: '>=4'} + + p-event@2.3.1: + resolution: {integrity: sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==} + engines: {node: '>=6'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-is-promise@1.1.0: + resolution: {integrity: sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==} + engines: {node: '>=4'} + + p-limit@1.3.0: + resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + p-locate@2.0.0: + resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} + engines: {node: '>=4'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-map-series@1.0.0: + resolution: {integrity: sha512-4k9LlvY6Bo/1FcIdV33wqZQES0Py+iKISU9Uc8p8AjWoZPnFKMpVIVD3s0EYn4jzLh1I+WeUZkJ0Yoa4Qfw3Kg==} + engines: {node: '>=4'} + + p-map@4.0.0: + resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} + engines: {node: '>=10'} + + p-pipe@4.0.0: + resolution: {integrity: sha512-HkPfFklpZQPUKBFXzKFB6ihLriIHxnmuQdK9WmLDwe4hf2PdhhfWT/FJa+pc3bA1ywvKXtedxIRmd4Y7BTXE4w==} + engines: {node: '>=12'} + + p-reduce@1.0.0: + resolution: {integrity: sha512-3Tx1T3oM1xO/Y8Gj0sWyE78EIJZ+t+aEmXUdvQgvGmSMri7aPTHoovbXEreWKkL5j21Er60XAWLTzKbAKYOujQ==} + engines: {node: '>=4'} + + p-timeout@1.2.1: + resolution: {integrity: sha512-gb0ryzr+K2qFqFv6qi3khoeqMZF/+ajxQipEF6NteZVnvz9tzdsfAVj3lYtn1gAXvH5lfLwfxEII799gt/mRIA==} + engines: {node: '>=4'} + + p-timeout@2.0.1: + resolution: {integrity: sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==} + engines: {node: '>=4'} + + p-try@1.0.0: + resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} + engines: {node: '>=4'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json@8.1.1: + resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} + engines: {node: '>=14.16'} + + packet-reader@1.0.0: + resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} + + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + + param-case@2.1.1: + resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-author@2.0.0: + resolution: {integrity: sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==} + engines: {node: '>=0.10.0'} + + parse-bmfont-ascii@1.0.6: + resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} + + parse-bmfont-binary@1.0.6: + resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} + + parse-bmfont-xml@1.1.4: + resolution: {integrity: sha512-bjnliEOmGv3y1aMEfREMBJ9tfL3WR0i0CKPj61DnSLaoxWR3nLrsQrEbCId/8rF4NyRF0cCqisSVXyQYWM+mCQ==} + + parse-headers@2.0.5: + resolution: {integrity: sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==} + + parse-json@2.2.0: + resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==} + engines: {node: '>=0.10.0'} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + + path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + + path-to-regexp@3.2.0: + resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} + + path-type@2.0.0: + resolution: {integrity: sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==} + engines: {node: '>=4'} + + path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@1.1.1: + resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} + + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + + peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + + pg-connection-string@2.5.0: + resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==} + + pg-connection-string@2.6.2: + resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.6.1: + resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.6.0: + resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.11.3: + resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + phin@2.9.3: + resolution: {integrity: sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==} + + picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pify@5.0.0: + resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} + engines: {node: '>=10'} + + pinia@2.0.36: + resolution: {integrity: sha512-4UKApwjlmJH+VuHKgA+zQMddcCb3ezYnyewQ9NVrsDqZ/j9dMv5+rh+1r48whKNdpFkZAWVxhBp5ewYaYX9JcQ==} + peerDependencies: + '@vue/composition-api': ^1.4.0 + typescript: '>=4.4.4' + vue: ^2.6.14 || ^3.2.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + typescript: + optional: true + + pinkie-promise@2.0.1: + resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} + engines: {node: '>=0.10.0'} + + pinkie@2.0.4: + resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} + engines: {node: '>=0.10.0'} + + pino-abstract-transport@1.1.0: + resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==} + + pino-std-serializers@6.2.2: + resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} + + pino@8.16.1: + resolution: {integrity: sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==} + hasBin: true + + pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + pixelmatch@4.0.2: + resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==} + hasBin: true + + pkg-types@1.0.3: + resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + png2icons@2.0.1: + resolution: {integrity: sha512-GDEQJr8OG4e6JMp7mABtXFSEpgJa1CCpbQiAR+EjhkHJHnUL9zPPtbOrjsMD8gUbikgv3j7x404b0YJsV3aVFA==} + hasBin: true + + pngjs@3.4.0: + resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} + engines: {node: '>=4.0.0'} + + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + + pngquant-bin@6.0.1: + resolution: {integrity: sha512-Q3PUyolfktf+hYio6wsg3SanQzEU/v8aICg/WpzxXcuCMRb7H2Q81okfpcEztbMvw25ILjd3a87doj2N9kvbpQ==} + engines: {node: '>=10'} + hasBin: true + + postcss-load-config@4.0.1: + resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-selector-parser@6.0.13: + resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + potrace@2.1.8: + resolution: {integrity: sha512-V9hI7UMJyEhNZjM8CbZaP/804ZRLgzWkCS9OOYnEZkszzj3zKR/erRdj0uFMcN3pp6x4B+AIZebmkQgGRinG/g==} + + prebuild-install@7.1.1: + resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} + engines: {node: '>=10'} + hasBin: true + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prepend-http@1.0.4: + resolution: {integrity: sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==} + engines: {node: '>=0.10.0'} + + prepend-http@2.0.0: + resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} + engines: {node: '>=4'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.0.3: + resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + proc-log@3.0.0: + resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@2.3.0: + resolution: {integrity: sha512-N6mp1+2jpQr3oCFMz6SeHRGbv6Slb20bRhj4v3xR99HqNToAcOe1MFOp4tytyzOfJn+QtN8Rf7U/h2KAn4kC6g==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + prom-client@15.0.0: + resolution: {integrity: sha512-UocpgIrKyA2TKLVZDSfm8rGkL13C19YrQBAiG3xo3aDFWcHedxRxI3z+cIcucoxpSO0h5lff5iv/SXoxyeopeA==} + engines: {node: ^16 || ^18 || >=20} + + promise-inflight@1.0.1: + resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} + peerDependencies: + bluebird: '*' + peerDependenciesMeta: + bluebird: + optional: true + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prosemirror-changeset@2.2.1: + resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.5.2: + resolution: {integrity: sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==} + + prosemirror-dropcursor@1.8.1: + resolution: {integrity: sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==} + + prosemirror-gapcursor@1.3.2: + resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} + + prosemirror-history@1.3.2: + resolution: {integrity: sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==} + + prosemirror-inputrules@1.3.0: + resolution: {integrity: sha512-z1GRP2vhh5CihYMQYsJSa1cOwXb3SYxALXOIfAkX8nZserARtl9LiL+CEl+T+OFIsXc3mJIHKhbsmRzC0HDAXA==} + + prosemirror-keymap@1.2.2: + resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==} + + prosemirror-markdown@1.11.2: + resolution: {integrity: sha512-Eu5g4WPiCdqDTGhdSsG9N6ZjACQRYrsAkrF9KYfdMaCmjIApH75aVncsWYOJvEk2i1B3i8jZppv3J/tnuHGiUQ==} + + prosemirror-menu@1.2.4: + resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==} + + prosemirror-model@1.18.3: + resolution: {integrity: sha512-yUVejauEY3F1r7PDy4UJKEGeIU+KFc71JQl5sNvG66CLVdKXRjhWpBW6KMeduGsmGOsw85f6EGrs6QxIKOVILA==} + + prosemirror-model@1.19.3: + resolution: {integrity: sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==} + + prosemirror-schema-basic@1.2.2: + resolution: {integrity: sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==} + + prosemirror-schema-list@1.3.0: + resolution: {integrity: sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A==} + + prosemirror-state@1.4.3: + resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} + + prosemirror-tables@1.3.4: + resolution: {integrity: sha512-z6uLSQ1BLC3rgbGwZmpfb+xkdvD7W/UOsURDfognZFYaTtc0gsk7u/t71Yijp2eLflVpffMk6X0u0+u+MMDvIw==} + + prosemirror-trailing-node@2.0.7: + resolution: {integrity: sha512-8zcZORYj/8WEwsGo6yVCRXFMOfBo0Ub3hCUvmoWIZYfMP26WqENU0mpEP27w7mt8buZWuGrydBewr0tOArPb1Q==} + peerDependencies: + prosemirror-model: ^1.19.0 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.31.2 + + prosemirror-transform@1.8.0: + resolution: {integrity: sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==} + + prosemirror-view@1.29.2: + resolution: {integrity: sha512-T4Wm+eTpTH0N9gBJfJR6iecjRX2hYTKewoJUwa92hQOoEz2bYVZy6sYeN+hfnRR506TRvRcuZYqftp4KA8dN+Q==} + + prosemirror-view@1.32.4: + resolution: {integrity: sha512-WoT+ZYePp0WQvp5coABAysheZg9WttW3TSEUNgsfDQXmVOJlnjkbFbXicKPvWFLiC0ZjKt1ykbyoVKqhVnCiSQ==} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pseudomap@1.0.2: + resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + + pump@3.0.0: + resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pupa@2.1.1: + resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} + engines: {node: '>=8'} + + pupa@3.1.0: + resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} + engines: {node: '>=12.20'} + + pure-rand@6.0.4: + resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} + + q@1.5.1: + resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + + qrcode@1.5.3: + resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} + engines: {node: '>=10.13.0'} + hasBin: true + + qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + + qs@6.11.2: + resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} + engines: {node: '>=0.6'} + + query-string@5.1.1: + resolution: {integrity: sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==} + engines: {node: '>=0.10.0'} + + querystring@0.2.1: + resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==} + engines: {node: '>=0.4.x'} + deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. + + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + queue-tick@1.0.1: + resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + quick-lru@4.0.1: + resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} + engines: {node: '>=8'} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + rcedit@3.1.0: + resolution: {integrity: sha512-WRlRdY1qZbu1L11DklT07KuHfRk42l0NFFJdaExELEu4fEQ982bP5Z6OWGPj/wLLIuKRQDCxZJGAwoFsxhZhNA==} + engines: {node: '>= 10.0.0'} + + react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + + read-chunk@4.0.3: + resolution: {integrity: sha512-wOYymxRWkxn3MlStSt7LxrMLRvynHKjzHVQPTCBbT29ViUwsT3EE09dE5iMDDGYQTL/s5TQZvBLuJTeZFeGQ4g==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + read-config-file@6.3.2: + resolution: {integrity: sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==} + engines: {node: '>=12.0.0'} + + read-pkg-up@2.0.0: + resolution: {integrity: sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==} + engines: {node: '>=4'} + + read-pkg-up@3.0.0: + resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} + engines: {node: '>=4'} + + read-pkg-up@7.0.1: + resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} + engines: {node: '>=8'} + + read-pkg@2.0.0: + resolution: {integrity: sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==} + engines: {node: '>=4'} + + read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + + read-pkg@5.2.0: + resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} + engines: {node: '>=8'} + + read-yaml-file@2.1.0: + resolution: {integrity: sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==} + engines: {node: '>=10.13'} + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.4.2: + resolution: {integrity: sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readable-web-to-node-stream@3.0.2: + resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} + engines: {node: '>=8'} + + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + redlock@5.0.0-beta.2: + resolution: {integrity: sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw==} + engines: {node: '>=12'} + + reflect-metadata@0.1.13: + resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + regenerator-runtime@0.14.0: + resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + + register-service-worker@1.7.2: + resolution: {integrity: sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==} + + registry-auth-token@5.0.2: + resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} + engines: {node: '>=14'} + + registry-url@6.0.1: + resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} + engines: {node: '>=12'} + + relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + + replace-ext@2.0.0: + resolution: {integrity: sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==} + engines: {node: '>= 10'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-global@1.0.0: + resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} + engines: {node: '>=8'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + responselike@1.0.2: + resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + responselike@3.0.0: + resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} + engines: {node: '>=14.16'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + ret@0.2.2: + resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} + engines: {node: '>=4'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.3.0: + resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + + rimraf@4.4.1: + resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} + engines: {node: '>=14'} + hasBin: true + + rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + + rollup-plugin-visualizer@5.9.2: + resolution: {integrity: sha512-waHktD5mlWrYFrhOLbti4YgQCn1uR24nYsNuXxg7LkPH8KdTXVWR9DNY1WU0QqokyMixVXJS4J04HNrVTMP01A==} + engines: {node: '>=14'} + hasBin: true + peerDependencies: + rollup: 2.x || 3.x + peerDependenciesMeta: + rollup: + optional: true + + rollup@2.77.3: + resolution: {integrity: sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==} + engines: {node: '>=10.0.0'} + hasBin: true + + rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + + run-applescript@5.0.0: + resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} + engines: {node: '>=12'} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex2@2.0.0: + resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==} + + safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-filename@1.6.3: + resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} + + sass@1.32.12: + resolution: {integrity: sha512-zmXn03k3hN0KaiVTjohgkg98C3UowhL1/VSGdj4/VAAiMKGQOE80PFPxFP2Kyq0OUskPKcY5lImkhBKEHlypJA==} + engines: {node: '>=8.9.0'} + hasBin: true + + sax@1.1.4: + resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==} + + sax@1.3.0: + resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + + scule@1.0.0: + resolution: {integrity: sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==} + + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + + seek-bzip@1.0.6: + resolution: {integrity: sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==} + hasBin: true + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver-diff@4.0.0: + resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} + engines: {node: '>=12'} + + semver-regex@2.0.0: + resolution: {integrity: sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==} + engines: {node: '>=6'} + + semver-truncate@1.1.2: + resolution: {integrity: sha512-V1fGg9i4CL3qesB6U0L6XAm4xOJiHmt4QAacazumuasc03BvtFGIMCduv01JWQ69Nv+JST9TqhSCiJoxoY031w==} + engines: {node: '>=0.10.0'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.0.0: + resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} + hasBin: true + + semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + + send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + serialize-javascript@6.0.1: + resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} + + serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-cookie-parser@2.6.0: + resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} + + set-function-length@1.1.1: + resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + + sharp@0.32.6: + resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} + engines: {node: '>=14.15.0'} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.1: + resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} + + showdown@2.1.0: + resolution: {integrity: sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==} + hasBin: true + + side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + simple-update-notifier@1.1.0: + resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} + engines: {node: '>=8.10.0'} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + slash@4.0.0: + resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} + engines: {node: '>=12'} + + slice-ansi@3.0.0: + resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} + engines: {node: '>=8'} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socket.io-adapter@2.5.2: + resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} + + socket.io-client@4.7.2: + resolution: {integrity: sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.7.2: + resolution: {integrity: sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==} + engines: {node: '>=10.2.0'} + + socks-proxy-agent@7.0.0: + resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} + engines: {node: '>= 10'} + + socks@2.7.1: + resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} + engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} + + sonic-boom@3.7.0: + resolution: {integrity: sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==} + + sort-keys-length@1.0.1: + resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} + engines: {node: '>=0.10.0'} + + sort-keys@1.1.2: + resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==} + engines: {node: '>=0.10.0'} + + sort-keys@2.0.0: + resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==} + engines: {node: '>=4'} + + source-map-js@1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + + sourcemap-codec@1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + deprecated: Please use @jridgewell/sourcemap-codec instead + + spawn-command@0.0.2: + resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.3.0: + resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.16: + resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==} + + split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + ssri@9.0.1: + resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + stack-trace@1.0.0-pre2: + resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} + engines: {node: '>=16'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + standard-version@9.5.0: + resolution: {integrity: sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q==} + engines: {node: '>=10'} + hasBin: true + + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@3.4.3: + resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==} + + streamx@2.15.2: + resolution: {integrity: sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==} + + strict-uri-encode@1.1.0: + resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-package@1.0.1: + resolution: {integrity: sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==} + deprecated: This module is not used anymore, and has been replaced by @npmcli/package-json + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-dirs@2.1.0: + resolution: {integrity: sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@1.3.0: + resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + + strip-outer@1.0.1: + resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} + engines: {node: '>=0.10.0'} + + stripe@14.3.0: + resolution: {integrity: sha512-R3s+3ONM1XFOTzbMSIML0tixbkuz+gFY/p1h1Qxd9OUftxS8m+rGeBv4ZnvoVhTUwOokArfzQtQlR2Re9XnyQw==} + engines: {node: '>=12.*'} + + strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + + sucrase@3.34.0: + resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} + engines: {node: '>=8'} + hasBin: true + + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + + superagent@8.1.2: + resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} + engines: {node: '>=6.4.0 <13 || >=14'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svgo@3.0.2: + resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==} + engines: {node: '>=14.0.0'} + hasBin: true + + synckit@0.8.5: + resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} + engines: {node: ^14.18.0 || >=16.0.0} + + syncpack@11.2.1: + resolution: {integrity: sha512-WoUtm+ZLmWUvy0cLJy8ds/smVRH3ivI6iANcGTPrsvareCc4SmRVMvr+TwjZyFm0FDGmEfMVsAX7z16+yxL6bQ==} + engines: {node: '>=16'} + hasBin: true + + table@6.8.1: + resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} + engines: {node: '>=10.0.0'} + + tar-fs@2.1.1: + resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + + tar-fs@3.0.4: + resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} + + tar-stream@1.6.2: + resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==} + engines: {node: '>= 0.8.0'} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar-stream@3.1.6: + resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} + + tar@6.2.0: + resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} + engines: {node: '>=10'} + + tarn@3.0.2: + resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==} + engines: {node: '>=8.0.0'} + + tdigest@0.1.2: + resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + + temp-dir@1.0.0: + resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} + engines: {node: '>=4'} + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + + tempfile@2.0.0: + resolution: {integrity: sha512-ZOn6nJUgvgC09+doCEF3oB+r3ag7kUvlsXEGX069QRD60p+P3uP7XG9N2/at+EyIRGSN//ZY3LyEotA1YpmjuA==} + engines: {node: '>=4'} + + text-extensions@1.9.0: + resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} + engines: {node: '>=0.10'} + + text-extensions@2.4.0: + resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} + engines: {node: '>=8'} + + text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + thirty-two@1.0.2: + resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} + engines: {node: '>=0.2.6'} + + thread-stream@2.4.1: + resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==} + + throttle-debounce@3.0.1: + resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} + engines: {node: '>=10'} + + through2@2.0.5: + resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tightrope@0.1.0: + resolution: {integrity: sha512-HHHNYdCAIYwl1jOslQBT455zQpdeSo8/A346xpIb/uuqhSg+tCvYNsP5f11QW+z9VZ3vSX8YIfzTApjjuGH63w==} + engines: {node: '>=14'} + + tildify@2.0.0: + resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} + engines: {node: '>=8'} + + timed-out@4.0.1: + resolution: {integrity: sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==} + engines: {node: '>=0.10.0'} + + timm@1.7.1: + resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==} + + tiny-lru@11.2.5: + resolution: {integrity: sha512-JpqM0K33lG6iQGKiigcwuURAKZlq6rHXfrgeL4/I8/REoyJTGU+tEMszvT/oTRVHG2OiylhGDjqPp1jWMlr3bw==} + engines: {node: '>=12'} + + tinybench@2.5.1: + resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==} + + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + tinypool@0.7.0: + resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} + engines: {node: '>=14.0.0'} + + tinyspy@2.2.0: + resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} + engines: {node: '>=14.0.0'} + + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + + titleize@3.0.0: + resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} + engines: {node: '>=12'} + + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmp@0.2.1: + resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} + engines: {node: '>=8.17.0'} + + to-buffer@1.1.1: + resolution: {integrity: sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==} + + to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + to-readable-stream@1.0.0: + resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} + engines: {node: '>=6'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toad-cache@3.3.0: + resolution: {integrity: sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==} + engines: {node: '>=12'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + + trim-repeated@1.0.0: + resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} + engines: {node: '>=0.10.0'} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + + ts-api-utils@1.0.3: + resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} + engines: {node: '>=16.13.0'} + peerDependencies: + typescript: '>=4.2.0' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-node-dev@2.0.0: + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + + ts-node@10.9.1: + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + ts-toolbelt@9.6.0: + resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} + + tsc-alias@1.7.1: + resolution: {integrity: sha512-P4+0i+OB0hX17Ca+U6EJ4WZZ+OSupqW32VJ34N7g7+Ch+bwSx1AqYOvDdIVYEKymBh3dfG0t1qxbxPlBbtB1lQ==} + hasBin: true + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + + tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + tsup@7.2.0: + resolution: {integrity: sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==} + engines: {node: '>=16.14'} + hasBin: true + peerDependencies: + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.1.0' + peerDependenciesMeta: + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + + turbo-darwin-64@1.10.16: + resolution: {integrity: sha512-+Jk91FNcp9e9NCLYlvDDlp2HwEDp14F9N42IoW3dmHI5ZkGSXzalbhVcrx3DOox3QfiNUHxzWg4d7CnVNCuuMg==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@1.10.16: + resolution: {integrity: sha512-jqGpFZipIivkRp/i+jnL8npX0VssE6IAVNKtu573LXtssZdV/S+fRGYA16tI46xJGxSAivrZ/IcgZrV6Jk80bw==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@1.10.16: + resolution: {integrity: sha512-PpqEZHwLoizQ6sTUvmImcRmACyRk9EWLXGlqceogPZsJ1jTRK3sfcF9fC2W56zkSIzuLEP07k5kl+ZxJd8JMcg==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@1.10.16: + resolution: {integrity: sha512-TMjFYz8to1QE0fKVXCIvG/4giyfnmqcQIwjdNfJvKjBxn22PpbjeuFuQ5kNXshUTRaTJihFbuuCcb5OYFNx4uw==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@1.10.16: + resolution: {integrity: sha512-+jsf68krs0N66FfC4/zZvioUap/Tq3sPFumnMV+EBo8jFdqs4yehd6+MxIwYTjSQLIcpH8KoNMB0gQYhJRLZzw==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@1.10.16: + resolution: {integrity: sha512-sKm3hcMM1bl0B3PLG4ifidicOGfoJmOEacM5JtgBkYM48ncMHjkHfFY7HrJHZHUnXM4l05RQTpLFoOl/uIo2HQ==} + cpu: [arm64] + os: [win32] + + turbo@1.10.16: + resolution: {integrity: sha512-2CEaK4FIuSZiP83iFa9GqMTQhroW2QryckVqUydmg4tx78baftTOS0O+oDAhvo9r9Nit4xUEtC1RAHoqs6ZEtg==} + hasBin: true + + turndown@7.1.2: + resolution: {integrity: sha512-ntI9R7fcUKjqBP6QU8rBK2Ehyt8LAzt3UBT9JR9tgo6GtuKvyUzpayWmeMKJw1DPdXzktvtIT8m2mVXz+bL/Qg==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.11.0: + resolution: {integrity: sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==} + engines: {node: '>=8'} + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + type-fest@0.18.1: + resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} + engines: {node: '>=10'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.6.0: + resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} + engines: {node: '>=8'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typedarray-to-buffer@3.1.5: + resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + + typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + + ufo@1.3.1: + resolution: {integrity: sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==} + + uglify-js@3.17.4: + resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} + engines: {node: '>=0.8.0'} + hasBin: true + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + unilogr@0.0.27: + resolution: {integrity: sha512-dI6zln0qOeVSLpEe6rXQKHysJTKzGAXnYlmOkGYbICVqJ70x9rGpq0AxdeFQ4W9t/rSMix/5hwUw7GDoBWhrQw==} + + unimport@1.3.0: + resolution: {integrity: sha512-fOkrdxglsHd428yegH0wPH/6IfaSdDeMXtdRGn6en/ccyzc2aaoxiUTMrJyc6Bu+xoa18RJRPMfLUHEzjz8atw==} + + unique-filename@2.0.1: + resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + unique-names-generator@4.7.1: + resolution: {integrity: sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==} + engines: {node: '>=8'} + + unique-slug@3.0.0: + resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + + unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unplugin-auto-import@0.11.5: + resolution: {integrity: sha512-nvbL2AQwLRR8wbHpJ6L1EBVNmjN045RSedTa4NtsGRkSQFXkI1iKHs4dTqJwcKZsnFrZOAKtLPiN1/oQTObLZw==} + engines: {node: '>=14'} + peerDependencies: + '@vueuse/core': '*' + peerDependenciesMeta: + '@vueuse/core': + optional: true + + unplugin-vue-components@0.22.12: + resolution: {integrity: sha512-FxyzsuBvMCYPIk+8cgscGBQ345tvwVu+qY5IhE++eorkyvA4Z1TiD/HCiim+Kbqozl10i4K+z+NCa2WO2jexRA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/parser': ^7.15.8 + vue: 2 || 3 + peerDependenciesMeta: + '@babel/parser': + optional: true + + unplugin@1.5.0: + resolution: {integrity: sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==} + + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + + unused-filename@2.1.0: + resolution: {integrity: sha512-BMiNwJbuWmqCpAM1FqxCTD7lXF97AvfQC8Kr/DIeA6VtvhJaMDupZ82+inbjl5yVP44PcxOuCSxye1QMS0wZyg==} + engines: {node: '>=8'} + + update-browserslist-db@1.0.13: + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + update-notifier@6.0.2: + resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==} + engines: {node: '>=14.16'} + + upper-case@1.1.3: + resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + + url-parse-lax@1.0.0: + resolution: {integrity: sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==} + engines: {node: '>=0.10.0'} + + url-parse-lax@3.0.0: + resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} + engines: {node: '>=4'} + + url-to-options@1.0.1: + resolution: {integrity: sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==} + engines: {node: '>= 4'} + + utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + + utf8-byte-length@1.0.4: + resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==} + + utif@2.0.1: + resolution: {integrity: sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + validate-npm-package-name@5.0.0: + resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + verror@1.10.1: + resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} + engines: {node: '>=0.6.0'} + + vite-node@0.34.6: + resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} + engines: {node: '>=v14.18.0'} + hasBin: true + + vite@2.9.16: + resolution: {integrity: sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==} + engines: {node: '>=12.2.0'} + hasBin: true + peerDependencies: + less: '*' + sass: '*' + stylus: '*' + peerDependenciesMeta: + less: + optional: true + sass: + optional: true + stylus: + optional: true + + vite@4.5.0: + resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@0.34.6: + resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} + engines: {node: '>=v14.18.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + playwright: '*' + safaridriver: '*' + webdriverio: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + + vue-demi@0.14.6: + resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-eslint-parser@9.3.2: + resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==} + engines: {node: ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '>=6.0.0' + + vue-i18n@9.6.5: + resolution: {integrity: sha512-dpUEjKHg7pEsaS7ZPPxp1CflaR7bGmsvZJEhnszHPKl9OTNyno5j/DvMtMSo41kpddq4felLA7GK2prjpnXVlw==} + engines: {node: '>= 16'} + peerDependencies: + vue: ^3.0.0 + + vue-router@4.2.5: + resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==} + peerDependencies: + vue: ^3.2.0 + + vue@3.2.47: + resolution: {integrity: sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==} + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + webpack-merge@5.10.0: + resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} + engines: {node: '>=10.0.0'} + + webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + + webpack-virtual-modules@0.5.0: + resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.2.2: + resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} + engines: {node: '>=8'} + hasBin: true + + wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + + widest-line@4.0.1: + resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} + engines: {node: '>=12'} + + wildcard@2.0.1: + resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} + + winston-transport@4.6.0: + resolution: {integrity: sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==} + engines: {node: '>= 12.0.0'} + + winston@3.11.0: + resolution: {integrity: sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==} + engines: {node: '>= 12.0.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@3.0.3: + resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + + ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + 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 + + xdg-basedir@5.1.0: + resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} + engines: {node: '>=12'} + + xhr@2.6.0: + resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xml-parse-from-string@1.0.1: + resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} + + xml2js@0.4.23: + resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} + engines: {node: '>=4.0.0'} + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y-prosemirror@1.0.20: + resolution: {integrity: sha512-LVMtu3qWo0emeYiP+0jgNcvZkqhzE/otOoro+87q0iVKxy/sMKuiJZnokfJdR4cn9qKx0Un5fIxXqbAlR2bFkA==} + peerDependencies: + prosemirror-model: ^1.7.1 + prosemirror-state: ^1.2.3 + prosemirror-view: ^1.9.10 + y-protocols: ^1.0.1 + yjs: ^13.3.2 + + y-protocols@1.0.6: + resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@2.1.2: + resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yaml-eslint-parser@0.3.2: + resolution: {integrity: sha512-32kYO6kJUuZzqte82t4M/gB6/+11WAuHiEnK7FreMo20xsCKPeFH5tDBU7iWxR7zeJpNnMXfJyXwne48D0hGrg==} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yaml@2.3.4: + resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} + engines: {node: '>= 14'} + + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} + + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yjs@13.6.8: + resolution: {integrity: sha512-ZPq0hpJQb6f59B++Ngg4cKexDJTvfOgeiv0sBc4sUm8CaBWH7OQC4kcCgrqbjJ/B2+6vO49exvTmYfdlPtcjbg==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.0.0: + resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} + engines: {node: '>=12.20'} + + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + + zod@3.22.4: + resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + +snapshots: + + 7zip-bin@5.1.1: {} + + '@_ueberdosis/prosemirror-tables@1.1.3': dependencies: prosemirror-keymap: 1.2.2 prosemirror-model: 1.18.3 prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 prosemirror-view: 1.29.2 - dev: false - /@aashutoshrathi/word-wrap@1.2.6: - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - dev: true + '@aashutoshrathi/word-wrap@1.2.6': {} - /@antfu/utils@0.7.6: - resolution: {integrity: sha512-pvFiLP2BeOKA/ZOS6jxx4XhKzdVLHDhGlFEaZ2flWWYf2xOqVniqpk38I04DFRyz+L0ASggl7SkItTc+ZLju4w==} - dev: true + '@antfu/utils@0.7.6': {} - /@babel/code-frame@7.22.13: - resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} - engines: {node: '>=6.9.0'} + '@babel/code-frame@7.22.13': dependencies: '@babel/highlight': 7.22.20 chalk: 2.4.2 - dev: true - /@babel/helper-string-parser@7.22.5: - resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} - engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.22.5': {} - /@babel/helper-validator-identifier@7.22.20: - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} - engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.22.20': {} - /@babel/highlight@7.22.20: - resolution: {integrity: sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==} - engines: {node: '>=6.9.0'} + '@babel/highlight@7.22.20': dependencies: '@babel/helper-validator-identifier': 7.22.20 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true - /@babel/parser@7.23.0: - resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==} - engines: {node: '>=6.0.0'} - hasBin: true + '@babel/parser@7.23.0': dependencies: '@babel/types': 7.23.0 - /@babel/runtime@7.23.2: - resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==} - engines: {node: '>=6.9.0'} + '@babel/runtime@7.23.2': dependencies: regenerator-runtime: 0.14.0 - dev: true - /@babel/types@7.23.0: - resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==} - engines: {node: '>=6.9.0'} + '@babel/types@7.23.0': dependencies: '@babel/helper-string-parser': 7.22.5 '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - /@capacitor/android@5.5.1(@capacitor/core@5.5.1): - resolution: {integrity: sha512-WTnPnpaEvTtaEtTNRbh06Y1afF7A4plY/4uajAL0WW8tdR1FxieadF357yKGiAT6CudI/B+eOu6rxn6qWuphKg==} - peerDependencies: - '@capacitor/core': ^5.5.0 + '@capacitor/android@5.5.1(@capacitor/core@5.5.1)': dependencies: '@capacitor/core': 5.5.1 - dev: false - /@capacitor/app@5.0.6(@capacitor/core@5.5.1): - resolution: {integrity: sha512-6ZXVdnNmaYILasC/RjQw+yfTmq2ZO7Q3v5lFcDVfq3PFGnybyYQh+RstBrYri+376OmXOXxBD7E6UxBhrMzXGA==} - peerDependencies: - '@capacitor/core': ^5.0.0 + '@capacitor/app@5.0.6(@capacitor/core@5.5.1)': dependencies: '@capacitor/core': 5.5.1 - dev: false - /@capacitor/cli@5.5.1: - resolution: {integrity: sha512-/oGd2IIc+k1H/fc7tUzP7vqMtZi0gNcJ4/4wUE2kzAnETxxxHXMM/2V62KfjCby/OOAzJbtI7n5OPlnWE9un1A==} - engines: {node: '>=16.0.0'} - hasBin: true + '@capacitor/cli@5.5.1': dependencies: '@ionic/cli-framework-output': 2.2.7 '@ionic/utils-fs': 3.1.7 @@ -1226,47 +8428,26 @@ packages: xml2js: 0.5.0 transitivePeerDependencies: - supports-color - dev: false - /@capacitor/clipboard@5.0.6(@capacitor/core@5.5.1): - resolution: {integrity: sha512-VsokRAn+0HVWj6riSRdspczEfqFoHbrhS/XRhGoEPsj0uvYPSufy0Kb2dpnSqkeeElhh2Jvn8jmVAzII2XeR9w==} - peerDependencies: - '@capacitor/core': ^5.0.0 + '@capacitor/clipboard@5.0.6(@capacitor/core@5.5.1)': dependencies: '@capacitor/core': 5.5.1 - dev: false - /@capacitor/core@5.5.1: - resolution: {integrity: sha512-VG6Iv8Q7ZAbvjodxpvjcSe0jfxUwZXnvjbi93ehuJ6eYP8U926qLSXyrT/DToZq+F6v/HyGyVgn3mrE/9jW2Tg==} + '@capacitor/core@5.5.1': dependencies: tslib: 2.6.2 - dev: false - /@capacitor/ios@5.5.1(@capacitor/core@5.5.1): - resolution: {integrity: sha512-h00qt8u32t8eEbIkuG4IjR0r34YZC0sIXglDH8fRDdA84xDkTybmz3WtdpRWDzh6ukE2RIY7rmD7p410WSJ2yA==} - peerDependencies: - '@capacitor/core': ^5.5.0 + '@capacitor/ios@5.5.1(@capacitor/core@5.5.1)': dependencies: '@capacitor/core': 5.5.1 - dev: false - /@capacitor/splash-screen@5.0.6(@capacitor/core@5.5.1): - resolution: {integrity: sha512-9B8wSm89D+LlshFw8B+mjPU8pJNf1WOx2mkMjMvcH0/EqxNaE+ZaO8lPCX+9WvWSEZs3O3l11qiSnOFHeK0t9A==} - peerDependencies: - '@capacitor/core': ^5.0.0 + '@capacitor/splash-screen@5.0.6(@capacitor/core@5.5.1)': dependencies: '@capacitor/core': 5.5.1 - dev: false - /@colors/colors@1.6.0: - resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} - engines: {node: '>=0.1.90'} - dev: false + '@colors/colors@1.6.0': {} - /@commitlint/cli@18.2.0(typescript@5.2.2): - resolution: {integrity: sha512-F/DCG791kMFmWg5eIdogakuGeg4OiI2kD430ed1a1Hh3epvrJdeIAgcGADAMIOmF+m0S1+VlIYUKG2dvQQ1Izw==} - engines: {node: '>=v18'} - hasBin: true + '@commitlint/cli@18.2.0(typescript@5.2.2)': dependencies: '@commitlint/format': 18.1.0 '@commitlint/lint': 18.1.0 @@ -1280,26 +8461,17 @@ packages: yargs: 17.7.2 transitivePeerDependencies: - typescript - dev: true - /@commitlint/config-conventional@18.1.0: - resolution: {integrity: sha512-8vvvtV3GOLEMHeKc8PjRL1lfP1Y4B6BG0WroFd9PJeRiOc3nFX1J0wlJenLURzl9Qus6YXVGWf+a/ZlbCKT3AA==} - engines: {node: '>=v18'} + '@commitlint/config-conventional@18.1.0': dependencies: conventional-changelog-conventionalcommits: 7.0.2 - dev: true - /@commitlint/config-validator@18.1.0: - resolution: {integrity: sha512-kbHkIuItXn93o2NmTdwi5Mk1ujyuSIysRE/XHtrcps/27GuUKEIqBJp6TdJ4Sq+ze59RlzYSHMKuDKZbfg9+uQ==} - engines: {node: '>=v18'} + '@commitlint/config-validator@18.1.0': dependencies: '@commitlint/types': 18.1.0 ajv: 8.12.0 - dev: true - /@commitlint/ensure@18.1.0: - resolution: {integrity: sha512-CkPzJ9UBumIo54VDcpmBlaVX81J++wzEhN3DJH9+6PaLeiIG+gkSx8t7C2gfwG7PaiW4HzQtdQlBN5ab+c4vFQ==} - engines: {node: '>=v18'} + '@commitlint/ensure@18.1.0': dependencies: '@commitlint/types': 18.1.0 lodash.camelcase: 4.3.0 @@ -1307,42 +8479,27 @@ packages: lodash.snakecase: 4.1.1 lodash.startcase: 4.4.0 lodash.upperfirst: 4.3.1 - dev: true - /@commitlint/execute-rule@18.1.0: - resolution: {integrity: sha512-w3Vt4K+O7+nSr9/gFSEfZ1exKUOPSlJaRpnk7Y+XowEhvwT7AIk1HNANH+gETf0zGZ020+hfiMW/Ome+SNCUsg==} - engines: {node: '>=v18'} - dev: true + '@commitlint/execute-rule@18.1.0': {} - /@commitlint/format@18.1.0: - resolution: {integrity: sha512-So/w217tGWMZZb1yXcUFNF2qFLyYtSVqbnGoMbX8a+JKcG4oB11Gc1adS0ssUOMivtiNpaLtkSHFynyiwtJtiQ==} - engines: {node: '>=v18'} + '@commitlint/format@18.1.0': dependencies: '@commitlint/types': 18.1.0 chalk: 4.1.2 - dev: true - /@commitlint/is-ignored@18.1.0: - resolution: {integrity: sha512-fa1fY93J/Nx2GH6r6WOLdBOiL7x9Uc1N7wcpmaJ1C5Qs6P+rPSUTkofe2IOhSJIJoboHfAH6W0ru4xtK689t0Q==} - engines: {node: '>=v18'} + '@commitlint/is-ignored@18.1.0': dependencies: '@commitlint/types': 18.1.0 semver: 7.5.4 - dev: true - /@commitlint/lint@18.1.0: - resolution: {integrity: sha512-LGB3eI5UYu5LLayibNrRM4bSbowr1z9uyqvp0c7+0KaSJi+xHxy/QEhb6fy4bMAtbXEvygY0sUu9HxSWg41rVQ==} - engines: {node: '>=v18'} + '@commitlint/lint@18.1.0': dependencies: '@commitlint/is-ignored': 18.1.0 '@commitlint/parse': 18.1.0 '@commitlint/rules': 18.1.0 '@commitlint/types': 18.1.0 - dev: true - /@commitlint/load@18.2.0(typescript@5.2.2): - resolution: {integrity: sha512-xjX3d3CRlOALwImhOsmLYZh14/+gW/KxsY7+bPKrzmGuFailf9K7ckhB071oYZVJdACnpY4hDYiosFyOC+MpAA==} - engines: {node: '>=v18'} + '@commitlint/load@18.2.0(typescript@5.2.2)': dependencies: '@commitlint/config-validator': 18.1.0 '@commitlint/execute-rule': 18.1.0 @@ -1358,36 +8515,24 @@ packages: resolve-from: 5.0.0 transitivePeerDependencies: - typescript - dev: true - /@commitlint/message@18.1.0: - resolution: {integrity: sha512-8dT/jJg73wf3o2Mut/fqEDTpBYSIEVtX5PWyuY/0uviEYeheZAczFo/VMIkeGzhJJn1IrcvAwWsvJ1lVGY2I/w==} - engines: {node: '>=v18'} - dev: true + '@commitlint/message@18.1.0': {} - /@commitlint/parse@18.1.0: - resolution: {integrity: sha512-23yv8uBweXWYn8bXk4PjHIsmVA+RkbqPh2h7irupBo2LthVlzMRc4LM6UStasScJ4OlXYYaWOmuP7jcExUF50Q==} - engines: {node: '>=v18'} + '@commitlint/parse@18.1.0': dependencies: '@commitlint/types': 18.1.0 conventional-changelog-angular: 6.0.0 conventional-commits-parser: 5.0.0 - dev: true - /@commitlint/read@18.1.0: - resolution: {integrity: sha512-rzfzoKUwxmvYO81tI5o1371Nwt3vhcQR36oTNfupPdU1jgSL3nzBIS3B93LcZh3IYKbCIMyMPN5WZ10BXdeoUg==} - engines: {node: '>=v18'} + '@commitlint/read@18.1.0': dependencies: '@commitlint/top-level': 18.1.0 '@commitlint/types': 18.1.0 fs-extra: 11.1.1 git-raw-commits: 2.0.11 minimist: 1.2.8 - dev: true - /@commitlint/resolve-extends@18.1.0: - resolution: {integrity: sha512-3mZpzOEJkELt7BbaZp6+bofJyxViyObebagFn0A7IHaLARhPkWTivXdjvZHS12nAORftv88Yhbh8eCPKfSvB7g==} - engines: {node: '>=v18'} + '@commitlint/resolve-extends@18.1.0': dependencies: '@commitlint/config-validator': 18.1.0 '@commitlint/types': 18.1.0 @@ -1395,64 +8540,41 @@ packages: lodash.mergewith: 4.6.2 resolve-from: 5.0.0 resolve-global: 1.0.0 - dev: true - /@commitlint/rules@18.1.0: - resolution: {integrity: sha512-VJNQ674CRv4znI0DbsjZLVnn647J+BTxHGcrDIsYv7c99gW7TUGeIe5kL80G7l8+5+N0se8v9yn+Prr8xEy6Yw==} - engines: {node: '>=v18'} + '@commitlint/rules@18.1.0': dependencies: '@commitlint/ensure': 18.1.0 '@commitlint/message': 18.1.0 '@commitlint/to-lines': 18.1.0 '@commitlint/types': 18.1.0 execa: 5.1.1 - dev: true - /@commitlint/to-lines@18.1.0: - resolution: {integrity: sha512-aHIoSDjG0ckxPLYDpODUeSLbEKmF6Jrs1B5JIssbbE9eemBtXtjm9yzdiAx9ZXcwoHlhbTp2fbndDb3YjlvJag==} - engines: {node: '>=v18'} - dev: true + '@commitlint/to-lines@18.1.0': {} - /@commitlint/top-level@18.1.0: - resolution: {integrity: sha512-1/USHlolIxJlsfLKecSXH+6PDojIvnzaJGPYwF7MtnTuuXCNQ4izkeqDsRuNMe9nU2VIKpK9OT8Q412kGNmgGw==} - engines: {node: '>=v18'} + '@commitlint/top-level@18.1.0': dependencies: find-up: 5.0.0 - dev: true - /@commitlint/types@18.1.0: - resolution: {integrity: sha512-65vGxZmbs+2OVwEItxhp3Ul7X2m2LyLfifYI/NdPwRqblmuES2w2aIRhIjb7cwUIBHHSTT8WXj4ixVHQibmvLQ==} - engines: {node: '>=v18'} + '@commitlint/types@18.1.0': dependencies: chalk: 4.1.2 - dev: true - /@cspotcode/source-map-support@0.8.1: - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 - dev: true - /@dabh/diagnostics@2.0.3: - resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + '@dabh/diagnostics@2.0.3': dependencies: colorspace: 1.1.4 enabled: 2.0.0 kuler: 2.0.0 - dev: false - /@deepnotes/html2canvas@1.4.2: - resolution: {integrity: sha512-JXDQkAZ8CJs6feHad9tTSyalZVd74MedZFFcgCAkNo227daCtNvE9cv50qUh1zAJaHvF//G7Nmky7h3G3fYFkw==} - engines: {node: '>=8.0.0'} + '@deepnotes/html2canvas@1.4.2': dependencies: css-line-break: 2.1.0 text-segmentation: 1.0.3 - dev: false - /@deepnotes/ioredis@5.3.1: - resolution: {integrity: sha512-ny3cufMR9HqgKNtjBg1GDUmoWD7hYPyxhiOi5Jm1TCOkbYU5WcGsiamX0w7YCo62BRo76VCYiYXtbpXOzP6rCA==} - engines: {node: '>=12.22.0'} + '@deepnotes/ioredis@5.3.1': dependencies: '@ioredis/commands': 1.2.0 cluster-key-slot: 1.1.2 @@ -1465,35 +8587,8 @@ packages: standard-as-callback: 2.1.0 transitivePeerDependencies: - supports-color - dev: false - /@deepnotes/quasar-app-vite@2.0.0-alpha.42(@deepnotes/quasar@2.13.2)(electron-builder@24.4.0)(electron-packager@17.1.1)(eslint@8.53.0)(pinia@2.0.36)(vue-router@4.2.5)(vue@3.2.47): - resolution: {integrity: sha512-MoBwXbOSqveO++KnfQ8oVzktZ8b0uNuJUvOxpdA7fFiyfK9+JqwgbAOiNtuFzQSODLuwwmGsv5EAtalrqi7hww==} - engines: {node: ^24 || ^22 || ^20 || ^18 || ^16 || ^14.19, npm: '>= 6.14.12', yarn: '>= 1.17.3'} - hasBin: true - peerDependencies: - electron-builder: '>= 22' - electron-packager: '>= 15' - eslint: ^8.11.0 - pinia: ^2.0.0 - quasar: ^2.8.0 - vue: ^3.2.29 - vue-router: ^4.0.12 - vuex: ^4.0.0 - workbox-build: '>= 6' - peerDependenciesMeta: - electron-builder: - optional: true - electron-packager: - optional: true - eslint: - optional: true - pinia: - optional: true - vuex: - optional: true - workbox-build: - optional: true + '@deepnotes/quasar-app-vite@2.0.0-alpha.42(@deepnotes/quasar@2.13.2)(electron-builder@24.4.0)(electron-packager@17.1.1)(eslint@8.53.0)(pinia@2.0.36)(vue-router@4.2.5)(vue@3.2.47)': dependencies: '@quasar/render-ssr-error': 1.0.2 '@quasar/vite-plugin': 1.6.0(@deepnotes/quasar@2.13.2)(@vitejs/plugin-vue@2.3.4)(vite@2.9.16)(vue@3.2.47) @@ -1525,7 +8620,7 @@ packages: minimist: 1.2.8 open: 8.4.2 pinia: 2.0.36(typescript@5.2.2)(vue@3.2.47) - quasar: /@deepnotes/quasar@2.13.2 + quasar: '@deepnotes/quasar@2.13.2' register-service-worker: 1.7.2 rollup-plugin-visualizer: 5.9.2 sass: 1.32.12 @@ -1541,27 +8636,16 @@ packages: - rollup - stylus - supports-color - dev: true - /@deepnotes/quasar@2.13.2: - resolution: {integrity: sha512-5WsHR7QcVQ1S5ye8QYNkxbVBYKoUocr4wYmANexmyLXzar2waN3PdTflGLaB8+fKY8ngRBl9jRLnZgAaRalXpg==} - engines: {node: '>= 10.18.1', npm: '>= 6.13.4', yarn: '>= 1.21.1'} + '@deepnotes/quasar@2.13.2': {} - /@deepnotes/simple-lru-cache@0.0.2: - resolution: {integrity: sha512-jD3J8lvxoq2xKgsTXD7GwWU02fBqrdpIXtIAqdb/yxbmGxByt7BrDGnaUTRF6STVq9lTri7bQmlWq+qqF+zHog==} - dev: false + '@deepnotes/simple-lru-cache@0.0.2': {} - /@deepnotes/superjson@1.12.4: - resolution: {integrity: sha512-5qT4by0h8kzPy59adyvlp6y9MQri9jAm5rCNuB9boMpauibceeetbUoMU59fPJZhdArKEndhiMvGss5sp/+6SA==} - engines: {node: '>=10'} + '@deepnotes/superjson@1.12.4': dependencies: copy-anything: 3.0.5 - dev: false - /@deepnotes/tiptap-extension-collaboration-cursor@2.0.0-beta.202(@tiptap/core@2.1.12)(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8): - resolution: {integrity: sha512-PUor3vgCHaQd3SpVN4v7bShryUx+xVJUaSvyoZd2CndiT5+Gnn1cvoma7Q9AQEiQgAUBK8kgy3ZWLL5bt/JMwg==} - peerDependencies: - '@tiptap/core': ^2.0.0-beta.193 + '@deepnotes/tiptap-extension-collaboration-cursor@2.0.0-beta.202(@tiptap/core@2.1.12)(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) y-prosemirror: 1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8) @@ -1571,62 +8655,36 @@ packages: - prosemirror-view - y-protocols - yjs - dev: false - /@develar/schema-utils@2.6.5: - resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==} - engines: {node: '>= 8.9.0'} + '@develar/schema-utils@2.6.5': dependencies: ajv: 6.12.6 ajv-keywords: 3.5.2(ajv@6.12.6) - dev: true - /@effect/data@0.17.1: - resolution: {integrity: sha512-QCYkLE5Y5Dm5Yax5R3GmW4ZIgTx7W+kSZ7yq5eqQ/mFWa8i4yxbLuu8cudqzdeZtRtTGZKlhDxfFfgVtMywXJg==} - dev: true + '@effect/data@0.17.1': {} - /@effect/io@0.38.0(@effect/data@0.17.1): - resolution: {integrity: sha512-qlVC9ASxNC+L2NKX5qOV9672CE5wWizfwBSFaX2XLI7CC118WRvohCTIPQ52n50Bj5TmR20+na+U9C7e4VkqzA==} - peerDependencies: - '@effect/data': ^0.17.1 + '@effect/io@0.38.0(@effect/data@0.17.1)': dependencies: '@effect/data': 0.17.1 - dev: true - /@effect/match@0.32.0(@effect/data@0.17.1)(@effect/schema@0.33.1): - resolution: {integrity: sha512-04QfnIgCcMnnNbGxTv2xa9/7q1c5kgpsBodqTUZ8eX86A/EdE8Czz+JkVarG00/xE+nYhQLXOXCN9Zj+dtqVkQ==} - peerDependencies: - '@effect/data': ^0.17.1 - '@effect/schema': ^0.33.0 + '@effect/match@0.32.0(@effect/data@0.17.1)(@effect/schema@0.33.1)': dependencies: '@effect/data': 0.17.1 '@effect/schema': 0.33.1(@effect/data@0.17.1)(@effect/io@0.38.0) - dev: true - /@effect/schema@0.33.1(@effect/data@0.17.1)(@effect/io@0.38.0): - resolution: {integrity: sha512-h+fQInui4q3we8fegAygL0Cs5B2DD/+oC3JWthOh8eLcbKkbYM9smCD/PsHuyQ+BaeWiSP5JdvREGlP4Sg+Ysw==} - peerDependencies: - '@effect/data': ^0.17.1 - '@effect/io': ^0.38.0 + '@effect/schema@0.33.1(@effect/data@0.17.1)(@effect/io@0.38.0)': dependencies: '@effect/data': 0.17.1 '@effect/io': 0.38.0(@effect/data@0.17.1) fast-check: 3.13.2 - dev: true - /@electron/asar@3.2.7: - resolution: {integrity: sha512-8FaSCAIiZGYFWyjeevPQt+0e9xCK9YmJ2Rjg5SXgdsXon6cRnU0Yxnbe6CvJbQn26baifur2Y2G5EBayRIsjyg==} - engines: {node: '>=10.12.0'} - hasBin: true + '@electron/asar@3.2.7': dependencies: commander: 5.1.0 glob: 7.2.3 minimatch: 3.1.2 - dev: true - /@electron/get@1.14.1: - resolution: {integrity: sha512-BrZYyL/6m0ZXz/lDxy/nlVhQz+WF+iPS6qXolEU8atw7h6v1aYkjwJZ63m+bJMBTxDE66X+r2tPS4a/8C82sZw==} - engines: {node: '>=8.6'} + '@electron/get@1.14.1': dependencies: debug: 4.3.4 env-paths: 2.2.1 @@ -1640,11 +8698,8 @@ packages: global-tunnel-ng: 2.7.1 transitivePeerDependencies: - supports-color - dev: true - /@electron/get@2.0.3: - resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} - engines: {node: '>=12'} + '@electron/get@2.0.3': dependencies: debug: 4.3.4 env-paths: 2.2.1 @@ -1657,22 +8712,15 @@ packages: global-agent: 3.0.0 transitivePeerDependencies: - supports-color - dev: true - /@electron/notarize@1.2.4: - resolution: {integrity: sha512-W5GQhJEosFNafewnS28d3bpQ37/s91CDWqxVchHfmv2dQSTWpOzNlUVQwYzC1ay5bChRV/A9BTL68yj0Pa+TSg==} - engines: {node: '>= 10.0.0'} + '@electron/notarize@1.2.4': dependencies: debug: 4.3.4 fs-extra: 9.1.0 transitivePeerDependencies: - supports-color - dev: true - /@electron/osx-sign@1.0.5: - resolution: {integrity: sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==} - engines: {node: '>=12.0.0'} - hasBin: true + '@electron/osx-sign@1.0.5': dependencies: compare-version: 0.1.2 debug: 4.3.4 @@ -1682,12 +8730,8 @@ packages: plist: 3.1.0 transitivePeerDependencies: - supports-color - dev: true - /@electron/rebuild@3.3.0: - resolution: {integrity: sha512-S1vgpzIOS1wCJmsYjdLz97MTUV6UTLcMk/HE3w90HYtVxvW+PQdwxLbgsrECX2bysqcnmM5a0K6mXj/gwVgYtQ==} - engines: {node: '>=12.13.0'} - hasBin: true + '@electron/rebuild@3.3.0': dependencies: '@malept/cross-spawn-promise': 2.0.0 chalk: 4.1.2 @@ -1705,11 +8749,8 @@ packages: transitivePeerDependencies: - bluebird - supports-color - dev: true - /@electron/universal@1.3.4: - resolution: {integrity: sha512-BdhBgm2ZBnYyYRLRgOjM5VHkyFItsbggJ0MHycOjKWdFGYwK97ZFXH54dTvUWEfha81vfvwr5On6XBjt99uDcg==} - engines: {node: '>=8.6'} + '@electron/universal@1.3.4': dependencies: '@electron/asar': 3.2.7 '@malept/cross-spawn-promise': 1.1.1 @@ -1720,11 +8761,8 @@ packages: plist: 3.1.0 transitivePeerDependencies: - supports-color - dev: true - /@electron/universal@1.4.5: - resolution: {integrity: sha512-3vE9WBQnvlulKylrPbyc+9M4xnD7t1JxuCOF0nrFz00XrrkgbqeqxDf90PNcjLiuB4hAZKr1JooVA6KwsXj94w==} - engines: {node: '>=8.6'} + '@electron/universal@1.4.5': dependencies: '@electron/asar': 3.2.7 '@malept/cross-spawn-promise': 1.1.1 @@ -1735,233 +8773,84 @@ packages: plist: 3.1.0 transitivePeerDependencies: - supports-color - dev: true - /@esbuild/android-arm64@0.18.20: - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true + '@esbuild/android-arm64@0.18.20': optional: true - /@esbuild/android-arm@0.18.20: - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true + '@esbuild/android-arm@0.18.20': optional: true - /@esbuild/android-x64@0.18.20: - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true + '@esbuild/android-x64@0.18.20': optional: true - /@esbuild/darwin-arm64@0.18.20: - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true + '@esbuild/darwin-arm64@0.18.20': optional: true - /@esbuild/darwin-x64@0.18.20: - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true + '@esbuild/darwin-x64@0.18.20': optional: true - /@esbuild/freebsd-arm64@0.18.20: - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true + '@esbuild/freebsd-arm64@0.18.20': optional: true - /@esbuild/freebsd-x64@0.18.20: - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true + '@esbuild/freebsd-x64@0.18.20': optional: true - /@esbuild/linux-arm64@0.18.20: - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-arm64@0.18.20': optional: true - /@esbuild/linux-arm@0.18.20: - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-arm@0.18.20': optional: true - /@esbuild/linux-ia32@0.18.20: - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-ia32@0.18.20': optional: true - /@esbuild/linux-loong64@0.14.54: - resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-loong64@0.14.54': optional: true - /@esbuild/linux-loong64@0.18.20: - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-loong64@0.18.20': optional: true - /@esbuild/linux-mips64el@0.18.20: - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-mips64el@0.18.20': optional: true - /@esbuild/linux-ppc64@0.18.20: - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-ppc64@0.18.20': optional: true - /@esbuild/linux-riscv64@0.18.20: - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-riscv64@0.18.20': optional: true - /@esbuild/linux-s390x@0.18.20: - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-s390x@0.18.20': optional: true - /@esbuild/linux-x64@0.18.20: - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-x64@0.18.20': optional: true - /@esbuild/netbsd-x64@0.18.20: - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true + '@esbuild/netbsd-x64@0.18.20': optional: true - /@esbuild/openbsd-x64@0.18.20: - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true + '@esbuild/openbsd-x64@0.18.20': optional: true - /@esbuild/sunos-x64@0.18.20: - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true + '@esbuild/sunos-x64@0.18.20': optional: true - /@esbuild/win32-arm64@0.18.20: - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true + '@esbuild/win32-arm64@0.18.20': optional: true - /@esbuild/win32-ia32@0.18.20: - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true + '@esbuild/win32-ia32@0.18.20': optional: true - /@esbuild/win32-x64@0.18.20: - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true + '@esbuild/win32-x64@0.18.20': optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.4.0(eslint@8.53.0)': dependencies: eslint: 8.53.0 eslint-visitor-keys: 3.4.3 - dev: true - /@eslint-community/regexpp@4.10.0: - resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true + '@eslint-community/regexpp@4.10.0': {} - /@eslint/eslintrc@2.1.3: - resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/eslintrc@2.1.3': dependencies: ajv: 6.12.6 debug: 4.3.4 @@ -1974,160 +8863,98 @@ packages: strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - dev: true - /@eslint/js@8.53.0: - resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + '@eslint/js@8.53.0': {} - /@fastify/ajv-compiler@3.5.0: - resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==} + '@fastify/ajv-compiler@3.5.0': dependencies: ajv: 8.12.0 ajv-formats: 2.1.1(ajv@8.12.0) fast-uri: 2.3.0 - dev: false - /@fastify/cookie@9.1.0: - resolution: {integrity: sha512-w/LlQjj7cmYlQNhEKNm4jQoLkFXCL73kFu1Jy3aL7IFbYEojEKur0f7ieCKUxBBaU65tpaWC83UM8xW7AzY6uw==} + '@fastify/cookie@9.1.0': dependencies: cookie: 0.5.0 fastify-plugin: 4.5.1 - dev: false - /@fastify/cors@8.4.1: - resolution: {integrity: sha512-iYQJtrY3pFiDS5mo5zRaudzg2OcUdJ96PD6xfkKOOEilly5nnrFZx/W6Sce2T79xxlEn2qpU3t5+qS2phS369w==} + '@fastify/cors@8.4.1': dependencies: fastify-plugin: 4.5.1 mnemonist: 0.39.5 - dev: false - /@fastify/deepmerge@1.3.0: - resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} - dev: false + '@fastify/deepmerge@1.3.0': {} - /@fastify/error@3.4.1: - resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} - dev: false + '@fastify/error@3.4.1': {} - /@fastify/fast-json-stringify-compiler@4.3.0: - resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} + '@fastify/fast-json-stringify-compiler@4.3.0': dependencies: fast-json-stringify: 5.9.1 - dev: false - /@fastify/helmet@11.1.1: - resolution: {integrity: sha512-pjJxjk6SLEimITWadtYIXt6wBMfFC1I6OQyH/jYVCqSAn36sgAIFjeNiibHtifjCd+e25442pObis3Rjtame6A==} + '@fastify/helmet@11.1.1': dependencies: fastify-plugin: 4.5.1 helmet: 7.0.0 - dev: false - /@fastify/rate-limit@8.0.3: - resolution: {integrity: sha512-7wbSKXGKKLI1VkpW2XvS7SFg4n4/uzYt0YA5O2pfCcM6PYaBSV3VhSKGJ9/hJceCSH+zNEDRrWpufqxbcDkTZg==} + '@fastify/rate-limit@8.0.3': dependencies: fastify-plugin: 4.5.1 ms: 2.1.3 tiny-lru: 11.2.5 - dev: false - /@fastify/websocket@8.2.0: - resolution: {integrity: sha512-B4tlHFBKCX7tenEG9aUcQEpksW2e0+dgRTaH/05+cro1Xsq1+kSj+9IB9Gep7a0KbHZGrat+zBsOas6lRs5dFQ==} + '@fastify/websocket@8.2.0': dependencies: fastify-plugin: 4.5.1 ws: 8.11.0(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate - dev: false - /@gar/promisify@1.1.3: - resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - dev: true + '@gar/promisify@1.1.3': {} - /@getbrevo/brevo@1.0.1: - resolution: {integrity: sha512-NwUOlkft6NwLSKTph9FWQujMM5ysSGWOa9Wdf0Bc/RezejOW5VG5KXvTmXrF1Q+MHmjzTh6GDWjq5EukQsDdnA==} + '@getbrevo/brevo@1.0.1': dependencies: querystring: 0.2.1 superagent: 8.1.2 transitivePeerDependencies: - supports-color - dev: false - /@hapi/address@4.1.0: - resolution: {integrity: sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==} - deprecated: Moved to 'npm install @sideway/address' + '@hapi/address@4.1.0': dependencies: '@hapi/hoek': 9.3.0 - dev: true - /@hapi/formula@2.0.0: - resolution: {integrity: sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==} - deprecated: Moved to 'npm install @sideway/formula' - dev: true + '@hapi/formula@2.0.0': {} - /@hapi/hoek@9.3.0: - resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - dev: true + '@hapi/hoek@9.3.0': {} - /@hapi/joi@17.1.1: - resolution: {integrity: sha512-p4DKeZAoeZW4g3u7ZeRo+vCDuSDgSvtsB/NpfjXEHTUjSeINAi/RrVOWiVQ1isaoLzMvFEhe8n5065mQq1AdQg==} - deprecated: Switch to 'npm install joi' + '@hapi/joi@17.1.1': dependencies: '@hapi/address': 4.1.0 '@hapi/formula': 2.0.0 '@hapi/hoek': 9.3.0 '@hapi/pinpoint': 2.0.1 '@hapi/topo': 5.1.0 - dev: true - /@hapi/pinpoint@2.0.1: - resolution: {integrity: sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==} - dev: true + '@hapi/pinpoint@2.0.1': {} - /@hapi/topo@5.1.0: - resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} + '@hapi/topo@5.1.0': dependencies: '@hapi/hoek': 9.3.0 - dev: true - /@humanwhocodes/config-array@0.11.13: - resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} - engines: {node: '>=10.10.0'} + '@humanwhocodes/config-array@0.11.13': dependencies: '@humanwhocodes/object-schema': 2.0.1 debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - dev: true - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true + '@humanwhocodes/module-importer@1.0.1': {} - /@humanwhocodes/object-schema@2.0.1: - resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} - dev: true + '@humanwhocodes/object-schema@2.0.1': {} - /@hutson/parse-repository-url@3.0.2: - resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} - engines: {node: '>=6.9.0'} - dev: true + '@hutson/parse-repository-url@3.0.2': {} - /@intlify/bundle-utils@2.2.2(vue-i18n@9.6.5): - resolution: {integrity: sha512-vngkvlIVV8ZJoyC5VqMvqJd2nvsx+qMN7pQjPiPjOrVndeiR7Dlue0k86Q8FsFUzyksW3HJZZi833ldxwbFzTA==} - engines: {node: '>= 12'} - peerDependencies: - petite-vue-i18n: '*' - vue-i18n: '*' - peerDependenciesMeta: - petite-vue-i18n: - optional: true - vue-i18n: - optional: true + '@intlify/bundle-utils@2.2.2(vue-i18n@9.6.5)': dependencies: '@intlify/message-compiler': 9.6.5 '@intlify/shared': 9.6.5 @@ -2135,38 +8962,20 @@ packages: source-map: 0.6.1 vue-i18n: 9.6.5(vue@3.2.47) yaml-eslint-parser: 0.3.2 - dev: true - /@intlify/core-base@9.6.5: - resolution: {integrity: sha512-LzbGXiZkMWPIHnHI0g6q554S87Cmh2mmCmjytK/3pDQfjI84l+dgGoeQuKj02q7EbULRuUUgYVZVqAwEUawXGg==} - engines: {node: '>= 16'} + '@intlify/core-base@9.6.5': dependencies: '@intlify/message-compiler': 9.6.5 '@intlify/shared': 9.6.5 - /@intlify/message-compiler@9.6.5: - resolution: {integrity: sha512-WeJ499thIj0p7JaIO1V3JaJbqdqfBykS5R8fElFs5hNeotHtPAMBs4IiA+8/KGFkAbjJusgFefCq6ajP7F7+4Q==} - engines: {node: '>= 16'} + '@intlify/message-compiler@9.6.5': dependencies: '@intlify/shared': 9.6.5 source-map-js: 1.0.2 - /@intlify/shared@9.6.5: - resolution: {integrity: sha512-gD7Ey47Xi4h/t6P+S04ymMSoA3wVRxGqjxuIMglwRO8POki9h164Epu2N8wk/GHXM/hR6ZGcsx2HArCCENjqSQ==} - engines: {node: '>= 16'} + '@intlify/shared@9.6.5': {} - /@intlify/vite-plugin-vue-i18n@3.4.0(vite@2.9.16)(vue-i18n@9.6.5): - resolution: {integrity: sha512-XXcZBgwJ+3FRu11c4ARoY9N00kElPii0/jNZ49qR045Ka7/YGCwb1Ku14BBlMSEHiHDSjLQknLwrJKSQGVZLyA==} - engines: {node: '>= 12'} - peerDependencies: - petite-vue-i18n: ^9.1.0 - vite: ^2.0.0 - vue-i18n: ^9.1.0 - peerDependenciesMeta: - petite-vue-i18n: - optional: true - vue-i18n: - optional: true + '@intlify/vite-plugin-vue-i18n@3.4.0(vite@2.9.16)(vue-i18n@9.6.5)': dependencies: '@intlify/bundle-utils': 2.2.2(vue-i18n@9.6.5) '@intlify/shared': 9.6.5 @@ -2178,32 +8987,23 @@ packages: vue-i18n: 9.6.5(vue@3.2.47) transitivePeerDependencies: - supports-color - dev: true - /@ionic/cli-framework-output@2.2.7: - resolution: {integrity: sha512-/BXeclqu3y+bsBF7VFRS9xtNbrXf2JYCj/LeJoyLpWA9PeXNfvFrn91W2lwS2HVDjEDWKl4Ye6edJDdtn76EnA==} - engines: {node: '>=16.0.0'} + '@ionic/cli-framework-output@2.2.7': dependencies: '@ionic/utils-terminal': 2.3.4 debug: 4.3.4 tslib: 2.6.2 transitivePeerDependencies: - supports-color - dev: false - /@ionic/utils-array@2.1.6: - resolution: {integrity: sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==} - engines: {node: '>=16.0.0'} + '@ionic/utils-array@2.1.6': dependencies: debug: 4.3.4 tslib: 2.6.2 transitivePeerDependencies: - supports-color - dev: false - /@ionic/utils-fs@3.1.7: - resolution: {integrity: sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==} - engines: {node: '>=16.0.0'} + '@ionic/utils-fs@3.1.7': dependencies: '@types/fs-extra': 8.1.5 debug: 4.3.4 @@ -2211,21 +9011,15 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - supports-color - dev: false - /@ionic/utils-object@2.1.6: - resolution: {integrity: sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==} - engines: {node: '>=16.0.0'} + '@ionic/utils-object@2.1.6': dependencies: debug: 4.3.4 tslib: 2.6.2 transitivePeerDependencies: - supports-color - dev: false - /@ionic/utils-process@2.1.11: - resolution: {integrity: sha512-Uavxn+x8j3rDlZEk1X7YnaN6wCgbCwYQOeIjv/m94i1dzslqWhqIHEqxEyeE8HsT5Negboagg7GtQiABy+BLbA==} - engines: {node: '>=16.0.0'} + '@ionic/utils-process@2.1.11': dependencies: '@ionic/utils-object': 2.1.6 '@ionic/utils-terminal': 2.3.4 @@ -2235,21 +9029,15 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - supports-color - dev: false - /@ionic/utils-stream@3.1.6: - resolution: {integrity: sha512-4+Kitey1lTA1yGtnigeYNhV/0tggI3lWBMjC7tBs1K9GXa/q7q4CtOISppdh8QgtOhrhAXS2Igp8rbko/Cj+lA==} - engines: {node: '>=16.0.0'} + '@ionic/utils-stream@3.1.6': dependencies: debug: 4.3.4 tslib: 2.6.2 transitivePeerDependencies: - supports-color - dev: false - /@ionic/utils-subprocess@2.1.14: - resolution: {integrity: sha512-nGYvyGVjU0kjPUcSRFr4ROTraT3w/7r502f5QJEsMRKTqa4eEzCshtwRk+/mpASm0kgBN5rrjYA5A/OZg8ahqg==} - engines: {node: '>=16.0.0'} + '@ionic/utils-subprocess@2.1.14': dependencies: '@ionic/utils-array': 2.1.6 '@ionic/utils-fs': 3.1.7 @@ -2261,11 +9049,8 @@ packages: tslib: 2.6.2 transitivePeerDependencies: - supports-color - dev: false - /@ionic/utils-terminal@2.3.4: - resolution: {integrity: sha512-cEiMFl3jklE0sW60r8JHH3ijFTwh/jkdEKWbylSyExQwZ8pPuwoXz7gpkWoJRLuoRHHSvg+wzNYyPJazIHfoJA==} - engines: {node: '>=16.0.0'} + '@ionic/utils-terminal@2.3.4': dependencies: '@types/slice-ansi': 4.0.0 debug: 4.3.4 @@ -2278,44 +9063,30 @@ packages: wrap-ansi: 7.0.0 transitivePeerDependencies: - supports-color - dev: false - /@ioredis/commands@1.2.0: - resolution: {integrity: sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==} - dev: false + '@ioredis/commands@1.2.0': {} - /@isaacs/cliui@8.0.2: - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 - string-width-cjs: /string-width@4.2.3 + string-width-cjs: string-width@4.2.3 strip-ansi: 7.1.0 - strip-ansi-cjs: /strip-ansi@6.0.1 + strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 - wrap-ansi-cjs: /wrap-ansi@7.0.0 - dev: true + wrap-ansi-cjs: wrap-ansi@7.0.0 - /@jest/schemas@29.6.3: - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jest/schemas@29.6.3': dependencies: '@sinclair/typebox': 0.27.8 - dev: true - /@jimp/bmp@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-5RkX6tSS7K3K3xNEb2ygPuvyL9whjanhoaB/WmmXlJS6ub4DjTqrapu8j4qnIWmO4YYtFeTbDTXV6v9P1yMA5A==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/bmp@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 bmp-js: 0.1.0 - dev: true - /@jimp/core@0.14.0: - resolution: {integrity: sha512-S62FcKdtLtj3yWsGfJRdFXSutjvHg7aQNiFogMbwq19RP4XJWqS2nOphu7ScB8KrSlyy5nPF2hkWNhLRLyD82w==} + '@jimp/core@0.14.0': dependencies: '@babel/runtime': 7.23.2 '@jimp/utils': 0.14.0 @@ -2328,102 +9099,62 @@ packages: phin: 2.9.3 pixelmatch: 4.0.2 tinycolor2: 1.6.0 - dev: true - /@jimp/custom@0.14.0: - resolution: {integrity: sha512-kQJMeH87+kWJdVw8F9GQhtsageqqxrvzg7yyOw3Tx/s7v5RToe8RnKyMM+kVtBJtNAG+Xyv/z01uYQ2jiZ3GwA==} + '@jimp/custom@0.14.0': dependencies: '@babel/runtime': 7.23.2 '@jimp/core': 0.14.0 - dev: true - /@jimp/gif@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-DHjoOSfCaCz72+oGGEh8qH0zE6pUBaBxPxxmpYJjkNyDZP7RkbBkZJScIYeQ7BmJxmGN4/dZn+MxamoQlr+UYg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/gif@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 gifwrap: 0.9.4 omggif: 1.0.10 - dev: true - /@jimp/jpeg@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-561neGbr+87S/YVQYnZSTyjWTHBm9F6F1obYHiyU3wVmF+1CLbxY3FQzt4YolwyQHIBv36Bo0PY2KkkU8BEeeQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/jpeg@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 jpeg-js: 0.4.4 - dev: true - /@jimp/plugin-blit@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-YoYOrnVHeX3InfgbJawAU601iTZMwEBZkyqcP1V/S33Qnz9uzH1Uj1NtC6fNgWzvX6I4XbCWwtr4RrGFb5CFrw==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blit@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-blur@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-9WhZcofLrT0hgI7t0chf7iBQZib//0gJh9WcQMUt5+Q1Bk04dWs8vTgLNj61GBqZXgHSPzE4OpCrrLDBG8zlhQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-blur@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-circle@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-o5L+wf6QA44tvTum5HeLyLSc5eVfIUd5ZDVi5iRfO4o6GT/zux9AxuTSkKwnjhsG8bn1dDmywAOQGAx7BjrQVA==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-circle@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-color@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-JJz512SAILYV0M5LzBb9sbOm/XEj2fGElMiHAxb7aLI6jx+n0agxtHpfpV/AePTLm1vzzDxx6AJxXbKv355hBQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-color@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 tinycolor2: 1.6.0 - dev: true - /@jimp/plugin-contain@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)(@jimp/plugin-resize@0.14.0)(@jimp/plugin-scale@0.14.0): - resolution: {integrity: sha512-RX2q233lGyaxiMY6kAgnm9ScmEkNSof0hdlaJAVDS1OgXphGAYAeSIAwzESZN4x3ORaWvkFefeVH9O9/698Evg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-blit': '>=0.3.5' - '@jimp/plugin-resize': '>=0.3.5' - '@jimp/plugin-scale': '>=0.3.5' + '@jimp/plugin-contain@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)(@jimp/plugin-resize@0.14.0)(@jimp/plugin-scale@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 - '@jimp/plugin-blit': 0.14.0(@jimp/custom@0.14.0) - '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) - '@jimp/plugin-scale': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0) - '@jimp/utils': 0.14.0 - dev: true - - /@jimp/plugin-cover@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-crop@0.14.0)(@jimp/plugin-resize@0.14.0)(@jimp/plugin-scale@0.14.0): - resolution: {integrity: sha512-0P/5XhzWES4uMdvbi3beUgfvhn4YuQ/ny8ijs5kkYIw6K8mHcl820HahuGpwWMx56DJLHRl1hFhJwo9CeTRJtQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-crop': '>=0.3.5' - '@jimp/plugin-resize': '>=0.3.5' - '@jimp/plugin-scale': '>=0.3.5' + '@jimp/plugin-blit': 0.14.0(@jimp/custom@0.14.0) + '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) + '@jimp/plugin-scale': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0) + '@jimp/utils': 0.14.0 + + '@jimp/plugin-cover@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-crop@0.14.0)(@jimp/plugin-resize@0.14.0)(@jimp/plugin-scale@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 @@ -2431,130 +9162,77 @@ packages: '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-scale': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0) '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-crop@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-Ojtih+XIe6/XSGtpWtbAXBozhCdsDMmy+THUJAGu2x7ZgKrMS0JotN+vN2YC3nwDpYkM+yOJImQeptSfZb2Sug==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-crop@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-displace@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-c75uQUzMgrHa8vegkgUvgRL/PRvD7paFbFJvzW0Ugs8Wl+CDMGIPYQ3j7IVaQkIS+cAxv+NJ3TIRBQyBrfVEOg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-displace@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-dither@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-g8SJqFLyYexXQQsoh4dc1VP87TwyOgeTElBcxSXX2LaaMZezypmxQfLTzOFzZoK8m39NuaoH21Ou1Ftsq7LzVQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-dither@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-fisheye@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-BFfUZ64EikCaABhCA6mR3bsltWhPpS321jpeIQfJyrILdpFsZ/OccNwCgpW1XlbldDHIoNtXTDGn3E+vCE7vDg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-fisheye@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-flip@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-rotate@0.14.0): - resolution: {integrity: sha512-WtL1hj6ryqHhApih+9qZQYA6Ye8a4HAmdTzLbYdTMrrrSUgIzFdiZsD0WeDHpgS/+QMsWwF+NFmTZmxNWqKfXw==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-rotate': '>=0.3.5' + '@jimp/plugin-flip@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-rotate@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/plugin-rotate': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)(@jimp/plugin-crop@0.14.0)(@jimp/plugin-resize@0.14.0) '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-gaussian@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-uaLwQ0XAQoydDlF9tlfc7iD9drYPriFe+jgYnWm8fbw5cN+eOIcnneEX9XCOOzwgLPkNCxGox6Kxjn8zY6GxtQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-gaussian@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-invert@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-UaQW9X9vx8orQXYSjT5VcITkJPwDaHwrBbxxPoDG+F/Zgv4oV9fP+udDD6qmkgI9taU+44Fy+zm/J/gGcMWrdg==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-invert@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-mask@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-tdiGM69OBaKtSPfYSQeflzFhEpoRZ+BvKfDEoivyTjauynbjpRiwB1CaiS8En1INTDwzLXTT0Be9SpI3LkJoEA==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-mask@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-normalize@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-AfY8sqlsbbdVwFGcyIPy5JH/7fnBzlmuweb+Qtx2vn29okq6+HelLjw2b+VT2btgGUmWWHGEHd86oRGSoWGyEQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-normalize@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-print@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0): - resolution: {integrity: sha512-MwP3sH+VS5AhhSTXk7pui+tEJFsxnTKFY3TraFJb8WFbA2Vo2qsRCZseEGwpTLhENB7p/JSsLvWoSSbpmxhFAQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-blit': '>=0.3.5' + '@jimp/plugin-print@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/plugin-blit': 0.14.0(@jimp/custom@0.14.0) '@jimp/utils': 0.14.0 load-bmfont: 1.4.1 - dev: true - /@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-qFeMOyXE/Bk6QXN0GQo89+CB2dQcXqoxUcDb2Ah8wdYlKqpi53skABkgVy5pW3EpiprDnzNDboMltdvDslNgLQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-rotate@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)(@jimp/plugin-crop@0.14.0)(@jimp/plugin-resize@0.14.0): - resolution: {integrity: sha512-aGaicts44bvpTcq5Dtf93/8TZFu5pMo/61lWWnYmwJJU1RqtQlxbCLEQpMyRhKDNSfPbuP8nyGmaqXlM/82J0Q==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-blit': '>=0.3.5' - '@jimp/plugin-crop': '>=0.3.5' - '@jimp/plugin-resize': '>=0.3.5' + '@jimp/plugin-rotate@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)(@jimp/plugin-crop@0.14.0)(@jimp/plugin-resize@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 @@ -2562,52 +9240,31 @@ packages: '@jimp/plugin-crop': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-scale@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0): - resolution: {integrity: sha512-ZcJk0hxY5ZKZDDwflqQNHEGRblgaR+piePZm7dPwPUOSeYEH31P0AwZ1ziceR74zd8N80M0TMft+e3Td6KGBHw==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-resize': '>=0.3.5' + '@jimp/plugin-scale@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-shadow@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blur@0.14.0)(@jimp/plugin-resize@0.14.0): - resolution: {integrity: sha512-p2igcEr/iGrLiTu0YePNHyby0WYAXM14c5cECZIVnq/UTOOIQ7xIcWZJ1lRbAEPxVVXPN1UibhZAbr3HAb5BjQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-blur': '>=0.3.5' - '@jimp/plugin-resize': '>=0.3.5' + '@jimp/plugin-shadow@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blur@0.14.0)(@jimp/plugin-resize@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/plugin-blur': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugin-threshold@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-color@0.14.0)(@jimp/plugin-resize@0.14.0): - resolution: {integrity: sha512-N4BlDgm/FoOMV/DQM2rSpzsgqAzkP0DXkWZoqaQrlRxQBo4zizQLzhEL00T/YCCMKnddzgEhnByaocgaaa0fKw==} - peerDependencies: - '@jimp/custom': '>=0.3.5' - '@jimp/plugin-color': '>=0.8.0' - '@jimp/plugin-resize': '>=0.8.0' + '@jimp/plugin-threshold@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-color@0.14.0)(@jimp/plugin-resize@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/plugin-color': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) '@jimp/utils': 0.14.0 - dev: true - /@jimp/plugins@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-vDO3XT/YQlFlFLq5TqNjQkISqjBHT8VMhpWhAfJVwuXIpilxz5Glu4IDLK6jp4IjPR6Yg2WO8TmRY/HI8vLrOw==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/plugins@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 @@ -2633,33 +9290,21 @@ packages: '@jimp/plugin-shadow': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blur@0.14.0)(@jimp/plugin-resize@0.14.0) '@jimp/plugin-threshold': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-color@0.14.0)(@jimp/plugin-resize@0.14.0) timm: 1.7.1 - dev: true - /@jimp/png@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-0RV/mEIDOrPCcNfXSPmPBqqSZYwGADNRVUTyMt47RuZh7sugbYdv/uvKmQSiqRdR0L1sfbCBMWUEa5G/8MSbdA==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/png@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 pngjs: 3.4.0 - dev: true - /@jimp/tiff@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-zBYDTlutc7j88G/7FBCn3kmQwWr0rmm1e0FKB4C3uJ5oYfT8645lftUsvosKVUEfkdmOaMAnhrf4ekaHcb5gQw==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/tiff@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 utif: 2.0.1 - dev: true - /@jimp/types@0.14.0(@jimp/custom@0.14.0): - resolution: {integrity: sha512-hx3cXAW1KZm+b+XCrY3LXtdWy2U+hNtq0rPyJ7NuXCjU7lZR3vIkpz1DLJ3yDdS70hTi5QDXY3Cd9kd6DtloHQ==} - peerDependencies: - '@jimp/custom': '>=0.3.5' + '@jimp/types@0.14.0(@jimp/custom@0.14.0)': dependencies: '@babel/runtime': 7.23.2 '@jimp/bmp': 0.14.0(@jimp/custom@0.14.0) @@ -2669,79 +9314,47 @@ packages: '@jimp/png': 0.14.0(@jimp/custom@0.14.0) '@jimp/tiff': 0.14.0(@jimp/custom@0.14.0) timm: 1.7.1 - dev: true - /@jimp/utils@0.14.0: - resolution: {integrity: sha512-MY5KFYUru0y74IsgM/9asDwb3ERxWxXEu3CRCZEvE7DtT86y1bR1XgtlSliMrptjz4qbivNGMQSvUBpEFJDp1A==} + '@jimp/utils@0.14.0': dependencies: '@babel/runtime': 7.23.2 regenerator-runtime: 0.13.11 - dev: true - /@jridgewell/gen-mapping@0.3.3: - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.3': dependencies: '@jridgewell/set-array': 1.1.2 '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.20 - dev: true - /@jridgewell/resolve-uri@3.1.1: - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} - dev: true + '@jridgewell/resolve-uri@3.1.1': {} - /@jridgewell/set-array@1.1.2: - resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} - engines: {node: '>=6.0.0'} - dev: true + '@jridgewell/set-array@1.1.2': {} - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true + '@jridgewell/sourcemap-codec@1.4.15': {} - /@jridgewell/trace-mapping@0.3.20: - resolution: {integrity: sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==} + '@jridgewell/trace-mapping@0.3.20': dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - /@jridgewell/trace-mapping@0.3.9: - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - /@lukeed/csprng@1.1.0: - resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} - engines: {node: '>=8'} - dev: true + '@lukeed/csprng@1.1.0': {} - /@lukeed/ms@2.0.1: - resolution: {integrity: sha512-Xs/4RZltsAL7pkvaNStUQt7netTkyxrS0K+RILcVr3TRMS/ToOg4I6uNfhB9SlGsnWBym4U+EaXq0f0cEMNkHA==} - engines: {node: '>=8'} - dev: false + '@lukeed/ms@2.0.1': {} - /@malept/cross-spawn-promise@1.1.1: - resolution: {integrity: sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==} - engines: {node: '>= 10'} + '@malept/cross-spawn-promise@1.1.1': dependencies: cross-spawn: 7.0.3 - dev: true - /@malept/cross-spawn-promise@2.0.0: - resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} - engines: {node: '>= 12.13.0'} + '@malept/cross-spawn-promise@2.0.0': dependencies: cross-spawn: 7.0.3 - dev: true - /@malept/flatpak-bundler@0.4.0: - resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} - engines: {node: '>= 10.0.0'} + '@malept/flatpak-bundler@0.4.0': dependencies: debug: 4.3.4 fs-extra: 9.1.0 @@ -2749,93 +9362,34 @@ packages: tmp-promise: 3.0.3 transitivePeerDependencies: - supports-color - dev: true - /@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2: - resolution: {integrity: sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2': optional: true - /@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2: - resolution: {integrity: sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.2': optional: true - /@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2: - resolution: {integrity: sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.2': optional: true - /@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2: - resolution: {integrity: sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.2': optional: true - /@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2: - resolution: {integrity: sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.2': optional: true - /@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2: - resolution: {integrity: sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.2': optional: true - /@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-rmpwcdvq2IWMmsUVP8rsdKub6uDWk7dwCYo0aif50JTwcvcxzaP3iKVFKoSgvp0RKYu8h15+/AEOfaInmPpl0Q==} - peerDependencies: - class-transformer: '*' - class-validator: '*' - reflect-metadata: ^0.1.12 - rxjs: ^7.1.0 - peerDependenciesMeta: - class-transformer: - optional: true - class-validator: - optional: true + '@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1)': dependencies: iterare: 1.2.1 reflect-metadata: 0.1.13 rxjs: 7.8.1 tslib: 2.6.2 uid: 2.0.2 - dev: true - /@nestjs/core@10.2.8(@nestjs/common@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-9+MZ2s8ixfY9Bl/M9ofChiyYymcwdK9ZWNH4GDMF7Am7XRAQ1oqde6MYGG05rhQwiVXuTwaYLlXciJKfsrg5qg==} - requiresBuild: true - peerDependencies: - '@nestjs/common': ^10.0.0 - '@nestjs/microservices': ^10.0.0 - '@nestjs/platform-express': ^10.0.0 - '@nestjs/websockets': ^10.0.0 - reflect-metadata: ^0.1.12 - rxjs: ^7.1.0 - peerDependenciesMeta: - '@nestjs/microservices': - optional: true - '@nestjs/platform-express': - optional: true - '@nestjs/websockets': - optional: true + '@nestjs/core@10.2.8(@nestjs/common@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 @@ -2848,134 +9402,78 @@ packages: uid: 2.0.2 transitivePeerDependencies: - encoding - dev: true - /@nestjs/jwt@10.2.0(@nestjs/common@10.2.8): - resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==} - peerDependencies: - '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/jwt@10.2.0(@nestjs/common@10.2.8)': dependencies: '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) '@types/jsonwebtoken': 9.0.5 jsonwebtoken: 9.0.2 - dev: true - /@nestjs/testing@10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8): - resolution: {integrity: sha512-9Kj5IQhM67/nj/MT6Wi2OmWr5YQnCMptwKVFrX1TDaikpY12196v7frk0jVjdT7wms7rV07GZle9I2z0aSjqtQ==} - peerDependencies: - '@nestjs/common': ^10.0.0 - '@nestjs/core': ^10.0.0 - '@nestjs/microservices': ^10.0.0 - '@nestjs/platform-express': ^10.0.0 - peerDependenciesMeta: - '@nestjs/microservices': - optional: true - '@nestjs/platform-express': - optional: true + '@nestjs/testing@10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)': dependencies: '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) tslib: 2.6.2 - dev: true - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 - dev: true - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true + '@nodelib/fs.stat@2.0.5': {} - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 - dev: true - /@npmcli/fs@2.1.2: - resolution: {integrity: sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + '@npmcli/fs@2.1.2': dependencies: '@gar/promisify': 1.1.3 semver: 7.5.4 - dev: true - /@npmcli/move-file@2.0.1: - resolution: {integrity: sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This functionality has been moved to @npmcli/fs + '@npmcli/move-file@2.0.1': dependencies: mkdirp: 1.0.4 rimraf: 3.0.2 - dev: true - /@nuxtjs/opencollective@0.3.2: - resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} - hasBin: true + '@nuxtjs/opencollective@0.3.2': dependencies: chalk: 4.1.2 consola: 2.15.3 node-fetch: 2.7.0 transitivePeerDependencies: - encoding - dev: true - /@opentelemetry/api@1.6.0: - resolution: {integrity: sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g==} - engines: {node: '>=8.0.0'} - dev: false + '@opentelemetry/api@1.6.0': {} - /@otplib/core@12.0.1: - resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} - dev: false + '@otplib/core@12.0.1': {} - /@otplib/plugin-crypto@12.0.1: - resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==} + '@otplib/plugin-crypto@12.0.1': dependencies: '@otplib/core': 12.0.1 - dev: false - /@otplib/plugin-thirty-two@12.0.1: - resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==} + '@otplib/plugin-thirty-two@12.0.1': dependencies: '@otplib/core': 12.0.1 thirty-two: 1.0.2 - dev: false - /@otplib/preset-default@12.0.1: - resolution: {integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==} + '@otplib/preset-default@12.0.1': dependencies: '@otplib/core': 12.0.1 '@otplib/plugin-crypto': 12.0.1 '@otplib/plugin-thirty-two': 12.0.1 - dev: false - /@otplib/preset-v11@12.0.1: - resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==} + '@otplib/preset-v11@12.0.1': dependencies: '@otplib/core': 12.0.1 '@otplib/plugin-crypto': 12.0.1 '@otplib/plugin-thirty-two': 12.0.1 - dev: false - /@pkgjs/parseargs@0.11.0: - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - requiresBuild: true - dev: true + '@pkgjs/parseargs@0.11.0': optional: true - /@pkgr/utils@2.4.2: - resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@pkgr/utils@2.4.2': dependencies: cross-spawn: 7.0.3 fast-glob: 3.3.2 @@ -2983,41 +9481,24 @@ packages: open: 9.1.0 picocolors: 1.0.0 tslib: 2.6.2 - dev: true - /@pnpm/config.env-replace@1.1.0: - resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} - engines: {node: '>=12.22.0'} - dev: true + '@pnpm/config.env-replace@1.1.0': {} - /@pnpm/network.ca-file@1.0.2: - resolution: {integrity: sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==} - engines: {node: '>=12.22.0'} + '@pnpm/network.ca-file@1.0.2': dependencies: graceful-fs: 4.2.10 - dev: true - /@pnpm/npm-conf@2.2.2: - resolution: {integrity: sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==} - engines: {node: '>=12'} + '@pnpm/npm-conf@2.2.2': dependencies: '@pnpm/config.env-replace': 1.1.0 '@pnpm/network.ca-file': 1.0.2 config-chain: 1.1.13 - dev: true - /@popperjs/core@2.11.8: - resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - dev: false + '@popperjs/core@2.11.8': {} - /@quasar/extras@1.16.7: - resolution: {integrity: sha512-nYF3gVE/si1YJ/D4qmAiHGwxoJIDCvTT8NI6ZmbTMPrur4J8xBKhfhfhyLoQ4k2jJZP6Rx0rUcB71FBNC2C8vQ==} - dev: false + '@quasar/extras@1.16.7': {} - /@quasar/icongenie@3.1.1: - resolution: {integrity: sha512-FreTVI4udcmdAssLN7e70BFGpyCXOTxur/cXJLTnuu42oTfPUVbQ9LZATksutBRppIAbPL4RmAuNML043Erj6w==} - engines: {node: '>= 14.19.0'} - hasBin: true + '@quasar/icongenie@3.1.1': dependencies: '@hapi/joi': 17.1.1 cross-spawn: 7.0.3 @@ -3036,40 +9517,23 @@ packages: svgo: 3.0.2 untildify: 4.0.0 update-notifier: 6.0.2 - dev: true - /@quasar/render-ssr-error@1.0.2: - resolution: {integrity: sha512-Y0wyqYHVxc1IOBH6pRiKMSWDqO1mwQu11Zo8rw4cBdclPOQqFb7f65UuRbk5LfbqlXV2hYvklNcy0SBAOiAQnw==} - engines: {node: '>= 16'} + '@quasar/render-ssr-error@1.0.2': dependencies: stack-trace: 1.0.0-pre2 - dev: true - /@quasar/vite-plugin@1.6.0(@deepnotes/quasar@2.13.2)(@vitejs/plugin-vue@2.3.4)(vite@2.9.16)(vue@3.2.47): - resolution: {integrity: sha512-LmbV76G1CwWZbrEQhqyZpkRQTJyO3xpW55aXY1zWN+JhyUeG77CcMCEWteBVnJ6I6ehUPFDC9ONd2+WlwH6rNQ==} - engines: {node: '>=12'} - peerDependencies: - '@vitejs/plugin-vue': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-beta.0 - quasar: ^2.8.0 - vite: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-beta.0 - vue: ^3.0.0 + '@quasar/vite-plugin@1.6.0(@deepnotes/quasar@2.13.2)(@vitejs/plugin-vue@2.3.4)(vite@2.9.16)(vue@3.2.47)': dependencies: '@vitejs/plugin-vue': 2.3.4(vite@2.9.16)(vue@3.2.47) - quasar: /@deepnotes/quasar@2.13.2 + quasar: '@deepnotes/quasar@2.13.2' vite: 2.9.16(sass@1.32.12) vue: 3.2.47 - dev: true - /@reactivedata/reactive@0.2.2: - resolution: {integrity: sha512-KnINM/Sng25QAv6sHkJO9q/XyslLegCF5jTsTSVu+AouY3uZDVf4Am99xNCqsfqFZFvnTBBDvCsHNdvTVGvPEA==} - dev: false + '@reactivedata/reactive@0.2.2': {} - /@remirror/core-constants@2.0.2: - resolution: {integrity: sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ==} - dev: false + '@remirror/core-constants@2.0.2': {} - /@remirror/core-helpers@3.0.0: - resolution: {integrity: sha512-tusEgQJIqg4qKj6HSBUFcyRnWnziw3neh4T9wOmsPGHFC3w9kl5KSrDb9UAgE8uX6y32FnS7vJ955mWOl3n50A==} + '@remirror/core-helpers@3.0.0': dependencies: '@remirror/core-constants': 2.0.2 '@remirror/types': 1.0.1 @@ -3084,478 +9548,252 @@ packages: object.omit: 3.0.0 object.pick: 1.3.0 throttle-debounce: 3.0.1 - dev: false - /@remirror/types@1.0.1: - resolution: {integrity: sha512-VlZQxwGnt1jtQ18D6JqdIF+uFZo525WEqrfp9BOc3COPpK4+AWCgdnAWL+ho6imWcoINlGjR/+3b6y5C1vBVEA==} + '@remirror/types@1.0.1': dependencies: type-fest: 2.19.0 - dev: false - /@revenuecat/purchases-capacitor@7.1.1(@capacitor/core@5.5.1): - resolution: {integrity: sha512-m7fTzFtGq0YaK2M3U0ygkzk53uHeY/JLoy6+gpJtgmrctQPWOkRTEQhCJ74l0LmWpai6j72XFTPPt6GMkV0bBw==} - peerDependencies: - '@capacitor/core': ^5.0.0 + '@revenuecat/purchases-capacitor@7.1.1(@capacitor/core@5.5.1)': dependencies: '@capacitor/core': 5.5.1 '@revenuecat/purchases-typescript-internal-esm': 7.3.3 - dev: false - /@revenuecat/purchases-typescript-internal-esm@7.3.3: - resolution: {integrity: sha512-Vfwj49iQunqxiuy/EDMVdDfqR2LM5BKfAOfFyzV3oDCHpXIsBjSUZT2/zdsoPVreeylVwf4alHf0U2ivA9OHPQ==} - dev: false + '@revenuecat/purchases-typescript-internal-esm@7.3.3': {} - /@rollup/pluginutils@4.2.1: - resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} - engines: {node: '>= 8.0.0'} + '@rollup/pluginutils@4.2.1': dependencies: estree-walker: 2.0.2 picomatch: 2.3.1 - dev: true - /@rollup/pluginutils@5.0.5: - resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true + '@rollup/pluginutils@5.0.5': dependencies: '@types/estree': 1.0.4 estree-walker: 2.0.2 picomatch: 2.3.1 - dev: true - /@sendgrid/client@7.7.0: - resolution: {integrity: sha512-SxH+y8jeAQSnDavrTD0uGDXYIIkFylCo+eDofVmZLQ0f862nnqbC3Vd1ej6b7Le7lboyzQF6F7Fodv02rYspuA==} - engines: {node: 6.* || 8.* || >=10.*} + '@sendgrid/client@7.7.0': dependencies: '@sendgrid/helpers': 7.7.0 axios: 0.26.1 transitivePeerDependencies: - debug - dev: false - /@sendgrid/helpers@7.7.0: - resolution: {integrity: sha512-3AsAxfN3GDBcXoZ/y1mzAAbKzTtUZ5+ZrHOmWQ279AuaFXUNCh9bPnRpN504bgveTqoW+11IzPg3I0WVgDINpw==} - engines: {node: '>= 6.0.0'} + '@sendgrid/helpers@7.7.0': dependencies: deepmerge: 4.3.1 - dev: false - /@sendgrid/mail@7.7.0: - resolution: {integrity: sha512-5+nApPE9wINBvHSUxwOxkkQqM/IAAaBYoP9hw7WwgDNQPxraruVqHizeTitVtKGiqWCKm2mnjh4XGN3fvFLqaw==} - engines: {node: 6.* || 8.* || >=10.*} + '@sendgrid/mail@7.7.0': dependencies: '@sendgrid/client': 7.7.0 '@sendgrid/helpers': 7.7.0 transitivePeerDependencies: - debug - dev: false - /@sinclair/typebox@0.27.8: - resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - dev: true + '@sinclair/typebox@0.27.8': {} - /@sindresorhus/is@0.14.0: - resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} - engines: {node: '>=6'} - dev: true + '@sindresorhus/is@0.14.0': {} - /@sindresorhus/is@0.7.0: - resolution: {integrity: sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==} - engines: {node: '>=4'} - dev: true + '@sindresorhus/is@0.7.0': {} - /@sindresorhus/is@4.6.0: - resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} - engines: {node: '>=10'} - dev: true + '@sindresorhus/is@4.6.0': {} - /@sindresorhus/is@5.6.0: - resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==} - engines: {node: '>=14.16'} - dev: true + '@sindresorhus/is@5.6.0': {} - /@socket.io/component-emitter@3.1.0: - resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} - dev: true + '@socket.io/component-emitter@3.1.0': {} - /@stripe/stripe-js@2.1.11: - resolution: {integrity: sha512-GRyInO+VPMjjgUzVPKpDtz+5s8JKssJ99uhWBGo09yxDQBb+bhkm6PxmVa8C+qsSd30JFO1Z+pgIJ0AMmmZJKg==} - dev: false + '@stripe/stripe-js@2.1.11': {} - /@syncedstore/core@0.6.0(yjs@13.6.8): - resolution: {integrity: sha512-6TtjEoYJsceYi8u1oRecXwbbLmjHaU0S7HvVfOaEdDfphZLGm/faVuA2fpazqc28F0yIFGvYzvPEBUJn9vqRNw==} - peerDependencies: - yjs: ^13.5.13 + '@syncedstore/core@0.6.0(yjs@13.6.8)': dependencies: '@reactivedata/reactive': 0.2.2 '@syncedstore/yjs-reactive-bindings': 0.6.0(yjs@13.6.8) yjs: 13.6.8 - dev: false - /@syncedstore/yjs-reactive-bindings@0.6.0(yjs@13.6.8): - resolution: {integrity: sha512-VF78h0J4iOt79YU9d6j5E6bFKu7WXYuiI2ue9ZnA+T4SNVn8viRvg0AHm3NqHzudZZUgYT3dpnbv1/ZmU7yPZQ==} - peerDependencies: - yjs: ^13.5.13 + '@syncedstore/yjs-reactive-bindings@0.6.0(yjs@13.6.8)': dependencies: yjs: 13.6.8 - dev: false - /@szmarczak/http-timer@1.1.2: - resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} - engines: {node: '>=6'} + '@szmarczak/http-timer@1.1.2': dependencies: defer-to-connect: 1.1.3 - dev: true - /@szmarczak/http-timer@4.0.6: - resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} - engines: {node: '>=10'} + '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 - dev: true - /@szmarczak/http-timer@5.0.1: - resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} - engines: {node: '>=14.16'} + '@szmarczak/http-timer@5.0.1': dependencies: defer-to-connect: 2.0.1 - dev: true - /@tiptap/core@2.1.12(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-ZGc3xrBJA9KY8kln5AYTj8y+GDrKxi7u95xIl2eccrqTY5CQeRu6HRNM1yT4mAjuSaG9jmazyjGRlQuhyxCKxQ==} - peerDependencies: - '@tiptap/pm': ^2.0.0 + '@tiptap/core@2.1.12(@tiptap/pm@2.1.12)': dependencies: '@tiptap/pm': 2.1.12 - dev: false - /@tiptap/extension-blockquote@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-Qb3YRlCfugx9pw7VgLTb+jY37OY4aBJeZnqHzx4QThSm13edNYjasokbX0nTwL1Up4NPTcY19JUeHt6fVaVVGg==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-blockquote@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-bold@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-AZGxIxcGU1/y6V2YEbKsq6BAibL8yQrbRm6EdcBnby41vj1WziewEKswhLGmZx5IKM2r2ldxld03KlfSIlKQZg==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-bold@2.1.12(@tiptap/core@2.1.12)': dependencies: - '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - - /@tiptap/extension-bubble-menu@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-gAGi21EQ4wvLmT7klgariAc2Hf+cIjaNU2NWze3ut6Ku9gUo5ZLqj1t9SKHmNf4d5JG63O8GxpErqpA7lHlRtw==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/pm': ^2.0.0 + '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) + + '@tiptap/extension-bubble-menu@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 tippy.js: 6.3.7 - dev: false - /@tiptap/extension-bullet-list@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-vtD8vWtNlmAZX8LYqt2yU9w3mU9rPCiHmbp4hDXJs2kBnI0Ju/qAyXFx6iJ3C3XyuMnMbJdDI9ee0spAvFz7cQ==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-bullet-list@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-code-block-lowlight@2.1.12(@tiptap/core@2.1.12)(@tiptap/extension-code-block@2.1.12)(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-dtIbpI9QrWa9TzNO4v5q/zf7+d83wpy5i9PEccdJAVtRZ0yOI8JIZAWzG5ex3zAoCA0CnQFdsPSVykYSDdxtDA==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/extension-code-block': ^2.0.0 - '@tiptap/pm': ^2.0.0 + '@tiptap/extension-code-block-lowlight@2.1.12(@tiptap/core@2.1.12)(@tiptap/extension-code-block@2.1.12)(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/extension-code-block': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - dev: false - /@tiptap/extension-code-block@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-RXtSYCVsnk8D+K80uNZShClfZjvv1EgO42JlXLVGWQdIgaNyuOv/6I/Jdf+ZzhnpsBnHufW+6TJjwP5vJPSPHA==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/pm': ^2.0.0 + '@tiptap/extension-code-block@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - dev: false - /@tiptap/extension-code@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-CRiRq5OTC1lFgSx6IMrECqmtb93a0ZZKujEnaRhzWliPBjLIi66va05f/P1vnV6/tHaC3yfXys6dxB5A4J8jxw==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-code@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-collaboration@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(y-prosemirror@1.0.20): - resolution: {integrity: sha512-U/2Vo1RyFIhi2oMW371wO145PU8mjQp7shCDdio/hNF+GasaNN9mrqkMBj8JBhH7UOTJcEqLPP4df1nEDo1BBQ==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/pm': ^2.0.0 - y-prosemirror: 1.0.20 + '@tiptap/extension-collaboration@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(y-prosemirror@1.0.20)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 y-prosemirror: 1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8) - dev: false - /@tiptap/extension-document@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-0QNfAkCcFlB9O8cUNSwTSIQMV9TmoEhfEaLz/GvbjwEq4skXK3bU+OQX7Ih07waCDVXIGAZ7YAZogbvrn/WbOw==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-document@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-dropcursor@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-0tT/q8nL4NBCYPxr9T0Brck+RQbWuczm9nV0bnxgt0IiQXoRHutfPWdS7GA65PTuVRBS/3LOco30fbjFhkfz/A==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/pm': ^2.0.0 + '@tiptap/extension-dropcursor@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - dev: false - /@tiptap/extension-floating-menu@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-uo0ydCJNg6AWwLT6cMUJYVChfvw2PY9ZfvKRhh9YJlGfM02jS4RUG/bJBts6R37f+a5FsOvAVwg8EvqPlNND1A==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/pm': ^2.0.0 + '@tiptap/extension-floating-menu@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 tippy.js: 6.3.7 - dev: false - /@tiptap/extension-gapcursor@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-zFYdZCqPgpwoB7whyuwpc8EYLYjUE5QYKb8vICvc+FraBUDM51ujYhFSgJC3rhs8EjI+8GcK8ShLbSMIn49YOQ==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/pm': ^2.0.0 + '@tiptap/extension-gapcursor@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - dev: false - /@tiptap/extension-hard-break@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-nqKcAYGEOafg9D+2cy1E4gHNGuL12LerVa0eS2SQOb+PT8vSel9OTKU1RyZldsWSQJ5rq/w4uIjmLnrSR2w6Yw==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-hard-break@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-heading@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-MoANP3POAP68Ko9YXarfDKLM/kXtscgp6m+xRagPAghRNujVY88nK1qBMZ3JdvTVN6b/ATJhp8UdrZX96TLV2w==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-heading@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-highlight@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-buen31cYPyiiHA2i0o2i/UcjRTg/42mNDCizGr1OJwvv3AELG3qOFc4Y58WJWIvWNv+1Dr4ZxHA3GNVn0ANWyg==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-highlight@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-history@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-6b7UFVkvPjq3LVoCTrYZAczt5sQrQUaoDWAieVClVZoFLfjga2Fwjcfgcie8IjdPt8YO2hG/sar/c07i9vM0Sg==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/pm': ^2.0.0 + '@tiptap/extension-history@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - dev: false - /@tiptap/extension-horizontal-rule@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-RRuoK4KxrXRrZNAjJW5rpaxjiP0FJIaqpi7nFbAua2oHXgsCsG8qbW2Y0WkbIoS8AJsvLZ3fNGsQ8gpdliuq3A==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/pm': ^2.0.0 + '@tiptap/extension-horizontal-rule@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - dev: false - /@tiptap/extension-image@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-VCgOTeNLuoR89WoCESLverpdZpPamOd7IprQbDIeG14sUySt7RHNgf2AEfyTYJEHij12rduvAwFzerPldVAIJg==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-image@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-italic@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-/XYrW4ZEWyqDvnXVKbgTXItpJOp2ycswk+fJ3vuexyolO6NSs0UuYC6X4f+FbHYL5VuWqVBv7EavGa+tB6sl3A==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-italic@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-link@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-Sti5hhlkCqi5vzdQjU/gbmr8kb578p+u0J4kWS+SSz3BknNThEm/7Id67qdjBTOQbwuN07lHjDaabJL0hSkzGQ==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/pm': ^2.0.0 + '@tiptap/extension-link@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 linkifyjs: 4.1.1 - dev: false - /@tiptap/extension-list-item@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-Gk7hBFofAPmNQ8+uw8w5QSsZOMEGf7KQXJnx5B022YAUJTYYxO3jYVuzp34Drk9p+zNNIcXD4kc7ff5+nFOTrg==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-list-item@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-ordered-list@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-tF6VGl+D2avCgn9U/2YLJ8qVmV6sPE/iEzVAFZuOSe6L0Pj7SQw4K6AO640QBob/d8VrqqJFHCb6l10amJOnXA==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-ordered-list@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-paragraph@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-hoH/uWPX+KKnNAZagudlsrr4Xu57nusGekkJWBcrb5MCDE91BS+DN2xifuhwXiTHxnwOMVFjluc0bPzQbkArsw==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-paragraph@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-strike@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-HlhrzIjYUT8oCH9nYzEL2QTTn8d1ECnVhKvzAe6x41xk31PjLMHTUy8aYjeQEkWZOWZ34tiTmslV1ce6R3Dt8g==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-strike@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-subscript@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-tb1jysEvf4SIiXwEOgDTXiyrG39RVNHvn/zsGMg5wy5t9qUp9m1k7kKYTH084ktuKDAPQonCcpn3hwc+ngTFzg==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-subscript@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-superscript@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-ek6L+DNsrjiJieArlgTvQt1VfJ56d8V19WAPW/ciRhq88YRlTEY9nSO3QuUCSUO1nGmE5OWQpgrsiW/XZbONVw==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-superscript@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-table-cell@2.0.0-beta.202(@tiptap/core@2.1.12): - resolution: {integrity: sha512-Ypmcq7zaMSZ0VNKwDPINOsSzyuH+gSIw+FrXy6O1dzVHAo1gNFJ2pEG/ZhQ2RqpDTpGfJFD8tNDx8wjCCAVlxA==} - peerDependencies: - '@tiptap/core': ^2.0.0-beta.193 + '@tiptap/extension-table-cell@2.0.0-beta.202(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-table-header@2.0.0-beta.202(@tiptap/core@2.1.12): - resolution: {integrity: sha512-/l0lz3Hmc+hikj+RfSW7F6B/jYV2dROGQnK1/EYjgbvOK0158ml1mB6/Dhm+BhldV73MI7eU8+3YLB9uhsPR4w==} - peerDependencies: - '@tiptap/core': ^2.0.0-beta.193 + '@tiptap/extension-table-header@2.0.0-beta.202(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-table-row@2.0.0-beta.202(@tiptap/core@2.1.12): - resolution: {integrity: sha512-IsHBT3lp//XSqcAWPIGWjPIKQ4okVaDJbwcElehlOo/rcRBeK0orT+c10T08PoOsozi4BeMYRo0nfA5tvrJMEw==} - peerDependencies: - '@tiptap/core': ^2.0.0-beta.193 + '@tiptap/extension-table-row@2.0.0-beta.202(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-table@2.0.0-beta.202(@tiptap/core@2.1.12): - resolution: {integrity: sha512-WMfXtDfx45FgU81WnfxGOSJbVoaDpe8hjuBJSGbwJj+Qj4HGhbK7/RbTtDrM8oqseHRzHuGWgNX+EfOUQppjdA==} - peerDependencies: - '@tiptap/core': ^2.0.0-beta.193 + '@tiptap/extension-table@2.0.0-beta.202(@tiptap/core@2.1.12)': dependencies: '@_ueberdosis/prosemirror-tables': 1.1.3 '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) prosemirror-model: 1.18.3 prosemirror-state: 1.4.3 prosemirror-view: 1.29.2 - dev: false - /@tiptap/extension-task-item@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-uqrDTO4JwukZUt40GQdvB6S+oDhdp4cKNPMi0sbteWziQugkSMLlkYvxU0Hfb/YeziaWWwFI7ssPu/hahyk6dQ==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/pm': ^2.0.0 + '@tiptap/extension-task-item@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - dev: false - /@tiptap/extension-task-list@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-BUpYlEWK+Q3kw9KIiOqvhd0tUPhMcOf1+fJmCkluJok+okAxMbP1umAtCEQ3QkoCwLr+vpHJov7h3yi9+dwgeQ==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-task-list@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-text-align@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-siMlwrkgVrAxxgmZn8GOc75J7UZi2CVrP9vDHkUPPyKm/fjssYekXwGCEk4Vswii1BbOh2gt+MDsRkeYRGyDlQ==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-text-align@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-text@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-rCNUd505p/PXwU9Jgxo4ZJv4A3cIBAyAqlx/dtcY6cjztCQuXJhuQILPhjGhBTOLEEL4kW2wQtqzCmb7O8i2jg==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-text@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-underline@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-NwwdhFT8gDD0VUNLQx85yFBhP9a8qg8GPuxlGzAP/lPTV8Ubh3vSeQ5N9k2ZF/vHlEvnugzeVCbmYn7wf8vn1g==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-underline@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/extension-youtube@2.1.12(@tiptap/core@2.1.12): - resolution: {integrity: sha512-Kr/sGESmWDNCKHEgbpAFCZJgvGYBuFNGUUY1eIxXRSz3w7sxS+cQ8xQ0+7jD2f2BVaJgdy7kf/V0wQ6ocGdHbw==} - peerDependencies: - '@tiptap/core': ^2.0.0 + '@tiptap/extension-youtube@2.1.12(@tiptap/core@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - dev: false - /@tiptap/pm@2.1.12: - resolution: {integrity: sha512-Q3MXXQABG4CZBesSp82yV84uhJh/W0Gag6KPm2HRWPimSFELM09Z9/5WK9RItAYE0aLhe4Krnyiczn9AAa1tQQ==} + '@tiptap/pm@2.1.12': dependencies: prosemirror-changeset: 2.2.1 prosemirror-collab: 1.3.1 @@ -3575,10 +9813,8 @@ packages: prosemirror-trailing-node: 2.0.7(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2) prosemirror-transform: 1.8.0 prosemirror-view: 1.29.2 - dev: false - /@tiptap/starter-kit@2.1.12(@tiptap/pm@2.1.12): - resolution: {integrity: sha512-+RoP1rWV7rSCit2+3wl2bjvSRiePRJE/7YNKbvH8Faz/+AMO23AFegHoUFynR7U0ouGgYDljGkkj35e0asbSDA==} + '@tiptap/starter-kit@2.1.12(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/extension-blockquote': 2.1.12(@tiptap/core@2.1.12) @@ -3601,449 +9837,265 @@ packages: '@tiptap/extension-text': 2.1.12(@tiptap/core@2.1.12) transitivePeerDependencies: - '@tiptap/pm' - dev: false - /@tiptap/vue-3@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(vue@3.2.47): - resolution: {integrity: sha512-yAcfmWw/9jtIUbhb0uGQVI9NoPYgHRasX2sAGWnm9Al+0aJktgmQ3mLCifXfXfjyEbeMF0p2L6Ul8tO7eho7aQ==} - peerDependencies: - '@tiptap/core': ^2.0.0 - '@tiptap/pm': ^2.0.0 - vue: ^3.0.0 + '@tiptap/vue-3@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(vue@3.2.47)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/extension-bubble-menu': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) '@tiptap/extension-floating-menu': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 vue: 3.2.47 - dev: false - /@tokenizer/token@0.3.0: - resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - dev: true + '@tokenizer/token@0.3.0': {} - /@tootallnate/once@2.0.0: - resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} - engines: {node: '>= 10'} - dev: true + '@tootallnate/once@2.0.0': {} - /@trpc/client@10.43.1(@trpc/server@10.43.1): - resolution: {integrity: sha512-pkPtbDS0ck/2WZo2cWaPV11NFMII2I/nzse1Ggs5Cr0YczsZk3Z0DM77Sfb9FTSjmccYfkEtumHqxfTj6fRbbg==} - peerDependencies: - '@trpc/server': 10.43.1 + '@trpc/client@10.43.1(@trpc/server@10.43.1)': dependencies: '@trpc/server': 10.43.1 - dev: false - /@trpc/server@10.43.1: - resolution: {integrity: sha512-rKOSCpJOb1MdTyJFqdf3QNNESDfPkbP+yBOZBM2x6iIOS4VlCfJqxsaSrb3uLPR6s8Ni7DhTu+cu/q1r0xOGcw==} - engines: {node: '>=18.0.0'} - dev: false + '@trpc/server@10.43.1': {} - /@trysound/sax@0.2.0: - resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} - engines: {node: '>=10.13.0'} - dev: true + '@trysound/sax@0.2.0': {} - /@tsconfig/node10@1.0.9: - resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} - dev: true + '@tsconfig/node10@1.0.9': {} - /@tsconfig/node12@1.0.11: - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - dev: true + '@tsconfig/node12@1.0.11': {} - /@tsconfig/node14@1.0.3: - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - dev: true + '@tsconfig/node14@1.0.3': {} - /@tsconfig/node16@1.0.4: - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - dev: true + '@tsconfig/node16@1.0.4': {} - /@types/argon2-browser@1.18.3: - resolution: {integrity: sha512-WmFgbCKUDqwVbidRRf+JbdvKlt8ptAUX4vND0BkV/vGcU9Zcw6tSb3aRY3UMDJj7yJxhjjkUOPk0Gt/F+WPFRA==} - dev: true + '@types/argon2-browser@1.18.3': {} - /@types/body-parser@1.19.4: - resolution: {integrity: sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==} + '@types/body-parser@1.19.4': dependencies: '@types/connect': 3.4.37 '@types/node': 20.9.1 - dev: true - /@types/cacheable-request@6.0.3: - resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + '@types/cacheable-request@6.0.3': dependencies: '@types/http-cache-semantics': 4.0.3 '@types/keyv': 3.1.4 '@types/node': 20.9.1 '@types/responselike': 1.0.2 - dev: true - /@types/chai-subset@1.3.4: - resolution: {integrity: sha512-CCWNXrJYSUIojZ1149ksLl3AN9cmZ5djf+yUoVVV+NuYrtydItQVlL2ZDqyC6M6O9LWRnVf8yYDxbXHO2TfQZg==} + '@types/chai-subset@1.3.4': dependencies: '@types/chai': 4.3.9 - dev: true - /@types/chai@4.3.9: - resolution: {integrity: sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==} - dev: true + '@types/chai@4.3.9': {} - /@types/chrome@0.0.208: - resolution: {integrity: sha512-VDU/JnXkF5qaI7WBz14Azpa2VseZTgML0ia/g/B1sr9OfdOnHiH/zZ7P7qCDqxSlkqJh76/bPc8jLFcx8rHJmw==} + '@types/chrome@0.0.208': dependencies: '@types/filesystem': 0.0.34 '@types/har-format': 1.2.14 - dev: true - /@types/color-convert@2.0.2: - resolution: {integrity: sha512-KGRIgCxwcgazts4MXRCikPbIMzBpjfdgEZSy8TRHU/gtg+f9sOfHdtK8unPfxIoBtyd2aTTwINVLSNENlC8U8A==} + '@types/color-convert@2.0.2': dependencies: '@types/color-name': 1.1.2 - dev: true - /@types/color-name@1.1.2: - resolution: {integrity: sha512-JWO/ZyxTKk0bLuOhAavGjnwLR73rUE7qzACnU7gMeyA/gdrSHm2xJwqNPipw2MtaZUaqQ2UG/q7pP6AQiZ8mqw==} - dev: true + '@types/color-name@1.1.2': {} - /@types/color@3.0.5: - resolution: {integrity: sha512-T9yHCNtd8ap9L/r8KEESu5RDMLkoWXHo7dTureNoI1dbp25NsCN054vOu09iniIjR21MXUL+LU9bkIWrbyg8gg==} + '@types/color@3.0.5': dependencies: '@types/color-convert': 2.0.2 - dev: true - /@types/compression@1.7.4: - resolution: {integrity: sha512-sdFVnQJRkQBX83ydsLCBm4A39p45y0QkxdAR689yOtAFNbbS9Acrp86RZWJj6BHRXyZH9tX4t1dU7XDiGdY3nA==} + '@types/compression@1.7.4': dependencies: '@types/express': 4.17.20 - dev: true - /@types/connect@3.4.37: - resolution: {integrity: sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==} + '@types/connect@3.4.37': dependencies: '@types/node': 20.9.1 - dev: true - /@types/cookie@0.4.1: - resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} - dev: true + '@types/cookie@0.4.1': {} - /@types/cookie@0.5.3: - resolution: {integrity: sha512-SLg07AS9z1Ab2LU+QxzU8RCmzsja80ywjf/t5oqw+4NSH20gIGlhLOrBDm1L3PBWzPa4+wkgFQVZAjE6Ioj2ug==} - dev: true + '@types/cookie@0.5.3': {} - /@types/cordova@0.0.34: - resolution: {integrity: sha512-rkiiTuf/z2wTd4RxFOb+clE7PF4AEJU0hsczbUdkHHBtkUmpWQpEddynNfJYKYtZFJKbq4F+brfekt1kx85IZA==} - dev: true + '@types/cordova@0.0.34': {} - /@types/cors@2.8.15: - resolution: {integrity: sha512-n91JxbNLD8eQIuXDIChAN1tCKNWCEgpceU9b7ZMbFA+P+Q4yIeh80jizFLEvolRPc1ES0VdwFlGv+kJTSirogw==} + '@types/cors@2.8.15': dependencies: '@types/node': 20.9.1 - dev: true - /@types/crypto-js@4.2.0: - resolution: {integrity: sha512-LW9TlhpMeoQKOu6I6HvOp7eInNNnvd7B+ndAfZb826HUl7eHJJApofbHnlAxrIVS/uiCjkkHGNEaKHoGxB5IHw==} - dev: true + '@types/crypto-js@4.2.0': {} - /@types/debug@4.1.10: - resolution: {integrity: sha512-tOSCru6s732pofZ+sMv9o4o3Zc+Sa8l3bxd/tweTQudFn06vAzb13ZX46Zi6m6EJ+RUbRTHvgQJ1gBtSgkaUYA==} + '@types/debug@4.1.10': dependencies: '@types/ms': 0.7.33 - dev: true - /@types/downloadjs@1.4.5: - resolution: {integrity: sha512-pr3zSKY0QwP+1tQumJGNfziGNi1xLVHtGsrclGsgjVpfWSPoW/9He42w6X+GTp4PO5I2wuHDU2wJdxI7dqjW+w==} - dev: true + '@types/downloadjs@1.4.5': {} - /@types/estree@1.0.4: - resolution: {integrity: sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==} - dev: true + '@types/estree@1.0.4': {} - /@types/express-serve-static-core@4.17.39: - resolution: {integrity: sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==} + '@types/express-serve-static-core@4.17.39': dependencies: '@types/node': 20.9.1 '@types/qs': 6.9.9 '@types/range-parser': 1.2.6 '@types/send': 0.17.3 - dev: true - /@types/express@4.17.20: - resolution: {integrity: sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==} + '@types/express@4.17.20': dependencies: '@types/body-parser': 1.19.4 '@types/express-serve-static-core': 4.17.39 '@types/qs': 6.9.9 '@types/serve-static': 1.15.4 - dev: true - /@types/file-saver@2.0.6: - resolution: {integrity: sha512-Mw671DVqoMHbjw0w4v2iiOro01dlT/WhWp5uwecBa0Wg8c+bcZOjgF1ndBnlaxhtvFCgTRBtsGivSVhrK/vnag==} - dev: true + '@types/file-saver@2.0.6': {} - /@types/filesystem@0.0.34: - resolution: {integrity: sha512-La4bGrgck8/rosDUA1DJJP8hrFcKq0BV6JaaVlNnOo1rJdJDcft3//slEbAmsWNUJwXRCc0DXpeO40yuATlexw==} + '@types/filesystem@0.0.34': dependencies: '@types/filewriter': 0.0.31 - dev: true - /@types/filewriter@0.0.31: - resolution: {integrity: sha512-12df1utOvPC80+UaVoOO1d81X8pa5MefHNS+gWX9R2ucSESpMz9K5QwlTWDGKASrzCpSFwj7NPYh+nTsolgEGA==} - dev: true + '@types/filewriter@0.0.31': {} - /@types/fs-extra@8.1.5: - resolution: {integrity: sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==} + '@types/fs-extra@8.1.5': dependencies: '@types/node': 20.9.1 - dev: false - /@types/fs-extra@9.0.13: - resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + '@types/fs-extra@9.0.13': dependencies: '@types/node': 20.9.1 - dev: true - /@types/har-format@1.2.14: - resolution: {integrity: sha512-pEmBAoccWvO6XbSI8A7KvIDGEoKtlLWtdqVCKoVBcCDSFvR4Ijd7zGLu7MWGEqk2r8D54uWlMRt+VZuSrfFMzQ==} - dev: true + '@types/har-format@1.2.14': {} - /@types/hast@2.3.7: - resolution: {integrity: sha512-EVLigw5zInURhzfXUM65eixfadfsHKomGKUakToXo84t8gGIJuTcD2xooM2See7GyQ7DRtYjhCHnSUQez8JaLw==} + '@types/hast@2.3.7': dependencies: '@types/unist': 2.0.9 - dev: false - /@types/http-cache-semantics@4.0.3: - resolution: {integrity: sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA==} - dev: true + '@types/http-cache-semantics@4.0.3': {} - /@types/http-errors@2.0.3: - resolution: {integrity: sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==} - dev: true + '@types/http-errors@2.0.3': {} - /@types/json-schema@7.0.14: - resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==} - dev: true + '@types/json-schema@7.0.14': {} - /@types/jsonwebtoken@9.0.4: - resolution: {integrity: sha512-8UYapdmR0QlxgvJmyE8lP7guxD0UGVMfknsdtCFZh4ovShdBl3iOI4zdvqBHrB/IS+xUj3PSx73Qkey1fhWz+g==} + '@types/jsonwebtoken@9.0.4': dependencies: '@types/node': 20.8.10 - dev: true - /@types/jsonwebtoken@9.0.5: - resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + '@types/jsonwebtoken@9.0.5': dependencies: '@types/node': 20.9.1 - dev: true - /@types/jws@3.2.8: - resolution: {integrity: sha512-xDIvCuI7hEicqUa+dSc2TnrC92yvJtzbLghMrMcwroWzU9RWlF64cqrWW8QGikq4f7fN0V7edDIWuMLeICk+2A==} + '@types/jws@3.2.8': dependencies: '@types/node': 20.9.1 - dev: true - /@types/katex@0.16.5: - resolution: {integrity: sha512-DD2Y3xMlTQvAnN6d8803xdgnOeYZ+HwMglb7/9YCf49J9RkJL53azf9qKa40MkEYhqVwxZ1GS2+VlShnz4Z1Bw==} - dev: true + '@types/katex@0.16.5': {} - /@types/keyv@3.1.4: - resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + '@types/keyv@3.1.4': dependencies: '@types/node': 20.9.1 - dev: true - /@types/libsodium-wrappers-sumo@0.7.7: - resolution: {integrity: sha512-L5KaYOEJqPlMZjP2kUaKjr0vQyv8LRR/QkwAKUazl3JrcEt/VXDdCAi2+Z5mSHOUjan7PEPRSxEPvwsIyXDLDA==} + '@types/libsodium-wrappers-sumo@0.7.7': dependencies: '@types/libsodium-wrappers': 0.7.12 - dev: true - /@types/libsodium-wrappers@0.7.12: - resolution: {integrity: sha512-NNUV6W5KFMYSazUh7bofvIqjHunu1ia24Q4gygXrhluXvvdPtkV6fXuciidYU7eBc9XTltAc6k727wYlrpo9Jg==} - dev: true + '@types/libsodium-wrappers@0.7.12': {} - /@types/lodash@4.14.200: - resolution: {integrity: sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==} - dev: true + '@types/lodash@4.14.200': {} - /@types/mime@1.3.4: - resolution: {integrity: sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==} - dev: true + '@types/mime@1.3.4': {} - /@types/mime@3.0.3: - resolution: {integrity: sha512-i8MBln35l856k5iOhKk2XJ4SeAWg75mLIpZB4v6imOagKL6twsukBZGDMNhdOVk7yRFTMPpfILocMos59Q1otQ==} - dev: true + '@types/mime@3.0.3': {} - /@types/minimist@1.2.4: - resolution: {integrity: sha512-Kfe/D3hxHTusnPNRbycJE1N77WHDsdS4AjUYIzlDzhDrS47NrwuL3YW4VITxwR7KCVpzwgy4Rbj829KSSQmwXQ==} - dev: true + '@types/minimist@1.2.4': {} - /@types/ms@0.7.33: - resolution: {integrity: sha512-AuHIyzR5Hea7ij0P9q7vx7xu4z0C28ucwjAZC0ja7JhINyCnOw8/DnvAPQQ9TfOlCtZAmCERKQX9+o1mgQhuOQ==} - dev: true + '@types/ms@0.7.33': {} - /@types/node-fetch@2.6.8: - resolution: {integrity: sha512-nnH5lV9QCMPsbEVdTb5Y+F3GQxLSw1xQgIydrb2gSfEavRPs50FnMr+KUaa+LoPSqibm2N+ZZxH7lavZlAT4GA==} + '@types/node-fetch@2.6.8': dependencies: '@types/node': 20.8.10 form-data: 4.0.0 - dev: true - /@types/node@16.18.60: - resolution: {integrity: sha512-ZUGPWx5vKfN+G2/yN7pcSNLkIkXEvlwNaJEd4e0ppX7W2S8XAkdc/37hM4OUNJB9sa0p12AOvGvxL4JCPiz9DA==} - dev: true + '@types/node@16.18.60': {} - /@types/node@16.9.1: - resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==} - dev: true + '@types/node@16.9.1': {} - /@types/node@18.18.8: - resolution: {integrity: sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ==} + '@types/node@18.18.8': dependencies: undici-types: 5.26.5 - dev: true - /@types/node@20.8.10: - resolution: {integrity: sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==} + '@types/node@20.8.10': dependencies: undici-types: 5.26.5 - /@types/node@20.9.1: - resolution: {integrity: sha512-HhmzZh5LSJNS5O8jQKpJ/3ZcrrlG6L70hpGqMIAoM9YVD0YBRNWYsfwcXq8VnSjlNpCpgLzMXdiPo+dxcvSmiA==} + '@types/node@20.9.1': dependencies: undici-types: 5.26.5 - /@types/normalize-package-data@2.4.3: - resolution: {integrity: sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==} - dev: true + '@types/normalize-package-data@2.4.3': {} - /@types/object.omit@3.0.3: - resolution: {integrity: sha512-xrq4bQTBGYY2cw+gV4PzoG2Lv3L0pjZ1uXStRRDQoATOYW1lCsFQHhQ+OkPhIcQoqLjAq7gYif7D14Qaa6Zbew==} - dev: false + '@types/object.omit@3.0.3': {} - /@types/object.pick@1.3.4: - resolution: {integrity: sha512-5PjwB0uP2XDp3nt5u5NJAG2DORHIRClPzWT/TTZhJ2Ekwe8M5bA9tvPdi9NO/n2uvu2/ictat8kgqvLfcIE1SA==} - dev: false + '@types/object.pick@1.3.4': {} - /@types/plist@3.0.5: - resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==} - requiresBuild: true + '@types/plist@3.0.5': dependencies: '@types/node': 20.9.1 xmlbuilder: 15.1.1 - dev: true optional: true - /@types/qrcode@1.5.4: - resolution: {integrity: sha512-ufYqUO7wUBq49hugJry+oIYKscvxIQerJSmXeny215aJKfrepN04DDZP8FCgxvV82kOqKPULCE4PIW3qUmZrRA==} + '@types/qrcode@1.5.4': dependencies: '@types/node': 20.8.10 - dev: true - /@types/qs@6.9.9: - resolution: {integrity: sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==} - dev: true + '@types/qs@6.9.9': {} - /@types/range-parser@1.2.6: - resolution: {integrity: sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==} - dev: true + '@types/range-parser@1.2.6': {} - /@types/responselike@1.0.2: - resolution: {integrity: sha512-/4YQT5Kp6HxUDb4yhRkm0bJ7TbjvTddqX7PZ5hz6qV3pxSo72f/6YPRo+Mu2DU307tm9IioO69l7uAwn5XNcFA==} + '@types/responselike@1.0.2': dependencies: '@types/node': 20.9.1 - dev: true - /@types/semver@7.5.4: - resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==} + '@types/semver@7.5.4': {} - /@types/send@0.17.3: - resolution: {integrity: sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==} + '@types/send@0.17.3': dependencies: '@types/mime': 1.3.4 '@types/node': 20.9.1 - dev: true - /@types/serve-static@1.15.4: - resolution: {integrity: sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==} + '@types/serve-static@1.15.4': dependencies: '@types/http-errors': 2.0.3 '@types/mime': 3.0.3 '@types/node': 20.9.1 - dev: true - /@types/showdown@2.0.3: - resolution: {integrity: sha512-cFuAcA3p2YPq8HR8KxvDXnOdccOZ74ypANB3kb3AL5Srji0QnteVw6vf4o7GJ8hMyz+uZ+nSQHVgXSgjYD1a5g==} - dev: true + '@types/showdown@2.0.3': {} - /@types/slice-ansi@4.0.0: - resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==} - dev: false + '@types/slice-ansi@4.0.0': {} - /@types/strip-bom@3.0.0: - resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} - dev: true + '@types/strip-bom@3.0.0': {} - /@types/strip-json-comments@0.0.30: - resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} - dev: true + '@types/strip-json-comments@0.0.30': {} - /@types/throttle-debounce@2.1.0: - resolution: {integrity: sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==} - dev: false + '@types/throttle-debounce@2.1.0': {} - /@types/triple-beam@1.3.4: - resolution: {integrity: sha512-HlJjF3wxV4R2VQkFpKe0YqJLilYNgtRtsqqZtby7RkVsSs+i+vbyzjtUwpFEdUCKcrGzCiEJE7F/0mKjh0sunA==} - dev: false + '@types/triple-beam@1.3.4': {} - /@types/turndown@5.0.3: - resolution: {integrity: sha512-2PCZA9g/dkeHIGTf6ESMOD3Gz5RMpDzODtvlBbkLAdtKa/yTQDAFudDEVolHjaBUnu8ugd8BeTCWk4x0STnqkA==} - dev: true + '@types/turndown@5.0.3': {} - /@types/unist@2.0.9: - resolution: {integrity: sha512-zC0iXxAv1C1ERURduJueYzkzZ2zaGyc+P2c95hgkikHPr3z8EdUZOlgEQ5X0DRmwDZn+hekycQnoeiiRVrmilQ==} - dev: false + '@types/unist@2.0.9': {} - /@types/verror@1.10.9: - resolution: {integrity: sha512-MLx9Z+9lGzwEuW16ubGeNkpBDE84RpB/NyGgg6z2BTpWzKkGU451cAY3UkUzZEp72RHF585oJ3V8JVNqIplcAQ==} - requiresBuild: true - dev: true + '@types/verror@1.10.9': optional: true - /@types/web-bluetooth@0.0.18: - resolution: {integrity: sha512-v/ZHEj9xh82usl8LMR3GarzFY1IrbXJw5L4QfQhokjRV91q+SelFqxQWSep1ucXEZ22+dSTwLFkXeur25sPIbw==} + '@types/web-bluetooth@0.0.18': {} - /@types/ws@8.5.3: - resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==} + '@types/ws@8.5.3': dependencies: '@types/node': 20.8.10 - dev: true - /@types/yauzl@2.10.3: - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - requiresBuild: true + '@types/yauzl@2.10.3': dependencies: '@types/node': 20.9.1 - dev: true optional: true - /@types/zxcvbn@4.4.3: - resolution: {integrity: sha512-AxZBi8J3V3lm+f2Vgg06D8y0womXpLf3ZEDYeLPGGK0ydR724sQH83T5tYgN+CN6VTRnAlevFKJkWTecCnk8ug==} - dev: true + '@types/zxcvbn@4.4.3': {} - /@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2)': dependencies: '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.2.2) @@ -4061,17 +10113,8 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2)': dependencies: '@typescript-eslint/scope-manager': 6.10.0 '@typescript-eslint/types': 6.10.0 @@ -4082,25 +10125,13 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/scope-manager@6.10.0: - resolution: {integrity: sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/scope-manager@6.10.0': dependencies: '@typescript-eslint/types': 6.10.0 '@typescript-eslint/visitor-keys': 6.10.0 - dev: true - /@typescript-eslint/type-utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/type-utils@6.10.0(eslint@8.53.0)(typescript@5.2.2)': dependencies: '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) @@ -4110,21 +10141,10 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/types@6.10.0: - resolution: {integrity: sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==} - engines: {node: ^16.0.0 || >=18.0.0} - dev: true + '@typescript-eslint/types@6.10.0': {} - /@typescript-eslint/typescript-estree@6.10.0(typescript@5.2.2): - resolution: {integrity: sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/typescript-estree@6.10.0(typescript@5.2.2)': dependencies: '@typescript-eslint/types': 6.10.0 '@typescript-eslint/visitor-keys': 6.10.0 @@ -4136,13 +10156,8 @@ packages: typescript: 5.2.2 transitivePeerDependencies: - supports-color - dev: true - /@typescript-eslint/utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 + '@typescript-eslint/utils@6.10.0(eslint@8.53.0)(typescript@5.2.2)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) '@types/json-schema': 7.0.14 @@ -4155,85 +10170,60 @@ packages: transitivePeerDependencies: - supports-color - typescript - dev: true - /@typescript-eslint/visitor-keys@6.10.0: - resolution: {integrity: sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==} - engines: {node: ^16.0.0 || >=18.0.0} + '@typescript-eslint/visitor-keys@6.10.0': dependencies: '@typescript-eslint/types': 6.10.0 eslint-visitor-keys: 3.4.3 - dev: true - - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: true - /@vitejs/plugin-vue@2.3.4(vite@2.9.16)(vue@3.2.47): - resolution: {integrity: sha512-IfFNbtkbIm36O9KB8QodlwwYvTEsJb4Lll4c2IwB3VHc2gie2mSPtSzL0eYay7X2jd/2WX02FjSGTWR6OPr/zg==} - engines: {node: '>=12.0.0'} - peerDependencies: - vite: ^2.5.10 - vue: ^3.2.25 + '@ungap/structured-clone@1.2.0': {} + + '@vitejs/plugin-vue@2.3.4(vite@2.9.16)(vue@3.2.47)': dependencies: vite: 2.9.16(sass@1.32.12) vue: 3.2.47 - dev: true - /@vitest/expect@0.34.6: - resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} + '@vitest/expect@0.34.6': dependencies: '@vitest/spy': 0.34.6 '@vitest/utils': 0.34.6 chai: 4.3.10 - dev: true - /@vitest/runner@0.34.6: - resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} + '@vitest/runner@0.34.6': dependencies: '@vitest/utils': 0.34.6 p-limit: 4.0.0 pathe: 1.1.1 - dev: true - /@vitest/snapshot@0.34.6: - resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} + '@vitest/snapshot@0.34.6': dependencies: magic-string: 0.30.5 pathe: 1.1.1 pretty-format: 29.7.0 - dev: true - /@vitest/spy@0.34.6: - resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} + '@vitest/spy@0.34.6': dependencies: tinyspy: 2.2.0 - dev: true - /@vitest/utils@0.34.6: - resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} + '@vitest/utils@0.34.6': dependencies: diff-sequences: 29.6.3 loupe: 2.3.7 pretty-format: 29.7.0 - dev: true - /@vue/compiler-core@3.2.47: - resolution: {integrity: sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig==} + '@vue/compiler-core@3.2.47': dependencies: '@babel/parser': 7.23.0 '@vue/shared': 3.2.47 estree-walker: 2.0.2 source-map: 0.6.1 - /@vue/compiler-dom@3.2.47: - resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==} + '@vue/compiler-dom@3.2.47': dependencies: '@vue/compiler-core': 3.2.47 '@vue/shared': 3.2.47 - /@vue/compiler-sfc@3.2.47: - resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==} + '@vue/compiler-sfc@3.2.47': dependencies: '@babel/parser': 7.23.0 '@vue/compiler-core': 3.2.47 @@ -4246,18 +10236,14 @@ packages: postcss: 8.4.31 source-map: 0.6.1 - /@vue/compiler-ssr@3.2.47: - resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==} + '@vue/compiler-ssr@3.2.47': dependencies: '@vue/compiler-dom': 3.2.47 '@vue/shared': 3.2.47 - /@vue/devtools-api@6.5.1: - resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==} + '@vue/devtools-api@6.5.1': {} - /@vue/devtools@6.5.1: - resolution: {integrity: sha512-3xSNDzebOTUHoCPFNsyklY8tC8RZNg6gy63zXAppdz9FV4gUG/hlWkOZd9xcuotaZ1HcurmLLfHckfUfbTheXw==} - hasBin: true + '@vue/devtools@6.5.1': dependencies: cross-spawn: 7.0.3 electron: 21.4.4 @@ -4269,10 +10255,8 @@ packages: transitivePeerDependencies: - bufferutil - supports-color - dev: true - /@vue/reactivity-transform@3.2.47: - resolution: {integrity: sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA==} + '@vue/reactivity-transform@3.2.47': dependencies: '@babel/parser': 7.23.0 '@vue/compiler-core': 3.2.47 @@ -4280,38 +10264,30 @@ packages: estree-walker: 2.0.2 magic-string: 0.25.9 - /@vue/reactivity@3.2.47: - resolution: {integrity: sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ==} + '@vue/reactivity@3.2.47': dependencies: '@vue/shared': 3.2.47 - /@vue/runtime-core@3.2.47: - resolution: {integrity: sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA==} + '@vue/runtime-core@3.2.47': dependencies: '@vue/reactivity': 3.2.47 '@vue/shared': 3.2.47 - /@vue/runtime-dom@3.2.47: - resolution: {integrity: sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA==} + '@vue/runtime-dom@3.2.47': dependencies: '@vue/runtime-core': 3.2.47 '@vue/shared': 3.2.47 csstype: 2.6.21 - /@vue/server-renderer@3.2.47(vue@3.2.47): - resolution: {integrity: sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA==} - peerDependencies: - vue: 3.2.47 + '@vue/server-renderer@3.2.47(vue@3.2.47)': dependencies: '@vue/compiler-ssr': 3.2.47 '@vue/shared': 3.2.47 vue: 3.2.47 - /@vue/shared@3.2.47: - resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==} + '@vue/shared@3.2.47': {} - /@vueuse/core@10.5.0(vue@3.2.47): - resolution: {integrity: sha512-z/tI2eSvxwLRjOhDm0h/SXAjNm8N5ld6/SC/JQs6o6kpJ6Ya50LnEL8g5hoYu005i28L0zqB5L5yAl8Jl26K3A==} + '@vueuse/core@10.5.0(vue@3.2.47)': dependencies: '@types/web-bluetooth': 0.0.18 '@vueuse/metadata': 10.5.0 @@ -4321,236 +10297,134 @@ packages: - '@vue/composition-api' - vue - /@vueuse/metadata@10.5.0: - resolution: {integrity: sha512-fEbElR+MaIYyCkeM0SzWkdoMtOpIwO72x8WsZHRE7IggiOlILttqttM69AS13nrDxosnDBYdyy3C5mR1LCxHsw==} + '@vueuse/metadata@10.5.0': {} - /@vueuse/shared@10.5.0(vue@3.2.47): - resolution: {integrity: sha512-18iyxbbHYLst9MqU1X1QNdMHIjks6wC7XTVf0KNOv5es/Ms6gjVFCAAWTVP2JStuGqydg3DT+ExpFORUEi9yhg==} + '@vueuse/shared@10.5.0(vue@3.2.47)': dependencies: vue-demi: 0.14.6(vue@3.2.47) transitivePeerDependencies: - '@vue/composition-api' - vue - /@xmldom/xmldom@0.8.10: - resolution: {integrity: sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==} - engines: {node: '>=10.0.0'} - requiresBuild: true + '@xmldom/xmldom@0.8.10': {} - /@zxcvbn-ts/core@3.0.4: - resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==} + '@zxcvbn-ts/core@3.0.4': dependencies: fastest-levenshtein: 1.0.16 - dev: false - /@zxcvbn-ts/language-common@3.0.4: - resolution: {integrity: sha512-viSNNnRYtc7ULXzxrQIVUNwHAPSXRtoIwy/Tq4XQQdIknBzw4vz36lQLF6mvhMlTIlpjoN/Z1GFu/fwiAlUSsw==} - dev: false + '@zxcvbn-ts/language-common@3.0.4': {} - /@zxcvbn-ts/language-en@3.0.2: - resolution: {integrity: sha512-Zp+zL+I6Un2Bj0tRXNs6VUBq3Djt+hwTwUz4dkt2qgsQz47U0/XthZ4ULrT/RxjwJRl5LwiaKOOZeOtmixHnjg==} - dev: false + '@zxcvbn-ts/language-en@3.0.2': {} - /JSONStream@1.3.5: - resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} - hasBin: true + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 through: 2.3.8 - dev: true - /abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - dev: true + abbrev@1.1.1: {} - /abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 - dev: false - /abstract-logging@2.0.1: - resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} - dev: false + abstract-logging@2.0.1: {} - /accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} + accepts@1.3.8: dependencies: mime-types: 2.1.35 negotiator: 0.6.3 - /acorn-jsx@5.3.2(acorn@7.4.1): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-jsx@5.3.2(acorn@7.4.1): dependencies: acorn: 7.4.1 - dev: true - /acorn-jsx@5.3.2(acorn@8.11.2): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-jsx@5.3.2(acorn@8.11.2): dependencies: acorn: 8.11.2 - dev: true - /acorn-walk@8.3.0: - resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} - engines: {node: '>=0.4.0'} - dev: true + acorn-walk@8.3.0: {} - /acorn@7.4.1: - resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true + acorn@7.4.1: {} - /acorn@8.11.2: - resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true + acorn@8.11.2: {} - /add-stream@1.0.0: - resolution: {integrity: sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==} - dev: true + add-stream@1.0.0: {} - /agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} + agent-base@6.0.2: dependencies: debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: true - /agentkeepalive@4.5.0: - resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} - engines: {node: '>= 8.0.0'} + agentkeepalive@4.5.0: dependencies: humanize-ms: 1.2.1 - dev: true - /aggregate-error@3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 indent-string: 4.0.0 - dev: true - /ajv-formats@2.1.1(ajv@8.12.0): - resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true + ajv-formats@2.1.1(ajv@8.12.0): dependencies: ajv: 8.12.0 - dev: false - /ajv-keywords@3.5.2(ajv@6.12.6): - resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} - peerDependencies: - ajv: ^6.9.1 + ajv-keywords@3.5.2(ajv@6.12.6): dependencies: ajv: 6.12.6 - dev: true - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 json-schema-traverse: 0.4.1 uri-js: 4.4.1 - dev: true - /ajv@8.12.0: - resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + ajv@8.12.0: dependencies: fast-deep-equal: 3.1.3 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 uri-js: 4.4.1 - /ansi-align@3.0.1: - resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-align@3.0.1: dependencies: string-width: 4.2.3 - dev: true - /ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - dev: true + ansi-colors@4.1.3: {} - /ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 - dev: true - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} + ansi-regex@5.0.1: {} - /ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - dev: true + ansi-regex@6.0.1: {} - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} + ansi-styles@3.2.1: dependencies: color-convert: 1.9.3 - dev: true - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - /ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - dev: true + ansi-styles@5.2.0: {} - /ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - dev: true + ansi-styles@6.2.1: {} - /any-base@1.1.0: - resolution: {integrity: sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg==} - dev: true + any-base@1.1.0: {} - /any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - dev: true + any-promise@1.3.0: {} - /anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 - dev: true - /app-builder-bin@4.0.0: - resolution: {integrity: sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA==} - dev: true + app-builder-bin@4.0.0: {} - /app-builder-lib@24.4.0: - resolution: {integrity: sha512-EcdqtWvg1LAApKCfyRBukcVkmsa94s2e1VKHjZLpvA9/D14QEt8rHhffYeaA+cH/pVeoNVn2ob735KnfJKEEow==} - engines: {node: '>=14.0.0'} + app-builder-lib@24.4.0: dependencies: 7zip-bin: 5.1.1 '@develar/schema-utils': 2.6.5 @@ -4584,26 +10458,16 @@ packages: transitivePeerDependencies: - bluebird - supports-color - dev: true - /aproba@2.0.0: - resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} - dev: true + aproba@2.0.0: {} - /arch@2.2.0: - resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} - dev: true + arch@2.2.0: {} - /archive-type@4.0.0: - resolution: {integrity: sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==} - engines: {node: '>=4'} + archive-type@4.0.0: dependencies: file-type: 4.4.0 - dev: true - /archiver-utils@2.1.0: - resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} - engines: {node: '>= 6'} + archiver-utils@2.1.0: dependencies: glob: 7.2.3 graceful-fs: 4.2.11 @@ -4615,11 +10479,8 @@ packages: lodash.union: 4.6.0 normalize-path: 3.0.0 readable-stream: 2.3.8 - dev: true - /archiver-utils@3.0.4: - resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} - engines: {node: '>= 10'} + archiver-utils@3.0.4: dependencies: glob: 7.2.3 graceful-fs: 4.2.11 @@ -4631,11 +10492,8 @@ packages: lodash.union: 4.6.0 normalize-path: 3.0.0 readable-stream: 3.6.2 - dev: true - /archiver@5.3.2: - resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} - engines: {node: '>= 10'} + archiver@5.3.2: dependencies: archiver-utils: 2.1.0 async: 3.2.5 @@ -4644,118 +10502,63 @@ packages: readdir-glob: 1.1.3 tar-stream: 2.2.0 zip-stream: 4.1.1 - dev: true - /archy@1.0.0: - resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==} - dev: false + archy@1.0.0: {} - /are-we-there-yet@3.0.1: - resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + are-we-there-yet@3.0.1: dependencies: delegates: 1.0.0 readable-stream: 3.6.2 - dev: true - /arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - dev: true + arg@4.1.3: {} - /argon2-browser@1.18.0: - resolution: {integrity: sha512-ImVAGIItnFnvET1exhsQB7apRztcoC5TnlSqernMJDUjbc/DLq3UEYeXFrLPrlaIl8cVfwnXb6wX2KpFf2zxHw==} - dev: false + argon2-browser@1.18.0: {} - /argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 - dev: false - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + argparse@2.0.1: {} - /array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + array-flatten@1.1.1: {} - /array-ify@1.0.0: - resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} - dev: true + array-ify@1.0.0: {} - /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - dev: true + array-union@2.1.0: {} - /array-union@3.0.1: - resolution: {integrity: sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==} - engines: {node: '>=12'} - dev: true + array-union@3.0.1: {} - /arrify@1.0.1: - resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} - engines: {node: '>=0.10.0'} - dev: true + arrify@1.0.1: {} - /asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - dev: false + asap@2.0.6: {} - /asn1.js@5.4.1: - resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + asn1.js@5.4.1: dependencies: bn.js: 4.12.0 inherits: 2.0.4 minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 - dev: false - /assert-plus@1.0.0: - resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} - engines: {node: '>=0.8'} - requiresBuild: true - dev: true + assert-plus@1.0.0: optional: true - /assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - dev: true + assertion-error@1.1.0: {} - /astral-regex@2.0.0: - resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} - engines: {node: '>=8'} + astral-regex@2.0.0: {} - /async-exit-hook@2.0.1: - resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} - engines: {node: '>=0.12.0'} - dev: true + async-exit-hook@2.0.1: {} - /async@3.2.5: - resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==} + async@3.2.5: {} - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + asynckit@0.4.0: {} - /at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} + at-least-node@1.0.0: {} - /atomic-sleep@1.0.0: - resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} - engines: {node: '>=8.0.0'} - dev: false + atomic-sleep@1.0.0: {} - /author-regex@1.0.0: - resolution: {integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==} - engines: {node: '>=0.8'} - dev: true + author-regex@1.0.0: {} - /autoprefixer@10.4.16(postcss@8.4.31): - resolution: {integrity: sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ==} - engines: {node: ^10 || ^12 || >=14} - hasBin: true - peerDependencies: - postcss: ^8.1.0 + autoprefixer@10.4.16(postcss@8.4.31): dependencies: browserslist: 4.22.1 caniuse-lite: 1.0.30001561 @@ -4764,112 +10567,75 @@ packages: picocolors: 1.0.0 postcss: 8.4.31 postcss-value-parser: 4.2.0 - dev: true - /avvio@8.2.1: - resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==} + avvio@8.2.1: dependencies: archy: 1.0.0 debug: 4.3.4 fastq: 1.15.0 transitivePeerDependencies: - supports-color - dev: false - /axios@0.26.1: - resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} + axios@0.26.1: dependencies: follow-redirects: 1.15.3 transitivePeerDependencies: - debug - dev: false - /axios@0.27.2: - resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} + axios@0.27.2: dependencies: follow-redirects: 1.15.3 form-data: 4.0.0 transitivePeerDependencies: - debug - dev: false - /axios@1.6.0: - resolution: {integrity: sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==} + axios@1.6.0: dependencies: follow-redirects: 1.15.3 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug - dev: false - /b4a@1.6.4: - resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==} - dev: true + b4a@1.6.4: {} - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + balanced-match@1.0.2: {} - /base64-arraybuffer@1.0.2: - resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} - engines: {node: '>= 0.6.0'} - dev: false + base64-arraybuffer@1.0.2: {} - /base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + base64-js@1.5.1: {} - /base64id@2.0.0: - resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} - engines: {node: ^4.5.0 || >= 5.9} - dev: true + base64id@2.0.0: {} - /big-integer@1.6.51: - resolution: {integrity: sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==} - engines: {node: '>=0.6'} + big-integer@1.6.51: {} - /bignumber.js@9.1.2: - resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} - dev: false + bignumber.js@9.1.2: {} - /bin-build@3.0.0: - resolution: {integrity: sha512-jcUOof71/TNAI2uM5uoUaDq2ePcVBQ3R/qhxAz1rX7UfvduAL/RXD3jXzvn8cVcDJdGVkiR1shal3OH0ImpuhA==} - engines: {node: '>=4'} + bin-build@3.0.0: dependencies: decompress: 4.2.1 download: 6.2.5 execa: 0.7.0 p-map-series: 1.0.0 tempfile: 2.0.0 - dev: true - /bin-check@4.1.0: - resolution: {integrity: sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==} - engines: {node: '>=4'} + bin-check@4.1.0: dependencies: execa: 0.7.0 executable: 4.1.1 - dev: true - /bin-version-check@4.0.0: - resolution: {integrity: sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==} - engines: {node: '>=6'} + bin-version-check@4.0.0: dependencies: bin-version: 3.1.0 semver: 5.7.2 semver-truncate: 1.1.2 - dev: true - /bin-version@3.1.0: - resolution: {integrity: sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==} - engines: {node: '>=6'} + bin-version@3.1.0: dependencies: execa: 1.0.0 find-versions: 3.2.0 - dev: true - /bin-wrapper@4.1.0: - resolution: {integrity: sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q==} - engines: {node: '>=6'} + bin-wrapper@4.1.0: dependencies: bin-check: 4.1.0 bin-version-check: 4.0.0 @@ -4877,53 +10643,33 @@ packages: import-lazy: 3.1.0 os-filter-obj: 2.0.0 pify: 4.0.1 - dev: true - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - dev: true + binary-extensions@2.2.0: {} - /bintrees@1.0.2: - resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} - dev: false + bintrees@1.0.2: {} - /bl@1.2.3: - resolution: {integrity: sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==} + bl@1.2.3: dependencies: readable-stream: 2.3.8 safe-buffer: 5.2.1 - dev: true - /bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 - dev: true - /bluebird-lst@1.0.9: - resolution: {integrity: sha512-7B1Rtx82hjnSD4PGLAjVWeYH3tHAcVUmChh85a3lltKQm6FresXh9ErQo6oAv6CqxttczC3/kEg8SY5NluPuUw==} + bluebird-lst@1.0.9: dependencies: bluebird: 3.7.2 - dev: true - /bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - dev: true + bluebird@3.7.2: {} - /bmp-js@0.1.0: - resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} - dev: true + bmp-js@0.1.0: {} - /bn.js@4.12.0: - resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} - dev: false + bn.js@4.12.0: {} - /body-parser@1.20.1: - resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@1.20.1: dependencies: bytes: 3.1.2 content-type: 1.0.5 @@ -4940,19 +10686,12 @@ packages: transitivePeerDependencies: - supports-color - /boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - dev: true + boolbase@1.0.0: {} - /boolean@3.2.0: - resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} - requiresBuild: true - dev: true + boolean@3.2.0: optional: true - /boxen@7.1.1: - resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} - engines: {node: '>=14.16'} + boxen@7.1.1: dependencies: ansi-align: 3.0.1 camelcase: 7.0.1 @@ -4962,127 +10701,81 @@ packages: type-fest: 2.19.0 widest-line: 4.0.1 wrap-ansi: 8.1.0 - dev: true - /bplist-parser@0.2.0: - resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} - engines: {node: '>= 5.10.0'} + bplist-parser@0.2.0: dependencies: big-integer: 1.6.51 - dev: true - /bplist-parser@0.3.2: - resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} - engines: {node: '>= 5.10.0'} + bplist-parser@0.3.2: dependencies: big-integer: 1.6.51 - dev: false - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true - /brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.1: dependencies: balanced-match: 1.0.2 - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} + braces@3.0.2: dependencies: fill-range: 7.0.1 - dev: true - /browserslist@4.22.1: - resolution: {integrity: sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true + browserslist@4.22.1: dependencies: caniuse-lite: 1.0.30001561 electron-to-chromium: 1.4.576 node-releases: 2.0.13 update-browserslist-db: 1.0.13(browserslist@4.22.1) - /buffer-alloc-unsafe@1.1.0: - resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} - dev: true + buffer-alloc-unsafe@1.1.0: {} - /buffer-alloc@1.2.0: - resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} + buffer-alloc@1.2.0: dependencies: buffer-alloc-unsafe: 1.1.0 buffer-fill: 1.0.0 - dev: true - /buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-crc32@0.2.13: {} - /buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-equal-constant-time@1.0.1: {} - /buffer-equal@0.0.1: - resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==} - engines: {node: '>=0.4.0'} - dev: true + buffer-equal@0.0.1: {} - /buffer-equal@1.0.1: - resolution: {integrity: sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==} - engines: {node: '>=0.4'} - dev: true + buffer-equal@1.0.1: {} - /buffer-fill@1.0.0: - resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} - dev: true + buffer-fill@1.0.0: {} - /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: true + buffer-from@1.1.2: {} - /buffer-writer@2.0.0: - resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} - engines: {node: '>=4'} - dev: false + buffer-writer@2.0.0: {} - /buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: true - /buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + buffer@6.0.3: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - dev: false - /builder-util-runtime@8.7.0: - resolution: {integrity: sha512-G1AqqVM2vYTrSFR982c1NNzwXKrGLQjVjaZaWQdn4O6Z3YKjdMDofw88aD9jpyK9ZXkrCxR0tI3Qe9wNbyTlXg==} - engines: {node: '>=8.2.0'} + builder-util-runtime@8.7.0: dependencies: debug: 4.3.4 sax: 1.3.0 transitivePeerDependencies: - supports-color - dev: false - /builder-util-runtime@9.2.1: - resolution: {integrity: sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA==} - engines: {node: '>=12.0.0'} + builder-util-runtime@9.2.1: dependencies: debug: 4.3.4 sax: 1.3.0 transitivePeerDependencies: - supports-color - dev: true - /builder-util@24.4.0: - resolution: {integrity: sha512-tONb/GIK1MKa1BcOPHE1naId3o5nj6gdka5kP7yUJh2DOfF+jMq3laiu+UOZH6A7ZtkMtnGNMYFKFTIv408n/A==} + builder-util@24.4.0: dependencies: 7zip-bin: 5.1.1 '@types/debug': 4.1.10 @@ -5102,47 +10795,27 @@ packages: temp-file: 3.4.0 transitivePeerDependencies: - supports-color - dev: true - /builtins@5.0.1: - resolution: {integrity: sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==} + builtins@5.0.1: dependencies: semver: 7.5.4 - dev: true - /bundle-name@3.0.0: - resolution: {integrity: sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==} - engines: {node: '>=12'} + bundle-name@3.0.0: dependencies: run-applescript: 5.0.0 - dev: true - /bundle-require@4.0.2(esbuild@0.18.20): - resolution: {integrity: sha512-jwzPOChofl67PSTW2SGubV9HBQAhhR2i6nskiOThauo9dzwDUgOWQScFVaJkjEfYX+UXiD+LEx8EblQMc2wIag==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - peerDependencies: - esbuild: '>=0.17' + bundle-require@4.0.2(esbuild@0.18.20): dependencies: esbuild: 0.18.20 load-tsconfig: 0.2.5 - dev: true - /bytes@3.0.0: - resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==} - engines: {node: '>= 0.8'} + bytes@3.0.0: {} - /bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} + bytes@3.1.2: {} - /cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - dev: true + cac@6.7.14: {} - /cacache@16.1.3: - resolution: {integrity: sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + cacache@16.1.3: dependencies: '@npmcli/fs': 2.1.2 '@npmcli/move-file': 2.0.1 @@ -5164,21 +10837,12 @@ packages: unique-filename: 2.0.1 transitivePeerDependencies: - bluebird - dev: true - /cacheable-lookup@5.0.4: - resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} - engines: {node: '>=10.6.0'} - dev: true + cacheable-lookup@5.0.4: {} - /cacheable-lookup@7.0.0: - resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} - engines: {node: '>=14.16'} - dev: true + cacheable-lookup@7.0.0: {} - /cacheable-request@10.2.14: - resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==} - engines: {node: '>=14.16'} + cacheable-request@10.2.14: dependencies: '@types/http-cache-semantics': 4.0.3 get-stream: 6.0.1 @@ -5187,10 +10851,8 @@ packages: mimic-response: 4.0.0 normalize-url: 8.0.0 responselike: 3.0.0 - dev: true - /cacheable-request@2.1.4: - resolution: {integrity: sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==} + cacheable-request@2.1.4: dependencies: clone-response: 1.0.2 get-stream: 3.0.0 @@ -5199,11 +10861,8 @@ packages: lowercase-keys: 1.0.0 normalize-url: 2.0.1 responselike: 1.0.2 - dev: true - /cacheable-request@6.1.0: - resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} - engines: {node: '>=8'} + cacheable-request@6.1.0: dependencies: clone-response: 1.0.3 get-stream: 5.2.0 @@ -5212,11 +10871,8 @@ packages: lowercase-keys: 2.0.0 normalize-url: 4.5.1 responselike: 1.0.2 - dev: true - /cacheable-request@7.0.4: - resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} - engines: {node: '>=8'} + cacheable-request@7.0.4: dependencies: clone-response: 1.0.3 get-stream: 5.2.0 @@ -5225,66 +10881,42 @@ packages: lowercase-keys: 2.0.0 normalize-url: 6.1.0 responselike: 2.0.1 - dev: true - /call-bind@1.0.5: - resolution: {integrity: sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==} + call-bind@1.0.5: dependencies: function-bind: 1.1.2 get-intrinsic: 1.2.2 set-function-length: 1.1.1 - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true + callsites@3.1.0: {} - /camel-case@3.0.0: - resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + camel-case@3.0.0: dependencies: no-case: 2.3.2 upper-case: 1.1.3 - dev: true - /camelcase-keys@6.2.2: - resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} - engines: {node: '>=8'} + camelcase-keys@6.2.2: dependencies: camelcase: 5.3.1 map-obj: 4.3.0 quick-lru: 4.0.1 - dev: true - /camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} + camelcase@5.3.1: {} - /camelcase@7.0.1: - resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==} - engines: {node: '>=14.16'} - dev: true + camelcase@7.0.1: {} - /caniuse-lite@1.0.30001561: - resolution: {integrity: sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==} + caniuse-lite@1.0.30001561: {} - /case-anything@2.1.13: - resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} - engines: {node: '>=12.13'} - dev: false + case-anything@2.1.13: {} - /caw@2.0.1: - resolution: {integrity: sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==} - engines: {node: '>=4'} + caw@2.0.1: dependencies: get-proxy: 2.1.0 isurl: 1.0.0 tunnel-agent: 0.6.0 url-to-options: 1.0.1 - dev: true - /chai@4.3.10: - resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} - engines: {node: '>=4'} + chai@4.3.10: dependencies: assertion-error: 1.1.0 check-error: 1.0.3 @@ -5293,43 +10925,27 @@ packages: loupe: 2.3.7 pathval: 1.1.1 type-detect: 4.0.8 - dev: true - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - dev: true - /chalk@5.3.0: - resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - dev: true + chalk@5.3.0: {} - /chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - dev: true + chardet@0.7.0: {} - /check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + check-error@1.0.3: dependencies: get-func-name: 2.0.2 - dev: true - /chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} + chokidar@3.5.3: dependencies: anymatch: 3.1.3 braces: 3.0.2 @@ -5340,250 +10956,147 @@ packages: readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 - dev: true - /chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - dev: true + chownr@1.1.4: {} - /chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} + chownr@2.0.0: {} - /chromium-pickle-js@0.2.0: - resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} - dev: true + chromium-pickle-js@0.2.0: {} - /ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - dev: true + ci-info@3.9.0: {} - /clean-css@4.2.4: - resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} - engines: {node: '>= 4.0'} + clean-css@4.2.4: dependencies: source-map: 0.6.1 - dev: true - /clean-stack@2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - dev: true + clean-stack@2.2.0: {} - /cli-boxes@3.0.0: - resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} - engines: {node: '>=10'} - dev: true + cli-boxes@3.0.0: {} - /cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 - dev: true - /cli-spinners@2.9.1: - resolution: {integrity: sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==} - engines: {node: '>=6'} - dev: true + cli-spinners@2.9.1: {} - /cli-truncate@2.1.0: - resolution: {integrity: sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==} - engines: {node: '>=8'} - requiresBuild: true + cli-truncate@2.1.0: dependencies: slice-ansi: 3.0.0 string-width: 4.2.3 - /cli-width@3.0.0: - resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} - engines: {node: '>= 10'} - dev: true + cli-width@3.0.0: {} - /cliui@6.0.0: - resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@6.0.0: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 6.2.0 - dev: false - /cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@7.0.4: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: true - /cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} + cliui@8.0.1: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - dev: true - /clone-deep@4.0.1: - resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} - engines: {node: '>=6'} + clone-deep@4.0.1: dependencies: is-plain-object: 2.0.4 kind-of: 6.0.3 shallow-clone: 3.0.1 - dev: true - /clone-response@1.0.2: - resolution: {integrity: sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==} + clone-response@1.0.2: dependencies: mimic-response: 1.0.1 - dev: true - /clone-response@1.0.3: - resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + clone-response@1.0.3: dependencies: mimic-response: 1.0.1 - dev: true - /clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - dev: true + clone@1.0.4: {} - /cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} - engines: {node: '>=0.10.0'} - dev: false + cluster-key-slot@1.1.2: {} - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@1.9.3: dependencies: color-name: 1.1.3 - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + color-convert@2.0.1: dependencies: color-name: 1.1.4 - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.3: {} - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-name@1.1.4: {} - /color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + color-string@1.9.1: dependencies: color-name: 1.1.4 simple-swizzle: 0.2.2 - /color-support@1.1.3: - resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true - dev: true + color-support@1.1.3: {} - /color@3.2.1: - resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + color@3.2.1: dependencies: color-convert: 1.9.3 color-string: 1.9.1 - dev: false - /color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} + color@4.2.3: dependencies: color-convert: 2.0.1 color-string: 1.9.1 - /colorette@2.0.19: - resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==} - dev: false + colorette@2.0.19: {} - /colorspace@1.1.4: - resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + colorspace@1.1.4: dependencies: color: 3.2.1 text-hex: 1.0.0 - dev: false - /combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - /commander@11.0.0: - resolution: {integrity: sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==} - engines: {node: '>=16'} - dev: true + commander@11.0.0: {} - /commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - dev: true + commander@2.20.3: {} - /commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - dev: true + commander@4.1.1: {} - /commander@5.1.0: - resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} - engines: {node: '>= 6'} - dev: true + commander@5.1.0: {} - /commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - dev: true + commander@7.2.0: {} - /commander@8.3.0: - resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} - engines: {node: '>= 12'} - dev: false + commander@8.3.0: {} - /commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} + commander@9.5.0: {} - /compare-func@2.0.0: - resolution: {integrity: sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==} + compare-func@2.0.0: dependencies: array-ify: 1.0.0 dot-prop: 5.3.0 - dev: true - /compare-version@0.1.2: - resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} - engines: {node: '>=0.10.0'} - dev: true + compare-version@0.1.2: {} - /component-emitter@1.3.0: - resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} - dev: false + component-emitter@1.3.0: {} - /compress-commons@4.1.2: - resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} - engines: {node: '>= 10'} + compress-commons@4.1.2: dependencies: buffer-crc32: 0.2.13 crc32-stream: 4.0.3 normalize-path: 3.0.0 readable-stream: 3.6.2 - dev: true - /compressible@2.0.18: - resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} - engines: {node: '>= 0.6'} + compressible@2.0.18: dependencies: mime-db: 1.52.0 - /compression@1.7.4: - resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==} - engines: {node: '>= 0.8.0'} + compression@1.7.4: dependencies: accepts: 1.3.8 bytes: 3.0.0 @@ -5595,24 +11108,16 @@ packages: transitivePeerDependencies: - supports-color - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true + concat-map@0.0.1: {} - /concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} + concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 inherits: 2.0.4 readable-stream: 3.6.2 typedarray: 0.0.6 - dev: true - /concurrently@8.2.2: - resolution: {integrity: sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==} - engines: {node: ^14.13.0 || >=16.0.0} - hasBin: true + concurrently@8.2.2: dependencies: chalk: 4.1.2 date-fns: 2.30.0 @@ -5623,104 +11128,65 @@ packages: supports-color: 8.1.1 tree-kill: 1.2.2 yargs: 17.7.2 - dev: true - /config-chain@1.1.13: - resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} - requiresBuild: true + config-chain@1.1.13: dependencies: ini: 1.3.8 proto-list: 1.2.4 - dev: true - /config-file-ts@0.2.4: - resolution: {integrity: sha512-cKSW0BfrSaAUnxpgvpXPLaaW/umg4bqg4k3GO1JqlRfpx+d5W0GDXznCMkWotJQek5Mmz1MJVChQnz3IVaeMZQ==} + config-file-ts@0.2.4: dependencies: glob: 7.2.3 typescript: 4.9.5 - dev: true - /configstore@6.0.0: - resolution: {integrity: sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==} - engines: {node: '>=12'} + configstore@6.0.0: dependencies: dot-prop: 6.0.1 graceful-fs: 4.2.11 unique-string: 3.0.0 write-file-atomic: 3.0.3 xdg-basedir: 5.1.0 - dev: true - /consola@2.15.3: - resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} - dev: true + consola@2.15.3: {} - /console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - dev: true + console-control-strings@1.1.0: {} - /content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} + content-disposition@0.5.4: dependencies: safe-buffer: 5.2.1 - /content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} + content-type@1.0.5: {} - /conventional-changelog-angular@5.0.13: - resolution: {integrity: sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA==} - engines: {node: '>=10'} + conventional-changelog-angular@5.0.13: dependencies: compare-func: 2.0.0 q: 1.5.1 - dev: true - /conventional-changelog-angular@6.0.0: - resolution: {integrity: sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==} - engines: {node: '>=14'} + conventional-changelog-angular@6.0.0: dependencies: compare-func: 2.0.0 - dev: true - /conventional-changelog-atom@2.0.8: - resolution: {integrity: sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw==} - engines: {node: '>=10'} + conventional-changelog-atom@2.0.8: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-codemirror@2.0.8: - resolution: {integrity: sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw==} - engines: {node: '>=10'} + conventional-changelog-codemirror@2.0.8: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-config-spec@2.1.0: - resolution: {integrity: sha512-IpVePh16EbbB02V+UA+HQnnPIohgXvJRxHcS5+Uwk4AT5LjzCZJm5sp/yqs5C6KZJ1jMsV4paEV13BN1pvDuxQ==} - dev: true + conventional-changelog-config-spec@2.1.0: {} - /conventional-changelog-conventionalcommits@4.6.3: - resolution: {integrity: sha512-LTTQV4fwOM4oLPad317V/QNQ1FY4Hju5qeBIM1uTHbrnCE+Eg4CdRZ3gO2pUeR+tzWdp80M2j3qFFEDWVqOV4g==} - engines: {node: '>=10'} + conventional-changelog-conventionalcommits@4.6.3: dependencies: compare-func: 2.0.0 lodash: 4.17.21 q: 1.5.1 - dev: true - /conventional-changelog-conventionalcommits@7.0.2: - resolution: {integrity: sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==} - engines: {node: '>=16'} + conventional-changelog-conventionalcommits@7.0.2: dependencies: compare-func: 2.0.0 - dev: true - /conventional-changelog-core@4.2.4: - resolution: {integrity: sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg==} - engines: {node: '>=10'} + conventional-changelog-core@4.2.4: dependencies: add-stream: 1.0.0 conventional-changelog-writer: 5.0.1 @@ -5736,53 +11202,31 @@ packages: read-pkg: 3.0.0 read-pkg-up: 3.0.0 through2: 4.0.2 - dev: true - /conventional-changelog-ember@2.0.9: - resolution: {integrity: sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A==} - engines: {node: '>=10'} + conventional-changelog-ember@2.0.9: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-eslint@3.0.9: - resolution: {integrity: sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA==} - engines: {node: '>=10'} + conventional-changelog-eslint@3.0.9: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-express@2.0.6: - resolution: {integrity: sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ==} - engines: {node: '>=10'} + conventional-changelog-express@2.0.6: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-jquery@3.0.11: - resolution: {integrity: sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw==} - engines: {node: '>=10'} + conventional-changelog-jquery@3.0.11: dependencies: q: 1.5.1 - dev: true - /conventional-changelog-jshint@2.0.9: - resolution: {integrity: sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA==} - engines: {node: '>=10'} + conventional-changelog-jshint@2.0.9: dependencies: compare-func: 2.0.0 q: 1.5.1 - dev: true - /conventional-changelog-preset-loader@2.3.4: - resolution: {integrity: sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g==} - engines: {node: '>=10'} - dev: true + conventional-changelog-preset-loader@2.3.4: {} - /conventional-changelog-writer@5.0.1: - resolution: {integrity: sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==} - engines: {node: '>=10'} - hasBin: true + conventional-changelog-writer@5.0.1: dependencies: conventional-commits-filter: 2.0.7 dateformat: 3.0.3 @@ -5793,11 +11237,8 @@ packages: semver: 6.3.1 split: 1.0.1 through2: 4.0.2 - dev: true - /conventional-changelog@3.1.25: - resolution: {integrity: sha512-ryhi3fd1mKf3fSjbLXOfK2D06YwKNic1nC9mWqybBHdObPd8KJ2vjaXZfYj1U23t+V8T8n0d7gwnc9XbIdFbyQ==} - engines: {node: '>=10'} + conventional-changelog@3.1.25: dependencies: conventional-changelog-angular: 5.0.13 conventional-changelog-atom: 2.0.8 @@ -5810,20 +11251,13 @@ packages: conventional-changelog-jquery: 3.0.11 conventional-changelog-jshint: 2.0.9 conventional-changelog-preset-loader: 2.3.4 - dev: true - /conventional-commits-filter@2.0.7: - resolution: {integrity: sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA==} - engines: {node: '>=10'} + conventional-commits-filter@2.0.7: dependencies: lodash.ismatch: 4.4.0 modify-values: 1.0.1 - dev: true - /conventional-commits-parser@3.2.4: - resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==} - engines: {node: '>=10'} - hasBin: true + conventional-commits-parser@3.2.4: dependencies: JSONStream: 1.3.5 is-text-path: 1.0.1 @@ -5831,23 +11265,15 @@ packages: meow: 8.1.2 split2: 3.2.2 through2: 4.0.2 - dev: true - /conventional-commits-parser@5.0.0: - resolution: {integrity: sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==} - engines: {node: '>=16'} - hasBin: true + conventional-commits-parser@5.0.0: dependencies: JSONStream: 1.3.5 is-text-path: 2.0.0 meow: 12.1.1 split2: 4.2.0 - dev: true - /conventional-recommended-bump@6.1.0: - resolution: {integrity: sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw==} - engines: {node: '>=10'} - hasBin: true + conventional-recommended-bump@6.1.0: dependencies: concat-stream: 2.0.0 conventional-changelog-preset-loader: 2.3.4 @@ -5857,356 +11283,204 @@ packages: git-semver-tags: 4.1.1 meow: 8.1.2 q: 1.5.1 - dev: true - /cookie-signature@1.0.6: - resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.0.6: {} - /cookie@0.4.2: - resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} - engines: {node: '>= 0.6'} - dev: true + cookie@0.4.2: {} - /cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} - engines: {node: '>= 0.6'} + cookie@0.5.0: {} - /cookiejar@2.1.4: - resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} - dev: false + cookiejar@2.1.4: {} - /copy-anything@3.0.5: - resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} - engines: {node: '>=12.13'} + copy-anything@3.0.5: dependencies: is-what: 4.1.16 - dev: false - /core-util-is@1.0.2: - resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} - requiresBuild: true - dev: true + core-util-is@1.0.2: optional: true - /core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: true + core-util-is@1.0.3: {} - /cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} + cors@2.8.5: dependencies: object-assign: 4.1.1 vary: 1.1.2 - dev: true - /cosmiconfig-typescript-loader@5.0.0(@types/node@18.18.8)(cosmiconfig@8.3.6)(typescript@5.2.2): - resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} - engines: {node: '>=v16'} - peerDependencies: - '@types/node': '*' - cosmiconfig: '>=8.2' - typescript: '>=4' + cosmiconfig-typescript-loader@5.0.0(@types/node@18.18.8)(cosmiconfig@8.3.6)(typescript@5.2.2): dependencies: '@types/node': 18.18.8 cosmiconfig: 8.3.6(typescript@5.2.2) jiti: 1.21.0 typescript: 5.2.2 - dev: true - /cosmiconfig@8.2.0: - resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==} - engines: {node: '>=14'} + cosmiconfig@8.2.0: dependencies: import-fresh: 3.3.0 js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 - dev: true - /cosmiconfig@8.3.6(typescript@5.2.2): - resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} - engines: {node: '>=14'} - peerDependencies: - typescript: '>=4.9.5' - peerDependenciesMeta: - typescript: - optional: true + cosmiconfig@8.3.6(typescript@5.2.2): dependencies: import-fresh: 3.3.0 js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 typescript: 5.2.2 - dev: true - /crc-32@1.2.2: - resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} - engines: {node: '>=0.8'} - hasBin: true - dev: true + crc-32@1.2.2: {} - /crc32-stream@4.0.3: - resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} - engines: {node: '>= 10'} + crc32-stream@4.0.3: dependencies: crc-32: 1.2.2 readable-stream: 3.6.2 - dev: true - /crc@3.8.0: - resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} - requiresBuild: true + crc@3.8.0: dependencies: buffer: 5.7.1 - dev: true optional: true - /create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - dev: true + create-require@1.1.1: {} - /crelt@1.0.6: - resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - dev: false + crelt@1.0.6: {} - /cross-env@7.0.3: - resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} - engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} - hasBin: true + cross-env@7.0.3: dependencies: cross-spawn: 7.0.3 - dev: true - /cross-spawn-windows-exe@1.2.0: - resolution: {integrity: sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==} - engines: {node: '>= 10'} + cross-spawn-windows-exe@1.2.0: dependencies: '@malept/cross-spawn-promise': 1.1.1 is-wsl: 2.2.0 which: 2.0.2 - dev: true - /cross-spawn@5.1.0: - resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} + cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 shebang-command: 1.2.0 which: 1.3.1 - dev: true - /cross-spawn@6.0.5: - resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} - engines: {node: '>=4.8'} + cross-spawn@6.0.5: dependencies: nice-try: 1.0.5 path-key: 2.0.1 semver: 5.7.2 shebang-command: 1.2.0 which: 1.3.1 - dev: true - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - /crypto-js@4.2.0: - resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - dev: false + crypto-js@4.2.0: {} - /crypto-random-string@4.0.0: - resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} - engines: {node: '>=12'} + crypto-random-string@4.0.0: dependencies: type-fest: 1.4.0 - dev: true - /css-line-break@2.1.0: - resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-line-break@2.1.0: dependencies: utrie: 1.0.2 - dev: false - /css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + css-select@5.1.0: dependencies: boolbase: 1.0.0 css-what: 6.1.0 domhandler: 5.0.3 domutils: 3.1.0 nth-check: 2.1.1 - dev: true - /css-tree@2.2.1: - resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + css-tree@2.2.1: dependencies: mdn-data: 2.0.28 source-map-js: 1.0.2 - dev: true - /css-tree@2.3.1: - resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@2.3.1: dependencies: mdn-data: 2.0.30 source-map-js: 1.0.2 - dev: true - /css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - dev: true + css-what@6.1.0: {} - /cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - dev: true + cssesc@3.0.0: {} - /csso@5.0.5: - resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} - engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + csso@5.0.5: dependencies: css-tree: 2.2.1 - dev: true - /csstype@2.6.21: - resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==} + csstype@2.6.21: {} - /dargs@7.0.0: - resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==} - engines: {node: '>=8'} - dev: true + dargs@7.0.0: {} - /dash-get@1.0.2: - resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==} - dev: false + dash-get@1.0.2: {} - /date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} + date-fns@2.30.0: dependencies: '@babel/runtime': 7.23.2 - dev: true - /dateformat@3.0.3: - resolution: {integrity: sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==} - dev: true + dateformat@3.0.3: {} - /db-errors@0.2.3: - resolution: {integrity: sha512-OOgqgDuCavHXjYSJoV2yGhv6SeG8nk42aoCSoyXLZUH7VwFG27rxbavU1z+VrZbZjphw5UkDQwUlD21MwZpUng==} - dev: false + db-errors@0.2.3: {} - /debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + debug@2.6.9: dependencies: ms: 2.0.0 - /debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + debug@3.2.7: dependencies: ms: 2.1.3 - dev: true - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + debug@4.3.4: dependencies: ms: 2.1.2 - /decamelize-keys@1.1.1: - resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} - engines: {node: '>=0.10.0'} + decamelize-keys@1.1.1: dependencies: decamelize: 1.2.0 map-obj: 1.0.1 - dev: true - /decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} + decamelize@1.2.0: {} - /decode-uri-component@0.2.2: - resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} - engines: {node: '>=0.10'} - dev: true + decode-uri-component@0.2.2: {} - /decompress-response@3.3.0: - resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} - engines: {node: '>=4'} + decompress-response@3.3.0: dependencies: mimic-response: 1.0.1 - dev: true - /decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 - dev: true - /decompress-tar@4.1.1: - resolution: {integrity: sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==} - engines: {node: '>=4'} + decompress-tar@4.1.1: dependencies: file-type: 5.2.0 is-stream: 1.1.0 tar-stream: 1.6.2 - dev: true - /decompress-tarbz2@4.1.1: - resolution: {integrity: sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==} - engines: {node: '>=4'} + decompress-tarbz2@4.1.1: dependencies: decompress-tar: 4.1.1 file-type: 6.2.0 is-stream: 1.1.0 seek-bzip: 1.0.6 unbzip2-stream: 1.4.3 - dev: true - /decompress-targz@4.1.1: - resolution: {integrity: sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==} - engines: {node: '>=4'} + decompress-targz@4.1.1: dependencies: decompress-tar: 4.1.1 file-type: 5.2.0 is-stream: 1.1.0 - dev: true - /decompress-unzip@4.0.1: - resolution: {integrity: sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==} - engines: {node: '>=4'} + decompress-unzip@4.0.1: dependencies: file-type: 3.9.0 get-stream: 2.3.1 pify: 2.3.0 yauzl: 2.10.0 - dev: true - /decompress@4.2.1: - resolution: {integrity: sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==} - engines: {node: '>=4'} + decompress@4.2.1: dependencies: decompress-tar: 4.1.1 decompress-tarbz2: 4.1.1 @@ -6216,169 +11490,94 @@ packages: make-dir: 1.3.0 pify: 2.3.0 strip-dirs: 2.1.0 - dev: true - /deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} - engines: {node: '>=6'} + deep-eql@4.1.3: dependencies: type-detect: 4.0.8 - dev: true - /deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - dev: true + deep-extend@0.6.0: {} - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true + deep-is@0.1.4: {} - /deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - dev: false + deepmerge@4.3.1: {} - /default-browser-id@3.0.0: - resolution: {integrity: sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==} - engines: {node: '>=12'} + default-browser-id@3.0.0: dependencies: bplist-parser: 0.2.0 untildify: 4.0.0 - dev: true - /default-browser@4.0.0: - resolution: {integrity: sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==} - engines: {node: '>=14.16'} + default-browser@4.0.0: dependencies: bundle-name: 3.0.0 default-browser-id: 3.0.0 execa: 7.2.0 titleize: 3.0.0 - dev: true - /defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + defaults@1.0.4: dependencies: clone: 1.0.4 - dev: true - /defer-to-connect@1.1.3: - resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} - dev: true + defer-to-connect@1.1.3: {} - /defer-to-connect@2.0.1: - resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} - engines: {node: '>=10'} - dev: true + defer-to-connect@2.0.1: {} - /define-data-property@1.1.1: - resolution: {integrity: sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==} - engines: {node: '>= 0.4'} + define-data-property@1.1.1: dependencies: get-intrinsic: 1.2.2 gopd: 1.0.1 has-property-descriptors: 1.0.1 - /define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} + define-lazy-prop@2.0.0: {} - /define-lazy-prop@3.0.0: - resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} - engines: {node: '>=12'} - dev: true + define-lazy-prop@3.0.0: {} - /define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - requiresBuild: true + define-properties@1.2.1: dependencies: define-data-property: 1.1.1 has-property-descriptors: 1.0.1 object-keys: 1.1.1 - dev: true optional: true - /delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} + delayed-stream@1.0.0: {} - /delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - dev: true + delegates@1.0.0: {} - /denque@2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - dev: false + denque@2.1.0: {} - /depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} + depd@2.0.0: {} - /destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + destroy@1.2.0: {} - /detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - dev: true + detect-indent@6.1.0: {} - /detect-libc@2.0.2: - resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} - engines: {node: '>=8'} - dev: true + detect-libc@2.0.2: {} - /detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - dev: true + detect-newline@3.1.0: {} - /detect-node@2.1.0: - resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - requiresBuild: true - dev: true + detect-node@2.1.0: optional: true - /dezalgo@1.0.4: - resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + dezalgo@1.0.4: dependencies: asap: 2.0.6 wrappy: 1.0.2 - dev: false - /diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - dev: true + diff-sequences@29.6.3: {} - /diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - dev: true + diff@4.0.2: {} - /dijkstrajs@1.0.3: - resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} - dev: false + dijkstrajs@1.0.3: {} - /dir-compare@3.3.0: - resolution: {integrity: sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg==} + dir-compare@3.3.0: dependencies: buffer-equal: 1.0.1 minimatch: 3.1.2 - dev: true - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} + dir-glob@3.0.1: dependencies: path-type: 4.0.0 - dev: true - /dmg-builder@24.4.0: - resolution: {integrity: sha512-p5z9Cx539GSBYb+b09Z+hMhuBTh/BrI71VRg4rgF6f2xtIRK/YlTGVS/O08k5OojoyhZcpS7JXxDVSmQoWgiiQ==} + dmg-builder@24.4.0: dependencies: app-builder-lib: 24.4.0 builder-util: 24.4.0 @@ -6391,14 +11590,8 @@ packages: transitivePeerDependencies: - bluebird - supports-color - dev: true - /dmg-license@1.0.11: - resolution: {integrity: sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==} - engines: {node: '>=8'} - os: [darwin] - hasBin: true - requiresBuild: true + dmg-license@1.0.11: dependencies: '@types/plist': 3.0.5 '@types/verror': 1.10.9 @@ -6408,118 +11601,72 @@ packages: plist: 3.1.0 smart-buffer: 4.2.0 verror: 1.10.1 - dev: true optional: true - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} + doctrine@3.0.0: dependencies: esutils: 2.0.3 - dev: true - /dom-serializer@1.4.1: - resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dom-serializer@1.4.1: dependencies: domelementtype: 2.3.0 domhandler: 4.3.1 entities: 2.2.0 - dev: false - /dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dom-serializer@2.0.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 entities: 4.5.0 - dev: true - /dom-walk@0.1.2: - resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} - dev: true + dom-walk@0.1.2: {} - /domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + domelementtype@2.3.0: {} - /domhandler@4.3.1: - resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} - engines: {node: '>= 4'} + domhandler@4.3.1: dependencies: domelementtype: 2.3.0 - dev: false - /domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} + domhandler@5.0.3: dependencies: domelementtype: 2.3.0 - dev: true - /domino@2.1.6: - resolution: {integrity: sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==} - dev: false + domino@2.1.6: {} - /domutils@2.8.0: - resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + domutils@2.8.0: dependencies: dom-serializer: 1.4.1 domelementtype: 2.3.0 domhandler: 4.3.1 - dev: false - /domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + domutils@3.1.0: dependencies: dom-serializer: 2.0.0 domelementtype: 2.3.0 domhandler: 5.0.3 - dev: true - /dot-prop@5.3.0: - resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} - engines: {node: '>=8'} + dot-prop@5.3.0: dependencies: is-obj: 2.0.0 - dev: true - /dot-prop@6.0.1: - resolution: {integrity: sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==} - engines: {node: '>=10'} + dot-prop@6.0.1: dependencies: is-obj: 2.0.0 - dev: true - /dotenv-expand@5.1.0: - resolution: {integrity: sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==} - dev: true + dotenv-expand@5.1.0: {} - /dotenv-expand@9.0.0(patch_hash=adqtadvolhkco6cfvcsvmd42ua): - resolution: {integrity: sha512-uW8Hrhp5ammm9x7kBLR6jDfujgaDarNA02tprvZdyrJ7MpdzD1KyrIHG4l+YoC2fJ2UcdFdNWNWIjt+sexBHJw==} - engines: {node: '>=12'} - dev: false - patched: true + dotenv-expand@9.0.0(patch_hash=adqtadvolhkco6cfvcsvmd42ua): {} - /dotenv@16.3.1: - resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} - engines: {node: '>=12'} - dev: false + dotenv@16.3.1: {} - /dotenv@9.0.2: - resolution: {integrity: sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg==} - engines: {node: '>=10'} - dev: true + dotenv@9.0.2: {} - /dotgitignore@2.1.0: - resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} - engines: {node: '>=6'} + dotgitignore@2.1.0: dependencies: find-up: 3.0.0 minimatch: 3.1.2 - dev: true - /download@6.2.5: - resolution: {integrity: sha512-DpO9K1sXAST8Cpzb7kmEhogJxymyVUd5qz/vCOSyvwtp2Klj2XcDt5YUuasgxka44SxF0q5RriKIwJmQHG2AuA==} - engines: {node: '>=4'} + download@6.2.5: dependencies: caw: 2.0.1 content-disposition: 0.5.4 @@ -6532,11 +11679,8 @@ packages: make-dir: 1.3.0 p-event: 1.3.0 pify: 3.0.0 - dev: true - /download@7.1.0: - resolution: {integrity: sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==} - engines: {node: '>=6'} + download@7.1.0: dependencies: archive-type: 4.0.0 caw: 2.0.1 @@ -6550,46 +11694,28 @@ packages: make-dir: 1.3.0 p-event: 2.3.1 pify: 3.0.0 - dev: true - /downloadjs@1.4.7: - resolution: {integrity: sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==} - dev: false + downloadjs@1.4.7: {} - /duplexer3@0.1.5: - resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} - dev: true + duplexer3@0.1.5: {} - /dynamic-dedupe@0.3.0: - resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + dynamic-dedupe@0.3.0: dependencies: xtend: 4.0.2 - dev: true - /eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - dev: true + eastasianwidth@0.2.0: {} - /ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ecdsa-sig-formatter@1.0.11: dependencies: safe-buffer: 5.2.1 - /ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + ee-first@1.1.1: {} - /ejs@3.1.9: - resolution: {integrity: sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==} - engines: {node: '>=0.10.0'} - hasBin: true + ejs@3.1.9: dependencies: jake: 10.8.7 - dev: true - /electron-builder@24.4.0: - resolution: {integrity: sha512-D5INxodxaUIJgEX6p/fqBd8wQNS8XRAToNIJ9SQC+taNS5D73ZsjLuXiRraFGCB0cVk9KeKhEkdEOH5AaVya4g==} - engines: {node: '>=14.0.0'} - hasBin: true + electron-builder@24.4.0: dependencies: app-builder-lib: 24.4.0 builder-util: 24.4.0 @@ -6605,37 +11731,24 @@ packages: transitivePeerDependencies: - bluebird - supports-color - dev: true - /electron-context-menu@3.6.1: - resolution: {integrity: sha512-lcpO6tzzKUROeirhzBjdBWNqayEThmdW+2I2s6H6QMrwqTVyT3EK47jW3Nxm60KTxl5/bWfEoIruoUNn57/QkQ==} + electron-context-menu@3.6.1: dependencies: cli-truncate: 2.1.0 electron-dl: 3.5.1 electron-is-dev: 2.0.0 - dev: false - /electron-dl@3.5.1: - resolution: {integrity: sha512-5Yb9s/iPVJ5mW5x3j6XkKxt7WEqREr/AhYxZmtEfW1ffQHs1+aGoiQ2fXCAU6UIXMnWog2MXK82vrxJsjA3nbQ==} - engines: {node: '>=12'} + electron-dl@3.5.1: dependencies: ext-name: 5.0.0 pupa: 2.1.1 unused-filename: 2.1.0 - dev: false - /electron-is-dev@2.0.0: - resolution: {integrity: sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA==} - dev: false + electron-is-dev@2.0.0: {} - /electron-log@4.4.8: - resolution: {integrity: sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA==} - dev: false + electron-log@4.4.8: {} - /electron-packager@17.1.1: - resolution: {integrity: sha512-r1NDtlajsq7gf2EXgjRfblCVPquvD2yeg+6XGErOKblvxOpDi0iulZLVhgYDP4AEF1P5/HgbX/vwjlkEv7PEIQ==} - engines: {node: '>= 14.17.5'} - hasBin: true + electron-packager@17.1.1: dependencies: '@electron/asar': 3.2.7 '@electron/get': 2.0.3 @@ -6658,10 +11771,8 @@ packages: yargs-parser: 21.1.1 transitivePeerDependencies: - supports-color - dev: true - /electron-publish@24.4.0: - resolution: {integrity: sha512-U3mnVSxIfNrLW7ZnwiedFhcLf6ExPFXgAsx89WpfQFsV4gFAt/LG+H74p0m9NSvsLXiZuF82yXoxi7Ou8GHq4Q==} + electron-publish@24.4.0: dependencies: '@types/fs-extra': 9.0.13 builder-util: 24.4.0 @@ -6672,13 +11783,10 @@ packages: mime: 2.6.0 transitivePeerDependencies: - supports-color - dev: true - /electron-to-chromium@1.4.576: - resolution: {integrity: sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==} + electron-to-chromium@1.4.576: {} - /electron-updater@4.3.1: - resolution: {integrity: sha512-UDC5AHCgeiHJYDYWZG/rsl1vdAFKqI/Lm7whN57LKAk8EfhTewhcEHzheRcncLgikMcQL8gFo1KeX51tf5a5Wg==} + electron-updater@4.3.1: dependencies: '@types/semver': 7.5.4 builder-util-runtime: 8.7.0 @@ -6689,75 +11797,47 @@ packages: semver: 7.5.4 transitivePeerDependencies: - supports-color - dev: false - /electron@21.4.4: - resolution: {integrity: sha512-N5O7y7Gtt7mDgkJLkW49ETiT8M3myZ9tNIEvGTKhpBduX4WdgMj6c3hYeYBD6XW7SvbRkWEQaTl25RNday8Xpw==} - engines: {node: '>= 10.17.0'} - hasBin: true - requiresBuild: true + electron@21.4.4: dependencies: '@electron/get': 1.14.1 '@types/node': 16.18.60 extract-zip: 2.0.1 transitivePeerDependencies: - supports-color - dev: true - /electron@25.2.0: - resolution: {integrity: sha512-I/rhcW2sV2fyiveVSBr2N7v5ZiCtdGY0UiNCDZgk2fpSC+irQjbeh7JT2b4vWmJ2ogOXBjqesrN9XszTIG6DHg==} - engines: {node: '>= 12.20.55'} - hasBin: true - requiresBuild: true + electron@25.2.0: dependencies: '@electron/get': 2.0.3 '@types/node': 18.18.8 extract-zip: 2.0.1 transitivePeerDependencies: - supports-color - dev: true - /elementtree@0.1.7: - resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==} - engines: {node: '>= 0.4.0'} + elementtree@0.1.7: dependencies: sax: 1.1.4 - /emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@8.0.0: {} - /emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - dev: true + emoji-regex@9.2.2: {} - /enabled@2.0.0: - resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} - dev: false + enabled@2.0.0: {} - /encode-utf8@1.0.3: - resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} - dev: false + encode-utf8@1.0.3: {} - /encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} + encodeurl@1.0.2: {} - /encoding@0.1.13: - resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} - requiresBuild: true + encoding@0.1.13: dependencies: iconv-lite: 0.6.3 - dev: true optional: true - /end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + end-of-stream@1.4.4: dependencies: once: 1.4.0 - dev: true - /engine.io-client@6.5.2(utf-8-validate@5.0.10): - resolution: {integrity: sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg==} + engine.io-client@6.5.2(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.0 debug: 4.3.4 @@ -6768,16 +11848,10 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: true - /engine.io-parser@5.2.1: - resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==} - engines: {node: '>=10.0.0'} - dev: true + engine.io-parser@5.2.1: {} - /engine.io@6.5.3(utf-8-validate@5.0.10): - resolution: {integrity: sha512-IML/R4eG/pUS5w7OfcDE0jKrljWS9nwnEfsxWCIJF5eO6AHo6+Hlv+lQbdlAYsiJPHzUthLm1RUjnBzWOs45cw==} - engines: {node: '>=10.2.0'} + engine.io@6.5.3(utf-8-validate@5.0.10): dependencies: '@types/cookie': 0.4.1 '@types/cors': 2.8.15 @@ -6793,415 +11867,150 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: true - /enquirer@2.4.1: - resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} - engines: {node: '>=8.6'} + enquirer@2.4.1: dependencies: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - dev: true - /entities@2.2.0: - resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} - dev: false + entities@2.2.0: {} - /entities@3.0.1: - resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} - engines: {node: '>=0.12'} - dev: false + entities@3.0.1: {} - /entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - dev: true + entities@4.5.0: {} - /env-paths@2.2.1: - resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} - engines: {node: '>=6'} + env-paths@2.2.1: {} - /err-code@2.0.3: - resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - dev: true + err-code@2.0.3: {} - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 - dev: true - - /es6-error@4.1.1: - resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} - requiresBuild: true - dev: true - optional: true - /esbuild-android-64@0.14.51: - resolution: {integrity: sha512-6FOuKTHnC86dtrKDmdSj2CkcKF8PnqkaIXqvgydqfJmqBazCPdw+relrMlhGjkvVdiiGV70rpdnyFmA65ekBCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true + es6-error@4.1.1: optional: true - /esbuild-android-64@0.14.54: - resolution: {integrity: sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true + esbuild-android-64@0.14.51: optional: true - /esbuild-android-arm64@0.14.51: - resolution: {integrity: sha512-vBtp//5VVkZWmYYvHsqBRCMMi1MzKuMIn5XDScmnykMTu9+TD9v0NMEDqQxvtFToeYmojdo5UCV2vzMQWJcJ4A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true + esbuild-android-64@0.14.54: optional: true - /esbuild-android-arm64@0.14.54: - resolution: {integrity: sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true + esbuild-android-arm64@0.14.51: optional: true - /esbuild-darwin-64@0.14.51: - resolution: {integrity: sha512-YFmXPIOvuagDcwCejMRtCDjgPfnDu+bNeh5FU2Ryi68ADDVlWEpbtpAbrtf/lvFTWPexbgyKgzppNgsmLPr8PA==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true + esbuild-android-arm64@0.14.54: optional: true - /esbuild-darwin-64@0.14.54: - resolution: {integrity: sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true + esbuild-darwin-64@0.14.51: optional: true - /esbuild-darwin-arm64@0.14.51: - resolution: {integrity: sha512-juYD0QnSKwAMfzwKdIF6YbueXzS6N7y4GXPDeDkApz/1RzlT42mvX9jgNmyOlWKN7YzQAYbcUEJmZJYQGdf2ow==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true + esbuild-darwin-64@0.14.54: optional: true - /esbuild-darwin-arm64@0.14.54: - resolution: {integrity: sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true + esbuild-darwin-arm64@0.14.51: optional: true - /esbuild-freebsd-64@0.14.51: - resolution: {integrity: sha512-cLEI/aXjb6vo5O2Y8rvVSQ7smgLldwYY5xMxqh/dQGfWO+R1NJOFsiax3IS4Ng300SVp7Gz3czxT6d6qf2cw0g==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true + esbuild-darwin-arm64@0.14.54: optional: true - /esbuild-freebsd-64@0.14.54: - resolution: {integrity: sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true + esbuild-freebsd-64@0.14.51: optional: true - /esbuild-freebsd-arm64@0.14.51: - resolution: {integrity: sha512-TcWVw/rCL2F+jUgRkgLa3qltd5gzKjIMGhkVybkjk6PJadYInPtgtUBp1/hG+mxyigaT7ib+od1Xb84b+L+1Mg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true + esbuild-freebsd-64@0.14.54: optional: true - /esbuild-freebsd-arm64@0.14.54: - resolution: {integrity: sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true + esbuild-freebsd-arm64@0.14.51: optional: true - /esbuild-linux-32@0.14.51: - resolution: {integrity: sha512-RFqpyC5ChyWrjx8Xj2K0EC1aN0A37H6OJfmUXIASEqJoHcntuV3j2Efr9RNmUhMfNE6yEj2VpYuDteZLGDMr0w==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true + esbuild-freebsd-arm64@0.14.54: optional: true - /esbuild-linux-32@0.14.54: - resolution: {integrity: sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-32@0.14.51: optional: true - /esbuild-linux-64@0.14.51: - resolution: {integrity: sha512-dxjhrqo5i7Rq6DXwz5v+MEHVs9VNFItJmHBe1CxROWNf4miOGoQhqSG8StStbDkQ1Mtobg6ng+4fwByOhoQoeA==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-32@0.14.54: optional: true - /esbuild-linux-64@0.14.54: - resolution: {integrity: sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-64@0.14.51: optional: true - /esbuild-linux-arm64@0.14.51: - resolution: {integrity: sha512-D9rFxGutoqQX3xJPxqd6o+kvYKeIbM0ifW2y0bgKk5HPgQQOo2k9/2Vpto3ybGYaFPCE5qTGtqQta9PoP6ZEzw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-64@0.14.54: optional: true - /esbuild-linux-arm64@0.14.54: - resolution: {integrity: sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-arm64@0.14.51: optional: true - /esbuild-linux-arm@0.14.51: - resolution: {integrity: sha512-LsJynDxYF6Neg7ZC7748yweCDD+N8ByCv22/7IAZglIEniEkqdF4HCaa49JNDLw1UQGlYuhOB8ZT/MmcSWzcWg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-arm64@0.14.54: optional: true - /esbuild-linux-arm@0.14.54: - resolution: {integrity: sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-arm@0.14.51: optional: true - /esbuild-linux-mips64le@0.14.51: - resolution: {integrity: sha512-vS54wQjy4IinLSlb5EIlLoln8buh1yDgliP4CuEHumrPk4PvvP4kTRIG4SzMXm6t19N0rIfT4bNdAxzJLg2k6A==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-arm@0.14.54: optional: true - /esbuild-linux-mips64le@0.14.54: - resolution: {integrity: sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-mips64le@0.14.51: optional: true - /esbuild-linux-ppc64le@0.14.51: - resolution: {integrity: sha512-xcdd62Y3VfGoyphNP/aIV9LP+RzFw5M5Z7ja+zdpQHHvokJM7d0rlDRMN+iSSwvUymQkqZO+G/xjb4/75du8BQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-mips64le@0.14.54: optional: true - /esbuild-linux-ppc64le@0.14.54: - resolution: {integrity: sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-ppc64le@0.14.51: optional: true - /esbuild-linux-riscv64@0.14.51: - resolution: {integrity: sha512-syXHGak9wkAnFz0gMmRBoy44JV0rp4kVCEA36P5MCeZcxFq8+fllBC2t6sKI23w3qd8Vwo9pTADCgjTSf3L3rA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-ppc64le@0.14.54: optional: true - /esbuild-linux-riscv64@0.14.54: - resolution: {integrity: sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-riscv64@0.14.51: optional: true - /esbuild-linux-s390x@0.14.51: - resolution: {integrity: sha512-kFAJY3dv+Wq8o28K/C7xkZk/X34rgTwhknSsElIqoEo8armCOjMJ6NsMxm48KaWY2h2RUYGtQmr+RGuUPKBhyw==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-riscv64@0.14.54: optional: true - /esbuild-linux-s390x@0.14.54: - resolution: {integrity: sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true + esbuild-linux-s390x@0.14.51: optional: true - /esbuild-netbsd-64@0.14.51: - resolution: {integrity: sha512-ZZBI7qrR1FevdPBVHz/1GSk1x5GDL/iy42Zy8+neEm/HA7ma+hH/bwPEjeHXKWUDvM36CZpSL/fn1/y9/Hb+1A==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true + esbuild-linux-s390x@0.14.54: optional: true - /esbuild-netbsd-64@0.14.54: - resolution: {integrity: sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true + esbuild-netbsd-64@0.14.51: optional: true - /esbuild-openbsd-64@0.14.51: - resolution: {integrity: sha512-7R1/p39M+LSVQVgDVlcY1KKm6kFKjERSX1lipMG51NPcspJD1tmiZSmmBXoY5jhHIu6JL1QkFDTx94gMYK6vfA==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true + esbuild-netbsd-64@0.14.54: optional: true - /esbuild-openbsd-64@0.14.54: - resolution: {integrity: sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true + esbuild-openbsd-64@0.14.51: optional: true - /esbuild-sunos-64@0.14.51: - resolution: {integrity: sha512-HoHaCswHxLEYN8eBTtyO0bFEWvA3Kdb++hSQ/lLG7TyKF69TeSG0RNoBRAs45x/oCeWaTDntEZlYwAfQlhEtJA==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true + esbuild-openbsd-64@0.14.54: optional: true - /esbuild-sunos-64@0.14.54: - resolution: {integrity: sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true + esbuild-sunos-64@0.14.51: optional: true - /esbuild-windows-32@0.14.51: - resolution: {integrity: sha512-4rtwSAM35A07CBt1/X8RWieDj3ZUHQqUOaEo5ZBs69rt5WAFjP4aqCIobdqOy4FdhYw1yF8Z0xFBTyc9lgPtEg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true + esbuild-sunos-64@0.14.54: optional: true - /esbuild-windows-32@0.14.54: - resolution: {integrity: sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true + esbuild-windows-32@0.14.51: optional: true - /esbuild-windows-64@0.14.51: - resolution: {integrity: sha512-HoN/5HGRXJpWODprGCgKbdMvrC3A2gqvzewu2eECRw2sYxOUoh2TV1tS+G7bHNapPGI79woQJGV6pFH7GH7qnA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true + esbuild-windows-32@0.14.54: optional: true - /esbuild-windows-64@0.14.54: - resolution: {integrity: sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true + esbuild-windows-64@0.14.51: optional: true - /esbuild-windows-arm64@0.14.51: - resolution: {integrity: sha512-JQDqPjuOH7o+BsKMSddMfmVJXrnYZxXDHsoLHc0xgmAZkOOCflRmC43q31pk79F9xuyWY45jDBPolb5ZgGOf9g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true + esbuild-windows-64@0.14.54: optional: true - /esbuild-windows-arm64@0.14.54: - resolution: {integrity: sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true + esbuild-windows-arm64@0.14.51: optional: true - /esbuild@0.14.51: - resolution: {integrity: sha512-+CvnDitD7Q5sT7F+FM65sWkF8wJRf+j9fPcprxYV4j+ohmzVj2W7caUqH2s5kCaCJAfcAICjSlKhDCcvDpU7nw==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true + esbuild-windows-arm64@0.14.54: + optional: true + + esbuild@0.14.51: optionalDependencies: esbuild-android-64: 0.14.51 esbuild-android-arm64: 0.14.51 @@ -7223,13 +12032,8 @@ packages: esbuild-windows-32: 0.14.51 esbuild-windows-64: 0.14.51 esbuild-windows-arm64: 0.14.51 - dev: true - /esbuild@0.14.54: - resolution: {integrity: sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true + esbuild@0.14.54: optionalDependencies: '@esbuild/linux-loong64': 0.14.54 esbuild-android-64: 0.14.54 @@ -7252,13 +12056,8 @@ packages: esbuild-windows-32: 0.14.54 esbuild-windows-64: 0.14.54 esbuild-windows-arm64: 0.14.54 - dev: true - /esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true + esbuild@0.18.20: optionalDependencies: '@esbuild/android-arm': 0.18.20 '@esbuild/android-arm64': 0.18.20 @@ -7282,97 +12081,44 @@ packages: '@esbuild/win32-arm64': 0.18.20 '@esbuild/win32-ia32': 0.18.20 '@esbuild/win32-x64': 0.18.20 - dev: true - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} + escalade@3.1.1: {} - /escape-goat@2.1.1: - resolution: {integrity: sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==} - engines: {node: '>=8'} - dev: false + escape-goat@2.1.1: {} - /escape-goat@4.0.0: - resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} - engines: {node: '>=12'} - dev: true + escape-goat@4.0.0: {} - /escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-html@1.0.3: {} - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - dev: true + escape-string-regexp@1.0.5: {} - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} + escape-string-regexp@4.0.0: {} - /escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - dev: true + escape-string-regexp@5.0.0: {} - /eslint-config-prettier@9.0.0(eslint@8.53.0): - resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' + eslint-config-prettier@9.0.0(eslint@8.53.0): dependencies: eslint: 8.53.0 - dev: true - /eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.0.3): - resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - '@types/eslint': '>=8.0.0' - eslint: '>=8.0.0' - eslint-config-prettier: '*' - prettier: '>=3.0.0' - peerDependenciesMeta: - '@types/eslint': - optional: true - eslint-config-prettier: - optional: true + eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.0.3): dependencies: eslint: 8.53.0 eslint-config-prettier: 9.0.0(eslint@8.53.0) prettier: 3.0.3 prettier-linter-helpers: 1.0.0 synckit: 0.8.5 - dev: true - /eslint-plugin-simple-import-sort@10.0.0(eslint@8.53.0): - resolution: {integrity: sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==} - peerDependencies: - eslint: '>=5.0.0' + eslint-plugin-simple-import-sort@10.0.0(eslint@8.53.0): dependencies: eslint: 8.53.0 - dev: true - /eslint-plugin-unused-imports@3.0.0(@typescript-eslint/eslint-plugin@6.10.0)(eslint@8.53.0): - resolution: {integrity: sha512-sduiswLJfZHeeBJ+MQaG+xYzSWdRXoSw61DpU13mzWumCkR0ufD0HmO4kdNokjrkluMHpj/7PJeN35pgbhW3kw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - '@typescript-eslint/eslint-plugin': ^6.0.0 - eslint: ^8.0.0 - peerDependenciesMeta: - '@typescript-eslint/eslint-plugin': - optional: true + eslint-plugin-unused-imports@3.0.0(@typescript-eslint/eslint-plugin@6.10.0)(eslint@8.53.0): dependencies: '@typescript-eslint/eslint-plugin': 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2) eslint: 8.53.0 eslint-rule-composer: 0.3.0 - dev: true - /eslint-plugin-vue@9.18.1(eslint@8.53.0): - resolution: {integrity: sha512-7hZFlrEgg9NIzuVik2I9xSnJA5RsmOfueYgsUGUokEDLJ1LHtxO0Pl4duje1BriZ/jDWb+44tcIlC3yi0tdlZg==} - engines: {node: ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.2.0 || ^7.0.0 || ^8.0.0 + eslint-plugin-vue@9.18.1(eslint@8.53.0): dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) eslint: 8.53.0 @@ -7384,42 +12130,23 @@ packages: xml-name-validator: 4.0.0 transitivePeerDependencies: - supports-color - dev: true - /eslint-rule-composer@0.3.0: - resolution: {integrity: sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==} - engines: {node: '>=4.0.0'} - dev: true + eslint-rule-composer@0.3.0: {} - /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@7.2.2: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 - dev: true - /eslint-utils@2.1.0: - resolution: {integrity: sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==} - engines: {node: '>=6'} + eslint-utils@2.1.0: dependencies: eslint-visitor-keys: 1.3.0 - dev: true - /eslint-visitor-keys@1.3.0: - resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} - engines: {node: '>=4'} - dev: true + eslint-visitor-keys@1.3.0: {} - /eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true + eslint-visitor-keys@3.4.3: {} - /eslint@8.53.0: - resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true + eslint@8.53.0: dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) '@eslint-community/regexpp': 4.10.0 @@ -7461,81 +12188,44 @@ packages: text-table: 0.2.0 transitivePeerDependencies: - supports-color - dev: true - /esm@3.2.25: - resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} - engines: {node: '>=6'} - dev: false + esm@3.2.25: {} - /espree@6.2.1: - resolution: {integrity: sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==} - engines: {node: '>=6.0.0'} + espree@6.2.1: dependencies: acorn: 7.4.1 acorn-jsx: 5.3.2(acorn@7.4.1) eslint-visitor-keys: 1.3.0 - dev: true - /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + espree@9.6.1: dependencies: acorn: 8.11.2 acorn-jsx: 5.3.2(acorn@8.11.2) eslint-visitor-keys: 3.4.3 - dev: true - /esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: false + esprima@4.0.1: {} - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} + esquery@1.5.0: dependencies: estraverse: 5.3.0 - dev: true - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 - dev: true - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true + estraverse@5.3.0: {} - /estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@2.0.2: {} - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true + esutils@2.0.3: {} - /etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} + etag@1.8.1: {} - /event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - dev: false + event-target-shim@5.0.1: {} - /events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - dev: false + events@3.3.0: {} - /execa@0.7.0: - resolution: {integrity: sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==} - engines: {node: '>=4'} + execa@0.7.0: dependencies: cross-spawn: 5.1.0 get-stream: 3.0.0 @@ -7544,11 +12234,8 @@ packages: p-finally: 1.0.0 signal-exit: 3.0.7 strip-eof: 1.0.0 - dev: true - /execa@1.0.0: - resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} - engines: {node: '>=6'} + execa@1.0.0: dependencies: cross-spawn: 6.0.5 get-stream: 4.1.0 @@ -7557,11 +12244,8 @@ packages: p-finally: 1.0.0 signal-exit: 3.0.7 strip-eof: 1.0.0 - dev: true - /execa@4.1.0: - resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} - engines: {node: '>=10'} + execa@4.1.0: dependencies: cross-spawn: 7.0.3 get-stream: 5.2.0 @@ -7572,11 +12256,8 @@ packages: onetime: 5.1.2 signal-exit: 3.0.7 strip-final-newline: 2.0.0 - dev: true - /execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} + execa@5.1.1: dependencies: cross-spawn: 7.0.3 get-stream: 6.0.1 @@ -7587,11 +12268,8 @@ packages: onetime: 5.1.2 signal-exit: 3.0.7 strip-final-newline: 2.0.0 - dev: true - /execa@7.2.0: - resolution: {integrity: sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==} - engines: {node: ^14.18.0 || ^16.14.0 || >=18.0.0} + execa@7.2.0: dependencies: cross-spawn: 7.0.3 get-stream: 6.0.1 @@ -7602,31 +12280,18 @@ packages: onetime: 6.0.0 signal-exit: 3.0.7 strip-final-newline: 3.0.0 - dev: true - /executable@4.1.1: - resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} - engines: {node: '>=4'} + executable@4.1.1: dependencies: pify: 2.3.0 - dev: true - /exif-parser@0.1.12: - resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} - dev: true + exif-parser@0.1.12: {} - /expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - dev: true + expand-template@2.0.3: {} - /exponential-backoff@3.1.1: - resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} - dev: true + exponential-backoff@3.1.1: {} - /express@4.18.2: - resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} - engines: {node: '>= 0.10.0'} + express@4.18.2: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 @@ -7662,32 +12327,22 @@ packages: transitivePeerDependencies: - supports-color - /ext-list@2.2.2: - resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} - engines: {node: '>=0.10.0'} + ext-list@2.2.2: dependencies: mime-db: 1.52.0 - /ext-name@5.0.0: - resolution: {integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==} - engines: {node: '>=4'} + ext-name@5.0.0: dependencies: ext-list: 2.2.2 sort-keys-length: 1.0.1 - /external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} + external-editor@3.1.0: dependencies: chardet: 0.7.0 iconv-lite: 0.4.24 tmp: 0.0.33 - dev: true - /extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true + extract-zip@2.0.1: dependencies: debug: 4.3.4 get-stream: 5.2.0 @@ -7696,70 +12351,43 @@ packages: '@types/yauzl': 2.10.3 transitivePeerDependencies: - supports-color - dev: true - /extsprintf@1.4.1: - resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} - engines: {'0': node >=0.6.0} - requiresBuild: true - dev: true + extsprintf@1.4.1: optional: true - /fast-check@3.13.2: - resolution: {integrity: sha512-ouTiFyeMoqmNg253xqy4NSacr5sHxH6pZpLOaHgaAdgZxFWdtsfxExwolpveoAE9CJdV+WYjqErNGef6SqA5Mg==} - engines: {node: '>=8.0.0'} + fast-check@3.13.2: dependencies: pure-rand: 6.0.4 - dev: true - /fast-content-type-parse@1.1.0: - resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} - dev: false + fast-content-type-parse@1.1.0: {} - /fast-decode-uri-component@1.0.1: - resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} - dev: false + fast-decode-uri-component@1.0.1: {} - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-deep-equal@3.1.3: {} - /fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - dev: true + fast-diff@1.3.0: {} - /fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - dev: true + fast-fifo@1.3.2: {} - /fast-glob@3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} - engines: {node: '>=8.6.0'} + fast-glob@3.2.12: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.5 - dev: true - /fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} + fast-glob@3.3.2: dependencies: '@nodelib/fs.stat': 2.0.5 '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.5 - dev: true - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - requiresBuild: true - dev: true + fast-json-stable-stringify@2.1.0: {} - /fast-json-stringify@5.9.1: - resolution: {integrity: sha512-NMrf+uU9UJnTzfxaumMDXK1NWqtPCfGoM9DYIE+ESlaTQqjlANFBy0VAbsm6FB88Mx0nceyi18zTo5kIEUlzxg==} + fast-json-stringify@5.9.1: dependencies: '@fastify/deepmerge': 1.3.0 ajv: 8.12.0 @@ -7768,60 +12396,37 @@ packages: fast-uri: 2.3.0 json-schema-ref-resolver: 1.0.1 rfdc: 1.3.0 - dev: false - /fast-jwt@3.3.1: - resolution: {integrity: sha512-1YuuIJeh1hEvfcYDe89P2oGACWI5hd2GadRDKHalSxkc1Z0z8I6yzuVK6SF15sW09QZngTV6d7g4+TFL9bvs5A==} - engines: {node: '>=16 <22'} + fast-jwt@3.3.1: dependencies: '@lukeed/ms': 2.0.1 asn1.js: 5.4.1 ecdsa-sig-formatter: 1.0.11 mnemonist: 0.39.5 - dev: false - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true + fast-levenshtein@2.0.6: {} - /fast-querystring@1.1.2: - resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fast-querystring@1.1.2: dependencies: fast-decode-uri-component: 1.0.1 - dev: false - /fast-redact@3.3.0: - resolution: {integrity: sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==} - engines: {node: '>=6'} - dev: false + fast-redact@3.3.0: {} - /fast-safe-stringify@2.1.1: - resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-safe-stringify@2.1.1: {} - /fast-uri@2.3.0: - resolution: {integrity: sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==} - dev: false + fast-uri@2.3.0: {} - /fastest-levenshtein@1.0.16: - resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} - engines: {node: '>= 4.9.1'} - dev: false + fastest-levenshtein@1.0.16: {} - /fastify-plugin@4.5.1: - resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} - dev: false + fastify-plugin@4.5.1: {} - /fastify-raw-body@4.2.2: - resolution: {integrity: sha512-6l4fXtxNn7WOQiylu5fv9/JfUTvWCg1ED4gF44hqnVesgttOXEUMnNkdV8ZxwufCstRyUYaYSBIN4VuRHDbJkw==} - engines: {node: '>= 10'} + fastify-raw-body@4.2.2: dependencies: fastify-plugin: 4.5.1 raw-body: 2.5.2 secure-json-parse: 2.7.0 - dev: false - /fastify@4.24.3: - resolution: {integrity: sha512-6HHJ+R2x2LS3y1PqxnwEIjOTZxFl+8h4kSC/TuDPXtA+v2JnV9yEtOsNSKK1RMD7sIR2y1ZsA4BEFaid/cK5pg==} + fastify@4.24.3: dependencies: '@fastify/ajv-compiler': 3.5.0 '@fastify/error': 3.4.1 @@ -7841,124 +12446,72 @@ packages: toad-cache: 3.3.0 transitivePeerDependencies: - supports-color - dev: false - /fastq@1.15.0: - resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} + fastq@1.15.0: dependencies: reusify: 1.0.4 - /fault@2.0.1: - resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + fault@2.0.1: dependencies: format: 0.2.2 - dev: false - /fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fd-slicer@1.1.0: dependencies: pend: 1.2.0 - /fecha@4.2.3: - resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} - dev: false + fecha@4.2.3: {} - /figures@3.2.0: - resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} - engines: {node: '>=8'} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 - dev: true - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@6.0.1: dependencies: flat-cache: 3.1.1 - dev: true - /file-saver@2.0.5: - resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} - dev: false + file-saver@2.0.5: {} - /file-type@16.5.4: - resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} - engines: {node: '>=10'} + file-type@16.5.4: dependencies: readable-web-to-node-stream: 3.0.2 strtok3: 6.3.0 token-types: 4.2.1 - dev: true - /file-type@3.9.0: - resolution: {integrity: sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==} - engines: {node: '>=0.10.0'} - dev: true + file-type@3.9.0: {} - /file-type@4.4.0: - resolution: {integrity: sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==} - engines: {node: '>=4'} - dev: true + file-type@4.4.0: {} - /file-type@5.2.0: - resolution: {integrity: sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==} - engines: {node: '>=4'} - dev: true + file-type@5.2.0: {} - /file-type@6.2.0: - resolution: {integrity: sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==} - engines: {node: '>=4'} - dev: true + file-type@6.2.0: {} - /file-type@8.1.0: - resolution: {integrity: sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==} - engines: {node: '>=6'} - dev: true + file-type@8.1.0: {} - /file-type@9.0.0: - resolution: {integrity: sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw==} - engines: {node: '>=6'} - dev: true + file-type@9.0.0: {} - /filelist@1.0.4: - resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} + filelist@1.0.4: dependencies: minimatch: 5.1.6 - dev: true - /filename-reserved-regex@2.0.0: - resolution: {integrity: sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==} - engines: {node: '>=4'} - dev: true + filename-reserved-regex@2.0.0: {} - /filenamify@2.1.0: - resolution: {integrity: sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==} - engines: {node: '>=4'} + filenamify@2.1.0: dependencies: filename-reserved-regex: 2.0.0 strip-outer: 1.0.1 trim-repeated: 1.0.0 - dev: true - /filenamify@4.3.0: - resolution: {integrity: sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==} - engines: {node: '>=8'} + filenamify@4.3.0: dependencies: filename-reserved-regex: 2.0.0 strip-outer: 1.0.1 trim-repeated: 1.0.0 - dev: true - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} + fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 - dev: true - /finalhandler@1.2.0: - resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} - engines: {node: '>= 0.8'} + finalhandler@1.2.0: dependencies: debug: 2.6.9 encodeurl: 1.0.2 @@ -7970,237 +12523,147 @@ packages: transitivePeerDependencies: - supports-color - /find-my-way@7.7.0: - resolution: {integrity: sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==} - engines: {node: '>=14'} + find-my-way@7.7.0: dependencies: fast-deep-equal: 3.1.3 fast-querystring: 1.1.2 safe-regex2: 2.0.0 - dev: false - /find-up@2.1.0: - resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} - engines: {node: '>=4'} + find-up@2.1.0: dependencies: locate-path: 2.0.0 - dev: true - /find-up@3.0.0: - resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} - engines: {node: '>=6'} + find-up@3.0.0: dependencies: locate-path: 3.0.0 - dev: true - /find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} + find-up@4.1.0: dependencies: locate-path: 5.0.0 path-exists: 4.0.0 - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} + find-up@5.0.0: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - dev: true - /find-versions@3.2.0: - resolution: {integrity: sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==} - engines: {node: '>=6'} + find-versions@3.2.0: dependencies: semver-regex: 2.0.0 - dev: true - /flat-cache@3.1.1: - resolution: {integrity: sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==} - engines: {node: '>=12.0.0'} + flat-cache@3.1.1: dependencies: flatted: 3.2.9 keyv: 4.5.4 rimraf: 3.0.2 - dev: true - /flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} - hasBin: true - dev: true + flat@5.0.2: {} - /flatted@3.2.9: - resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} - dev: true + flatted@3.2.9: {} - /flora-colossus@1.0.1: - resolution: {integrity: sha512-d+9na7t9FyH8gBJoNDSi28mE4NgQVGGvxQ4aHtFRetjyh5SXjuus+V5EZaxFmFdXVemSOrx0lsgEl/ZMjnOWJA==} - engines: {node: '>= 6.0.0'} + flora-colossus@1.0.1: dependencies: debug: 4.3.4 fs-extra: 7.0.1 transitivePeerDependencies: - supports-color - dev: true - /fn.name@1.1.0: - resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} - dev: false + fn.name@1.1.0: {} - /follow-redirects@1.15.3: - resolution: {integrity: sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: false + follow-redirects@1.15.3: {} - /foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} + foreground-child@3.1.1: dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 - dev: true - /form-data-encoder@2.1.4: - resolution: {integrity: sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==} - engines: {node: '>= 14.17'} - dev: true + form-data-encoder@2.1.4: {} - /form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} + form-data@4.0.0: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 - /format@0.2.2: - resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} - engines: {node: '>=0.4.x'} - dev: false + format@0.2.2: {} - /formidable@2.1.2: - resolution: {integrity: sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==} + formidable@2.1.2: dependencies: dezalgo: 1.0.4 hexoid: 1.0.0 once: 1.4.0 qs: 6.11.2 - dev: false - - /forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - /fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - dev: true + forwarded@0.2.0: {} - /fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} + fraction.js@4.3.7: {} - /from2@2.3.0: - resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + fresh@0.5.2: {} + + from2@2.3.0: dependencies: inherits: 2.0.4 readable-stream: 2.3.8 - dev: true - /fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - dev: true + fs-constants@1.0.0: {} - /fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.1 - dev: true - /fs-extra@11.1.1: - resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} - engines: {node: '>=14.14'} + fs-extra@11.1.1: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.1 - dev: true - /fs-extra@4.0.3: - resolution: {integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==} + fs-extra@4.0.3: dependencies: graceful-fs: 4.2.11 jsonfile: 4.0.0 universalify: 0.1.2 - dev: true - /fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} + fs-extra@7.0.1: dependencies: graceful-fs: 4.2.11 jsonfile: 4.0.0 universalify: 0.1.2 - dev: true - /fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} + fs-extra@8.1.0: dependencies: graceful-fs: 4.2.11 jsonfile: 4.0.0 universalify: 0.1.2 - dev: true - /fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} + fs-extra@9.1.0: dependencies: at-least-node: 1.0.0 graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.1 - /fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} + fs-minipass@2.1.0: dependencies: minipass: 3.3.6 - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fs.realpath@1.0.0: {} - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true + fsevents@2.3.3: optional: true - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function-bind@1.1.2: {} - /galactus@0.2.1: - resolution: {integrity: sha512-mDc8EQJKtxjp9PMYS3PbpjjbX3oXhBTxoGaPahw620XZBIHJ4+nvw5KN/tRtmmSDR9dypstGNvqQ3C29QGoGHQ==} + galactus@0.2.1: dependencies: debug: 3.2.7 flora-colossus: 1.0.1 fs-extra: 4.0.3 transitivePeerDependencies: - supports-color - dev: true - /gauge@4.0.4: - resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + gauge@4.0.4: dependencies: aproba: 2.0.0 color-support: 1.1.3 @@ -8210,27 +12673,19 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 wide-align: 1.1.5 - dev: true - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} + get-caller-file@2.0.5: {} - /get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - dev: true + get-func-name@2.0.2: {} - /get-intrinsic@1.2.2: - resolution: {integrity: sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==} + get-intrinsic@1.2.2: dependencies: function-bind: 1.1.2 has-proto: 1.0.1 has-symbols: 1.0.3 hasown: 2.0.0 - /get-package-info@1.0.0: - resolution: {integrity: sha512-SCbprXGAPdIhKAXiG+Mk6yeoFH61JlYunqdFQFHDtLjJlDjFf6x07dsS8acO+xWt52jpdVo49AlVDnUVK1sDNw==} - engines: {node: '>= 4.0'} + get-package-info@1.0.0: dependencies: bluebird: 3.7.2 debug: 2.6.9 @@ -8238,145 +12693,87 @@ packages: read-pkg-up: 2.0.0 transitivePeerDependencies: - supports-color - dev: true - /get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - dev: false + get-package-type@0.1.0: {} - /get-pkg-repo@4.2.1: - resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} - engines: {node: '>=6.9.0'} - hasBin: true + get-pkg-repo@4.2.1: dependencies: '@hutson/parse-repository-url': 3.0.2 hosted-git-info: 4.1.0 through2: 2.0.5 yargs: 16.2.0 - dev: true - /get-proxy@2.1.0: - resolution: {integrity: sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==} - engines: {node: '>=4'} + get-proxy@2.1.0: dependencies: npm-conf: 1.1.3 - dev: true - /get-stream@2.3.1: - resolution: {integrity: sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==} - engines: {node: '>=0.10.0'} + get-stream@2.3.1: dependencies: object-assign: 4.1.1 pinkie-promise: 2.0.1 - dev: true - /get-stream@3.0.0: - resolution: {integrity: sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==} - engines: {node: '>=4'} - dev: true + get-stream@3.0.0: {} - /get-stream@4.1.0: - resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} - engines: {node: '>=6'} + get-stream@4.1.0: dependencies: pump: 3.0.0 - dev: true - /get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} + get-stream@5.2.0: dependencies: pump: 3.0.0 - dev: true - /get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - dev: true + get-stream@6.0.1: {} - /getopts@2.3.0: - resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} - dev: false + getopts@2.3.0: {} - /gifwrap@0.9.4: - resolution: {integrity: sha512-MDMwbhASQuVeD4JKd1fKgNgCRL3fGqMM4WaqpNhWO0JiMOAjbQdumbs4BbBZEy9/M00EHEjKN3HieVhCUlwjeQ==} + gifwrap@0.9.4: dependencies: image-q: 4.0.0 omggif: 1.0.10 - dev: true - /git-raw-commits@2.0.11: - resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==} - engines: {node: '>=10'} - hasBin: true + git-raw-commits@2.0.11: dependencies: dargs: 7.0.0 lodash: 4.17.21 meow: 8.1.2 split2: 3.2.2 through2: 4.0.2 - dev: true - /git-remote-origin-url@2.0.0: - resolution: {integrity: sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==} - engines: {node: '>=4'} + git-remote-origin-url@2.0.0: dependencies: gitconfiglocal: 1.0.0 pify: 2.3.0 - dev: true - /git-semver-tags@4.1.1: - resolution: {integrity: sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA==} - engines: {node: '>=10'} - hasBin: true + git-semver-tags@4.1.1: dependencies: meow: 8.1.2 semver: 6.3.1 - dev: true - /gitconfiglocal@1.0.0: - resolution: {integrity: sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==} + gitconfiglocal@1.0.0: dependencies: ini: 1.3.8 - dev: true - /github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - dev: true + github-from-package@0.0.0: {} - /github-slugger@2.0.0: - resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} - dev: false + github-slugger@2.0.0: {} - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 - dev: true - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 - dev: true - /glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true + glob@10.3.10: dependencies: foreground-child: 3.1.1 jackspeak: 2.3.6 minimatch: 9.0.3 minipass: 7.0.4 path-scurry: 1.10.1 - dev: true - /glob@7.1.6: - resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + glob@7.1.6: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -8384,10 +12781,8 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + glob@7.2.3: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -8395,33 +12790,23 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true - /glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} + glob@8.1.0: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 inherits: 2.0.4 minimatch: 5.1.6 once: 1.4.0 - dev: true - /glob@9.3.5: - resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} - engines: {node: '>=16 || 14 >=14.17'} + glob@9.3.5: dependencies: fs.realpath: 1.0.0 minimatch: 8.0.4 minipass: 4.2.8 path-scurry: 1.10.1 - dev: false - /global-agent@3.0.0: - resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} - engines: {node: '>=10.0'} - requiresBuild: true + global-agent@3.0.0: dependencies: boolean: 3.2.0 es6-error: 4.1.1 @@ -8429,61 +12814,39 @@ packages: roarr: 2.15.4 semver: 7.5.4 serialize-error: 7.0.1 - dev: true optional: true - /global-dirs@0.1.1: - resolution: {integrity: sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==} - engines: {node: '>=4'} + global-dirs@0.1.1: dependencies: ini: 1.3.8 - dev: true - /global-dirs@3.0.1: - resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} - engines: {node: '>=10'} + global-dirs@3.0.1: dependencies: ini: 2.0.0 - dev: true - /global-tunnel-ng@2.7.1: - resolution: {integrity: sha512-4s+DyciWBV0eK148wqXxcmVAbFVPqtc3sEtUE/GTQfuU80rySLcMhUmHKSHI7/LDj8q0gDYI1lIhRRB7ieRAqg==} - engines: {node: '>=0.10'} - requiresBuild: true + global-tunnel-ng@2.7.1: dependencies: encodeurl: 1.0.2 lodash: 4.17.21 npm-conf: 1.1.3 tunnel: 0.0.6 - dev: true optional: true - /global@4.4.0: - resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + global@4.4.0: dependencies: min-document: 2.19.0 process: 0.11.10 - dev: true - /globals@13.23.0: - resolution: {integrity: sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==} - engines: {node: '>=8'} + globals@13.23.0: dependencies: type-fest: 0.20.2 - dev: true - /globalthis@1.0.3: - resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} - engines: {node: '>= 0.4'} - requiresBuild: true + globalthis@1.0.3: dependencies: define-properties: 1.2.1 - dev: true optional: true - /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} + globby@11.1.0: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 @@ -8491,11 +12854,8 @@ packages: ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 - dev: true - /globby@12.2.0: - resolution: {integrity: sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + globby@12.2.0: dependencies: array-union: 3.0.1 dir-glob: 3.0.1 @@ -8503,16 +12863,12 @@ packages: ignore: 5.2.4 merge2: 1.4.1 slash: 4.0.0 - dev: true - /gopd@1.0.1: - resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + gopd@1.0.1: dependencies: get-intrinsic: 1.2.2 - /got@11.8.6: - resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} - engines: {node: '>=10.19.0'} + got@11.8.6: dependencies: '@sindresorhus/is': 4.6.0 '@szmarczak/http-timer': 4.0.6 @@ -8525,11 +12881,8 @@ packages: lowercase-keys: 2.0.0 p-cancelable: 2.1.1 responselike: 2.0.1 - dev: true - /got@12.6.1: - resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==} - engines: {node: '>=14.16'} + got@12.6.1: dependencies: '@sindresorhus/is': 5.6.0 '@szmarczak/http-timer': 5.0.1 @@ -8542,11 +12895,8 @@ packages: lowercase-keys: 3.0.0 p-cancelable: 3.0.0 responselike: 3.0.0 - dev: true - /got@7.1.0: - resolution: {integrity: sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==} - engines: {node: '>=4'} + got@7.1.0: dependencies: '@types/keyv': 3.1.4 '@types/responselike': 1.0.2 @@ -8564,11 +12914,8 @@ packages: timed-out: 4.0.1 url-parse-lax: 1.0.0 url-to-options: 1.0.1 - dev: true - /got@8.3.2: - resolution: {integrity: sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==} - engines: {node: '>=4'} + got@8.3.2: dependencies: '@sindresorhus/is': 0.7.0 '@types/keyv': 3.1.4 @@ -8589,11 +12936,8 @@ packages: timed-out: 4.0.1 url-parse-lax: 3.0.0 url-to-options: 1.0.1 - dev: true - /got@9.6.0: - resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} - engines: {node: '>=8.6'} + got@9.6.0: dependencies: '@sindresorhus/is': 0.14.0 '@szmarczak/http-timer': 1.1.2 @@ -8608,23 +12952,14 @@ packages: p-cancelable: 1.1.0 to-readable-stream: 1.0.0 url-parse-lax: 3.0.0 - dev: true - /graceful-fs@4.2.10: - resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} - dev: true + graceful-fs@4.2.10: {} - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graceful-fs@4.2.11: {} - /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true + graphemer@1.4.0: {} - /handlebars@4.7.8: - resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} - engines: {node: '>=0.4.7'} - hasBin: true + handlebars@4.7.8: dependencies: minimist: 1.2.8 neo-async: 2.6.2 @@ -8632,107 +12967,56 @@ packages: wordwrap: 1.0.0 optionalDependencies: uglify-js: 3.17.4 - dev: true - /hard-rejection@2.1.0: - resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} - engines: {node: '>=6'} - dev: true + hard-rejection@2.1.0: {} - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - dev: true + has-flag@3.0.0: {} - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true + has-flag@4.0.0: {} - /has-property-descriptors@1.0.1: - resolution: {integrity: sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==} + has-property-descriptors@1.0.1: dependencies: get-intrinsic: 1.2.2 - /has-proto@1.0.1: - resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} - engines: {node: '>= 0.4'} + has-proto@1.0.1: {} - /has-symbol-support-x@1.4.2: - resolution: {integrity: sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==} - dev: true + has-symbol-support-x@1.4.2: {} - /has-symbols@1.0.3: - resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} - engines: {node: '>= 0.4'} + has-symbols@1.0.3: {} - /has-to-string-tag-x@1.4.1: - resolution: {integrity: sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==} + has-to-string-tag-x@1.4.1: dependencies: has-symbol-support-x: 1.4.2 - dev: true - /has-unicode@2.0.1: - resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} - dev: true + has-unicode@2.0.1: {} - /has-yarn@3.0.0: - resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + has-yarn@3.0.0: {} - /hasown@2.0.0: - resolution: {integrity: sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==} - engines: {node: '>= 0.4'} + hasown@2.0.0: dependencies: function-bind: 1.1.2 - /he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true + he@1.2.0: {} - /helmet@7.0.0: - resolution: {integrity: sha512-MsIgYmdBh460ZZ8cJC81q4XJknjG567wzEmv46WOBblDb6TUd3z8/GhgmsM9pn8g2B80tAJ4m5/d3Bi1KrSUBQ==} - engines: {node: '>=16.0.0'} - dev: false + helmet@7.0.0: {} - /hexoid@1.0.0: - resolution: {integrity: sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==} - engines: {node: '>=8'} - dev: false + hexoid@1.0.0: {} - /highlight.js@11.8.0: - resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==} - engines: {node: '>=12.0.0'} - dev: false + highlight.js@11.8.0: {} - /highlight.js@11.9.0: - resolution: {integrity: sha512-fJ7cW7fQGCYAkgv4CPfwFHrfd/cLS4Hau96JuJ+ZTOWhjnhoeN1ub1tFmALm/+lW5z4WCAuAV9bm05AP0mS6Gw==} - engines: {node: '>=12.0.0'} - dev: false + highlight.js@11.9.0: {} - /hosted-git-info@2.8.9: - resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} - dev: true + hosted-git-info@2.8.9: {} - /hosted-git-info@4.1.0: - resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} - engines: {node: '>=10'} + hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 - dev: true - /hosted-git-info@6.1.1: - resolution: {integrity: sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hosted-git-info@6.1.1: dependencies: lru-cache: 7.18.3 - dev: true - /html-minifier@4.0.0: - resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==} - engines: {node: '>=6'} - hasBin: true + html-minifier@4.0.0: dependencies: camel-case: 3.0.0 clean-css: 4.2.4 @@ -8741,39 +13025,26 @@ packages: param-case: 2.1.1 relateurl: 0.2.7 uglify-js: 3.17.4 - dev: true - /html-to-text@7.1.1: - resolution: {integrity: sha512-c9QWysrfnRZevVpS8MlE7PyOdSuIOjg8Bt8ZE10jMU/BEngA6j3llj4GRfAmtQzcd1FjKE0sWu5IHXRUH9YxIQ==} - engines: {node: '>=10.23.2'} - hasBin: true + html-to-text@7.1.1: dependencies: deepmerge: 4.3.1 he: 1.2.0 htmlparser2: 6.1.0 minimist: 1.2.8 - dev: false - /htmlparser2@6.1.0: - resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + htmlparser2@6.1.0: dependencies: domelementtype: 2.3.0 domhandler: 4.3.1 domutils: 2.8.0 entities: 2.2.0 - dev: false - /http-cache-semantics@3.8.1: - resolution: {integrity: sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==} - dev: true + http-cache-semantics@3.8.1: {} - /http-cache-semantics@4.1.1: - resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} - dev: true + http-cache-semantics@4.1.1: {} - /http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} + http-errors@2.0.0: dependencies: depd: 2.0.0 inherits: 2.0.4 @@ -8781,127 +13052,76 @@ packages: statuses: 2.0.1 toidentifier: 1.0.1 - /http-proxy-agent@5.0.0: - resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} - engines: {node: '>= 6'} + http-proxy-agent@5.0.0: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: true - /http-status-codes@2.3.0: - resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} - dev: false + http-status-codes@2.3.0: {} - /http2-wrapper@1.0.3: - resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} - engines: {node: '>=10.19.0'} + http2-wrapper@1.0.3: dependencies: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - dev: true - /http2-wrapper@2.2.0: - resolution: {integrity: sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==} - engines: {node: '>=10.19.0'} + http2-wrapper@2.2.0: dependencies: quick-lru: 5.1.1 resolve-alpn: 1.2.1 - dev: true - /https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: true - /human-signals@1.1.1: - resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} - engines: {node: '>=8.12.0'} - dev: true + human-signals@1.1.1: {} - /human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - dev: true + human-signals@2.1.0: {} - /human-signals@4.3.1: - resolution: {integrity: sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==} - engines: {node: '>=14.18.0'} - dev: true + human-signals@4.3.1: {} - /humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + humanize-ms@1.2.1: dependencies: ms: 2.1.3 - dev: true - /husky@8.0.3: - resolution: {integrity: sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==} - engines: {node: '>=14'} - hasBin: true - dev: true + husky@8.0.3: {} - /iconv-corefoundation@1.1.7: - resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==} - engines: {node: ^8.11.2 || >=10} - os: [darwin] - requiresBuild: true + iconv-corefoundation@1.1.7: dependencies: cli-truncate: 2.1.0 node-addon-api: 1.7.2 - dev: true optional: true - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 - /iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 - dev: true - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - requiresBuild: true + ieee754@1.2.1: {} - /ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} - engines: {node: '>= 4'} - dev: true + ignore@5.2.4: {} - /image-q@4.0.0: - resolution: {integrity: sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw==} + image-q@4.0.0: dependencies: '@types/node': 16.9.1 - dev: true - /imagemin-pngquant@9.0.2: - resolution: {integrity: sha512-cj//bKo8+Frd/DM8l6Pg9pws1pnDUjgb7ae++sUX1kUVdv2nrngPykhiUOgFeE0LGY/LmUbCf4egCHC4YUcZSg==} - engines: {node: '>=10'} + imagemin-pngquant@9.0.2: dependencies: execa: 4.1.0 is-png: 2.0.0 is-stream: 2.0.1 ow: 0.17.0 pngquant-bin: 6.0.1 - dev: true - /imagemin@8.0.1: - resolution: {integrity: sha512-Q/QaPi+5HuwbZNtQRqUVk6hKacI6z9iWiCSQBisAv7uBynZwO7t1svkryKl7+iSQbkU/6t9DWnHz04cFs2WY7w==} - engines: {node: '>=12'} + imagemin@8.0.1: dependencies: file-type: 16.5.4 globby: 12.2.0 @@ -8910,68 +13130,36 @@ packages: p-pipe: 4.0.0 replace-ext: 2.0.0 slash: 3.0.0 - dev: true - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + import-fresh@3.3.0: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - dev: true - /import-lazy@3.1.0: - resolution: {integrity: sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==} - engines: {node: '>=6'} - dev: true + import-lazy@3.1.0: {} - /import-lazy@4.0.0: - resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} - engines: {node: '>=8'} - dev: true + import-lazy@4.0.0: {} - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true + imurmurhash@0.1.4: {} - /indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - dev: true + indent-string@4.0.0: {} - /infer-owner@1.0.4: - resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} - dev: true + infer-owner@1.0.4: {} - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + inflight@1.0.6: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inherits@2.0.4: {} - /ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - requiresBuild: true - dev: true + ini@1.3.8: {} - /ini@2.0.0: - resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} - engines: {node: '>=10'} - dev: true + ini@2.0.0: {} - /ini@3.0.1: - resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - dev: false + ini@3.0.1: {} - /inquirer@8.2.6: - resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} - engines: {node: '>=12.0.0'} + inquirer@8.2.6: dependencies: ansi-escapes: 4.3.2 chalk: 4.1.2 @@ -8988,420 +13176,225 @@ packages: strip-ansi: 6.0.1 through: 2.3.8 wrap-ansi: 6.2.0 - dev: true - /interpret@2.2.0: - resolution: {integrity: sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==} - engines: {node: '>= 0.10'} - dev: false + interpret@2.2.0: {} - /into-stream@3.1.0: - resolution: {integrity: sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==} - engines: {node: '>=4'} + into-stream@3.1.0: dependencies: from2: 2.3.0 p-is-promise: 1.1.0 - dev: true - /ip@1.1.8: - resolution: {integrity: sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==} - dev: true + ip@1.1.8: {} - /ip@2.0.0: - resolution: {integrity: sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==} - dev: true + ip@2.0.0: {} - /ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} + ipaddr.js@1.9.1: {} - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true + is-arrayish@0.2.1: {} - /is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + is-arrayish@0.3.2: {} - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.2.0 - dev: true - /is-ci@3.0.1: - resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==} - hasBin: true + is-ci@3.0.1: dependencies: ci-info: 3.9.0 - dev: true - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + is-core-module@2.13.1: dependencies: hasown: 2.0.0 - /is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true + is-docker@2.2.1: {} - /is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - dev: true + is-docker@3.0.0: {} - /is-extendable@1.0.1: - resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} - engines: {node: '>=0.10.0'} + is-extendable@1.0.1: dependencies: is-plain-object: 2.0.4 - dev: false - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true + is-extglob@2.1.1: {} - /is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} + is-fullwidth-code-point@3.0.0: {} - /is-function@1.0.2: - resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} - dev: true + is-function@1.0.2: {} - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - dev: true - /is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 - dev: true - /is-installed-globally@0.4.0: - resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} - engines: {node: '>=10'} + is-installed-globally@0.4.0: dependencies: global-dirs: 3.0.1 is-path-inside: 3.0.3 - dev: true - /is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - dev: true - - /is-lambda@1.0.1: - resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} - dev: true + is-interactive@1.0.0: {} - /is-natural-number@4.0.1: - resolution: {integrity: sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==} - dev: true + is-lambda@1.0.1: {} - /is-npm@6.0.0: - resolution: {integrity: sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + is-natural-number@4.0.1: {} - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true + is-npm@6.0.0: {} - /is-obj@2.0.0: - resolution: {integrity: sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==} - engines: {node: '>=8'} - dev: true + is-number@7.0.0: {} - /is-object@1.0.2: - resolution: {integrity: sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==} - dev: true + is-obj@2.0.0: {} - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true + is-object@1.0.2: {} - /is-plain-obj@1.1.0: - resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} - engines: {node: '>=0.10.0'} + is-path-inside@3.0.3: {} - /is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} + is-plain-obj@1.1.0: {} + + is-plain-object@2.0.4: dependencies: isobject: 3.0.1 - /is-png@2.0.0: - resolution: {integrity: sha512-4KPGizaVGj2LK7xwJIz8o5B2ubu1D/vcQsgOGFEDlpcvgZHto4gBnyd0ig7Ws+67ixmwKoNmu0hYnpo6AaKb5g==} - engines: {node: '>=8'} - dev: true + is-png@2.0.0: {} - /is-png@3.0.1: - resolution: {integrity: sha512-8TqC8+bdsm3YkpI2aECCDycFDl1hTB0HMVRnP3xRRa3Tqx2oVE7sBi1G6CuO9IqEyWSzbBZr1mGqdb3it9h/pg==} - engines: {node: '>=12'} - dev: true + is-png@3.0.1: {} - /is-retry-allowed@1.2.0: - resolution: {integrity: sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==} - engines: {node: '>=0.10.0'} - dev: true + is-retry-allowed@1.2.0: {} - /is-stream@1.1.0: - resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} - engines: {node: '>=0.10.0'} - dev: true + is-stream@1.1.0: {} - /is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} + is-stream@2.0.1: {} - /is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + is-stream@3.0.0: {} - /is-text-path@1.0.1: - resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==} - engines: {node: '>=0.10.0'} + is-text-path@1.0.1: dependencies: text-extensions: 1.9.0 - dev: true - /is-text-path@2.0.0: - resolution: {integrity: sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==} - engines: {node: '>=8'} + is-text-path@2.0.0: dependencies: text-extensions: 2.4.0 - dev: true - /is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - dev: true + is-typedarray@1.0.0: {} - /is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - dev: true + is-unicode-supported@0.1.0: {} - /is-what@4.1.16: - resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} - engines: {node: '>=12.13'} - dev: false + is-what@4.1.16: {} - /is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} + is-wsl@2.2.0: dependencies: is-docker: 2.2.1 - /is-yarn-global@0.4.1: - resolution: {integrity: sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==} - engines: {node: '>=12'} - dev: true + is-yarn-global@0.4.1: {} - /isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - dev: true + isarray@1.0.0: {} - /isbinaryfile@4.0.10: - resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} - engines: {node: '>= 8.0.0'} - dev: true + isbinaryfile@4.0.10: {} - /isbinaryfile@5.0.0: - resolution: {integrity: sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg==} - engines: {node: '>= 14.0.0'} - dev: true + isbinaryfile@5.0.0: {} - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@2.0.0: {} - /isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} + isobject@3.0.1: {} - /isomorphic.js@0.2.5: - resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} - dev: false + isomorphic.js@0.2.5: {} - /isurl@1.0.0: - resolution: {integrity: sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==} - engines: {node: '>= 4'} + isurl@1.0.0: dependencies: has-to-string-tag-x: 1.4.1 is-object: 1.0.2 - dev: true - /iterare@1.2.1: - resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} - engines: {node: '>=6'} - dev: true + iterare@1.2.1: {} - /jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} + jackspeak@2.3.6: dependencies: '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 - dev: true - /jake@10.8.7: - resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==} - engines: {node: '>=10'} - hasBin: true + jake@10.8.7: dependencies: async: 3.2.5 chalk: 4.1.2 filelist: 1.0.4 minimatch: 3.1.2 - dev: true - /jimp@0.14.0: - resolution: {integrity: sha512-8BXU+J8+SPmwwyq9ELihpSV4dWPTiOKBWCEgtkbnxxAVMjXdf3yGmyaLSshBfXc8sP/JQ9OZj5R8nZzz2wPXgA==} + jimp@0.14.0: dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/plugins': 0.14.0(@jimp/custom@0.14.0) '@jimp/types': 0.14.0(@jimp/custom@0.14.0) regenerator-runtime: 0.13.11 - dev: true - /jiti@1.21.0: - resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} - hasBin: true - dev: true + jiti@1.21.0: {} - /joycon@3.1.1: - resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} - engines: {node: '>=10'} - dev: true + joycon@3.1.1: {} - /jpeg-js@0.4.4: - resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} - dev: true + jpeg-js@0.4.4: {} - /js-base64@3.7.5: - resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} - dev: false + js-base64@3.7.5: {} - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - dev: true + js-tokens@4.0.0: {} - /js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true + js-yaml@3.14.1: dependencies: argparse: 1.0.10 esprima: 4.0.1 - dev: false - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true + js-yaml@4.1.0: dependencies: argparse: 2.0.1 - dev: true - /json-bigint@1.0.0: - resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + json-bigint@1.0.0: dependencies: bignumber.js: 9.1.2 - dev: false - /json-buffer@3.0.0: - resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} - dev: true + json-buffer@3.0.0: {} - /json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - dev: true + json-buffer@3.0.1: {} - /json-parse-better-errors@1.0.2: - resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - dev: true + json-parse-better-errors@1.0.2: {} - /json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true + json-parse-even-better-errors@2.3.1: {} - /json-schema-ref-resolver@1.0.1: - resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==} + json-schema-ref-resolver@1.0.1: dependencies: fast-deep-equal: 3.1.3 - dev: false - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - requiresBuild: true - dev: true + json-schema-traverse@0.4.1: {} - /json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-traverse@1.0.0: {} - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true + json-stable-stringify-without-jsonify@1.0.1: {} - /json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - requiresBuild: true - dev: true + json-stringify-safe@5.0.1: {} - /json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - dev: true + json5@2.2.3: {} - /jsonc-eslint-parser@1.4.1: - resolution: {integrity: sha512-hXBrvsR1rdjmB2kQmUjf1rEIa+TqHBGMge8pwi++C+Si1ad7EjZrJcpgwym+QGK/pqTx+K7keFAtLlVNdLRJOg==} - engines: {node: '>=8.10.0'} + jsonc-eslint-parser@1.4.1: dependencies: acorn: 7.4.1 eslint-utils: 2.1.0 eslint-visitor-keys: 1.3.0 espree: 6.2.1 semver: 6.3.1 - dev: true - /jsonc-parser@3.2.0: - resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} - dev: true + jsonc-parser@3.2.0: {} - /jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 - dev: true - /jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.1.0: dependencies: universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 - /jsonparse@1.3.1: - resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} - engines: {'0': node >= 0.2.0} - dev: true + jsonparse@1.3.1: {} - /jsonwebtoken@9.0.2: - resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} - engines: {node: '>=12', npm: '>=6'} + jsonwebtoken@9.0.2: dependencies: jws: 3.2.2 lodash.includes: 4.3.0 @@ -9414,105 +13407,53 @@ packages: ms: 2.1.3 semver: 7.5.4 - /junk@3.1.0: - resolution: {integrity: sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==} - engines: {node: '>=8'} - dev: true + junk@3.1.0: {} - /jwa@1.4.1: - resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + jwa@1.4.1: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - /jwa@2.0.0: - resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} + jwa@2.0.0: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - dev: false - /jws@3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + jws@3.2.2: dependencies: jwa: 1.4.1 safe-buffer: 5.2.1 - /jws@4.0.0: - resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} + jws@4.0.0: dependencies: jwa: 2.0.0 safe-buffer: 5.2.1 - dev: false - /katex@0.16.9: - resolution: {integrity: sha512-fsSYjWS0EEOwvy81j3vRA8TEAhQhKiqO+FQaKWp0m39qwOzHVBgAUBIXWj1pB+O2W3fIpNa6Y9KSKCVbfPhyAQ==} - hasBin: true + katex@0.16.9: dependencies: commander: 8.3.0 - dev: false - /keyv@3.0.0: - resolution: {integrity: sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==} + keyv@3.0.0: dependencies: json-buffer: 3.0.0 - dev: true - /keyv@3.1.0: - resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} + keyv@3.1.0: dependencies: json-buffer: 3.0.0 - dev: true - /keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 - dev: true - /kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: true + kind-of@6.0.3: {} - /kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} + kleur@3.0.3: {} - /kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - dev: false + kleur@4.1.5: {} - /knex@2.3.0(pg@8.11.3): - resolution: {integrity: sha512-WMizPaq9wRMkfnwKXKXgBZeZFOSHGdtoSz5SaLAVNs3WRDfawt9O89T4XyH52PETxjV8/kRk0Yf+8WBEP/zbYw==} - engines: {node: '>=12'} - hasBin: true - peerDependencies: - better-sqlite3: '*' - mysql: '*' - mysql2: '*' - pg: '*' - pg-native: '*' - sqlite3: '*' - tedious: '*' - peerDependenciesMeta: - better-sqlite3: - optional: true - mysql: - optional: true - mysql2: - optional: true - pg: - optional: true - pg-native: - optional: true - sqlite3: - optional: true - tedious: - optional: true + knex@2.3.0(pg@8.11.3): dependencies: colorette: 2.0.19 commander: 9.5.0 @@ -9531,88 +13472,53 @@ packages: tildify: 2.0.0 transitivePeerDependencies: - supports-color - dev: false - /kolorist@1.8.0: - resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} - dev: true + kolorist@1.8.0: {} - /kuler@2.0.0: - resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} - dev: false + kuler@2.0.0: {} - /latest-version@7.0.0: - resolution: {integrity: sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==} - engines: {node: '>=14.16'} + latest-version@7.0.0: dependencies: package-json: 8.1.1 - dev: true - /lazy-val@1.0.5: - resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + lazy-val@1.0.5: {} - /lazystream@1.0.1: - resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} - engines: {node: '>= 0.6.3'} + lazystream@1.0.1: dependencies: readable-stream: 2.3.8 - dev: true - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true - /lib0@0.2.87: - resolution: {integrity: sha512-TbB63XJixvNToW2IHWAFsCJj9tVnajmwjE14p69i51Rx8byOQd2IP4ourE8v4d7vhyO++nVm1sQk3ePslfbucg==} - engines: {node: '>=16'} - hasBin: true + lib0@0.2.87: dependencies: isomorphic.js: 0.2.5 - dev: false - /libsodium-sumo@0.7.13: - resolution: {integrity: sha512-zTGdLu4b9zSNLfovImpBCbdAA4xkpkZbMnSQjP8HShyOutnGjRHmSOKlsylh1okao6QhLiz7nG98EGn+04cZjQ==} - dev: false + libsodium-sumo@0.7.13: {} - /libsodium-wrappers-sumo@0.7.13: - resolution: {integrity: sha512-lz4YdplzDRh6AhnLGF2Dj2IUj94xRN6Bh8T0HLNwzYGwPehQJX6c7iYVrFUPZ3QqxE0bqC+K0IIqqZJYWumwSQ==} + libsodium-wrappers-sumo@0.7.13: dependencies: libsodium-sumo: 0.7.13 - dev: false - /light-my-request@5.11.0: - resolution: {integrity: sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==} + light-my-request@5.11.0: dependencies: cookie: 0.5.0 process-warning: 2.3.0 set-cookie-parser: 2.6.0 - dev: false - /lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - dev: true + lilconfig@2.1.0: {} - /lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true + lines-and-columns@1.2.4: {} - /linkify-it@4.0.1: - resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} + linkify-it@4.0.1: dependencies: uc.micro: 1.0.6 - dev: false - /linkifyjs@4.1.1: - resolution: {integrity: sha512-zFN/CTVmbcVef+WaDXT63dNzzkfRBKT1j464NJQkV7iSgJU0sLBus9W0HBwnXK13/hf168pbrx/V/bjEHOXNHA==} - dev: false + linkifyjs@4.1.1: {} - /load-bmfont@1.4.1: - resolution: {integrity: sha512-8UyQoYmdRDy81Brz6aLAUhfZLwr5zV0L3taTQ4hju7m6biuwiWiJXjPhBJxbUQJA8PrkvJ/7Enqmwk2sM14soA==} + load-bmfont@1.4.1: dependencies: buffer-equal: 0.0.1 mime: 1.6.0 @@ -9622,177 +13528,103 @@ packages: phin: 2.9.3 xhr: 2.6.0 xtend: 4.0.2 - dev: true - /load-json-file@2.0.0: - resolution: {integrity: sha512-3p6ZOGNbiX4CdvEd1VcE6yi78UrGNpjHO33noGwHCnT/o2fyllJDepsm8+mFFv/DvtwFHht5HIHSyOy5a+ChVQ==} - engines: {node: '>=4'} + load-json-file@2.0.0: dependencies: graceful-fs: 4.2.11 parse-json: 2.2.0 pify: 2.3.0 strip-bom: 3.0.0 - dev: true - /load-json-file@4.0.0: - resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} - engines: {node: '>=4'} + load-json-file@4.0.0: dependencies: graceful-fs: 4.2.11 parse-json: 4.0.0 pify: 3.0.0 strip-bom: 3.0.0 - dev: true - /load-tsconfig@0.2.5: - resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + load-tsconfig@0.2.5: {} - /local-pkg@0.4.3: - resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} - engines: {node: '>=14'} - dev: true + local-pkg@0.4.3: {} - /locate-path@2.0.0: - resolution: {integrity: sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==} - engines: {node: '>=4'} + locate-path@2.0.0: dependencies: p-locate: 2.0.0 path-exists: 3.0.0 - dev: true - /locate-path@3.0.0: - resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} - engines: {node: '>=6'} + locate-path@3.0.0: dependencies: p-locate: 3.0.0 path-exists: 3.0.0 - dev: true - /locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} + locate-path@5.0.0: dependencies: p-locate: 4.1.0 - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 - dev: true - /lodash.camelcase@4.3.0: - resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} - dev: true + lodash.camelcase@4.3.0: {} - /lodash.defaults@4.2.0: - resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.defaults@4.2.0: {} - /lodash.difference@4.5.0: - resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} - dev: true + lodash.difference@4.5.0: {} - /lodash.flatten@4.4.0: - resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} - dev: true + lodash.flatten@4.4.0: {} - /lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - dev: true + lodash.get@4.4.2: {} - /lodash.includes@4.3.0: - resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + lodash.includes@4.3.0: {} - /lodash.isarguments@3.1.0: - resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - dev: false + lodash.isarguments@3.1.0: {} - /lodash.isboolean@3.0.3: - resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + lodash.isboolean@3.0.3: {} - /lodash.isequal@4.5.0: - resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - dev: false + lodash.isequal@4.5.0: {} - /lodash.isfunction@3.0.9: - resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} - dev: true + lodash.isfunction@3.0.9: {} - /lodash.isinteger@4.0.4: - resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + lodash.isinteger@4.0.4: {} - /lodash.ismatch@4.4.0: - resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} - dev: true + lodash.ismatch@4.4.0: {} - /lodash.isnumber@3.0.3: - resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + lodash.isnumber@3.0.3: {} - /lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + lodash.isplainobject@4.0.6: {} - /lodash.isstring@4.0.1: - resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.isstring@4.0.1: {} - /lodash.kebabcase@4.1.1: - resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==} - dev: true + lodash.kebabcase@4.1.1: {} - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true + lodash.merge@4.6.2: {} - /lodash.mergewith@4.6.2: - resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} - dev: true + lodash.mergewith@4.6.2: {} - /lodash.once@4.1.1: - resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.once@4.1.1: {} - /lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - dev: true + lodash.snakecase@4.1.1: {} - /lodash.sortby@4.7.0: - resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} - dev: true + lodash.sortby@4.7.0: {} - /lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - dev: true + lodash.startcase@4.4.0: {} - /lodash.truncate@4.4.2: - resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} - dev: true + lodash.truncate@4.4.2: {} - /lodash.union@4.6.0: - resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} - dev: true + lodash.union@4.6.0: {} - /lodash.uniq@4.5.0: - resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} - dev: true + lodash.uniq@4.5.0: {} - /lodash.upperfirst@4.3.1: - resolution: {integrity: sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==} - dev: true + lodash.upperfirst@4.3.1: {} - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.17.21: {} - /log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} + log-symbols@4.1.0: dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 - dev: true - /logform@2.6.0: - resolution: {integrity: sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==} - engines: {node: '>= 12.0.0'} + logform@2.6.0: dependencies: '@colors/colors': 1.6.0 '@types/triple-beam': 1.3.4 @@ -9800,107 +13632,63 @@ packages: ms: 2.1.3 safe-stable-stringify: 2.4.3 triple-beam: 1.4.1 - dev: false - /loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + loupe@2.3.7: dependencies: get-func-name: 2.0.2 - dev: true - /lower-case@1.1.4: - resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} - dev: true + lower-case@1.1.4: {} - /lowercase-keys@1.0.0: - resolution: {integrity: sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==} - engines: {node: '>=0.10.0'} - dev: true + lowercase-keys@1.0.0: {} - /lowercase-keys@1.0.1: - resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} - engines: {node: '>=0.10.0'} - dev: true + lowercase-keys@1.0.1: {} - /lowercase-keys@2.0.0: - resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} - engines: {node: '>=8'} - dev: true + lowercase-keys@2.0.0: {} - /lowercase-keys@3.0.0: - resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + lowercase-keys@3.0.0: {} - /lowlight@2.9.0: - resolution: {integrity: sha512-OpcaUTCLmHuVuBcyNckKfH5B0oA4JUavb/M/8n9iAvanJYNQkrVm4pvyX0SUaqkBG4dnWHKt7p50B3ngAG2Rfw==} + lowlight@2.9.0: dependencies: '@types/hast': 2.3.7 fault: 2.0.1 highlight.js: 11.8.0 - dev: false - /lru-cache@10.0.1: - resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} - engines: {node: 14 || >=16.14} + lru-cache@10.0.1: {} - /lru-cache@4.1.5: - resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + lru-cache@4.1.5: dependencies: pseudomap: 1.0.2 yallist: 2.1.2 - dev: true - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} + lru-cache@6.0.0: dependencies: yallist: 4.0.0 - /lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - dev: true + lru-cache@7.18.3: {} - /magic-string@0.25.9: - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + magic-string@0.25.9: dependencies: sourcemap-codec: 1.4.8 - /magic-string@0.26.7: - resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==} - engines: {node: '>=12'} + magic-string@0.26.7: dependencies: sourcemap-codec: 1.4.8 - dev: true - /magic-string@0.27.0: - resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} - engines: {node: '>=12'} + magic-string@0.27.0: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - /magic-string@0.30.5: - resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} - engines: {node: '>=12'} + magic-string@0.30.5: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - /make-dir@1.3.0: - resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} - engines: {node: '>=4'} + make-dir@1.3.0: dependencies: pify: 3.0.0 - dev: true - /make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + make-error@1.3.6: {} - /make-fetch-happen@10.2.1: - resolution: {integrity: sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + make-fetch-happen@10.2.1: dependencies: agentkeepalive: 4.5.0 cacache: 16.1.3 @@ -9921,77 +13709,42 @@ packages: transitivePeerDependencies: - bluebird - supports-color - dev: true - /map-obj@1.0.1: - resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} - engines: {node: '>=0.10.0'} - dev: true + map-obj@1.0.1: {} - /map-obj@4.3.0: - resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} - engines: {node: '>=8'} - dev: true + map-obj@4.3.0: {} - /markdown-it@13.0.2: - resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==} - hasBin: true + markdown-it@13.0.2: dependencies: argparse: 2.0.1 entities: 3.0.1 linkify-it: 4.0.1 mdurl: 1.0.1 uc.micro: 1.0.6 - dev: false - /marked-gfm-heading-id@3.1.1(marked@9.1.5): - resolution: {integrity: sha512-PATvg4bpYxYY7SiTkknZWNiuKtfgpIctCHsbCHZiEUB+7eZ6SjGMlpL//X0JzE3/Z9B9aqLgQS9UTMFfYs6CEg==} - peerDependencies: - marked: '>=4 <11' + marked-gfm-heading-id@3.1.1(marked@9.1.5): dependencies: github-slugger: 2.0.0 marked: 9.1.5 - dev: false - /marked@9.1.5: - resolution: {integrity: sha512-14QG3shv8Kg/xc0Yh6TNkMj90wXH9mmldi5941I2OevfJ/FQAFLEwtwU2/FfgSAOMlWHrEukWSGQf8MiVYNG2A==} - engines: {node: '>= 16'} - hasBin: true - dev: false + marked@9.1.5: {} - /matcher@3.0.0: - resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} - engines: {node: '>=10'} - requiresBuild: true + matcher@3.0.0: dependencies: escape-string-regexp: 4.0.0 - dev: true optional: true - /mdn-data@2.0.28: - resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} - dev: true + mdn-data@2.0.28: {} - /mdn-data@2.0.30: - resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} - dev: true + mdn-data@2.0.30: {} - /mdurl@1.0.1: - resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} - dev: false + mdurl@1.0.1: {} - /media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} + media-typer@0.3.0: {} - /meow@12.1.1: - resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} - engines: {node: '>=16.10'} - dev: true + meow@12.1.1: {} - /meow@8.1.2: - resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} - engines: {node: '>=10'} + meow@8.1.2: dependencies: '@types/minimist': 1.2.4 camelcase-keys: 6.2.2 @@ -10004,250 +13757,141 @@ packages: trim-newlines: 3.0.1 type-fest: 0.18.1 yargs-parser: 20.2.9 - dev: true - /merge-descriptors@1.0.1: - resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + merge-descriptors@1.0.1: {} - /merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - dev: true + merge-stream@2.0.0: {} - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: true + merge2@1.4.1: {} - /methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} + methods@1.1.2: {} - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} + micromatch@4.0.5: dependencies: braces: 3.0.2 picomatch: 2.3.1 - dev: true - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} + mime-db@1.52.0: {} - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 - /mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true + mime@1.6.0: {} - /mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} - hasBin: true + mime@2.6.0: {} - /mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - dev: true + mimic-fn@2.1.0: {} - /mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - dev: true + mimic-fn@4.0.0: {} - /mimic-response@1.0.1: - resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} - engines: {node: '>=4'} - dev: true + mimic-response@1.0.1: {} - /mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} - dev: true + mimic-response@3.1.0: {} - /mimic-response@4.0.0: - resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - dev: true + mimic-response@4.0.0: {} - /min-document@2.19.0: - resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + min-document@2.19.0: dependencies: dom-walk: 0.1.2 - dev: true - /min-indent@1.0.1: - resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} - engines: {node: '>=4'} - dev: true + min-indent@1.0.1: {} - /minimalistic-assert@1.0.1: - resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - dev: false + minimalistic-assert@1.0.1: {} - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 - dev: true - /minimatch@5.1.6: - resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} - engines: {node: '>=10'} + minimatch@5.1.6: dependencies: brace-expansion: 2.0.1 - dev: true - /minimatch@8.0.4: - resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} - engines: {node: '>=16 || 14 >=14.17'} + minimatch@8.0.4: dependencies: brace-expansion: 2.0.1 - dev: false - /minimatch@9.0.3: - resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} - engines: {node: '>=16 || 14 >=14.17'} + minimatch@9.0.3: dependencies: brace-expansion: 2.0.1 - dev: true - /minimist-options@4.1.0: - resolution: {integrity: sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==} - engines: {node: '>= 6'} + minimist-options@4.1.0: dependencies: arrify: 1.0.1 is-plain-obj: 1.1.0 kind-of: 6.0.3 - dev: true - /minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minimist@1.2.8: {} - /minipass-collect@1.0.2: - resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} - engines: {node: '>= 8'} + minipass-collect@1.0.2: dependencies: minipass: 3.3.6 - dev: true - /minipass-fetch@2.1.2: - resolution: {integrity: sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + minipass-fetch@2.1.2: dependencies: minipass: 3.3.6 minipass-sized: 1.0.3 minizlib: 2.1.2 optionalDependencies: encoding: 0.1.13 - dev: true - /minipass-flush@1.0.5: - resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} - engines: {node: '>= 8'} + minipass-flush@1.0.5: dependencies: minipass: 3.3.6 - dev: true - /minipass-pipeline@1.2.4: - resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} - engines: {node: '>=8'} + minipass-pipeline@1.2.4: dependencies: minipass: 3.3.6 - dev: true - /minipass-sized@1.0.3: - resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} - engines: {node: '>=8'} + minipass-sized@1.0.3: dependencies: minipass: 3.3.6 - dev: true - /minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} + minipass@3.3.6: dependencies: yallist: 4.0.0 - /minipass@4.2.8: - resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} - engines: {node: '>=8'} - dev: false + minipass@4.2.8: {} - /minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} + minipass@5.0.0: {} - /minipass@7.0.4: - resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} - engines: {node: '>=16 || 14 >=14.17'} + minipass@7.0.4: {} - /minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} + minizlib@2.1.2: dependencies: minipass: 3.3.6 yallist: 4.0.0 - /mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - dev: true + mkdirp-classic@0.5.3: {} - /mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true + mkdirp@0.5.6: dependencies: minimist: 1.2.8 - dev: true - /mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true + mkdirp@1.0.4: {} - /mlly@1.4.2: - resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} + mlly@1.4.2: dependencies: acorn: 8.11.2 pathe: 1.1.1 pkg-types: 1.0.3 ufo: 1.3.1 - dev: true - /mnemonist@0.39.5: - resolution: {integrity: sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==} + mnemonist@0.39.5: dependencies: obliterator: 2.0.4 - dev: false - /modify-filename@1.1.0: - resolution: {integrity: sha512-EickqnKq3kVVaZisYuCxhtKbZjInCuwgwZWyAmRIp1NTMhri7r3380/uqwrUHfaDiPzLVTuoNy4whX66bxPVog==} - engines: {node: '>=0.10.0'} - dev: false + modify-filename@1.1.0: {} - /modify-values@1.0.1: - resolution: {integrity: sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==} - engines: {node: '>=0.10.0'} - dev: true + modify-values@1.0.1: {} - /ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.0.0: {} - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.2: {} - /ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + ms@2.1.3: {} - /msgpackr-extract@3.0.2: - resolution: {integrity: sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==} - hasBin: true - requiresBuild: true + msgpackr-extract@3.0.2: dependencies: node-gyp-build-optional-packages: 5.0.7 optionalDependencies: @@ -10257,45 +13901,27 @@ packages: '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.2 '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.2 '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.2 - dev: false optional: true - /msgpackr@1.9.9: - resolution: {integrity: sha512-sbn6mioS2w0lq1O6PpGtsv6Gy8roWM+o3o4Sqjd6DudrL/nOugY+KyJUimoWzHnf9OkO0T6broHFnYE/R05t9A==} + msgpackr@1.9.9: optionalDependencies: msgpackr-extract: 3.0.2 - dev: false - /mute-stream@0.0.8: - resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} - dev: true + mute-stream@0.0.8: {} - /mylas@2.1.13: - resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} - engines: {node: '>=12.0.0'} - dev: true + mylas@2.1.13: {} - /mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + mz@2.7.0: dependencies: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 - dev: true - /nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true + nanoid@3.3.7: {} - /napi-build-utils@1.0.2: - resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} - dev: true + napi-build-utils@1.0.2: {} - /native-run@1.7.4: - resolution: {integrity: sha512-yDEwTp66vmXpqFiSQzz4sVQgyq5U58gGRovglY4GHh12ITyWa6mh6Lbpm2gViVOVD1JYFtYnwcgr7GTFBinXNA==} - engines: {node: '>=12.13.0'} - hasBin: true + native-run@1.7.4: dependencies: '@ionic/utils-fs': 3.1.7 '@ionic/utils-terminal': 2.3.4 @@ -10310,83 +13936,44 @@ packages: yauzl: 2.10.0 transitivePeerDependencies: - supports-color - dev: false - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true + natural-compare@1.4.0: {} - /negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} + negotiator@0.6.3: {} - /neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - dev: true + neo-async@2.6.2: {} - /nice-try@1.0.5: - resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - dev: true + nice-try@1.0.5: {} - /no-case@2.3.2: - resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + no-case@2.3.2: dependencies: lower-case: 1.1.4 - dev: true - /node-abi@3.51.0: - resolution: {integrity: sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==} - engines: {node: '>=10'} + node-abi@3.51.0: dependencies: semver: 7.5.4 - dev: true - /node-abort-controller@3.1.1: - resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} - dev: false + node-abort-controller@3.1.1: {} - /node-addon-api@1.7.2: - resolution: {integrity: sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==} - requiresBuild: true - dev: true + node-addon-api@1.7.2: optional: true - /node-addon-api@6.1.0: - resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} - dev: true + node-addon-api@6.1.0: {} - /node-api-version@0.1.4: - resolution: {integrity: sha512-KGXihXdUChwJAOHO53bv9/vXcLmdUsZ6jIptbvYvkpKfth+r7jw44JkVxQFA3kX5nQjzjmGu1uAu/xNNLNlI5g==} + node-api-version@0.1.4: dependencies: semver: 7.5.4 - dev: true - /node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 - /node-gyp-build-optional-packages@5.0.7: - resolution: {integrity: sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==} - hasBin: true - requiresBuild: true - dev: false + node-gyp-build-optional-packages@5.0.7: optional: true - /node-gyp-build@4.6.1: - resolution: {integrity: sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==} - hasBin: true + node-gyp-build@4.6.1: {} - /node-gyp@9.4.1: - resolution: {integrity: sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ==} - engines: {node: ^12.13 || ^14.13 || >=16} - hasBin: true + node-gyp@9.4.1: dependencies: env-paths: 2.2.1 exponential-backoff: 3.1.1 @@ -10402,260 +13989,155 @@ packages: transitivePeerDependencies: - bluebird - supports-color - dev: true - /node-mailjet@6.0.4: - resolution: {integrity: sha512-gNWfbVnsH+KxkhfDLPA8OrQ2Q25OgyKp19C7DSJYmN2zNfqTKIXzhB9BZwgxZtErmPxz2Fp1NR18WPCmrJDuwg==} - engines: {node: '>= 12.0.0', npm: '>= 6.9.0'} + node-mailjet@6.0.4: dependencies: axios: 0.27.2 json-bigint: 1.0.0 url-join: 4.0.1 transitivePeerDependencies: - debug - dev: false - /node-releases@2.0.13: - resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} + node-releases@2.0.13: {} - /nodemailer-html-to-text@3.2.0: - resolution: {integrity: sha512-RJUC6640QV1PzTHHapOrc6IzrAJUZtk2BdVdINZ9VTLm+mcQNyBO9LYyhrnufkzqiD9l8hPLJ97rSyK4WanPNg==} - engines: {node: '>= 10.23.0'} + nodemailer-html-to-text@3.2.0: dependencies: html-to-text: 7.1.1 - dev: false - /nodemailer@6.9.7: - resolution: {integrity: sha512-rUtR77ksqex/eZRLmQ21LKVH5nAAsVicAtAYudK7JgwenEDZ0UIQ1adUGqErz7sMkWYxWTTU1aeP2Jga6WQyJw==} - engines: {node: '>=6.0.0'} - dev: false + nodemailer@6.9.7: {} - /nopt@6.0.0: - resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - hasBin: true + nopt@6.0.0: dependencies: abbrev: 1.1.1 - dev: true - /normalize-package-data@2.5.0: - resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 resolve: 1.22.8 semver: 5.7.2 validate-npm-package-license: 3.0.4 - dev: true - /normalize-package-data@3.0.3: - resolution: {integrity: sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==} - engines: {node: '>=10'} + normalize-package-data@3.0.3: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.13.1 semver: 7.5.4 validate-npm-package-license: 3.0.4 - dev: true - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - dev: true + normalize-path@3.0.0: {} - /normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} - dev: true + normalize-range@0.1.2: {} - /normalize-url@2.0.1: - resolution: {integrity: sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==} - engines: {node: '>=4'} + normalize-url@2.0.1: dependencies: prepend-http: 2.0.0 query-string: 5.1.1 sort-keys: 2.0.0 - dev: true - /normalize-url@4.5.1: - resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} - engines: {node: '>=8'} - dev: true + normalize-url@4.5.1: {} - /normalize-url@6.1.0: - resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} - engines: {node: '>=10'} - dev: true + normalize-url@6.1.0: {} - /normalize-url@8.0.0: - resolution: {integrity: sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==} - engines: {node: '>=14.16'} - dev: true + normalize-url@8.0.0: {} - /npm-conf@1.1.3: - resolution: {integrity: sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==} - engines: {node: '>=4'} - requiresBuild: true + npm-conf@1.1.3: dependencies: config-chain: 1.1.13 pify: 3.0.0 - dev: true - /npm-package-arg@10.1.0: - resolution: {integrity: sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + npm-package-arg@10.1.0: dependencies: hosted-git-info: 6.1.1 proc-log: 3.0.0 semver: 7.5.4 validate-npm-package-name: 5.0.0 - dev: true - /npm-run-path@2.0.2: - resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} - engines: {node: '>=4'} + npm-run-path@2.0.2: dependencies: path-key: 2.0.1 - dev: true - /npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} + npm-run-path@4.0.1: dependencies: path-key: 3.1.1 - dev: true - /npm-run-path@5.1.0: - resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm-run-path@5.1.0: dependencies: path-key: 4.0.0 - dev: true - /npmlog@6.0.2: - resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + npmlog@6.0.2: dependencies: are-we-there-yet: 3.0.1 console-control-strings: 1.1.0 gauge: 4.0.4 set-blocking: 2.0.0 - dev: true - /nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 - dev: true - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: true + object-assign@4.1.1: {} - /object-inspect@1.13.1: - resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + object-inspect@1.13.1: {} - /object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - requiresBuild: true - dev: true + object-keys@1.1.1: optional: true - /object.omit@3.0.0: - resolution: {integrity: sha512-EO+BCv6LJfu+gBIF3ggLicFebFLN5zqzz/WWJlMFfkMyGth+oBkhxzDl0wx2W4GkLzuQs/FsSkXZb2IMWQqmBQ==} - engines: {node: '>=0.10.0'} + object.omit@3.0.0: dependencies: is-extendable: 1.0.1 - dev: false - /object.pick@1.3.0: - resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} - engines: {node: '>=0.10.0'} + object.pick@1.3.0: dependencies: isobject: 3.0.1 - dev: false - /objection@3.0.1(knex@2.3.0): - resolution: {integrity: sha512-rqNnyQE+C55UHjdpTOJEKQHJGZ/BGtBBtgxdUpKG4DQXRUmqxfmgS/MhPWxB9Pw0mLSVLEltr6soD4c0Sddy0Q==} - engines: {node: '>=12.0.0'} - peerDependencies: - knex: '>=0.95.0' + objection@3.0.1(knex@2.3.0): dependencies: ajv: 8.12.0 db-errors: 0.2.3 knex: 2.3.0(pg@8.11.3) - dev: false - /obliterator@2.0.4: - resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} - dev: false + obliterator@2.0.4: {} - /omggif@1.0.10: - resolution: {integrity: sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==} - dev: true + omggif@1.0.10: {} - /on-exit-leak-free@2.1.2: - resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} - engines: {node: '>=14.0.0'} - dev: false + on-exit-leak-free@2.1.2: {} - /on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 - /on-headers@1.0.2: - resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} - engines: {node: '>= 0.8'} + on-headers@1.0.2: {} - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + once@1.4.0: dependencies: wrappy: 1.0.2 - /one-time@1.0.0: - resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + one-time@1.0.0: dependencies: fn.name: 1.1.0 - dev: false - /onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 - dev: true - /onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} + onetime@6.0.0: dependencies: mimic-fn: 4.0.0 - dev: true - /open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} + open@8.4.2: dependencies: define-lazy-prop: 2.0.0 is-docker: 2.2.1 is-wsl: 2.2.0 - /open@9.1.0: - resolution: {integrity: sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==} - engines: {node: '>=14.16'} + open@9.1.0: dependencies: default-browser: 4.0.0 define-lazy-prop: 3.0.0 is-inside-container: 1.0.0 is-wsl: 2.2.0 - dev: true - /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} - engines: {node: '>= 0.8.0'} + optionator@0.9.3: dependencies: '@aashutoshrathi/word-wrap': 1.2.6 deep-is: 0.1.4 @@ -10663,11 +14145,8 @@ packages: levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 - dev: true - /ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} + ora@5.4.1: dependencies: bl: 4.1.0 chalk: 4.1.2 @@ -10678,405 +14157,220 @@ packages: log-symbols: 4.1.0 strip-ansi: 6.0.1 wcwidth: 1.0.1 - dev: true - /orderedmap@2.1.1: - resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} - dev: false + orderedmap@2.1.1: {} - /os-filter-obj@2.0.0: - resolution: {integrity: sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==} - engines: {node: '>=4'} + os-filter-obj@2.0.0: dependencies: arch: 2.2.0 - dev: true - /os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - dev: true + os-tmpdir@1.0.2: {} - /otplib@12.0.1: - resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} + otplib@12.0.1: dependencies: '@otplib/core': 12.0.1 '@otplib/preset-default': 12.0.1 '@otplib/preset-v11': 12.0.1 - dev: false - /ow@0.17.0: - resolution: {integrity: sha512-i3keDzDQP5lWIe4oODyDFey1qVrq2hXKTuTH2VpqwpYtzPiKZt2ziRI4NBQmgW40AnV5Euz17OyWweCb+bNEQA==} - engines: {node: '>=10'} + ow@0.17.0: dependencies: type-fest: 0.11.0 - dev: true - /p-cancelable@0.3.0: - resolution: {integrity: sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==} - engines: {node: '>=4'} - dev: true + p-cancelable@0.3.0: {} - /p-cancelable@0.4.1: - resolution: {integrity: sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==} - engines: {node: '>=4'} - dev: true + p-cancelable@0.4.1: {} - /p-cancelable@1.1.0: - resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} - engines: {node: '>=6'} - dev: true + p-cancelable@1.1.0: {} - /p-cancelable@2.1.1: - resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} - engines: {node: '>=8'} - dev: true + p-cancelable@2.1.1: {} - /p-cancelable@3.0.0: - resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} - engines: {node: '>=12.20'} - dev: true + p-cancelable@3.0.0: {} - /p-event@1.3.0: - resolution: {integrity: sha512-hV1zbA7gwqPVFcapfeATaNjQ3J0NuzorHPyG8GPL9g/Y/TplWVBVoCKCXL6Ej2zscrCEv195QNWJXuBH6XZuzA==} - engines: {node: '>=4'} + p-event@1.3.0: dependencies: p-timeout: 1.2.1 - dev: true - /p-event@2.3.1: - resolution: {integrity: sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==} - engines: {node: '>=6'} + p-event@2.3.1: dependencies: p-timeout: 2.0.1 - dev: true - /p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} - dev: true + p-finally@1.0.0: {} - /p-is-promise@1.1.0: - resolution: {integrity: sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==} - engines: {node: '>=4'} - dev: true + p-is-promise@1.1.0: {} - /p-limit@1.3.0: - resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} - engines: {node: '>=4'} + p-limit@1.3.0: dependencies: p-try: 1.0.0 - dev: true - /p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} + p-limit@2.3.0: dependencies: p-try: 2.2.0 - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - dev: true - /p-limit@4.0.0: - resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + p-limit@4.0.0: dependencies: yocto-queue: 1.0.0 - dev: true - /p-locate@2.0.0: - resolution: {integrity: sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==} - engines: {node: '>=4'} + p-locate@2.0.0: dependencies: p-limit: 1.3.0 - dev: true - /p-locate@3.0.0: - resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} - engines: {node: '>=6'} + p-locate@3.0.0: dependencies: p-limit: 2.3.0 - dev: true - /p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} + p-locate@4.1.0: dependencies: p-limit: 2.3.0 - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} + p-locate@5.0.0: dependencies: p-limit: 3.1.0 - dev: true - /p-map-series@1.0.0: - resolution: {integrity: sha512-4k9LlvY6Bo/1FcIdV33wqZQES0Py+iKISU9Uc8p8AjWoZPnFKMpVIVD3s0EYn4jzLh1I+WeUZkJ0Yoa4Qfw3Kg==} - engines: {node: '>=4'} + p-map-series@1.0.0: dependencies: p-reduce: 1.0.0 - dev: true - /p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} + p-map@4.0.0: dependencies: aggregate-error: 3.1.0 - dev: true - /p-pipe@4.0.0: - resolution: {integrity: sha512-HkPfFklpZQPUKBFXzKFB6ihLriIHxnmuQdK9WmLDwe4hf2PdhhfWT/FJa+pc3bA1ywvKXtedxIRmd4Y7BTXE4w==} - engines: {node: '>=12'} - dev: true + p-pipe@4.0.0: {} - /p-reduce@1.0.0: - resolution: {integrity: sha512-3Tx1T3oM1xO/Y8Gj0sWyE78EIJZ+t+aEmXUdvQgvGmSMri7aPTHoovbXEreWKkL5j21Er60XAWLTzKbAKYOujQ==} - engines: {node: '>=4'} - dev: true + p-reduce@1.0.0: {} - /p-timeout@1.2.1: - resolution: {integrity: sha512-gb0ryzr+K2qFqFv6qi3khoeqMZF/+ajxQipEF6NteZVnvz9tzdsfAVj3lYtn1gAXvH5lfLwfxEII799gt/mRIA==} - engines: {node: '>=4'} + p-timeout@1.2.1: dependencies: p-finally: 1.0.0 - dev: true - /p-timeout@2.0.1: - resolution: {integrity: sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==} - engines: {node: '>=4'} + p-timeout@2.0.1: dependencies: p-finally: 1.0.0 - dev: true - /p-try@1.0.0: - resolution: {integrity: sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==} - engines: {node: '>=4'} - dev: true + p-try@1.0.0: {} - /p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} + p-try@2.2.0: {} - /package-json@8.1.1: - resolution: {integrity: sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==} - engines: {node: '>=14.16'} + package-json@8.1.1: dependencies: got: 12.6.1 registry-auth-token: 5.0.2 registry-url: 6.0.1 semver: 7.5.4 - dev: true - /packet-reader@1.0.0: - resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} - dev: false + packet-reader@1.0.0: {} - /pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - dev: true + pako@1.0.11: {} - /param-case@2.1.1: - resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + param-case@2.1.1: dependencies: no-case: 2.3.2 - dev: true - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + parent-module@1.0.1: dependencies: callsites: 3.1.0 - dev: true - /parse-author@2.0.0: - resolution: {integrity: sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==} - engines: {node: '>=0.10.0'} + parse-author@2.0.0: dependencies: author-regex: 1.0.0 - dev: true - /parse-bmfont-ascii@1.0.6: - resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} - dev: true + parse-bmfont-ascii@1.0.6: {} - /parse-bmfont-binary@1.0.6: - resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} - dev: true + parse-bmfont-binary@1.0.6: {} - /parse-bmfont-xml@1.1.4: - resolution: {integrity: sha512-bjnliEOmGv3y1aMEfREMBJ9tfL3WR0i0CKPj61DnSLaoxWR3nLrsQrEbCId/8rF4NyRF0cCqisSVXyQYWM+mCQ==} + parse-bmfont-xml@1.1.4: dependencies: xml-parse-from-string: 1.0.1 xml2js: 0.4.23 - dev: true - /parse-headers@2.0.5: - resolution: {integrity: sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==} - dev: true + parse-headers@2.0.5: {} - /parse-json@2.2.0: - resolution: {integrity: sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==} - engines: {node: '>=0.10.0'} + parse-json@2.2.0: dependencies: error-ex: 1.3.2 - dev: true - /parse-json@4.0.0: - resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} - engines: {node: '>=4'} + parse-json@4.0.0: dependencies: error-ex: 1.3.2 json-parse-better-errors: 1.0.2 - dev: true - - /parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.22.13 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - dev: true - /parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} + parseurl@1.3.3: {} - /path-exists@3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - dev: true + path-exists@3.0.0: {} - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} + path-exists@4.0.0: {} - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true + path-is-absolute@1.0.1: {} - /path-key@2.0.1: - resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} - engines: {node: '>=4'} - dev: true + path-key@2.0.1: {} - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} + path-key@3.1.1: {} - /path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - dev: true + path-key@4.0.0: {} - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-parse@1.0.7: {} - /path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} - engines: {node: '>=16 || 14 >=14.17'} + path-scurry@1.10.1: dependencies: lru-cache: 10.0.1 minipass: 7.0.4 - /path-to-regexp@0.1.7: - resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + path-to-regexp@0.1.7: {} - /path-to-regexp@3.2.0: - resolution: {integrity: sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==} - dev: true + path-to-regexp@3.2.0: {} - /path-type@2.0.0: - resolution: {integrity: sha512-dUnb5dXUf+kzhC/W/F4e5/SkluXIFf5VUHolW1Eg1irn1hGWjPGdsRcvYJ1nD6lhk8Ir7VM0bHJKsYTx8Jx9OQ==} - engines: {node: '>=4'} + path-type@2.0.0: dependencies: pify: 2.3.0 - dev: true - /path-type@3.0.0: - resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} - engines: {node: '>=4'} + path-type@3.0.0: dependencies: pify: 3.0.0 - dev: true - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true + path-type@4.0.0: {} - /pathe@1.1.1: - resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} - dev: true + pathe@1.1.1: {} - /pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - dev: true + pathval@1.1.1: {} - /peek-readable@4.1.0: - resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} - engines: {node: '>=8'} - dev: true + peek-readable@4.1.0: {} - /pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + pend@1.2.0: {} - /pg-cloudflare@1.1.1: - resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} - requiresBuild: true - dev: false + pg-cloudflare@1.1.1: optional: true - /pg-connection-string@2.5.0: - resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==} - dev: false + pg-connection-string@2.5.0: {} - /pg-connection-string@2.6.2: - resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} - dev: false + pg-connection-string@2.6.2: {} - /pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - dev: false + pg-int8@1.0.1: {} - /pg-pool@3.6.1(pg@8.11.3): - resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} - peerDependencies: - pg: '>=8.0' + pg-pool@3.6.1(pg@8.11.3): dependencies: pg: 8.11.3 - dev: false - /pg-protocol@1.6.0: - resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} - dev: false + pg-protocol@1.6.0: {} - /pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} + pg-types@2.2.0: dependencies: pg-int8: 1.0.1 postgres-array: 2.0.0 postgres-bytea: 1.0.0 postgres-date: 1.0.7 postgres-interval: 1.2.0 - dev: false - /pg@8.11.3: - resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} - engines: {node: '>= 8.0.0'} - peerDependencies: - pg-native: '>=3.0.1' - peerDependenciesMeta: - pg-native: - optional: true + pg@8.11.3: dependencies: buffer-writer: 2.0.0 packet-reader: 1.0.0 @@ -11087,90 +14381,46 @@ packages: pgpass: 1.0.5 optionalDependencies: pg-cloudflare: 1.1.1 - dev: false - /pgpass@1.0.5: - resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + pgpass@1.0.5: dependencies: split2: 4.2.0 - dev: false - /phin@2.9.3: - resolution: {integrity: sha512-CzFr90qM24ju5f88quFC/6qohjC144rehe5n6DH900lgXmUe86+xCKc10ev56gRKC4/BkHUoG4uSiQgBiIXwDA==} - dev: true + phin@2.9.3: {} - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + picocolors@1.0.0: {} - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true + picomatch@2.3.1: {} - /pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - dev: true + pify@2.3.0: {} - /pify@3.0.0: - resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} - engines: {node: '>=4'} - requiresBuild: true - dev: true + pify@3.0.0: {} - /pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - dev: true + pify@4.0.1: {} - /pify@5.0.0: - resolution: {integrity: sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==} - engines: {node: '>=10'} - dev: true + pify@5.0.0: {} - /pinia@2.0.36(typescript@5.2.2)(vue@3.2.47): - resolution: {integrity: sha512-4UKApwjlmJH+VuHKgA+zQMddcCb3ezYnyewQ9NVrsDqZ/j9dMv5+rh+1r48whKNdpFkZAWVxhBp5ewYaYX9JcQ==} - peerDependencies: - '@vue/composition-api': ^1.4.0 - typescript: '>=4.4.4' - vue: ^2.6.14 || ^3.2.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - typescript: - optional: true + pinia@2.0.36(typescript@5.2.2)(vue@3.2.47): dependencies: '@vue/devtools-api': 6.5.1 typescript: 5.2.2 vue: 3.2.47 vue-demi: 0.14.6(vue@3.2.47) - /pinkie-promise@2.0.1: - resolution: {integrity: sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==} - engines: {node: '>=0.10.0'} + pinkie-promise@2.0.1: dependencies: pinkie: 2.0.4 - dev: true - /pinkie@2.0.4: - resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} - engines: {node: '>=0.10.0'} - dev: true + pinkie@2.0.4: {} - /pino-abstract-transport@1.1.0: - resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==} + pino-abstract-transport@1.1.0: dependencies: readable-stream: 4.4.2 split2: 4.2.0 - dev: false - /pino-std-serializers@6.2.2: - resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==} - dev: false + pino-std-serializers@6.2.2: {} - /pino@8.16.1: - resolution: {integrity: sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==} - hasBin: true + pino@8.16.1: dependencies: atomic-sleep: 1.0.0 fast-redact: 3.3.0 @@ -11183,139 +14433,76 @@ packages: safe-stable-stringify: 2.4.3 sonic-boom: 3.7.0 thread-stream: 2.4.1 - dev: false - /pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - dev: true + pirates@4.0.6: {} - /pixelmatch@4.0.2: - resolution: {integrity: sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==} - hasBin: true + pixelmatch@4.0.2: dependencies: pngjs: 3.4.0 - dev: true - /pkg-types@1.0.3: - resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} + pkg-types@1.0.3: dependencies: jsonc-parser: 3.2.0 mlly: 1.4.2 pathe: 1.1.1 - dev: true - /plimit-lit@1.6.1: - resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} - engines: {node: '>=12'} + plimit-lit@1.6.1: dependencies: queue-lit: 1.5.2 - dev: true - /plist@3.1.0: - resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} - engines: {node: '>=10.4.0'} + plist@3.1.0: dependencies: '@xmldom/xmldom': 0.8.10 base64-js: 1.5.1 xmlbuilder: 15.1.1 - /png2icons@2.0.1: - resolution: {integrity: sha512-GDEQJr8OG4e6JMp7mABtXFSEpgJa1CCpbQiAR+EjhkHJHnUL9zPPtbOrjsMD8gUbikgv3j7x404b0YJsV3aVFA==} - hasBin: true - dev: true + png2icons@2.0.1: {} - /pngjs@3.4.0: - resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} - engines: {node: '>=4.0.0'} - dev: true + pngjs@3.4.0: {} - /pngjs@5.0.0: - resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} - engines: {node: '>=10.13.0'} - dev: false + pngjs@5.0.0: {} - /pngquant-bin@6.0.1: - resolution: {integrity: sha512-Q3PUyolfktf+hYio6wsg3SanQzEU/v8aICg/WpzxXcuCMRb7H2Q81okfpcEztbMvw25ILjd3a87doj2N9kvbpQ==} - engines: {node: '>=10'} - hasBin: true - requiresBuild: true + pngquant-bin@6.0.1: dependencies: bin-build: 3.0.0 bin-wrapper: 4.1.0 execa: 4.1.0 - dev: true - /postcss-load-config@4.0.1(postcss@8.4.31)(ts-node@10.9.1): - resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} - engines: {node: '>= 14'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true + postcss-load-config@4.0.1(postcss@8.4.31)(ts-node@10.9.1): dependencies: lilconfig: 2.1.0 postcss: 8.4.31 ts-node: 10.9.1(@types/node@20.9.1)(typescript@5.2.2) yaml: 2.3.4 - dev: true - /postcss-selector-parser@6.0.13: - resolution: {integrity: sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==} - engines: {node: '>=4'} + postcss-selector-parser@6.0.13: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - dev: true - /postcss-value-parser@4.2.0: - resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - dev: true + postcss-value-parser@4.2.0: {} - /postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} - engines: {node: ^10 || ^12 || >=14} + postcss@8.4.31: dependencies: nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.0.2 - /postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - dev: false + postgres-array@2.0.0: {} - /postgres-bytea@1.0.0: - resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} - engines: {node: '>=0.10.0'} - dev: false + postgres-bytea@1.0.0: {} - /postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - dev: false + postgres-date@1.0.7: {} - /postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} + postgres-interval@1.2.0: dependencies: xtend: 4.0.2 - dev: false - /potrace@2.1.8: - resolution: {integrity: sha512-V9hI7UMJyEhNZjM8CbZaP/804ZRLgzWkCS9OOYnEZkszzj3zKR/erRdj0uFMcN3pp6x4B+AIZebmkQgGRinG/g==} + potrace@2.1.8: dependencies: jimp: 0.14.0 - dev: true - /prebuild-install@7.1.1: - resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} - engines: {node: '>=10'} - hasBin: true + prebuild-install@7.1.1: dependencies: detect-libc: 2.0.2 expand-template: 2.0.3 @@ -11329,225 +14516,141 @@ packages: simple-get: 4.0.1 tar-fs: 2.1.1 tunnel-agent: 0.6.0 - dev: true - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true + prelude-ls@1.2.1: {} - /prepend-http@1.0.4: - resolution: {integrity: sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==} - engines: {node: '>=0.10.0'} - dev: true + prepend-http@1.0.4: {} - /prepend-http@2.0.0: - resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} - engines: {node: '>=4'} - dev: true + prepend-http@2.0.0: {} - /prettier-linter-helpers@1.0.0: - resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} - engines: {node: '>=6.0.0'} + prettier-linter-helpers@1.0.0: dependencies: fast-diff: 1.3.0 - dev: true - /prettier@3.0.3: - resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} - engines: {node: '>=14'} - hasBin: true - dev: true + prettier@3.0.3: {} - /pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 react-is: 18.2.0 - dev: true - /proc-log@3.0.0: - resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - dev: true + proc-log@3.0.0: {} - /process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - dev: true + process-nextick-args@2.0.1: {} - /process-warning@2.3.0: - resolution: {integrity: sha512-N6mp1+2jpQr3oCFMz6SeHRGbv6Slb20bRhj4v3xR99HqNToAcOe1MFOp4tytyzOfJn+QtN8Rf7U/h2KAn4kC6g==} - dev: false + process-warning@2.3.0: {} - /process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} + process@0.11.10: {} - /progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - dev: true + progress@2.0.3: {} - /prom-client@15.0.0: - resolution: {integrity: sha512-UocpgIrKyA2TKLVZDSfm8rGkL13C19YrQBAiG3xo3aDFWcHedxRxI3z+cIcucoxpSO0h5lff5iv/SXoxyeopeA==} - engines: {node: ^16 || ^18 || >=20} + prom-client@15.0.0: dependencies: '@opentelemetry/api': 1.6.0 tdigest: 0.1.2 - dev: false - /promise-inflight@1.0.1: - resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} - peerDependencies: - bluebird: '*' - peerDependenciesMeta: - bluebird: - optional: true - dev: true + promise-inflight@1.0.1: {} - /promise-retry@2.0.1: - resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} - engines: {node: '>=10'} + promise-retry@2.0.1: dependencies: err-code: 2.0.3 retry: 0.12.0 - dev: true - /prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} + prompts@2.4.2: dependencies: kleur: 3.0.3 sisteransi: 1.0.5 - /prosemirror-changeset@2.2.1: - resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==} + prosemirror-changeset@2.2.1: dependencies: prosemirror-transform: 1.8.0 - dev: false - /prosemirror-collab@1.3.1: - resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + prosemirror-collab@1.3.1: dependencies: prosemirror-state: 1.4.3 - dev: false - /prosemirror-commands@1.5.2: - resolution: {integrity: sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ==} + prosemirror-commands@1.5.2: dependencies: prosemirror-model: 1.18.3 prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 - dev: false - /prosemirror-dropcursor@1.8.1: - resolution: {integrity: sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==} + prosemirror-dropcursor@1.8.1: dependencies: prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 prosemirror-view: 1.29.2 - dev: false - /prosemirror-gapcursor@1.3.2: - resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} + prosemirror-gapcursor@1.3.2: dependencies: prosemirror-keymap: 1.2.2 prosemirror-model: 1.18.3 prosemirror-state: 1.4.3 prosemirror-view: 1.29.2 - dev: false - /prosemirror-history@1.3.2: - resolution: {integrity: sha512-/zm0XoU/N/+u7i5zepjmZAEnpvjDtzoPWW6VmKptcAnPadN/SStsBjMImdCEbb3seiNTpveziPTIrXQbHLtU1g==} + prosemirror-history@1.3.2: dependencies: prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 prosemirror-view: 1.32.4 rope-sequence: 1.3.4 - dev: false - /prosemirror-inputrules@1.3.0: - resolution: {integrity: sha512-z1GRP2vhh5CihYMQYsJSa1cOwXb3SYxALXOIfAkX8nZserARtl9LiL+CEl+T+OFIsXc3mJIHKhbsmRzC0HDAXA==} + prosemirror-inputrules@1.3.0: dependencies: prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 - dev: false - /prosemirror-keymap@1.2.2: - resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==} + prosemirror-keymap@1.2.2: dependencies: prosemirror-state: 1.4.3 w3c-keyname: 2.2.8 - dev: false - /prosemirror-markdown@1.11.2: - resolution: {integrity: sha512-Eu5g4WPiCdqDTGhdSsG9N6ZjACQRYrsAkrF9KYfdMaCmjIApH75aVncsWYOJvEk2i1B3i8jZppv3J/tnuHGiUQ==} + prosemirror-markdown@1.11.2: dependencies: markdown-it: 13.0.2 prosemirror-model: 1.18.3 - dev: false - /prosemirror-menu@1.2.4: - resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==} + prosemirror-menu@1.2.4: dependencies: crelt: 1.0.6 prosemirror-commands: 1.5.2 prosemirror-history: 1.3.2 prosemirror-state: 1.4.3 - dev: false - /prosemirror-model@1.18.3: - resolution: {integrity: sha512-yUVejauEY3F1r7PDy4UJKEGeIU+KFc71JQl5sNvG66CLVdKXRjhWpBW6KMeduGsmGOsw85f6EGrs6QxIKOVILA==} + prosemirror-model@1.18.3: dependencies: orderedmap: 2.1.1 - dev: false - /prosemirror-model@1.19.3: - resolution: {integrity: sha512-tgSnwN7BS7/UM0sSARcW+IQryx2vODKX4MI7xpqY2X+iaepJdKBPc7I4aACIsDV/LTaTjt12Z56MhDr9LsyuZQ==} + prosemirror-model@1.19.3: dependencies: orderedmap: 2.1.1 - dev: false - /prosemirror-schema-basic@1.2.2: - resolution: {integrity: sha512-/dT4JFEGyO7QnNTe9UaKUhjDXbTNkiWTq/N4VpKaF79bBjSExVV2NXmJpcM7z/gD7mbqNjxbmWW5nf1iNSSGnw==} + prosemirror-schema-basic@1.2.2: dependencies: prosemirror-model: 1.19.3 - dev: false - /prosemirror-schema-list@1.3.0: - resolution: {integrity: sha512-Hz/7gM4skaaYfRPNgr421CU4GSwotmEwBVvJh5ltGiffUJwm7C8GfN/Bc6DR1EKEp5pDKhODmdXXyi9uIsZl5A==} + prosemirror-schema-list@1.3.0: dependencies: prosemirror-model: 1.18.3 prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 - dev: false - /prosemirror-state@1.4.3: - resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} + prosemirror-state@1.4.3: dependencies: prosemirror-model: 1.18.3 prosemirror-transform: 1.8.0 prosemirror-view: 1.29.2 - dev: false - /prosemirror-tables@1.3.4: - resolution: {integrity: sha512-z6uLSQ1BLC3rgbGwZmpfb+xkdvD7W/UOsURDfognZFYaTtc0gsk7u/t71Yijp2eLflVpffMk6X0u0+u+MMDvIw==} + prosemirror-tables@1.3.4: dependencies: prosemirror-keymap: 1.2.2 prosemirror-model: 1.18.3 prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 prosemirror-view: 1.29.2 - dev: false - /prosemirror-trailing-node@2.0.7(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2): - resolution: {integrity: sha512-8zcZORYj/8WEwsGo6yVCRXFMOfBo0Ub3hCUvmoWIZYfMP26WqENU0mpEP27w7mt8buZWuGrydBewr0tOArPb1Q==} - peerDependencies: - prosemirror-model: ^1.19.0 - prosemirror-state: ^1.4.2 - prosemirror-view: ^1.31.2 + prosemirror-trailing-node@2.0.7(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2): dependencies: '@remirror/core-constants': 2.0.2 '@remirror/core-helpers': 3.0.0 @@ -11555,209 +14658,126 @@ packages: prosemirror-model: 1.18.3 prosemirror-state: 1.4.3 prosemirror-view: 1.29.2 - dev: false - /prosemirror-transform@1.8.0: - resolution: {integrity: sha512-BaSBsIMv52F1BVVMvOmp1yzD3u65uC3HTzCBQV1WDPqJRQ2LuHKcyfn0jwqodo8sR9vVzMzZyI+Dal5W9E6a9A==} + prosemirror-transform@1.8.0: dependencies: prosemirror-model: 1.18.3 - dev: false - /prosemirror-view@1.29.2: - resolution: {integrity: sha512-T4Wm+eTpTH0N9gBJfJR6iecjRX2hYTKewoJUwa92hQOoEz2bYVZy6sYeN+hfnRR506TRvRcuZYqftp4KA8dN+Q==} + prosemirror-view@1.29.2: dependencies: prosemirror-model: 1.18.3 prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 - dev: false - /prosemirror-view@1.32.4: - resolution: {integrity: sha512-WoT+ZYePp0WQvp5coABAysheZg9WttW3TSEUNgsfDQXmVOJlnjkbFbXicKPvWFLiC0ZjKt1ykbyoVKqhVnCiSQ==} + prosemirror-view@1.32.4: dependencies: prosemirror-model: 1.18.3 prosemirror-state: 1.4.3 prosemirror-transform: 1.8.0 - dev: false - /proto-list@1.2.4: - resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - requiresBuild: true - dev: true + proto-list@1.2.4: {} - /proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 - /proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - dev: false + proxy-from-env@1.1.0: {} - /pseudomap@1.0.2: - resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} - dev: true + pseudomap@1.0.2: {} - /pump@3.0.0: - resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + pump@3.0.0: dependencies: end-of-stream: 1.4.4 once: 1.4.0 - dev: true - /punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} + punycode@2.3.1: {} - /pupa@2.1.1: - resolution: {integrity: sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==} - engines: {node: '>=8'} + pupa@2.1.1: dependencies: escape-goat: 2.1.1 - dev: false - /pupa@3.1.0: - resolution: {integrity: sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==} - engines: {node: '>=12.20'} + pupa@3.1.0: dependencies: escape-goat: 4.0.0 - dev: true - /pure-rand@6.0.4: - resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==} - dev: true + pure-rand@6.0.4: {} - /q@1.5.1: - resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} - engines: {node: '>=0.6.0', teleport: '>=0.2.0'} - dev: true + q@1.5.1: {} - /qrcode@1.5.3: - resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==} - engines: {node: '>=10.13.0'} - hasBin: true + qrcode@1.5.3: dependencies: dijkstrajs: 1.0.3 encode-utf8: 1.0.3 pngjs: 5.0.0 yargs: 15.4.1 - dev: false - /qs@6.11.0: - resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} - engines: {node: '>=0.6'} + qs@6.11.0: dependencies: side-channel: 1.0.4 - /qs@6.11.2: - resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} - engines: {node: '>=0.6'} + qs@6.11.2: dependencies: side-channel: 1.0.4 - dev: false - /query-string@5.1.1: - resolution: {integrity: sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==} - engines: {node: '>=0.10.0'} + query-string@5.1.1: dependencies: decode-uri-component: 0.2.2 object-assign: 4.1.1 strict-uri-encode: 1.1.0 - dev: true - /querystring@0.2.1: - resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==} - engines: {node: '>=0.4.x'} - deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. - dev: false + querystring@0.2.1: {} - /queue-lit@1.5.2: - resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} - engines: {node: '>=12'} - dev: true + queue-lit@1.5.2: {} - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true + queue-microtask@1.2.3: {} - /queue-tick@1.0.1: - resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} - dev: true + queue-tick@1.0.1: {} - /quick-format-unescaped@4.0.4: - resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - dev: false + quick-format-unescaped@4.0.4: {} - /quick-lru@4.0.1: - resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==} - engines: {node: '>=8'} - dev: true + quick-lru@4.0.1: {} - /quick-lru@5.1.1: - resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} - engines: {node: '>=10'} - dev: true + quick-lru@5.1.1: {} - /randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 - /range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} + range-parser@1.2.1: {} - /raw-body@2.5.1: - resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} - engines: {node: '>= 0.8'} + raw-body@2.5.1: dependencies: bytes: 3.1.2 http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 - /raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} + raw-body@2.5.2: dependencies: bytes: 3.1.2 http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 - dev: false - /rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true + rc@1.2.8: dependencies: deep-extend: 0.6.0 ini: 1.3.8 minimist: 1.2.8 strip-json-comments: 2.0.1 - dev: true - /rcedit@3.1.0: - resolution: {integrity: sha512-WRlRdY1qZbu1L11DklT07KuHfRk42l0NFFJdaExELEu4fEQ982bP5Z6OWGPj/wLLIuKRQDCxZJGAwoFsxhZhNA==} - engines: {node: '>= 10.0.0'} + rcedit@3.1.0: dependencies: cross-spawn-windows-exe: 1.2.0 - dev: true - /react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - dev: true + react-is@18.2.0: {} - /read-chunk@4.0.3: - resolution: {integrity: sha512-wOYymxRWkxn3MlStSt7LxrMLRvynHKjzHVQPTCBbT29ViUwsT3EE09dE5iMDDGYQTL/s5TQZvBLuJTeZFeGQ4g==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + read-chunk@4.0.3: dependencies: pify: 5.0.0 - dev: true - /read-config-file@6.3.2: - resolution: {integrity: sha512-M80lpCjnE6Wt6zb98DoW8WHR09nzMSpu8XHtPkiTHrJ5Az9CybfeQhTJ8D7saeBHpGhLPIVyA8lcL6ZmdKwY6Q==} - engines: {node: '>=12.0.0'} + read-config-file@6.3.2: dependencies: config-file-ts: 0.2.4 dotenv: 9.0.2 @@ -11765,71 +14785,48 @@ packages: js-yaml: 4.1.0 json5: 2.2.3 lazy-val: 1.0.5 - dev: true - /read-pkg-up@2.0.0: - resolution: {integrity: sha512-1orxQfbWGUiTn9XsPlChs6rLie/AV9jwZTGmu2NZw/CUDJQchXJFYE0Fq5j7+n558T1JhDWLdhyd1Zj+wLY//w==} - engines: {node: '>=4'} + read-pkg-up@2.0.0: dependencies: find-up: 2.1.0 read-pkg: 2.0.0 - dev: true - /read-pkg-up@3.0.0: - resolution: {integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==} - engines: {node: '>=4'} + read-pkg-up@3.0.0: dependencies: find-up: 2.1.0 read-pkg: 3.0.0 - dev: true - /read-pkg-up@7.0.1: - resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} - engines: {node: '>=8'} + read-pkg-up@7.0.1: dependencies: find-up: 4.1.0 read-pkg: 5.2.0 type-fest: 0.8.1 - dev: true - /read-pkg@2.0.0: - resolution: {integrity: sha512-eFIBOPW7FGjzBuk3hdXEuNSiTZS/xEMlH49HxMyzb0hyPfu4EhVjT2DH32K1hSSmVq4sebAWnZuuY5auISUTGA==} - engines: {node: '>=4'} + read-pkg@2.0.0: dependencies: load-json-file: 2.0.0 normalize-package-data: 2.5.0 path-type: 2.0.0 - dev: true - /read-pkg@3.0.0: - resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} - engines: {node: '>=4'} + read-pkg@3.0.0: dependencies: load-json-file: 4.0.0 normalize-package-data: 2.5.0 path-type: 3.0.0 - dev: true - /read-pkg@5.2.0: - resolution: {integrity: sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==} - engines: {node: '>=8'} + read-pkg@5.2.0: dependencies: '@types/normalize-package-data': 2.4.3 normalize-package-data: 2.5.0 parse-json: 5.2.0 type-fest: 0.6.0 - dev: true - /read-yaml-file@2.1.0: - resolution: {integrity: sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==} - engines: {node: '>=10.13'} + read-yaml-file@2.1.0: dependencies: js-yaml: 4.1.0 strip-bom: 4.0.0 - dev: true - /readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 inherits: 2.0.4 @@ -11838,245 +14835,138 @@ packages: safe-buffer: 5.1.2 string_decoder: 1.1.1 util-deprecate: 1.0.2 - dev: true - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - /readable-stream@4.4.2: - resolution: {integrity: sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readable-stream@4.4.2: dependencies: abort-controller: 3.0.0 buffer: 6.0.3 events: 3.3.0 process: 0.11.10 string_decoder: 1.3.0 - dev: false - - /readable-web-to-node-stream@3.0.2: - resolution: {integrity: sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==} - engines: {node: '>=8'} + + readable-web-to-node-stream@3.0.2: dependencies: readable-stream: 3.6.2 - dev: true - /readdir-glob@1.1.3: - resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdir-glob@1.1.3: dependencies: minimatch: 5.1.6 - dev: true - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} + readdirp@3.6.0: dependencies: picomatch: 2.3.1 - dev: true - /real-require@0.2.0: - resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} - engines: {node: '>= 12.13.0'} - dev: false + real-require@0.2.0: {} - /rechoir@0.8.0: - resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} - engines: {node: '>= 10.13.0'} + rechoir@0.8.0: dependencies: resolve: 1.22.8 - dev: false - /redent@3.0.0: - resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} - engines: {node: '>=8'} + redent@3.0.0: dependencies: indent-string: 4.0.0 strip-indent: 3.0.0 - dev: true - /redis-errors@1.2.0: - resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} - engines: {node: '>=4'} - dev: false + redis-errors@1.2.0: {} - /redis-parser@3.0.0: - resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} - engines: {node: '>=4'} + redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 - dev: false - /redlock@5.0.0-beta.2: - resolution: {integrity: sha512-2RDWXg5jgRptDrB1w9O/JgSZC0j7y4SlaXnor93H/UJm/QyDiFgBKNtrh0TI6oCXqYSaSoXxFh6Sd3VtYfhRXw==} - engines: {node: '>=12'} + redlock@5.0.0-beta.2: dependencies: node-abort-controller: 3.1.1 - dev: false - /reflect-metadata@0.1.13: - resolution: {integrity: sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==} - dev: true + reflect-metadata@0.1.13: {} - /regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - dev: true + regenerator-runtime@0.13.11: {} - /regenerator-runtime@0.14.0: - resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} - dev: true + regenerator-runtime@0.14.0: {} - /register-service-worker@1.7.2: - resolution: {integrity: sha512-CiD3ZSanZqcMPRhtfct5K9f7i3OLCcBBWsJjLh1gW9RO/nS94sVzY59iS+fgYBOBqaBpf4EzfqUF3j9IG+xo8A==} - dev: true + register-service-worker@1.7.2: {} - /registry-auth-token@5.0.2: - resolution: {integrity: sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==} - engines: {node: '>=14'} + registry-auth-token@5.0.2: dependencies: '@pnpm/npm-conf': 2.2.2 - dev: true - /registry-url@6.0.1: - resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} - engines: {node: '>=12'} + registry-url@6.0.1: dependencies: rc: 1.2.8 - dev: true - /relateurl@0.2.7: - resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} - engines: {node: '>= 0.10'} - dev: true + relateurl@0.2.7: {} - /replace-ext@2.0.0: - resolution: {integrity: sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==} - engines: {node: '>= 10'} - dev: true + replace-ext@2.0.0: {} - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} + require-directory@2.1.1: {} - /require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} + require-from-string@2.0.2: {} - /require-main-filename@2.0.0: - resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - dev: false + require-main-filename@2.0.0: {} - /resolve-alpn@1.2.1: - resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} - dev: true + resolve-alpn@1.2.1: {} - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true + resolve-from@4.0.0: {} - /resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} + resolve-from@5.0.0: {} - /resolve-global@1.0.0: - resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} - engines: {node: '>=8'} + resolve-global@1.0.0: dependencies: global-dirs: 0.1.1 - dev: true - /resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true + resolve@1.22.8: dependencies: is-core-module: 2.13.1 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - /responselike@1.0.2: - resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} + responselike@1.0.2: dependencies: lowercase-keys: 1.0.1 - dev: true - /responselike@2.0.1: - resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + responselike@2.0.1: dependencies: lowercase-keys: 2.0.0 - dev: true - /responselike@3.0.0: - resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} - engines: {node: '>=14.16'} + responselike@3.0.0: dependencies: lowercase-keys: 3.0.0 - dev: true - /restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} + restore-cursor@3.1.0: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 - dev: true - /ret@0.2.2: - resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==} - engines: {node: '>=4'} - dev: false + ret@0.2.2: {} - /retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - dev: true + retry@0.12.0: {} - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + reusify@1.0.4: {} - /rfdc@1.3.0: - resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} - dev: false + rfdc@1.3.0: {} - /rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - hasBin: true + rimraf@2.7.1: dependencies: glob: 7.2.3 - dev: true - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true + rimraf@3.0.2: dependencies: glob: 7.2.3 - dev: true - /rimraf@4.4.1: - resolution: {integrity: sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==} - engines: {node: '>=14'} - hasBin: true + rimraf@4.4.1: dependencies: glob: 9.3.5 - dev: false - /rimraf@5.0.5: - resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} - engines: {node: '>=14'} - hasBin: true + rimraf@5.0.5: dependencies: glob: 10.3.10 - dev: true - /roarr@2.15.4: - resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} - engines: {node: '>=8.0'} - requiresBuild: true + roarr@2.15.4: dependencies: boolean: 3.2.0 detect-node: 2.1.0 @@ -12084,174 +14974,95 @@ packages: json-stringify-safe: 5.0.1 semver-compare: 1.0.0 sprintf-js: 1.1.3 - dev: true optional: true - /rollup-plugin-visualizer@5.9.2: - resolution: {integrity: sha512-waHktD5mlWrYFrhOLbti4YgQCn1uR24nYsNuXxg7LkPH8KdTXVWR9DNY1WU0QqokyMixVXJS4J04HNrVTMP01A==} - engines: {node: '>=14'} - hasBin: true - peerDependencies: - rollup: 2.x || 3.x - peerDependenciesMeta: - rollup: - optional: true + rollup-plugin-visualizer@5.9.2: dependencies: open: 8.4.2 picomatch: 2.3.1 source-map: 0.7.4 yargs: 17.7.2 - dev: true - /rollup@2.77.3: - resolution: {integrity: sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==} - engines: {node: '>=10.0.0'} - hasBin: true + rollup@2.77.3: optionalDependencies: fsevents: 2.3.3 - dev: true - /rollup@3.29.4: - resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} - engines: {node: '>=14.18.0', npm: '>=8.0.0'} - hasBin: true + rollup@3.29.4: optionalDependencies: fsevents: 2.3.3 - dev: true - /rope-sequence@1.3.4: - resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - dev: false + rope-sequence@1.3.4: {} - /run-applescript@5.0.0: - resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} - engines: {node: '>=12'} + run-applescript@5.0.0: dependencies: execa: 5.1.1 - dev: true - /run-async@2.4.1: - resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} - engines: {node: '>=0.12.0'} - dev: true + run-async@2.4.1: {} - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 - dev: true - /rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + rxjs@7.8.1: dependencies: tslib: 2.6.2 - dev: true - /safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.1.2: {} - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-buffer@5.2.1: {} - /safe-regex2@2.0.0: - resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==} + safe-regex2@2.0.0: dependencies: ret: 0.2.2 - dev: false - /safe-stable-stringify@2.4.3: - resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} - engines: {node: '>=10'} - dev: false + safe-stable-stringify@2.4.3: {} - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + safer-buffer@2.1.2: {} - /sanitize-filename@1.6.3: - resolution: {integrity: sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==} + sanitize-filename@1.6.3: dependencies: truncate-utf8-bytes: 1.0.2 - dev: true - /sass@1.32.12: - resolution: {integrity: sha512-zmXn03k3hN0KaiVTjohgkg98C3UowhL1/VSGdj4/VAAiMKGQOE80PFPxFP2Kyq0OUskPKcY5lImkhBKEHlypJA==} - engines: {node: '>=8.9.0'} - hasBin: true + sass@1.32.12: dependencies: chokidar: 3.5.3 - dev: true - /sax@1.1.4: - resolution: {integrity: sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==} + sax@1.1.4: {} - /sax@1.3.0: - resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==} + sax@1.3.0: {} - /scule@1.0.0: - resolution: {integrity: sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ==} - dev: true + scule@1.0.0: {} - /secure-json-parse@2.7.0: - resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} - dev: false + secure-json-parse@2.7.0: {} - /seek-bzip@1.0.6: - resolution: {integrity: sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==} - hasBin: true + seek-bzip@1.0.6: dependencies: commander: 2.20.3 - dev: true - /semver-compare@1.0.0: - resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} - requiresBuild: true - dev: true + semver-compare@1.0.0: optional: true - /semver-diff@4.0.0: - resolution: {integrity: sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==} - engines: {node: '>=12'} + semver-diff@4.0.0: dependencies: semver: 7.5.4 - dev: true - /semver-regex@2.0.0: - resolution: {integrity: sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==} - engines: {node: '>=6'} - dev: true + semver-regex@2.0.0: {} - /semver-truncate@1.1.2: - resolution: {integrity: sha512-V1fGg9i4CL3qesB6U0L6XAm4xOJiHmt4QAacazumuasc03BvtFGIMCduv01JWQ69Nv+JST9TqhSCiJoxoY031w==} - engines: {node: '>=0.10.0'} + semver-truncate@1.1.2: dependencies: semver: 5.7.2 - dev: true - /semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - dev: true + semver@5.7.2: {} - /semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - dev: true + semver@6.3.1: {} - /semver@7.0.0: - resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==} - hasBin: true - dev: true + semver@7.0.0: {} - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true + semver@7.5.4: dependencies: lru-cache: 6.0.0 - /send@0.18.0: - resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} - engines: {node: '>= 0.8.0'} + send@0.18.0: dependencies: debug: 2.6.9 depd: 2.0.0 @@ -12269,23 +15080,16 @@ packages: transitivePeerDependencies: - supports-color - /serialize-error@7.0.1: - resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} - engines: {node: '>=10'} - requiresBuild: true + serialize-error@7.0.1: dependencies: type-fest: 0.13.1 - dev: true optional: true - /serialize-javascript@6.0.1: - resolution: {integrity: sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==} + serialize-javascript@6.0.1: dependencies: randombytes: 2.1.0 - /serve-static@1.15.0: - resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} - engines: {node: '>= 0.8.0'} + serve-static@1.15.0: dependencies: encodeurl: 1.0.2 escape-html: 1.0.3 @@ -12294,36 +15098,24 @@ packages: transitivePeerDependencies: - supports-color - /set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-blocking@2.0.0: {} - /set-cookie-parser@2.6.0: - resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==} - dev: false + set-cookie-parser@2.6.0: {} - /set-function-length@1.1.1: - resolution: {integrity: sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==} - engines: {node: '>= 0.4'} + set-function-length@1.1.1: dependencies: define-data-property: 1.1.1 get-intrinsic: 1.2.2 gopd: 1.0.1 has-property-descriptors: 1.0.1 - /setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + setprototypeof@1.2.0: {} - /shallow-clone@3.0.1: - resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} - engines: {node: '>=8'} + shallow-clone@3.0.1: dependencies: kind-of: 6.0.3 - dev: true - /sharp@0.32.6: - resolution: {integrity: sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==} - engines: {node: '>=14.15.0'} - requiresBuild: true + sharp@0.32.6: dependencies: color: 4.2.3 detect-libc: 2.0.2 @@ -12333,132 +15125,81 @@ packages: simple-get: 4.0.1 tar-fs: 3.0.4 tunnel-agent: 0.6.0 - dev: true - /shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} + shebang-command@1.2.0: dependencies: shebang-regex: 1.0.0 - dev: true - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 - /shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - dev: true + shebang-regex@1.0.0: {} - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} + shebang-regex@3.0.0: {} - /shell-quote@1.8.1: - resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} - dev: true + shell-quote@1.8.1: {} - /showdown@2.1.0: - resolution: {integrity: sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==} - hasBin: true + showdown@2.1.0: dependencies: commander: 9.5.0 - dev: false - /side-channel@1.0.4: - resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + side-channel@1.0.4: dependencies: call-bind: 1.0.5 get-intrinsic: 1.2.2 object-inspect: 1.13.1 - /siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - dev: true + siginfo@2.0.0: {} - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@3.0.7: {} - /signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - dev: true + signal-exit@4.1.0: {} - /simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - dev: true + simple-concat@1.0.1: {} - /simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + simple-get@4.0.1: dependencies: decompress-response: 6.0.0 once: 1.4.0 simple-concat: 1.0.1 - dev: true - /simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 - /simple-update-notifier@1.1.0: - resolution: {integrity: sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==} - engines: {node: '>=8.10.0'} + simple-update-notifier@1.1.0: dependencies: semver: 7.0.0 - dev: true - /sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + sisteransi@1.0.5: {} - /slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - dev: true + slash@3.0.0: {} - /slash@4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} - dev: true + slash@4.0.0: {} - /slice-ansi@3.0.0: - resolution: {integrity: sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==} - engines: {node: '>=8'} - requiresBuild: true + slice-ansi@3.0.0: dependencies: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - /slice-ansi@4.0.0: - resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} - engines: {node: '>=10'} + slice-ansi@4.0.0: dependencies: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - /smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - requiresBuild: true - dev: true + smart-buffer@4.2.0: {} - /socket.io-adapter@2.5.2(utf-8-validate@5.0.10): - resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} + socket.io-adapter@2.5.2(utf-8-validate@5.0.10): dependencies: ws: 8.11.0(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate - dev: true - /socket.io-client@4.7.2(utf-8-validate@5.0.10): - resolution: {integrity: sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==} - engines: {node: '>=10.0.0'} + socket.io-client@4.7.2(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.0 debug: 4.3.4 @@ -12468,21 +15209,15 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: true - /socket.io-parser@4.2.4: - resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} - engines: {node: '>=10.0.0'} + socket.io-parser@4.2.4: dependencies: '@socket.io/component-emitter': 3.1.0 debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: true - /socket.io@4.7.2(utf-8-validate@5.0.10): - resolution: {integrity: sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==} - engines: {node: '>=10.2.0'} + socket.io@4.7.2(utf-8-validate@5.0.10): dependencies: accepts: 1.3.8 base64id: 2.0.0 @@ -12495,163 +15230,97 @@ packages: - bufferutil - supports-color - utf-8-validate - dev: true - /socks-proxy-agent@7.0.0: - resolution: {integrity: sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==} - engines: {node: '>= 10'} + socks-proxy-agent@7.0.0: dependencies: agent-base: 6.0.2 debug: 4.3.4 socks: 2.7.1 transitivePeerDependencies: - supports-color - dev: true - /socks@2.7.1: - resolution: {integrity: sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==} - engines: {node: '>= 10.13.0', npm: '>= 3.0.0'} + socks@2.7.1: dependencies: ip: 2.0.0 smart-buffer: 4.2.0 - dev: true - /sonic-boom@3.7.0: - resolution: {integrity: sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==} + sonic-boom@3.7.0: dependencies: atomic-sleep: 1.0.0 - dev: false - /sort-keys-length@1.0.1: - resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} - engines: {node: '>=0.10.0'} + sort-keys-length@1.0.1: dependencies: sort-keys: 1.1.2 - /sort-keys@1.1.2: - resolution: {integrity: sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==} - engines: {node: '>=0.10.0'} + sort-keys@1.1.2: dependencies: is-plain-obj: 1.1.0 - /sort-keys@2.0.0: - resolution: {integrity: sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==} - engines: {node: '>=4'} + sort-keys@2.0.0: dependencies: is-plain-obj: 1.1.0 - dev: true - /source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} + source-map-js@1.0.2: {} - /source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 - dev: true - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} + source-map@0.6.1: {} - /source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} - dev: true + source-map@0.7.4: {} - /source-map@0.8.0-beta.0: - resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} - engines: {node: '>= 8'} + source-map@0.8.0-beta.0: dependencies: whatwg-url: 7.1.0 - dev: true - /sourcemap-codec@1.4.8: - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - deprecated: Please use @jridgewell/sourcemap-codec instead + sourcemap-codec@1.4.8: {} - /spawn-command@0.0.2: - resolution: {integrity: sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==} - dev: true + spawn-command@0.0.2: {} - /spdx-correct@3.2.0: - resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + spdx-correct@3.2.0: dependencies: spdx-expression-parse: 3.0.1 spdx-license-ids: 3.0.16 - dev: true - /spdx-exceptions@2.3.0: - resolution: {integrity: sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==} - dev: true + spdx-exceptions@2.3.0: {} - /spdx-expression-parse@3.0.1: - resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + spdx-expression-parse@3.0.1: dependencies: spdx-exceptions: 2.3.0 spdx-license-ids: 3.0.16 - dev: true - /spdx-license-ids@3.0.16: - resolution: {integrity: sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==} - dev: true + spdx-license-ids@3.0.16: {} - /split2@3.2.2: - resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + split2@3.2.2: dependencies: readable-stream: 3.6.2 - dev: true - /split2@4.2.0: - resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} - engines: {node: '>= 10.x'} + split2@4.2.0: {} - /split@1.0.1: - resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + split@1.0.1: dependencies: through: 2.3.8 - dev: true - /sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: false + sprintf-js@1.0.3: {} - /sprintf-js@1.1.3: - resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} - requiresBuild: true - dev: true + sprintf-js@1.1.3: optional: true - /ssri@9.0.1: - resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + ssri@9.0.1: dependencies: minipass: 3.3.6 - dev: true - /stack-trace@0.0.10: - resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} - dev: false + stack-trace@0.0.10: {} - /stack-trace@1.0.0-pre2: - resolution: {integrity: sha512-2ztBJRek8IVofG9DBJqdy2N5kulaacX30Nz7xmkYF6ale9WBVmIy6mFBchvGX7Vx/MyjBhx+Rcxqrj+dbOnQ6A==} - engines: {node: '>=16'} - dev: true + stack-trace@1.0.0-pre2: {} - /stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - dev: true + stackback@0.0.2: {} - /standard-as-callback@2.1.0: - resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - dev: false + standard-as-callback@2.1.0: {} - /standard-version@9.5.0: - resolution: {integrity: sha512-3zWJ/mmZQsOaO+fOlsa0+QK90pwhNd042qEcw6hKFNoLFs7peGyvPffpEBbK/DSGPbyOvli0mUIFv5A4qTjh2Q==} - engines: {node: '>=10'} - hasBin: true + standard-version@9.5.0: dependencies: chalk: 2.4.2 conventional-changelog: 3.1.25 @@ -12667,160 +15336,91 @@ packages: semver: 7.5.4 stringify-package: 1.0.1 yargs: 16.2.0 - dev: true - /stat-mode@1.0.0: - resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} - engines: {node: '>= 6'} - dev: true + stat-mode@1.0.0: {} - /statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} + statuses@2.0.1: {} - /std-env@3.4.3: - resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==} - dev: true + std-env@3.4.3: {} - /streamx@2.15.2: - resolution: {integrity: sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==} + streamx@2.15.2: dependencies: fast-fifo: 1.3.2 queue-tick: 1.0.1 - dev: true - /strict-uri-encode@1.1.0: - resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==} - engines: {node: '>=0.10.0'} - dev: true + strict-uri-encode@1.1.0: {} - /string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - /string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} + string-width@5.1.2: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 strip-ansi: 7.1.0 - dev: true - /string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 - dev: true - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 - /stringify-package@1.0.1: - resolution: {integrity: sha512-sa4DUQsYciMP1xhKWGuFM04fB0LG/9DlluZoSVywUMRNvzid6XucHK0/90xGxRoHrAaROrcHK1aPKaijCtSrhg==} - deprecated: This module is not used anymore, and has been replaced by @npmcli/package-json - dev: true + stringify-package@1.0.1: {} - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - /strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} + strip-ansi@7.1.0: dependencies: ansi-regex: 6.0.1 - dev: true - /strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - dev: true + strip-bom@3.0.0: {} - /strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - dev: true + strip-bom@4.0.0: {} - /strip-dirs@2.1.0: - resolution: {integrity: sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==} + strip-dirs@2.1.0: dependencies: is-natural-number: 4.0.1 - dev: true - /strip-eof@1.0.0: - resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} - engines: {node: '>=0.10.0'} - dev: true + strip-eof@1.0.0: {} - /strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - dev: true + strip-final-newline@2.0.0: {} - /strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - dev: true + strip-final-newline@3.0.0: {} - /strip-indent@3.0.0: - resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} - engines: {node: '>=8'} + strip-indent@3.0.0: dependencies: min-indent: 1.0.1 - dev: true - - /strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - dev: true - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true + strip-json-comments@2.0.1: {} - /strip-literal@1.3.0: - resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} + strip-json-comments@3.1.1: {} + + strip-literal@1.3.0: dependencies: acorn: 8.11.2 - dev: true - /strip-outer@1.0.1: - resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} - engines: {node: '>=0.10.0'} + strip-outer@1.0.1: dependencies: escape-string-regexp: 1.0.5 - dev: true - /stripe@14.3.0: - resolution: {integrity: sha512-R3s+3ONM1XFOTzbMSIML0tixbkuz+gFY/p1h1Qxd9OUftxS8m+rGeBv4ZnvoVhTUwOokArfzQtQlR2Re9XnyQw==} - engines: {node: '>=12.*'} + stripe@14.3.0: dependencies: '@types/node': 20.8.10 qs: 6.11.2 - dev: false - /strtok3@6.3.0: - resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} - engines: {node: '>=10'} + strtok3@6.3.0: dependencies: '@tokenizer/token': 0.3.0 peek-readable: 4.1.0 - dev: true - /sucrase@3.34.0: - resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} - engines: {node: '>=8'} - hasBin: true + sucrase@3.34.0: dependencies: '@jridgewell/gen-mapping': 0.3.3 commander: 4.1.1 @@ -12829,20 +15429,14 @@ packages: mz: 2.7.0 pirates: 4.0.6 ts-interface-checker: 0.1.13 - dev: true - /sumchecker@3.0.1: - resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} - engines: {node: '>= 8.0'} + sumchecker@3.0.1: dependencies: debug: 4.3.4 transitivePeerDependencies: - supports-color - dev: true - /superagent@8.1.2: - resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} - engines: {node: '>=6.4.0 <13 || >=14'} + superagent@8.1.2: dependencies: component-emitter: 1.3.0 cookiejar: 2.1.4 @@ -12856,37 +15450,22 @@ packages: semver: 7.5.4 transitivePeerDependencies: - supports-color - dev: false - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + supports-color@5.5.0: dependencies: has-flag: 3.0.0 - dev: true - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 - dev: true - /supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} + supports-color@8.1.1: dependencies: has-flag: 4.0.0 - dev: true - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} + supports-preserve-symlinks-flag@1.0.0: {} - /svgo@3.0.2: - resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==} - engines: {node: '>=14.0.0'} - hasBin: true + svgo@3.0.2: dependencies: '@trysound/sax': 0.2.0 commander: 7.2.0 @@ -12894,20 +15473,13 @@ packages: css-tree: 2.3.1 csso: 5.0.5 picocolors: 1.0.0 - dev: true - /synckit@0.8.5: - resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} - engines: {node: ^14.18.0 || >=16.0.0} + synckit@0.8.5: dependencies: '@pkgr/utils': 2.4.2 tslib: 2.6.2 - dev: true - /syncpack@11.2.1: - resolution: {integrity: sha512-WoUtm+ZLmWUvy0cLJy8ds/smVRH3ivI6iANcGTPrsvareCc4SmRVMvr+TwjZyFm0FDGmEfMVsAX7z16+yxL6bQ==} - engines: {node: '>=16'} - hasBin: true + syncpack@11.2.1: dependencies: '@effect/data': 0.17.1 '@effect/io': 0.38.0(@effect/data@0.17.1) @@ -12926,39 +15498,29 @@ packages: semver: 7.5.4 tightrope: 0.1.0 ts-toolbelt: 9.6.0 - dev: true - /table@6.8.1: - resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==} - engines: {node: '>=10.0.0'} + table@6.8.1: dependencies: ajv: 8.12.0 lodash.truncate: 4.4.2 slice-ansi: 4.0.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true - /tar-fs@2.1.1: - resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + tar-fs@2.1.1: dependencies: chownr: 1.1.4 mkdirp-classic: 0.5.3 pump: 3.0.0 tar-stream: 2.2.0 - dev: true - /tar-fs@3.0.4: - resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==} + tar-fs@3.0.4: dependencies: mkdirp-classic: 0.5.3 pump: 3.0.0 tar-stream: 3.1.6 - dev: true - /tar-stream@1.6.2: - resolution: {integrity: sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==} - engines: {node: '>= 0.8.0'} + tar-stream@1.6.2: dependencies: bl: 1.2.3 buffer-alloc: 1.2.0 @@ -12967,30 +15529,22 @@ packages: readable-stream: 2.3.8 to-buffer: 1.1.1 xtend: 4.0.2 - dev: true - /tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} + tar-stream@2.2.0: dependencies: bl: 4.1.0 end-of-stream: 1.4.4 fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 - dev: true - /tar-stream@3.1.6: - resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==} + tar-stream@3.1.6: dependencies: b4a: 1.6.4 fast-fifo: 1.3.2 streamx: 2.15.2 - dev: true - /tar@6.2.0: - resolution: {integrity: sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==} - engines: {node: '>=10'} + tar@6.2.0: dependencies: chownr: 2.0.0 fs-minipass: 2.1.0 @@ -12999,275 +15553,145 @@ packages: mkdirp: 1.0.4 yallist: 4.0.0 - /tarn@3.0.2: - resolution: {integrity: sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==} - engines: {node: '>=8.0.0'} - dev: false + tarn@3.0.2: {} - /tdigest@0.1.2: - resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} + tdigest@0.1.2: dependencies: bintrees: 1.0.2 - dev: false - /temp-dir@1.0.0: - resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} - engines: {node: '>=4'} - dev: true + temp-dir@1.0.0: {} - /temp-file@3.4.0: - resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + temp-file@3.4.0: dependencies: async-exit-hook: 2.0.1 fs-extra: 10.1.0 - dev: true - /tempfile@2.0.0: - resolution: {integrity: sha512-ZOn6nJUgvgC09+doCEF3oB+r3ag7kUvlsXEGX069QRD60p+P3uP7XG9N2/at+EyIRGSN//ZY3LyEotA1YpmjuA==} - engines: {node: '>=4'} + tempfile@2.0.0: dependencies: temp-dir: 1.0.0 uuid: 3.4.0 - dev: true - /text-extensions@1.9.0: - resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} - engines: {node: '>=0.10'} - dev: true + text-extensions@1.9.0: {} - /text-extensions@2.4.0: - resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==} - engines: {node: '>=8'} - dev: true + text-extensions@2.4.0: {} - /text-hex@1.0.0: - resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} - dev: false + text-hex@1.0.0: {} - /text-segmentation@1.0.3: - resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-segmentation@1.0.3: dependencies: utrie: 1.0.2 - dev: false - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true + text-table@0.2.0: {} - /thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} + thenify-all@1.6.0: dependencies: thenify: 3.3.1 - dev: true - /thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thenify@3.3.1: dependencies: any-promise: 1.3.0 - dev: true - /thirty-two@1.0.2: - resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} - engines: {node: '>=0.2.6'} - dev: false + thirty-two@1.0.2: {} - /thread-stream@2.4.1: - resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==} + thread-stream@2.4.1: dependencies: real-require: 0.2.0 - dev: false - /throttle-debounce@3.0.1: - resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} - engines: {node: '>=10'} - dev: false + throttle-debounce@3.0.1: {} - /through2@2.0.5: - resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} + through2@2.0.5: dependencies: readable-stream: 2.3.8 xtend: 4.0.2 - dev: true - /through2@4.0.2: - resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + through2@4.0.2: dependencies: readable-stream: 3.6.2 - /through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - dev: true + through@2.3.8: {} - /tightrope@0.1.0: - resolution: {integrity: sha512-HHHNYdCAIYwl1jOslQBT455zQpdeSo8/A346xpIb/uuqhSg+tCvYNsP5f11QW+z9VZ3vSX8YIfzTApjjuGH63w==} - engines: {node: '>=14'} - dev: true + tightrope@0.1.0: {} - /tildify@2.0.0: - resolution: {integrity: sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==} - engines: {node: '>=8'} - dev: false + tildify@2.0.0: {} - /timed-out@4.0.1: - resolution: {integrity: sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==} - engines: {node: '>=0.10.0'} - dev: true + timed-out@4.0.1: {} - /timm@1.7.1: - resolution: {integrity: sha512-IjZc9KIotudix8bMaBW6QvMuq64BrJWFs1+4V0lXwWGQZwH+LnX87doAYhem4caOEusRP9/g6jVDQmZ8XOk1nw==} - dev: true + timm@1.7.1: {} - /tiny-lru@11.2.5: - resolution: {integrity: sha512-JpqM0K33lG6iQGKiigcwuURAKZlq6rHXfrgeL4/I8/REoyJTGU+tEMszvT/oTRVHG2OiylhGDjqPp1jWMlr3bw==} - engines: {node: '>=12'} - dev: false + tiny-lru@11.2.5: {} - /tinybench@2.5.1: - resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==} - dev: true + tinybench@2.5.1: {} - /tinycolor2@1.6.0: - resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - dev: true + tinycolor2@1.6.0: {} - /tinypool@0.7.0: - resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} - engines: {node: '>=14.0.0'} - dev: true + tinypool@0.7.0: {} - /tinyspy@2.2.0: - resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} - engines: {node: '>=14.0.0'} - dev: true + tinyspy@2.2.0: {} - /tippy.js@6.3.7: - resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + tippy.js@6.3.7: dependencies: '@popperjs/core': 2.11.8 - dev: false - /titleize@3.0.0: - resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} - engines: {node: '>=12'} - dev: true + titleize@3.0.0: {} - /tmp-promise@3.0.3: - resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + tmp-promise@3.0.3: dependencies: tmp: 0.2.1 - dev: true - /tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} + tmp@0.0.33: dependencies: os-tmpdir: 1.0.2 - dev: true - /tmp@0.2.1: - resolution: {integrity: sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==} - engines: {node: '>=8.17.0'} + tmp@0.2.1: dependencies: rimraf: 3.0.2 - dev: true - /to-buffer@1.1.1: - resolution: {integrity: sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==} - dev: true + to-buffer@1.1.1: {} - /to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} + to-fast-properties@2.0.0: {} - /to-readable-stream@1.0.0: - resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} - engines: {node: '>=6'} - dev: true + to-readable-stream@1.0.0: {} - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - dev: true - /toad-cache@3.3.0: - resolution: {integrity: sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==} - engines: {node: '>=12'} - dev: false + toad-cache@3.3.0: {} - /toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} + toidentifier@1.0.1: {} - /token-types@4.2.1: - resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} - engines: {node: '>=10'} + token-types@4.2.1: dependencies: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - dev: true - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@0.0.3: {} - /tr46@1.0.1: - resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tr46@1.0.1: dependencies: punycode: 2.3.1 - dev: true - /tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true + tree-kill@1.2.2: {} - /trim-newlines@3.0.1: - resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} - engines: {node: '>=8'} - dev: true + trim-newlines@3.0.1: {} - /trim-repeated@1.0.0: - resolution: {integrity: sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==} - engines: {node: '>=0.10.0'} + trim-repeated@1.0.0: dependencies: escape-string-regexp: 1.0.5 - dev: true - /triple-beam@1.4.1: - resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} - engines: {node: '>= 14.0.0'} - dev: false + triple-beam@1.4.1: {} - /truncate-utf8-bytes@1.0.2: - resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + truncate-utf8-bytes@1.0.2: dependencies: utf8-byte-length: 1.0.4 - dev: true - /ts-api-utils@1.0.3(typescript@5.2.2): - resolution: {integrity: sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==} - engines: {node: '>=16.13.0'} - peerDependencies: - typescript: '>=4.2.0' + ts-api-utils@1.0.3(typescript@5.2.2): dependencies: typescript: 5.2.2 - dev: true - /ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - dev: true + ts-interface-checker@0.1.13: {} - /ts-node-dev@2.0.0(@types/node@20.9.1)(typescript@5.2.2): - resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} - engines: {node: '>=0.8.0'} - hasBin: true - peerDependencies: - node-notifier: '*' - typescript: '*' - peerDependenciesMeta: - node-notifier: - optional: true + ts-node-dev@2.0.0(@types/node@20.9.1)(typescript@5.2.2): dependencies: chokidar: 3.5.3 dynamic-dedupe: 0.3.0 @@ -13284,21 +15708,8 @@ packages: - '@swc/core' - '@swc/wasm' - '@types/node' - dev: true - /ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2): - resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true + ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.9 @@ -13315,15 +15726,10 @@ packages: typescript: 5.2.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - dev: true - /ts-toolbelt@9.6.0: - resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} - dev: true + ts-toolbelt@9.6.0: {} - /tsc-alias@1.7.1: - resolution: {integrity: sha512-P4+0i+OB0hX17Ca+U6EJ4WZZ+OSupqW32VJ34N7g7+Ch+bwSx1AqYOvDdIVYEKymBh3dfG0t1qxbxPlBbtB1lQ==} - hasBin: true + tsc-alias@1.7.1: dependencies: chokidar: 3.5.3 commander: 9.5.0 @@ -13331,44 +15737,23 @@ packages: mylas: 2.1.13 normalize-path: 3.0.0 plimit-lit: 1.6.1 - dev: true - /tsconfig-paths@4.2.0: - resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} - engines: {node: '>=6'} + tsconfig-paths@4.2.0: dependencies: json5: 2.2.3 minimist: 1.2.8 strip-bom: 3.0.0 - dev: true - /tsconfig@7.0.0: - resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + tsconfig@7.0.0: dependencies: '@types/strip-bom': 3.0.0 '@types/strip-json-comments': 0.0.30 strip-bom: 3.0.0 strip-json-comments: 2.0.1 - dev: true - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + tslib@2.6.2: {} - /tsup@7.2.0(postcss@8.4.31)(ts-node@10.9.1)(typescript@5.2.2): - resolution: {integrity: sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==} - engines: {node: '>=16.14'} - hasBin: true - peerDependencies: - '@swc/core': ^1 - postcss: ^8.4.12 - typescript: '>=4.1.0' - peerDependenciesMeta: - '@swc/core': - optional: true - postcss: - optional: true - typescript: - optional: true + tsup@7.2.0(postcss@8.4.31)(ts-node@10.9.1)(typescript@5.2.2): dependencies: bundle-require: 4.0.2(esbuild@0.18.20) cac: 6.7.14 @@ -13389,72 +15774,33 @@ packages: transitivePeerDependencies: - supports-color - ts-node - dev: true - /tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 - dev: true - /tunnel@0.0.6: - resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} - engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - requiresBuild: true - dev: true + tunnel@0.0.6: optional: true - /turbo-darwin-64@1.10.16: - resolution: {integrity: sha512-+Jk91FNcp9e9NCLYlvDDlp2HwEDp14F9N42IoW3dmHI5ZkGSXzalbhVcrx3DOox3QfiNUHxzWg4d7CnVNCuuMg==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true + turbo-darwin-64@1.10.16: optional: true - /turbo-darwin-arm64@1.10.16: - resolution: {integrity: sha512-jqGpFZipIivkRp/i+jnL8npX0VssE6IAVNKtu573LXtssZdV/S+fRGYA16tI46xJGxSAivrZ/IcgZrV6Jk80bw==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true + turbo-darwin-arm64@1.10.16: optional: true - /turbo-linux-64@1.10.16: - resolution: {integrity: sha512-PpqEZHwLoizQ6sTUvmImcRmACyRk9EWLXGlqceogPZsJ1jTRK3sfcF9fC2W56zkSIzuLEP07k5kl+ZxJd8JMcg==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + turbo-linux-64@1.10.16: optional: true - /turbo-linux-arm64@1.10.16: - resolution: {integrity: sha512-TMjFYz8to1QE0fKVXCIvG/4giyfnmqcQIwjdNfJvKjBxn22PpbjeuFuQ5kNXshUTRaTJihFbuuCcb5OYFNx4uw==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + turbo-linux-arm64@1.10.16: optional: true - /turbo-windows-64@1.10.16: - resolution: {integrity: sha512-+jsf68krs0N66FfC4/zZvioUap/Tq3sPFumnMV+EBo8jFdqs4yehd6+MxIwYTjSQLIcpH8KoNMB0gQYhJRLZzw==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true + turbo-windows-64@1.10.16: optional: true - /turbo-windows-arm64@1.10.16: - resolution: {integrity: sha512-sKm3hcMM1bl0B3PLG4ifidicOGfoJmOEacM5JtgBkYM48ncMHjkHfFY7HrJHZHUnXM4l05RQTpLFoOl/uIo2HQ==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true + turbo-windows-arm64@1.10.16: optional: true - /turbo@1.10.16: - resolution: {integrity: sha512-2CEaK4FIuSZiP83iFa9GqMTQhroW2QryckVqUydmg4tx78baftTOS0O+oDAhvo9r9Nit4xUEtC1RAHoqs6ZEtg==} - hasBin: true + turbo@1.10.16: optionalDependencies: turbo-darwin-64: 1.10.16 turbo-darwin-arm64: 1.10.16 @@ -13462,139 +15808,71 @@ packages: turbo-linux-arm64: 1.10.16 turbo-windows-64: 1.10.16 turbo-windows-arm64: 1.10.16 - dev: true - /turndown@7.1.2: - resolution: {integrity: sha512-ntI9R7fcUKjqBP6QU8rBK2Ehyt8LAzt3UBT9JR9tgo6GtuKvyUzpayWmeMKJw1DPdXzktvtIT8m2mVXz+bL/Qg==} + turndown@7.1.2: dependencies: domino: 2.1.6 - dev: false - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - dev: true - /type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - dev: true + type-detect@4.0.8: {} - /type-fest@0.11.0: - resolution: {integrity: sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==} - engines: {node: '>=8'} - dev: true + type-fest@0.11.0: {} - /type-fest@0.13.1: - resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} - engines: {node: '>=10'} - requiresBuild: true - dev: true + type-fest@0.13.1: optional: true - /type-fest@0.18.1: - resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} - engines: {node: '>=10'} - dev: true + type-fest@0.18.1: {} - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true + type-fest@0.20.2: {} - /type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - dev: true + type-fest@0.21.3: {} - /type-fest@0.6.0: - resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} - engines: {node: '>=8'} - dev: true + type-fest@0.6.0: {} - /type-fest@0.8.1: - resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} - engines: {node: '>=8'} - dev: true + type-fest@0.8.1: {} - /type-fest@1.4.0: - resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} - engines: {node: '>=10'} - dev: true + type-fest@1.4.0: {} - /type-fest@2.19.0: - resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} - engines: {node: '>=12.20'} + type-fest@2.19.0: {} - /type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} + type-is@1.6.18: dependencies: media-typer: 0.3.0 mime-types: 2.1.35 - /typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} + typedarray-to-buffer@3.1.5: dependencies: is-typedarray: 1.0.0 - dev: true - /typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - dev: true + typedarray@0.0.6: {} - /typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} - hasBin: true - dev: true + typescript@4.9.5: {} - /typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} - engines: {node: '>=14.17'} - hasBin: true + typescript@5.2.2: {} - /uc.micro@1.0.6: - resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} - dev: false + uc.micro@1.0.6: {} - /ufo@1.3.1: - resolution: {integrity: sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==} - dev: true + ufo@1.3.1: {} - /uglify-js@3.17.4: - resolution: {integrity: sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==} - engines: {node: '>=0.8.0'} - hasBin: true - requiresBuild: true - dev: true + uglify-js@3.17.4: {} - /uid@2.0.2: - resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} - engines: {node: '>=8'} + uid@2.0.2: dependencies: '@lukeed/csprng': 1.1.0 - dev: true - /unbzip2-stream@1.4.3: - resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + unbzip2-stream@1.4.3: dependencies: buffer: 5.7.1 through: 2.3.8 - dev: true - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - requiresBuild: true + undici-types@5.26.5: {} - /unilogr@0.0.27: - resolution: {integrity: sha512-dI6zln0qOeVSLpEe6rXQKHysJTKzGAXnYlmOkGYbICVqJ70x9rGpq0AxdeFQ4W9t/rSMix/5hwUw7GDoBWhrQw==} - dev: false + unilogr@0.0.27: {} - /unimport@1.3.0: - resolution: {integrity: sha512-fOkrdxglsHd428yegH0wPH/6IfaSdDeMXtdRGn6en/ccyzc2aaoxiUTMrJyc6Bu+xoa18RJRPMfLUHEzjz8atw==} + unimport@1.3.0: dependencies: '@rollup/pluginutils': 5.0.5 escape-string-regexp: 5.0.0 @@ -13609,55 +15887,28 @@ packages: unplugin: 1.5.0 transitivePeerDependencies: - rollup - dev: true - /unique-filename@2.0.1: - resolution: {integrity: sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + unique-filename@2.0.1: dependencies: unique-slug: 3.0.0 - dev: true - /unique-names-generator@4.7.1: - resolution: {integrity: sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==} - engines: {node: '>=8'} - dev: false + unique-names-generator@4.7.1: {} - /unique-slug@3.0.0: - resolution: {integrity: sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + unique-slug@3.0.0: dependencies: imurmurhash: 0.1.4 - dev: true - - /unique-string@3.0.0: - resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} - engines: {node: '>=12'} - dependencies: - crypto-random-string: 4.0.0 - dev: true - /universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - dev: true - - /universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - /unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - - /unplugin-auto-import@0.11.5(@vueuse/core@10.5.0): - resolution: {integrity: sha512-nvbL2AQwLRR8wbHpJ6L1EBVNmjN045RSedTa4NtsGRkSQFXkI1iKHs4dTqJwcKZsnFrZOAKtLPiN1/oQTObLZw==} - engines: {node: '>=14'} - peerDependencies: - '@vueuse/core': '*' - peerDependenciesMeta: - '@vueuse/core': - optional: true + unique-string@3.0.0: + dependencies: + crypto-random-string: 4.0.0 + + universalify@0.1.2: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unplugin-auto-import@0.11.5(@vueuse/core@10.5.0): dependencies: '@antfu/utils': 0.7.6 '@rollup/pluginutils': 5.0.5 @@ -13668,17 +15919,8 @@ packages: unplugin: 1.5.0 transitivePeerDependencies: - rollup - dev: true - /unplugin-vue-components@0.22.12(vue@3.2.47): - resolution: {integrity: sha512-FxyzsuBvMCYPIk+8cgscGBQ345tvwVu+qY5IhE++eorkyvA4Z1TiD/HCiim+Kbqozl10i4K+z+NCa2WO2jexRA==} - engines: {node: '>=14'} - peerDependencies: - '@babel/parser': ^7.15.8 - vue: 2 || 3 - peerDependenciesMeta: - '@babel/parser': - optional: true + unplugin-vue-components@0.22.12(vue@3.2.47): dependencies: '@antfu/utils': 0.7.6 '@rollup/pluginutils': 5.0.5 @@ -13694,42 +15936,28 @@ packages: transitivePeerDependencies: - rollup - supports-color - dev: true - /unplugin@1.5.0: - resolution: {integrity: sha512-9ZdRwbh/4gcm1JTOkp9lAkIDrtOyOxgHmY7cjuwI8L/2RTikMcVG25GsZwNAgRuap3iDw2jeq7eoqtAsz5rW3A==} + unplugin@1.5.0: dependencies: acorn: 8.11.2 chokidar: 3.5.3 webpack-sources: 3.2.3 webpack-virtual-modules: 0.5.0 - dev: true - /untildify@4.0.0: - resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} - engines: {node: '>=8'} + untildify@4.0.0: {} - /unused-filename@2.1.0: - resolution: {integrity: sha512-BMiNwJbuWmqCpAM1FqxCTD7lXF97AvfQC8Kr/DIeA6VtvhJaMDupZ82+inbjl5yVP44PcxOuCSxye1QMS0wZyg==} - engines: {node: '>=8'} + unused-filename@2.1.0: dependencies: modify-filename: 1.1.0 path-exists: 4.0.0 - dev: false - /update-browserslist-db@1.0.13(browserslist@4.22.1): - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' + update-browserslist-db@1.0.13(browserslist@4.22.1): dependencies: browserslist: 4.22.1 escalade: 3.1.1 picocolors: 1.0.0 - /update-notifier@6.0.2: - resolution: {integrity: sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==} - engines: {node: '>=14.16'} + update-notifier@6.0.2: dependencies: boxen: 7.1.1 chalk: 5.3.0 @@ -13745,113 +15973,66 @@ packages: semver: 7.5.4 semver-diff: 4.0.0 xdg-basedir: 5.1.0 - dev: true - /upper-case@1.1.3: - resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} - dev: true + upper-case@1.1.3: {} - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + uri-js@4.4.1: dependencies: punycode: 2.3.1 - /url-join@4.0.1: - resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} - dev: false + url-join@4.0.1: {} - /url-parse-lax@1.0.0: - resolution: {integrity: sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==} - engines: {node: '>=0.10.0'} + url-parse-lax@1.0.0: dependencies: prepend-http: 1.0.4 - dev: true - /url-parse-lax@3.0.0: - resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} - engines: {node: '>=4'} + url-parse-lax@3.0.0: dependencies: prepend-http: 2.0.0 - dev: true - /url-to-options@1.0.1: - resolution: {integrity: sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==} - engines: {node: '>= 4'} - dev: true + url-to-options@1.0.1: {} - /utf-8-validate@5.0.10: - resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} - engines: {node: '>=6.14.2'} - requiresBuild: true + utf-8-validate@5.0.10: dependencies: node-gyp-build: 4.6.1 - /utf8-byte-length@1.0.4: - resolution: {integrity: sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA==} - dev: true + utf8-byte-length@1.0.4: {} - /utif@2.0.1: - resolution: {integrity: sha512-Z/S1fNKCicQTf375lIP9G8Sa1H/phcysstNrrSdZKj1f9g58J4NMgb5IgiEZN9/nLMPDwF0W7hdOe9Qq2IYoLg==} + utif@2.0.1: dependencies: pako: 1.0.11 - dev: true - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + util-deprecate@1.0.2: {} - /utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} + utils-merge@1.0.1: {} - /utrie@1.0.2: - resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + utrie@1.0.2: dependencies: base64-arraybuffer: 1.0.2 - dev: false - /uuid@3.4.0: - resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. - hasBin: true - dev: true + uuid@3.4.0: {} - /v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - dev: true + v8-compile-cache-lib@3.0.1: {} - /validate-npm-package-license@3.0.4: - resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 - dev: true - /validate-npm-package-name@5.0.0: - resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + validate-npm-package-name@5.0.0: dependencies: builtins: 5.0.1 - dev: true - /vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} + vary@1.1.2: {} - /verror@1.10.1: - resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} - engines: {node: '>=0.6.0'} - requiresBuild: true + verror@1.10.1: dependencies: assert-plus: 1.0.0 core-util-is: 1.0.2 extsprintf: 1.4.1 - dev: true optional: true - /vite-node@0.34.6(@types/node@20.8.10): - resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} - engines: {node: '>=v14.18.0'} - hasBin: true + vite-node@0.34.6(@types/node@20.8.10): dependencies: cac: 6.7.14 debug: 4.3.4 @@ -13868,23 +16049,8 @@ packages: - sugarss - supports-color - terser - dev: true - /vite@2.9.16(sass@1.32.12): - resolution: {integrity: sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==} - engines: {node: '>=12.2.0'} - hasBin: true - peerDependencies: - less: '*' - sass: '*' - stylus: '*' - peerDependenciesMeta: - less: - optional: true - sass: - optional: true - stylus: - optional: true + vite@2.9.16(sass@1.32.12): dependencies: esbuild: 0.14.54 postcss: 8.4.31 @@ -13893,35 +16059,8 @@ packages: sass: 1.32.12 optionalDependencies: fsevents: 2.3.3 - dev: true - /vite@4.5.0(@types/node@20.8.10): - resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} - engines: {node: ^14.18.0 || >=16.0.0} - hasBin: true - peerDependencies: - '@types/node': '>= 14' - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true + vite@4.5.0(@types/node@20.8.10): dependencies: '@types/node': 20.8.10 esbuild: 0.18.20 @@ -13929,38 +16068,8 @@ packages: rollup: 3.29.4 optionalDependencies: fsevents: 2.3.3 - dev: true - /vitest@0.34.6: - resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} - engines: {node: '>=v14.18.0'} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@vitest/browser': '*' - '@vitest/ui': '*' - happy-dom: '*' - jsdom: '*' - playwright: '*' - safaridriver: '*' - webdriverio: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - playwright: - optional: true - safaridriver: - optional: true - webdriverio: - optional: true + vitest@0.34.6: dependencies: '@types/chai': 4.3.9 '@types/chai-subset': 1.3.4 @@ -13994,27 +16103,12 @@ packages: - sugarss - supports-color - terser - dev: true - /vue-demi@0.14.6(vue@3.2.47): - resolution: {integrity: sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true + vue-demi@0.14.6(vue@3.2.47): dependencies: vue: 3.2.47 - /vue-eslint-parser@9.3.2(eslint@8.53.0): - resolution: {integrity: sha512-q7tWyCVaV9f8iQyIA5Mkj/S6AoJ9KBN8IeUSf3XEmBrOtxOZnfTg5s4KClbZBCK3GtnT/+RyCLZyDHuZwTuBjg==} - engines: {node: ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: '>=6.0.0' + vue-eslint-parser@9.3.2(eslint@8.53.0): dependencies: debug: 4.3.4 eslint: 8.53.0 @@ -14026,29 +16120,20 @@ packages: semver: 7.5.4 transitivePeerDependencies: - supports-color - dev: true - /vue-i18n@9.6.5(vue@3.2.47): - resolution: {integrity: sha512-dpUEjKHg7pEsaS7ZPPxp1CflaR7bGmsvZJEhnszHPKl9OTNyno5j/DvMtMSo41kpddq4felLA7GK2prjpnXVlw==} - engines: {node: '>= 16'} - peerDependencies: - vue: ^3.0.0 + vue-i18n@9.6.5(vue@3.2.47): dependencies: '@intlify/core-base': 9.6.5 '@intlify/shared': 9.6.5 '@vue/devtools-api': 6.5.1 vue: 3.2.47 - /vue-router@4.2.5(vue@3.2.47): - resolution: {integrity: sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw==} - peerDependencies: - vue: ^3.2.0 + vue-router@4.2.5(vue@3.2.47): dependencies: '@vue/devtools-api': 6.5.1 vue: 3.2.47 - /vue@3.2.47: - resolution: {integrity: sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ==} + vue@3.2.47: dependencies: '@vue/compiler-dom': 3.2.47 '@vue/compiler-sfc': 3.2.47 @@ -14056,111 +16141,69 @@ packages: '@vue/server-renderer': 3.2.47(vue@3.2.47) '@vue/shared': 3.2.47 - /w3c-keyname@2.2.8: - resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - dev: false + w3c-keyname@2.2.8: {} - /wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + wcwidth@1.0.1: dependencies: defaults: 1.0.4 - dev: true - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@3.0.1: {} - /webidl-conversions@4.0.2: - resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} - dev: true + webidl-conversions@4.0.2: {} - /webpack-merge@5.10.0: - resolution: {integrity: sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==} - engines: {node: '>=10.0.0'} + webpack-merge@5.10.0: dependencies: clone-deep: 4.0.1 flat: 5.0.2 wildcard: 2.0.1 - dev: true - /webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} - engines: {node: '>=10.13.0'} - dev: true + webpack-sources@3.2.3: {} - /webpack-virtual-modules@0.5.0: - resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} - dev: true + webpack-virtual-modules@0.5.0: {} - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - /whatwg-url@7.1.0: - resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 tr46: 1.0.1 webidl-conversions: 4.0.2 - dev: true - /which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - dev: false + which-module@2.0.1: {} - /which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true + which@1.3.1: dependencies: isexe: 2.0.0 - dev: true - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true + which@2.0.2: dependencies: isexe: 2.0.0 - /why-is-node-running@2.2.2: - resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} - engines: {node: '>=8'} - hasBin: true + why-is-node-running@2.2.2: dependencies: siginfo: 2.0.0 stackback: 0.0.2 - dev: true - /wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + wide-align@1.1.5: dependencies: string-width: 4.2.3 - dev: true - /widest-line@4.0.1: - resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} - engines: {node: '>=12'} + widest-line@4.0.1: dependencies: string-width: 5.1.2 - dev: true - /wildcard@2.0.1: - resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} - dev: true + wildcard@2.0.1: {} - /winston-transport@4.6.0: - resolution: {integrity: sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==} - engines: {node: '>= 12.0.0'} + winston-transport@4.6.0: dependencies: logform: 2.6.0 readable-stream: 3.6.2 triple-beam: 1.4.1 - dev: false - /winston@3.11.0: - resolution: {integrity: sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==} - engines: {node: '>= 12.0.0'} + winston@3.11.0: dependencies: '@colors/colors': 1.6.0 '@dabh/diagnostics': 2.0.3 @@ -14173,127 +16216,72 @@ packages: stack-trace: 0.0.10 triple-beam: 1.4.1 winston-transport: 4.6.0 - dev: false - /wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - dev: true + wordwrap@1.0.0: {} - /wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - /wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - /wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} + wrap-ansi@8.1.0: dependencies: ansi-styles: 6.2.1 string-width: 5.1.2 strip-ansi: 7.1.0 - dev: true - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + wrappy@1.0.2: {} - /write-file-atomic@3.0.3: - resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} + write-file-atomic@3.0.3: dependencies: imurmurhash: 0.1.4 is-typedarray: 1.0.0 signal-exit: 3.0.7 typedarray-to-buffer: 3.1.5 - dev: true - /ws@8.11.0(utf-8-validate@5.0.10): - resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} - 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 + ws@8.11.0(utf-8-validate@5.0.10): dependencies: utf-8-validate: 5.0.10 - /xdg-basedir@5.1.0: - resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} - engines: {node: '>=12'} - dev: true + xdg-basedir@5.1.0: {} - /xhr@2.6.0: - resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} + xhr@2.6.0: dependencies: global: 4.4.0 is-function: 1.0.2 parse-headers: 2.0.5 xtend: 4.0.2 - dev: true - /xml-name-validator@4.0.0: - resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} - engines: {node: '>=12'} - dev: true + xml-name-validator@4.0.0: {} - /xml-parse-from-string@1.0.1: - resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} - dev: true + xml-parse-from-string@1.0.1: {} - /xml2js@0.4.23: - resolution: {integrity: sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==} - engines: {node: '>=4.0.0'} + xml2js@0.4.23: dependencies: sax: 1.3.0 xmlbuilder: 11.0.1 - dev: true - /xml2js@0.5.0: - resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} - engines: {node: '>=4.0.0'} + xml2js@0.5.0: dependencies: sax: 1.3.0 xmlbuilder: 11.0.1 - dev: false - /xmlbuilder@11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} + xmlbuilder@11.0.1: {} - /xmlbuilder@15.1.1: - resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} - engines: {node: '>=8.0'} + xmlbuilder@15.1.1: {} - /xmlhttprequest-ssl@2.0.0: - resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} - engines: {node: '>=0.4.0'} - dev: true + xmlhttprequest-ssl@2.0.0: {} - /xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} + xtend@4.0.2: {} - /y-prosemirror@1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8): - resolution: {integrity: sha512-LVMtu3qWo0emeYiP+0jgNcvZkqhzE/otOoro+87q0iVKxy/sMKuiJZnokfJdR4cn9qKx0Un5fIxXqbAlR2bFkA==} - peerDependencies: - prosemirror-model: ^1.7.1 - prosemirror-state: ^1.2.3 - prosemirror-view: ^1.9.10 - y-protocols: ^1.0.1 - yjs: ^13.3.2 + y-prosemirror@1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8): dependencies: lib0: 0.2.87 prosemirror-model: 1.18.3 @@ -14301,73 +16289,40 @@ packages: prosemirror-view: 1.29.2 y-protocols: 1.0.6(yjs@13.6.8) yjs: 13.6.8 - dev: false - /y-protocols@1.0.6(yjs@13.6.8): - resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} - engines: {node: '>=16.0.0', npm: '>=8.0.0'} - peerDependencies: - yjs: ^13.0.0 + y-protocols@1.0.6(yjs@13.6.8): dependencies: lib0: 0.2.87 yjs: 13.6.8 - dev: false - /y18n@4.0.3: - resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - dev: false + y18n@4.0.3: {} - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: true + y18n@5.0.8: {} - /yallist@2.1.2: - resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} - dev: true + yallist@2.1.2: {} - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@4.0.0: {} - /yaml-eslint-parser@0.3.2: - resolution: {integrity: sha512-32kYO6kJUuZzqte82t4M/gB6/+11WAuHiEnK7FreMo20xsCKPeFH5tDBU7iWxR7zeJpNnMXfJyXwne48D0hGrg==} + yaml-eslint-parser@0.3.2: dependencies: eslint-visitor-keys: 1.3.0 lodash: 4.17.21 yaml: 1.10.2 - dev: true - /yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - dev: true + yaml@1.10.2: {} - /yaml@2.3.4: - resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} - engines: {node: '>= 14'} - dev: true + yaml@2.3.4: {} - /yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 decamelize: 1.2.0 - dev: false - /yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - dev: true + yargs-parser@20.2.9: {} - /yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - dev: true + yargs-parser@21.1.1: {} - /yargs@15.4.1: - resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} - engines: {node: '>=8'} + yargs@15.4.1: dependencies: cliui: 6.0.0 decamelize: 1.2.0 @@ -14380,11 +16335,8 @@ packages: which-module: 2.0.1 y18n: 4.0.3 yargs-parser: 18.1.3 - dev: false - /yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} + yargs@16.2.0: dependencies: cliui: 7.0.4 escalade: 3.1.1 @@ -14393,11 +16345,8 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 20.2.9 - dev: true - /yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} + yargs@17.7.2: dependencies: cliui: 8.0.1 escalade: 3.1.1 @@ -14406,45 +16355,26 @@ packages: string-width: 4.2.3 y18n: 5.0.8 yargs-parser: 21.1.1 - dev: true - /yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + yauzl@2.10.0: dependencies: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 - /yjs@13.6.8: - resolution: {integrity: sha512-ZPq0hpJQb6f59B++Ngg4cKexDJTvfOgeiv0sBc4sUm8CaBWH7OQC4kcCgrqbjJ/B2+6vO49exvTmYfdlPtcjbg==} - engines: {node: '>=16.0.0', npm: '>=8.0.0'} + yjs@13.6.8: dependencies: lib0: 0.2.87 - dev: false - /yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - dev: true + yn@3.1.1: {} - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true + yocto-queue@0.1.0: {} - /yocto-queue@1.0.0: - resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} - engines: {node: '>=12.20'} - dev: true + yocto-queue@1.0.0: {} - /zip-stream@4.1.1: - resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} - engines: {node: '>= 10'} + zip-stream@4.1.1: dependencies: archiver-utils: 3.0.4 compress-commons: 4.1.2 readable-stream: 3.6.2 - dev: true - /zod@3.22.4: - resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} - dev: false + zod@3.22.4: {} From e2f6fb0652f60d9261640c2d6f25abbeeae7df16 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sat, 14 Dec 2024 22:26:50 -0300 Subject: [PATCH 003/243] chore: replace ts-node with tsx --- apps/app-server/package.json | 4 +- apps/app-server/tsconfig.json | 6 - apps/collab-server/package.json | 4 +- apps/collab-server/tsconfig.json | 6 - apps/manager/package.json | 4 +- apps/manager/tsconfig.json | 6 - apps/realtime-server/package.json | 4 +- apps/realtime-server/tsconfig.json | 6 - apps/scheduler/package.json | 4 +- package.json | 3 +- pnpm-lock.yaml | 799 +++++++++++++++++++---------- 11 files changed, 537 insertions(+), 309 deletions(-) diff --git a/apps/app-server/package.json b/apps/app-server/package.json index 59cf4872..b0020e48 100644 --- a/apps/app-server/package.json +++ b/apps/app-server/package.json @@ -59,12 +59,12 @@ "build:watch": "concurrently \"tsc --build ./tsconfig.json --watch\" \"tsc-alias -p tsconfig.json --watch\"", "bundle": "tsup", "clean": "rimraf --glob ./dist *.tsbuildinfo", - "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", + "dev": "tsx --inspect-brk ./src/index.ts", "fix": "eslint --fix --ext .js,.ts,.vue ./", "npkill": "rimraf --glob ./node_modules", "preinstall": "npx only-allow pnpm", "repo:build": "tsc-alias -p tsconfig.json", "repo:build:watch": "tsc-alias -p tsconfig.json --watch", - "start": "ts-node -r tsconfig-paths/register ./src/index.ts" + "start": "tsx ./src/index.ts" } } diff --git a/apps/app-server/tsconfig.json b/apps/app-server/tsconfig.json index 82d3c79e..9899e002 100644 --- a/apps/app-server/tsconfig.json +++ b/apps/app-server/tsconfig.json @@ -3,12 +3,6 @@ "extends": "@deeplib/tsconfig/base.json", - // Necessary for ts-node-dev to work - "ts-node": { - "files": true - }, - "files": ["src/env.d.ts"], - "compilerOptions": { "baseUrl": ".", diff --git a/apps/collab-server/package.json b/apps/collab-server/package.json index e9450f3a..c8bd5352 100644 --- a/apps/collab-server/package.json +++ b/apps/collab-server/package.json @@ -41,12 +41,12 @@ "build:watch": "concurrently \"tsc --build ./tsconfig.json --watch\" \"tsc-alias -p tsconfig.json --watch\"", "bundle": "tsup", "clean": "rimraf --glob ./dist *.tsbuildinfo", - "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", + "dev": "tsx --inspect-brk ./src/index.ts", "fix": "eslint --fix --ext .js,.ts,.vue ./", "npkill": "rimraf --glob ./node_modules", "preinstall": "npx only-allow pnpm", "repo:build": "tsc-alias -p tsconfig.json", "repo:build:watch": "tsc-alias -p tsconfig.json --watch", - "start": "ts-node -r tsconfig-paths/register ./src/index.ts" + "start": "tsx ./src/index.ts" } } diff --git a/apps/collab-server/tsconfig.json b/apps/collab-server/tsconfig.json index 9ac452e5..57b5e54d 100644 --- a/apps/collab-server/tsconfig.json +++ b/apps/collab-server/tsconfig.json @@ -3,12 +3,6 @@ "extends": "@deeplib/tsconfig/base.json", - // Necessary for ts-node-dev to work - "ts-node": { - "files": true - }, - "files": ["src/types/env.d.ts", "src/types/http.d.ts", "src/types/ws.d.ts"], - "compilerOptions": { "baseUrl": ".", diff --git a/apps/manager/package.json b/apps/manager/package.json index ba69f95a..a4936c77 100644 --- a/apps/manager/package.json +++ b/apps/manager/package.json @@ -25,12 +25,12 @@ "build:watch": "concurrently \"tsc --build ./tsconfig.json --watch\" \"tsc-alias -p tsconfig.json --watch\"", "bundle": "tsup", "clean": "rimraf --glob ./dist *.tsbuildinfo", - "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", + "dev": "tsx --inspect-brk ./src/index.ts", "fix": "eslint --fix --ext .js,.ts,.vue ./", "npkill": "rimraf --glob ./node_modules", "preinstall": "npx only-allow pnpm", "repo:build": "tsc-alias -p tsconfig.json", "repo:build:watch": "tsc-alias -p tsconfig.json --watch", - "start": "ts-node -r tsconfig-paths/register ./src/index.ts" + "start": "tsx ./src/index.ts" } } diff --git a/apps/manager/tsconfig.json b/apps/manager/tsconfig.json index 68507d17..6bfe52f4 100644 --- a/apps/manager/tsconfig.json +++ b/apps/manager/tsconfig.json @@ -3,12 +3,6 @@ "extends": "@deeplib/tsconfig/base.json", - // Necessary for ts-node-dev to work - "ts-node": { - "files": true - }, - "files": ["src/types/env.d.ts"], - "compilerOptions": { "baseUrl": ".", diff --git a/apps/realtime-server/package.json b/apps/realtime-server/package.json index 38bdaaf1..ca3213a3 100644 --- a/apps/realtime-server/package.json +++ b/apps/realtime-server/package.json @@ -34,12 +34,12 @@ "build:watch": "concurrently \"tsc --build ./tsconfig.json --watch\" \"tsc-alias -p tsconfig.json --watch\"", "bundle": "tsup", "clean": "rimraf --glob ./dist *.tsbuildinfo", - "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", + "dev": "tsx --inspect-brk ./src/index.ts", "fix": "eslint --fix --ext .js,.ts,.vue ./", "npkill": "rimraf --glob ./node_modules", "preinstall": "npx only-allow pnpm", "repo:build": "tsc-alias -p tsconfig.json", "repo:build:watch": "tsc-alias -p tsconfig.json --watch", - "start": "ts-node -r tsconfig-paths/register ./src/index.ts" + "start": "tsx ./src/index.ts" } } diff --git a/apps/realtime-server/tsconfig.json b/apps/realtime-server/tsconfig.json index 2b4892b1..53bda32a 100644 --- a/apps/realtime-server/tsconfig.json +++ b/apps/realtime-server/tsconfig.json @@ -3,12 +3,6 @@ "extends": "@deeplib/tsconfig/base.json", - // Necessary for ts-node-dev to work - "ts-node": { - "files": true - }, - "files": ["src/types/env.d.ts", "src/types/http.d.ts", "src/types/ws.d.ts"], - "compilerOptions": { "baseUrl": ".", diff --git a/apps/scheduler/package.json b/apps/scheduler/package.json index 7513baf9..7462eea9 100644 --- a/apps/scheduler/package.json +++ b/apps/scheduler/package.json @@ -23,12 +23,12 @@ "build:watch": "concurrently \"tsc --build ./tsconfig.json --watch\" \"tsc-alias -p tsconfig.json --watch\"", "bundle": "tsup", "clean": "rimraf --glob ./dist *.tsbuildinfo", - "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", + "dev": "tsx --inspect-brk ./src/index.ts", "fix": "eslint --fix --ext .js,.ts,.vue ./", "npkill": "rimraf --glob ./node_modules", "preinstall": "npx only-allow pnpm", "repo:build": "tsc-alias -p tsconfig.json", "repo:build:watch": "tsc-alias -p tsconfig.json --watch", - "start": "ts-node -r tsconfig-paths/register ./src/index.ts" + "start": "tsx ./src/index.ts" } } diff --git a/package.json b/package.json index d60a4aa6..3c96030f 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,9 @@ "rimraf": "^5.0.5", "standard-version": "^9.5.0", "syncpack": "^11.2.1", - "ts-node": "^10.9.1", - "ts-node-dev": "^2.0.0", "tsc-alias": "~1.7.1", "tsconfig-paths": "^4.2.0", + "tsx": "^4.19.2", "turbo": "^1.10.16", "typescript": "^5.2.2", "vitest": "^0.34.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1fb189ec..892bc356 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,7 +27,7 @@ importers: version: 3.1.1 '@typescript-eslint/eslint-plugin': specifier: ^6.10.0 - version: 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2) + version: 6.10.0(@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2))(eslint@8.53.0)(typescript@5.2.2) '@typescript-eslint/parser': specifier: ^6.10.0 version: 6.10.0(eslint@8.53.0)(typescript@5.2.2) @@ -48,13 +48,13 @@ importers: version: 9.0.0(eslint@8.53.0) eslint-plugin-prettier: specifier: ^5.0.1 - version: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.0.3) + version: 5.0.1(eslint-config-prettier@9.0.0(eslint@8.53.0))(eslint@8.53.0)(prettier@3.0.3) eslint-plugin-simple-import-sort: specifier: ^10.0.0 version: 10.0.0(eslint@8.53.0) eslint-plugin-unused-imports: specifier: ^3.0.0 - version: 3.0.0(@typescript-eslint/eslint-plugin@6.10.0)(eslint@8.53.0) + version: 3.0.0(@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2))(eslint@8.53.0)(typescript@5.2.2))(eslint@8.53.0) eslint-plugin-vue: specifier: ^9.18.1 version: 9.18.1(eslint@8.53.0) @@ -73,18 +73,15 @@ importers: syncpack: specifier: ^11.2.1 version: 11.2.1 - ts-node: - specifier: ^10.9.1 - version: 10.9.1(@types/node@20.9.1)(typescript@5.2.2) - ts-node-dev: - specifier: ^2.0.0 - version: 2.0.0(@types/node@20.9.1)(typescript@5.2.2) tsc-alias: specifier: ~1.7.1 version: 1.7.1 tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 + tsx: + specifier: ^4.19.2 + version: 4.19.2 turbo: specifier: ^1.10.16 version: 1.10.16 @@ -93,7 +90,7 @@ importers: version: 5.2.2 vitest: specifier: ^0.34.6 - version: 0.34.6 + version: 0.34.6(sass@1.32.12) apps/app-server: dependencies: @@ -123,7 +120,7 @@ importers: version: 8.0.3 '@fastify/websocket': specifier: ^8.2.0 - version: 8.2.0 + version: 8.2.0(utf-8-validate@5.0.10) '@getbrevo/brevo': specifier: ^1.0.1 version: 1.0.1 @@ -204,7 +201,7 @@ importers: version: 3.2.0 objection: specifier: 3.0.1 - version: 3.0.1(knex@2.3.0) + version: 3.0.1(knex@2.3.0(pg@8.11.3)) otplib: specifier: ^12.0.1 version: 12.0.1 @@ -238,7 +235,7 @@ importers: version: 8.5.3 tsup: specifier: ^7.2.0 - version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1)(typescript@5.2.2) + version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2))(typescript@5.2.2) apps/client: dependencies: @@ -286,64 +283,64 @@ importers: version: 2.1.12(@tiptap/pm@2.1.12) '@tiptap/extension-code-block-lowlight': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/extension-code-block@2.1.12)(@tiptap/pm@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/extension-code-block@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) '@tiptap/extension-collaboration': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(y-prosemirror@1.0.20) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)(y-prosemirror@1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6(yjs@13.6.8))(yjs@13.6.8)) '@tiptap/extension-collaboration-cursor': specifier: npm:@deepnotes/tiptap-extension-collaboration-cursor@^2.0.0-beta.202 - version: '@deepnotes/tiptap-extension-collaboration-cursor@2.0.0-beta.202(@tiptap/core@2.1.12)(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8)' + version: '@deepnotes/tiptap-extension-collaboration-cursor@2.0.0-beta.202(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6(yjs@13.6.8))(yjs@13.6.8)' '@tiptap/extension-highlight': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/extension-horizontal-rule': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) '@tiptap/extension-image': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/extension-link': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) '@tiptap/extension-subscript': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/extension-superscript': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/extension-table': specifier: 2.0.0-beta.202 - version: 2.0.0-beta.202(@tiptap/core@2.1.12) + version: 2.0.0-beta.202(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/extension-table-cell': specifier: 2.0.0-beta.202 - version: 2.0.0-beta.202(@tiptap/core@2.1.12) + version: 2.0.0-beta.202(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/extension-table-header': specifier: 2.0.0-beta.202 - version: 2.0.0-beta.202(@tiptap/core@2.1.12) + version: 2.0.0-beta.202(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/extension-table-row': specifier: 2.0.0-beta.202 - version: 2.0.0-beta.202(@tiptap/core@2.1.12) + version: 2.0.0-beta.202(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/extension-task-item': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) '@tiptap/extension-task-list': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/extension-text-align': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/extension-underline': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/extension-youtube': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) '@tiptap/starter-kit': specifier: ^2.1.12 version: 2.1.12(@tiptap/pm@2.1.12) '@tiptap/vue-3': specifier: ^2.1.12 - version: 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(vue@3.2.47) + version: 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)(vue@3.2.47) '@trpc/client': specifier: ^10.43.1 version: 10.43.1(@trpc/server@10.43.1) @@ -445,10 +442,10 @@ importers: version: 3.3.7 node-fetch: specifier: ^2.7.0 - version: 2.7.0 + version: 2.7.0(encoding@0.1.13) objection: specifier: 3.0.1 - version: 3.0.1(knex@2.3.0) + version: 3.0.1(knex@2.3.0(pg@8.11.3)) pinia: specifier: ~2.0.36 version: 2.0.36(typescript@5.2.2)(vue@3.2.47) @@ -502,7 +499,7 @@ importers: version: 4.2.5(vue@3.2.47) y-prosemirror: specifier: ~1.0.20 - version: 1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8) + version: 1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6(yjs@13.6.8))(yjs@13.6.8) y-protocols: specifier: ^1.0.6 version: 1.0.6(yjs@13.6.8) @@ -515,10 +512,10 @@ importers: devDependencies: '@intlify/vite-plugin-vue-i18n': specifier: ^3.4.0 - version: 3.4.0(vite@2.9.16)(vue-i18n@9.6.5) + version: 3.4.0(vite@2.9.16(sass@1.32.12))(vue-i18n@9.6.5(vue@3.2.47)) '@quasar/app-vite': specifier: npm:@deepnotes/quasar-app-vite@^2.0.0-alpha.42 - version: '@deepnotes/quasar-app-vite@2.0.0-alpha.42(@deepnotes/quasar@2.13.2)(electron-builder@24.4.0)(electron-packager@17.1.1)(eslint@8.53.0)(pinia@2.0.36)(vue-router@4.2.5)(vue@3.2.47)' + version: '@deepnotes/quasar-app-vite@2.0.0-alpha.42(@deepnotes/quasar@2.13.2)(electron-builder@24.4.0)(electron-packager@17.1.1)(eslint@8.53.0)(pinia@2.0.36(typescript@5.2.2)(vue@3.2.47))(rollup@3.29.4)(vue-router@4.2.5(vue@3.2.47))(vue@3.2.47)' '@types/argon2-browser': specifier: ^1.18.3 version: 1.18.3 @@ -575,13 +572,13 @@ importers: version: 17.1.1 tsup: specifier: ^7.2.0 - version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1)(typescript@5.2.2) + version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2))(typescript@5.2.2) unplugin-auto-import: specifier: ^0.11.5 - version: 0.11.5(@vueuse/core@10.5.0) + version: 0.11.5(@vueuse/core@10.5.0(vue@3.2.47))(rollup@3.29.4) unplugin-vue-components: specifier: ^0.22.12 - version: 0.22.12(vue@3.2.47) + version: 0.22.12(@babel/parser@7.23.0)(rollup@3.29.4)(vue@3.2.47) vite: specifier: ^2.9.16 version: 2.9.16(sass@1.32.12) @@ -668,7 +665,7 @@ importers: version: 1.9.9 objection: specifier: 3.0.1 - version: 3.0.1(knex@2.3.0) + version: 3.0.1(knex@2.3.0(pg@8.11.3)) prom-client: specifier: ^15.0.0 version: 15.0.0 @@ -702,7 +699,7 @@ importers: version: 8.5.3 tsup: specifier: ^7.2.0 - version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1)(typescript@5.2.2) + version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2))(typescript@5.2.2) apps/manager: dependencies: @@ -738,7 +735,7 @@ importers: version: 4.17.21 objection: specifier: 3.0.1 - version: 3.0.1(knex@2.3.0) + version: 3.0.1(knex@2.3.0(pg@8.11.3)) stripe: specifier: ^14.3.0 version: 14.3.0 @@ -748,7 +745,7 @@ importers: version: 4.14.200 tsup: specifier: ^7.2.0 - version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1)(typescript@5.2.2) + version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2))(typescript@5.2.2) apps/realtime-server: dependencies: @@ -796,7 +793,7 @@ importers: version: 1.9.9 objection: specifier: 3.0.1 - version: 3.0.1(knex@2.3.0) + version: 3.0.1(knex@2.3.0(pg@8.11.3)) prom-client: specifier: ^15.0.0 version: 15.0.0 @@ -821,7 +818,7 @@ importers: version: 8.5.3 tsup: specifier: ^7.2.0 - version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1)(typescript@5.2.2) + version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2))(typescript@5.2.2) apps/scheduler: dependencies: @@ -854,14 +851,14 @@ importers: version: 4.17.21 objection: specifier: 3.0.1 - version: 3.0.1(knex@2.3.0) + version: 3.0.1(knex@2.3.0(pg@8.11.3)) devDependencies: '@types/lodash': specifier: ^4.14.200 version: 4.14.200 tsup: specifier: ^7.2.0 - version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1)(typescript@5.2.2) + version: 7.2.0(postcss@8.4.31)(ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2))(typescript@5.2.2) packages/@deeplib/data: dependencies: @@ -897,7 +894,7 @@ importers: version: 1.9.9 objection: specifier: 3.0.1 - version: 3.0.1(knex@2.3.0) + version: 3.0.1(knex@2.3.0(pg@8.11.3)) unilogr: specifier: ^0.0.27 version: 0.0.27 @@ -925,7 +922,7 @@ importers: version: 4.17.21 objection: specifier: 3.0.1 - version: 3.0.1(knex@2.3.0) + version: 3.0.1(knex@2.3.0(pg@8.11.3)) pg: specifier: ^8.11.3 version: 8.11.3 @@ -1042,7 +1039,7 @@ importers: version: 3.3.7 objection: specifier: 3.0.1 - version: 3.0.1(knex@2.3.0) + version: 3.0.1(knex@2.3.0(pg@8.11.3)) unilogr: specifier: ^0.0.27 version: 0.0.27 @@ -1058,7 +1055,7 @@ importers: dependencies: objection: specifier: 3.0.1 - version: 3.0.1(knex@2.3.0) + version: 3.0.1(knex@2.3.0(pg@8.11.3)) packages/@stdlib/misc: dependencies: @@ -1080,13 +1077,13 @@ importers: version: 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': specifier: ^10.2.8 - version: 10.2.8(@nestjs/common@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + version: 10.2.8(@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1))(encoding@0.1.13)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/jwt': specifier: ^10.1.1 - version: 10.2.0(@nestjs/common@10.2.8) + version: 10.2.0(@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1)) '@nestjs/testing': specifier: ^10.2.8 - version: 10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8) + version: 10.2.8(@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1))(@nestjs/core@10.2.8(@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1))(encoding@0.1.13)(reflect-metadata@0.1.13)(rxjs@7.8.1)) packages/@stdlib/redlock: dependencies: @@ -1382,66 +1379,132 @@ packages: resolution: {integrity: sha512-3vE9WBQnvlulKylrPbyc+9M4xnD7t1JxuCOF0nrFz00XrrkgbqeqxDf90PNcjLiuB4hAZKr1JooVA6KwsXj94w==} engines: {node: '>=8.6'} + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.18.20': resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} engines: {node: '>=12'} cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.18.20': resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} engines: {node: '>=12'} cpu: [arm] os: [android] + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.18.20': resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} engines: {node: '>=12'} cpu: [x64] os: [android] + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.18.20': resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} engines: {node: '>=12'} cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.18.20': resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} engines: {node: '>=12'} cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} engines: {node: '>=12'} cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} engines: {node: '>=12'} cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.18.20': resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} engines: {node: '>=12'} cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.18.20': resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} engines: {node: '>=12'} cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.18.20': resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} engines: {node: '>=12'} cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.14.54': resolution: {integrity: sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==} engines: {node: '>=12'} @@ -1454,72 +1517,150 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.18.20': resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} engines: {node: '>=12'} cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.18.20': resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} engines: {node: '>=12'} cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.18.20': resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} engines: {node: '>=12'} cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.18.20': resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} engines: {node: '>=12'} cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.18.20': resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} engines: {node: '>=12'} cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-x64@0.18.20': resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} engines: {node: '>=12'} cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} engines: {node: '>=12'} cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.18.20': resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} engines: {node: '>=12'} cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.18.20': resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} engines: {node: '>=12'} cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.18.20': resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} engines: {node: '>=12'} cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.18.20': resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} engines: {node: '>=12'} cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -1595,6 +1736,7 @@ packages: '@humanwhocodes/config-array@0.11.13': resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} @@ -1602,6 +1744,7 @@ packages: '@humanwhocodes/object-schema@2.0.1': resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} + deprecated: Use @eslint/object-schema instead '@hutson/parse-repository-url@3.0.2': resolution: {integrity: sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q==} @@ -2581,12 +2724,6 @@ packages: '@types/slice-ansi@4.0.0': resolution: {integrity: sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==} - '@types/strip-bom@3.0.0': - resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} - - '@types/strip-json-comments@0.0.30': - resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} - '@types/throttle-debounce@2.1.0': resolution: {integrity: sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==} @@ -2909,6 +3046,7 @@ packages: are-we-there-yet@3.0.1: resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -3930,9 +4068,6 @@ packages: duplexer3@0.1.5: resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} - dynamic-dedupe@0.3.0: - resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -3968,6 +4103,7 @@ packages: electron-packager@17.1.1: resolution: {integrity: sha512-r1NDtlajsq7gf2EXgjRfblCVPquvD2yeg+6XGErOKblvxOpDi0iulZLVhgYDP4AEF1P5/HgbX/vwjlkEv7PEIQ==} engines: {node: '>= 14.17.5'} + deprecated: Please use @electron/packager moving forward. There is no API change, just a package name change hasBin: true electron-publish@24.4.0: @@ -4309,6 +4445,11 @@ packages: engines: {node: '>=12'} hasBin: true + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.1: resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} engines: {node: '>=6'} @@ -4781,6 +4922,7 @@ packages: gauge@4.0.4: resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} @@ -4829,6 +4971,9 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + getopts@2.3.0: resolution: {integrity: sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==} @@ -4873,13 +5018,16 @@ packages: glob@7.1.6: resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Glob versions prior to v9 are no longer supported glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} + deprecated: Glob versions prior to v9 are no longer supported glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} @@ -5146,6 +5294,7 @@ packages: inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -6123,6 +6272,7 @@ packages: npmlog@6.0.2: resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + deprecated: This package is no longer supported. nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -6972,6 +7122,9 @@ packages: resolution: {integrity: sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -7005,12 +7158,9 @@ packages: rfdc@1.3.0: resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} - rimraf@2.7.1: - resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} - hasBin: true - rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true rimraf@4.4.1: @@ -7464,6 +7614,7 @@ packages: superagent@8.1.2: resolution: {integrity: sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==} engines: {node: '>=6.4.0 <13 || >=14'} + deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} @@ -7694,17 +7845,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-node-dev@2.0.0: - resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} - engines: {node: '>=0.8.0'} - hasBin: true - peerDependencies: - node-notifier: '*' - typescript: '*' - peerDependenciesMeta: - node-notifier: - optional: true - ts-node@10.9.1: resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true @@ -7730,9 +7870,6 @@ packages: resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} engines: {node: '>=6'} - tsconfig@7.0.0: - resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} - tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} @@ -7752,6 +7889,11 @@ packages: typescript: optional: true + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -8508,7 +8650,7 @@ snapshots: '@types/node': 18.18.8 chalk: 4.1.2 cosmiconfig: 8.3.6(typescript@5.2.2) - cosmiconfig-typescript-loader: 5.0.0(@types/node@18.18.8)(cosmiconfig@8.3.6)(typescript@5.2.2) + cosmiconfig-typescript-loader: 5.0.0(@types/node@18.18.8)(cosmiconfig@8.3.6(typescript@5.2.2))(typescript@5.2.2) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -8562,6 +8704,7 @@ snapshots: '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 + optional: true '@dabh/diagnostics@2.0.3': dependencies: @@ -8588,27 +8731,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@deepnotes/quasar-app-vite@2.0.0-alpha.42(@deepnotes/quasar@2.13.2)(electron-builder@24.4.0)(electron-packager@17.1.1)(eslint@8.53.0)(pinia@2.0.36)(vue-router@4.2.5)(vue@3.2.47)': + '@deepnotes/quasar-app-vite@2.0.0-alpha.42(@deepnotes/quasar@2.13.2)(electron-builder@24.4.0)(electron-packager@17.1.1)(eslint@8.53.0)(pinia@2.0.36(typescript@5.2.2)(vue@3.2.47))(rollup@3.29.4)(vue-router@4.2.5(vue@3.2.47))(vue@3.2.47)': dependencies: '@quasar/render-ssr-error': 1.0.2 - '@quasar/vite-plugin': 1.6.0(@deepnotes/quasar@2.13.2)(@vitejs/plugin-vue@2.3.4)(vite@2.9.16)(vue@3.2.47) + '@quasar/vite-plugin': 1.6.0(@deepnotes/quasar@2.13.2)(@vitejs/plugin-vue@2.3.4(vite@2.9.16(sass@1.32.12))(vue@3.2.47))(vite@2.9.16(sass@1.32.12))(vue@3.2.47) '@rollup/pluginutils': 4.2.1 '@types/chrome': 0.0.208 '@types/compression': 1.7.4 '@types/cordova': 0.0.34 '@types/express': 4.17.20 - '@vitejs/plugin-vue': 2.3.4(vite@2.9.16)(vue@3.2.47) + '@vitejs/plugin-vue': 2.3.4(vite@2.9.16(sass@1.32.12))(vue@3.2.47) archiver: 5.3.2 chokidar: 3.5.3 ci-info: 3.9.0 compression: 1.7.4 cross-spawn: 7.0.3 dot-prop: 6.0.1 - electron-builder: 24.4.0 - electron-packager: 17.1.1 elementtree: 0.1.7 esbuild: 0.14.51 - eslint: 8.53.0 express: 4.18.2 fast-glob: 3.2.12 fs-extra: 11.1.1 @@ -8619,10 +8759,9 @@ snapshots: lodash: 4.17.21 minimist: 1.2.8 open: 8.4.2 - pinia: 2.0.36(typescript@5.2.2)(vue@3.2.47) quasar: '@deepnotes/quasar@2.13.2' register-service-worker: 1.7.2 - rollup-plugin-visualizer: 5.9.2 + rollup-plugin-visualizer: 5.9.2(rollup@3.29.4) sass: 1.32.12 semver: 7.5.4 serialize-javascript: 6.0.1 @@ -8631,6 +8770,11 @@ snapshots: vue: 3.2.47 vue-router: 4.2.5(vue@3.2.47) webpack-merge: 5.10.0 + optionalDependencies: + electron-builder: 24.4.0 + electron-packager: 17.1.1 + eslint: 8.53.0 + pinia: 2.0.36(typescript@5.2.2)(vue@3.2.47) transitivePeerDependencies: - less - rollup @@ -8645,10 +8789,10 @@ snapshots: dependencies: copy-anything: 3.0.5 - '@deepnotes/tiptap-extension-collaboration-cursor@2.0.0-beta.202(@tiptap/core@2.1.12)(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8)': + '@deepnotes/tiptap-extension-collaboration-cursor@2.0.0-beta.202(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6(yjs@13.6.8))(yjs@13.6.8)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - y-prosemirror: 1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8) + y-prosemirror: 1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6(yjs@13.6.8))(yjs@13.6.8) transitivePeerDependencies: - prosemirror-model - prosemirror-state @@ -8667,12 +8811,12 @@ snapshots: dependencies: '@effect/data': 0.17.1 - '@effect/match@0.32.0(@effect/data@0.17.1)(@effect/schema@0.33.1)': + '@effect/match@0.32.0(@effect/data@0.17.1)(@effect/schema@0.33.1(@effect/data@0.17.1)(@effect/io@0.38.0(@effect/data@0.17.1)))': dependencies: '@effect/data': 0.17.1 - '@effect/schema': 0.33.1(@effect/data@0.17.1)(@effect/io@0.38.0) + '@effect/schema': 0.33.1(@effect/data@0.17.1)(@effect/io@0.38.0(@effect/data@0.17.1)) - '@effect/schema@0.33.1(@effect/data@0.17.1)(@effect/io@0.38.0)': + '@effect/schema@0.33.1(@effect/data@0.17.1)(@effect/io@0.38.0(@effect/data@0.17.1))': dependencies: '@effect/data': 0.17.1 '@effect/io': 0.38.0(@effect/data@0.17.1) @@ -8774,75 +8918,147 @@ snapshots: transitivePeerDependencies: - supports-color + '@esbuild/aix-ppc64@0.23.1': + optional: true + '@esbuild/android-arm64@0.18.20': optional: true + '@esbuild/android-arm64@0.23.1': + optional: true + '@esbuild/android-arm@0.18.20': optional: true + '@esbuild/android-arm@0.23.1': + optional: true + '@esbuild/android-x64@0.18.20': optional: true + '@esbuild/android-x64@0.23.1': + optional: true + '@esbuild/darwin-arm64@0.18.20': optional: true + '@esbuild/darwin-arm64@0.23.1': + optional: true + '@esbuild/darwin-x64@0.18.20': optional: true + '@esbuild/darwin-x64@0.23.1': + optional: true + '@esbuild/freebsd-arm64@0.18.20': optional: true + '@esbuild/freebsd-arm64@0.23.1': + optional: true + '@esbuild/freebsd-x64@0.18.20': optional: true + '@esbuild/freebsd-x64@0.23.1': + optional: true + '@esbuild/linux-arm64@0.18.20': optional: true + '@esbuild/linux-arm64@0.23.1': + optional: true + '@esbuild/linux-arm@0.18.20': optional: true + '@esbuild/linux-arm@0.23.1': + optional: true + '@esbuild/linux-ia32@0.18.20': optional: true + '@esbuild/linux-ia32@0.23.1': + optional: true + '@esbuild/linux-loong64@0.14.54': optional: true '@esbuild/linux-loong64@0.18.20': optional: true + '@esbuild/linux-loong64@0.23.1': + optional: true + '@esbuild/linux-mips64el@0.18.20': optional: true + '@esbuild/linux-mips64el@0.23.1': + optional: true + '@esbuild/linux-ppc64@0.18.20': optional: true + '@esbuild/linux-ppc64@0.23.1': + optional: true + '@esbuild/linux-riscv64@0.18.20': optional: true + '@esbuild/linux-riscv64@0.23.1': + optional: true + '@esbuild/linux-s390x@0.18.20': optional: true + '@esbuild/linux-s390x@0.23.1': + optional: true + '@esbuild/linux-x64@0.18.20': optional: true + '@esbuild/linux-x64@0.23.1': + optional: true + '@esbuild/netbsd-x64@0.18.20': optional: true + '@esbuild/netbsd-x64@0.23.1': + optional: true + + '@esbuild/openbsd-arm64@0.23.1': + optional: true + '@esbuild/openbsd-x64@0.18.20': optional: true + '@esbuild/openbsd-x64@0.23.1': + optional: true + '@esbuild/sunos-x64@0.18.20': optional: true + '@esbuild/sunos-x64@0.23.1': + optional: true + '@esbuild/win32-arm64@0.18.20': optional: true + '@esbuild/win32-arm64@0.23.1': + optional: true + '@esbuild/win32-ia32@0.18.20': optional: true + '@esbuild/win32-ia32@0.23.1': + optional: true + '@esbuild/win32-x64@0.18.20': optional: true + '@esbuild/win32-x64@0.23.1': + optional: true + '@eslint-community/eslint-utils@4.4.0(eslint@8.53.0)': dependencies: eslint: 8.53.0 @@ -8901,7 +9117,7 @@ snapshots: ms: 2.1.3 tiny-lru: 11.2.5 - '@fastify/websocket@8.2.0': + '@fastify/websocket@8.2.0(utf-8-validate@5.0.10)': dependencies: fastify-plugin: 4.5.1 ws: 8.11.0(utf-8-validate@5.0.10) @@ -8954,14 +9170,15 @@ snapshots: '@hutson/parse-repository-url@3.0.2': {} - '@intlify/bundle-utils@2.2.2(vue-i18n@9.6.5)': + '@intlify/bundle-utils@2.2.2(vue-i18n@9.6.5(vue@3.2.47))': dependencies: '@intlify/message-compiler': 9.6.5 '@intlify/shared': 9.6.5 jsonc-eslint-parser: 1.4.1 source-map: 0.6.1 - vue-i18n: 9.6.5(vue@3.2.47) yaml-eslint-parser: 0.3.2 + optionalDependencies: + vue-i18n: 9.6.5(vue@3.2.47) '@intlify/core-base@9.6.5': dependencies: @@ -8975,15 +9192,16 @@ snapshots: '@intlify/shared@9.6.5': {} - '@intlify/vite-plugin-vue-i18n@3.4.0(vite@2.9.16)(vue-i18n@9.6.5)': + '@intlify/vite-plugin-vue-i18n@3.4.0(vite@2.9.16(sass@1.32.12))(vue-i18n@9.6.5(vue@3.2.47))': dependencies: - '@intlify/bundle-utils': 2.2.2(vue-i18n@9.6.5) + '@intlify/bundle-utils': 2.2.2(vue-i18n@9.6.5(vue@3.2.47)) '@intlify/shared': 9.6.5 '@rollup/pluginutils': 4.2.1 debug: 4.3.4 fast-glob: 3.3.2 source-map: 0.6.1 vite: 2.9.16(sass@1.32.12) + optionalDependencies: vue-i18n: 9.6.5(vue@3.2.47) transitivePeerDependencies: - supports-color @@ -9145,22 +9363,22 @@ snapshots: '@jimp/utils': 0.14.0 tinycolor2: 1.6.0 - '@jimp/plugin-contain@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)(@jimp/plugin-resize@0.14.0)(@jimp/plugin-scale@0.14.0)': + '@jimp/plugin-contain@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-scale@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0)))': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/plugin-blit': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) - '@jimp/plugin-scale': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0) + '@jimp/plugin-scale': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0)) '@jimp/utils': 0.14.0 - '@jimp/plugin-cover@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-crop@0.14.0)(@jimp/plugin-resize@0.14.0)(@jimp/plugin-scale@0.14.0)': + '@jimp/plugin-cover@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-crop@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-scale@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0)))': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/plugin-crop': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) - '@jimp/plugin-scale': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0) + '@jimp/plugin-scale': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0)) '@jimp/utils': 0.14.0 '@jimp/plugin-crop@0.14.0(@jimp/custom@0.14.0)': @@ -9187,11 +9405,11 @@ snapshots: '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - '@jimp/plugin-flip@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-rotate@0.14.0)': + '@jimp/plugin-flip@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-rotate@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-crop@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0)))': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 - '@jimp/plugin-rotate': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)(@jimp/plugin-crop@0.14.0)(@jimp/plugin-resize@0.14.0) + '@jimp/plugin-rotate': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-crop@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0)) '@jimp/utils': 0.14.0 '@jimp/plugin-gaussian@0.14.0(@jimp/custom@0.14.0)': @@ -9218,7 +9436,7 @@ snapshots: '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - '@jimp/plugin-print@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)': + '@jimp/plugin-print@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0(@jimp/custom@0.14.0))': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 @@ -9232,7 +9450,7 @@ snapshots: '@jimp/custom': 0.14.0 '@jimp/utils': 0.14.0 - '@jimp/plugin-rotate@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)(@jimp/plugin-crop@0.14.0)(@jimp/plugin-resize@0.14.0)': + '@jimp/plugin-rotate@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-crop@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0))': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 @@ -9241,14 +9459,14 @@ snapshots: '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) '@jimp/utils': 0.14.0 - '@jimp/plugin-scale@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0)': + '@jimp/plugin-scale@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0))': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) '@jimp/utils': 0.14.0 - '@jimp/plugin-shadow@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blur@0.14.0)(@jimp/plugin-resize@0.14.0)': + '@jimp/plugin-shadow@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blur@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0))': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 @@ -9256,7 +9474,7 @@ snapshots: '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) '@jimp/utils': 0.14.0 - '@jimp/plugin-threshold@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-color@0.14.0)(@jimp/plugin-resize@0.14.0)': + '@jimp/plugin-threshold@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-color@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0))': dependencies: '@babel/runtime': 7.23.2 '@jimp/custom': 0.14.0 @@ -9272,23 +9490,23 @@ snapshots: '@jimp/plugin-blur': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-circle': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-color': 0.14.0(@jimp/custom@0.14.0) - '@jimp/plugin-contain': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)(@jimp/plugin-resize@0.14.0)(@jimp/plugin-scale@0.14.0) - '@jimp/plugin-cover': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-crop@0.14.0)(@jimp/plugin-resize@0.14.0)(@jimp/plugin-scale@0.14.0) + '@jimp/plugin-contain': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-scale@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0))) + '@jimp/plugin-cover': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-crop@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-scale@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0))) '@jimp/plugin-crop': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-displace': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-dither': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-fisheye': 0.14.0(@jimp/custom@0.14.0) - '@jimp/plugin-flip': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-rotate@0.14.0) + '@jimp/plugin-flip': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-rotate@0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-crop@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0))) '@jimp/plugin-gaussian': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-invert': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-mask': 0.14.0(@jimp/custom@0.14.0) '@jimp/plugin-normalize': 0.14.0(@jimp/custom@0.14.0) - '@jimp/plugin-print': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0) + '@jimp/plugin-print': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0(@jimp/custom@0.14.0)) '@jimp/plugin-resize': 0.14.0(@jimp/custom@0.14.0) - '@jimp/plugin-rotate': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0)(@jimp/plugin-crop@0.14.0)(@jimp/plugin-resize@0.14.0) - '@jimp/plugin-scale': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0) - '@jimp/plugin-shadow': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blur@0.14.0)(@jimp/plugin-resize@0.14.0) - '@jimp/plugin-threshold': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-color@0.14.0)(@jimp/plugin-resize@0.14.0) + '@jimp/plugin-rotate': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blit@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-crop@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0)) + '@jimp/plugin-scale': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0)) + '@jimp/plugin-shadow': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-blur@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0)) + '@jimp/plugin-threshold': 0.14.0(@jimp/custom@0.14.0)(@jimp/plugin-color@0.14.0(@jimp/custom@0.14.0))(@jimp/plugin-resize@0.14.0(@jimp/custom@0.14.0)) timm: 1.7.1 '@jimp/png@0.14.0(@jimp/custom@0.14.0)': @@ -9341,6 +9559,7 @@ snapshots: dependencies: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 + optional: true '@lukeed/csprng@1.1.0': {} @@ -9389,10 +9608,10 @@ snapshots: tslib: 2.6.2 uid: 2.0.2 - '@nestjs/core@10.2.8(@nestjs/common@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1)': + '@nestjs/core@10.2.8(@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1))(encoding@0.1.13)(reflect-metadata@0.1.13)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nuxtjs/opencollective': 0.3.2 + '@nuxtjs/opencollective': 0.3.2(encoding@0.1.13) fast-safe-stringify: 2.1.1 iterare: 1.2.1 path-to-regexp: 3.2.0 @@ -9403,16 +9622,16 @@ snapshots: transitivePeerDependencies: - encoding - '@nestjs/jwt@10.2.0(@nestjs/common@10.2.8)': + '@nestjs/jwt@10.2.0(@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1))': dependencies: '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) '@types/jsonwebtoken': 9.0.5 jsonwebtoken: 9.0.2 - '@nestjs/testing@10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)': + '@nestjs/testing@10.2.8(@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1))(@nestjs/core@10.2.8(@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1))(encoding@0.1.13)(reflect-metadata@0.1.13)(rxjs@7.8.1))': dependencies: '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1))(encoding@0.1.13)(reflect-metadata@0.1.13)(rxjs@7.8.1) tslib: 2.6.2 '@nodelib/fs.scandir@2.1.5': @@ -9437,11 +9656,11 @@ snapshots: mkdirp: 1.0.4 rimraf: 3.0.2 - '@nuxtjs/opencollective@0.3.2': + '@nuxtjs/opencollective@0.3.2(encoding@0.1.13)': dependencies: chalk: 4.1.2 consola: 2.15.3 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -9522,9 +9741,9 @@ snapshots: dependencies: stack-trace: 1.0.0-pre2 - '@quasar/vite-plugin@1.6.0(@deepnotes/quasar@2.13.2)(@vitejs/plugin-vue@2.3.4)(vite@2.9.16)(vue@3.2.47)': + '@quasar/vite-plugin@1.6.0(@deepnotes/quasar@2.13.2)(@vitejs/plugin-vue@2.3.4(vite@2.9.16(sass@1.32.12))(vue@3.2.47))(vite@2.9.16(sass@1.32.12))(vue@3.2.47)': dependencies: - '@vitejs/plugin-vue': 2.3.4(vite@2.9.16)(vue@3.2.47) + '@vitejs/plugin-vue': 2.3.4(vite@2.9.16(sass@1.32.12))(vue@3.2.47) quasar: '@deepnotes/quasar@2.13.2' vite: 2.9.16(sass@1.32.12) vue: 3.2.47 @@ -9565,11 +9784,13 @@ snapshots: estree-walker: 2.0.2 picomatch: 2.3.1 - '@rollup/pluginutils@5.0.5': + '@rollup/pluginutils@5.0.5(rollup@3.29.4)': dependencies: '@types/estree': 1.0.4 estree-walker: 2.0.2 picomatch: 2.3.1 + optionalDependencies: + rollup: 3.29.4 '@sendgrid/client@7.7.0': dependencies: @@ -9629,138 +9850,138 @@ snapshots: dependencies: '@tiptap/pm': 2.1.12 - '@tiptap/extension-blockquote@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-blockquote@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-bold@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-bold@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-bubble-menu@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': + '@tiptap/extension-bubble-menu@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 tippy.js: 6.3.7 - '@tiptap/extension-bullet-list@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-bullet-list@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-code-block-lowlight@2.1.12(@tiptap/core@2.1.12)(@tiptap/extension-code-block@2.1.12)(@tiptap/pm@2.1.12)': + '@tiptap/extension-code-block-lowlight@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/extension-code-block@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-code-block': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) + '@tiptap/extension-code-block': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - '@tiptap/extension-code-block@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': + '@tiptap/extension-code-block@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - '@tiptap/extension-code@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-code@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-collaboration@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(y-prosemirror@1.0.20)': + '@tiptap/extension-collaboration@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)(y-prosemirror@1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6(yjs@13.6.8))(yjs@13.6.8))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - y-prosemirror: 1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8) + y-prosemirror: 1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6(yjs@13.6.8))(yjs@13.6.8) - '@tiptap/extension-document@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-document@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-dropcursor@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': + '@tiptap/extension-dropcursor@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - '@tiptap/extension-floating-menu@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': + '@tiptap/extension-floating-menu@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 tippy.js: 6.3.7 - '@tiptap/extension-gapcursor@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': + '@tiptap/extension-gapcursor@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - '@tiptap/extension-hard-break@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-hard-break@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-heading@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-heading@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-highlight@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-highlight@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-history@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': + '@tiptap/extension-history@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - '@tiptap/extension-horizontal-rule@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': + '@tiptap/extension-horizontal-rule@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - '@tiptap/extension-image@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-image@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-italic@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-italic@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-link@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': + '@tiptap/extension-link@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 linkifyjs: 4.1.1 - '@tiptap/extension-list-item@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-list-item@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-ordered-list@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-ordered-list@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-paragraph@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-paragraph@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-strike@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-strike@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-subscript@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-subscript@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-superscript@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-superscript@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-table-cell@2.0.0-beta.202(@tiptap/core@2.1.12)': + '@tiptap/extension-table-cell@2.0.0-beta.202(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-table-header@2.0.0-beta.202(@tiptap/core@2.1.12)': + '@tiptap/extension-table-header@2.0.0-beta.202(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-table-row@2.0.0-beta.202(@tiptap/core@2.1.12)': + '@tiptap/extension-table-row@2.0.0-beta.202(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-table@2.0.0-beta.202(@tiptap/core@2.1.12)': + '@tiptap/extension-table@2.0.0-beta.202(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@_ueberdosis/prosemirror-tables': 1.1.3 '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) @@ -9768,28 +9989,28 @@ snapshots: prosemirror-state: 1.4.3 prosemirror-view: 1.29.2 - '@tiptap/extension-task-item@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)': + '@tiptap/extension-task-item@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 - '@tiptap/extension-task-list@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-task-list@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-text-align@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-text-align@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-text@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-text@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-underline@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-underline@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-youtube@2.1.12(@tiptap/core@2.1.12)': + '@tiptap/extension-youtube@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) @@ -9817,32 +10038,32 @@ snapshots: '@tiptap/starter-kit@2.1.12(@tiptap/pm@2.1.12)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-blockquote': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-bold': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-bullet-list': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-code': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-code-block': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) - '@tiptap/extension-document': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-dropcursor': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) - '@tiptap/extension-gapcursor': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) - '@tiptap/extension-hard-break': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-heading': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-history': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) - '@tiptap/extension-horizontal-rule': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) - '@tiptap/extension-italic': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-list-item': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-ordered-list': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-paragraph': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-strike': 2.1.12(@tiptap/core@2.1.12) - '@tiptap/extension-text': 2.1.12(@tiptap/core@2.1.12) + '@tiptap/extension-blockquote': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-bold': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-bullet-list': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-code': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-code-block': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) + '@tiptap/extension-document': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-dropcursor': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) + '@tiptap/extension-gapcursor': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) + '@tiptap/extension-hard-break': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-heading': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-history': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) + '@tiptap/extension-horizontal-rule': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) + '@tiptap/extension-italic': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-list-item': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-ordered-list': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-paragraph': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-strike': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) + '@tiptap/extension-text': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12)) transitivePeerDependencies: - '@tiptap/pm' - '@tiptap/vue-3@2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12)(vue@3.2.47)': + '@tiptap/vue-3@2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12)(vue@3.2.47)': dependencies: '@tiptap/core': 2.1.12(@tiptap/pm@2.1.12) - '@tiptap/extension-bubble-menu': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) - '@tiptap/extension-floating-menu': 2.1.12(@tiptap/core@2.1.12)(@tiptap/pm@2.1.12) + '@tiptap/extension-bubble-menu': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) + '@tiptap/extension-floating-menu': 2.1.12(@tiptap/core@2.1.12(@tiptap/pm@2.1.12))(@tiptap/pm@2.1.12) '@tiptap/pm': 2.1.12 vue: 3.2.47 @@ -9858,13 +10079,17 @@ snapshots: '@trysound/sax@0.2.0': {} - '@tsconfig/node10@1.0.9': {} + '@tsconfig/node10@1.0.9': + optional: true - '@tsconfig/node12@1.0.11': {} + '@tsconfig/node12@1.0.11': + optional: true - '@tsconfig/node14@1.0.3': {} + '@tsconfig/node14@1.0.3': + optional: true - '@tsconfig/node16@1.0.4': {} + '@tsconfig/node16@1.0.4': + optional: true '@types/argon2-browser@1.18.3': {} @@ -10067,10 +10292,6 @@ snapshots: '@types/slice-ansi@4.0.0': {} - '@types/strip-bom@3.0.0': {} - - '@types/strip-json-comments@0.0.30': {} - '@types/throttle-debounce@2.1.0': {} '@types/triple-beam@1.3.4': {} @@ -10095,7 +10316,7 @@ snapshots: '@types/zxcvbn@4.4.3': {} - '@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2)': + '@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2))(eslint@8.53.0)(typescript@5.2.2)': dependencies: '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.2.2) @@ -10110,6 +10331,7 @@ snapshots: natural-compare: 1.4.0 semver: 7.5.4 ts-api-utils: 1.0.3(typescript@5.2.2) + optionalDependencies: typescript: 5.2.2 transitivePeerDependencies: - supports-color @@ -10122,6 +10344,7 @@ snapshots: '@typescript-eslint/visitor-keys': 6.10.0 debug: 4.3.4 eslint: 8.53.0 + optionalDependencies: typescript: 5.2.2 transitivePeerDependencies: - supports-color @@ -10138,6 +10361,7 @@ snapshots: debug: 4.3.4 eslint: 8.53.0 ts-api-utils: 1.0.3(typescript@5.2.2) + optionalDependencies: typescript: 5.2.2 transitivePeerDependencies: - supports-color @@ -10153,6 +10377,7 @@ snapshots: is-glob: 4.0.3 semver: 7.5.4 ts-api-utils: 1.0.3(typescript@5.2.2) + optionalDependencies: typescript: 5.2.2 transitivePeerDependencies: - supports-color @@ -10178,7 +10403,7 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-vue@2.3.4(vite@2.9.16)(vue@3.2.47)': + '@vitejs/plugin-vue@2.3.4(vite@2.9.16(sass@1.32.12))(vue@3.2.47)': dependencies: vite: 2.9.16(sass@1.32.12) vue: 3.2.47 @@ -10366,7 +10591,7 @@ snapshots: indent-string: 4.0.0 ajv-formats@2.1.1(ajv@8.12.0): - dependencies: + optionalDependencies: ajv: 8.12.0 ajv-keywords@3.5.2(ajv@6.12.6): @@ -10510,7 +10735,8 @@ snapshots: delegates: 1.0.0 readable-stream: 3.6.2 - arg@4.1.3: {} + arg@4.1.3: + optional: true argon2-browser@1.18.0: {} @@ -11306,7 +11532,7 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig-typescript-loader@5.0.0(@types/node@18.18.8)(cosmiconfig@8.3.6)(typescript@5.2.2): + cosmiconfig-typescript-loader@5.0.0(@types/node@18.18.8)(cosmiconfig@8.3.6(typescript@5.2.2))(typescript@5.2.2): dependencies: '@types/node': 18.18.8 cosmiconfig: 8.3.6(typescript@5.2.2) @@ -11326,6 +11552,7 @@ snapshots: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 + optionalDependencies: typescript: 5.2.2 crc-32@1.2.2: {} @@ -11340,7 +11567,8 @@ snapshots: buffer: 5.7.1 optional: true - create-require@1.1.1: {} + create-require@1.1.1: + optional: true crelt@1.0.6: {} @@ -11564,7 +11792,8 @@ snapshots: diff-sequences@29.6.3: {} - diff@4.0.2: {} + diff@4.0.2: + optional: true dijkstrajs@1.0.3: {} @@ -11699,10 +11928,6 @@ snapshots: duplexer3@0.1.5: {} - dynamic-dedupe@0.3.0: - dependencies: - xtend: 4.0.2 - eastasianwidth@0.2.0: {} ecdsa-sig-formatter@1.0.11: @@ -12082,6 +12307,33 @@ snapshots: '@esbuild/win32-ia32': 0.18.20 '@esbuild/win32-x64': 0.18.20 + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + escalade@3.1.1: {} escape-goat@2.1.1: {} @@ -12100,23 +12352,25 @@ snapshots: dependencies: eslint: 8.53.0 - eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0)(eslint@8.53.0)(prettier@3.0.3): + eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0(eslint@8.53.0))(eslint@8.53.0)(prettier@3.0.3): dependencies: eslint: 8.53.0 - eslint-config-prettier: 9.0.0(eslint@8.53.0) prettier: 3.0.3 prettier-linter-helpers: 1.0.0 synckit: 0.8.5 + optionalDependencies: + eslint-config-prettier: 9.0.0(eslint@8.53.0) eslint-plugin-simple-import-sort@10.0.0(eslint@8.53.0): dependencies: eslint: 8.53.0 - eslint-plugin-unused-imports@3.0.0(@typescript-eslint/eslint-plugin@6.10.0)(eslint@8.53.0): + eslint-plugin-unused-imports@3.0.0(@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2))(eslint@8.53.0)(typescript@5.2.2))(eslint@8.53.0): dependencies: - '@typescript-eslint/eslint-plugin': 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2) eslint: 8.53.0 eslint-rule-composer: 0.3.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 6.10.0(@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2))(eslint@8.53.0)(typescript@5.2.2) eslint-plugin-vue@9.18.1(eslint@8.53.0): dependencies: @@ -12724,6 +12978,10 @@ snapshots: get-stream@6.0.1: {} + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + getopts@2.3.0: {} gifwrap@0.9.4: @@ -13464,12 +13722,13 @@ snapshots: getopts: 2.3.0 interpret: 2.2.0 lodash: 4.17.21 - pg: 8.11.3 pg-connection-string: 2.5.0 rechoir: 0.8.0 resolve-from: 5.0.0 tarn: 3.0.2 tildify: 2.0.0 + optionalDependencies: + pg: 8.11.3 transitivePeerDependencies: - supports-color @@ -13964,9 +14223,11 @@ snapshots: dependencies: semver: 7.5.4 - node-fetch@2.7.0: + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 node-gyp-build-optional-packages@5.0.7: optional: true @@ -14090,7 +14351,7 @@ snapshots: dependencies: isobject: 3.0.1 - objection@3.0.1(knex@2.3.0): + objection@3.0.1(knex@2.3.0(pg@8.11.3)): dependencies: ajv: 8.12.0 db-errors: 0.2.3 @@ -14403,9 +14664,10 @@ snapshots: pinia@2.0.36(typescript@5.2.2)(vue@3.2.47): dependencies: '@vue/devtools-api': 6.5.1 - typescript: 5.2.2 vue: 3.2.47 vue-demi: 0.14.6(vue@3.2.47) + optionalDependencies: + typescript: 5.2.2 pinkie-promise@2.0.1: dependencies: @@ -14468,12 +14730,13 @@ snapshots: bin-wrapper: 4.1.0 execa: 4.1.0 - postcss-load-config@4.0.1(postcss@8.4.31)(ts-node@10.9.1): + postcss-load-config@4.0.1(postcss@8.4.31)(ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2)): dependencies: lilconfig: 2.1.0 + yaml: 2.3.4 + optionalDependencies: postcss: 8.4.31 ts-node: 10.9.1(@types/node@20.9.1)(typescript@5.2.2) - yaml: 2.3.4 postcss-selector-parser@6.0.13: dependencies: @@ -14919,6 +15182,8 @@ snapshots: dependencies: global-dirs: 0.1.1 + resolve-pkg-maps@1.0.0: {} + resolve@1.22.8: dependencies: is-core-module: 2.13.1 @@ -14950,10 +15215,6 @@ snapshots: rfdc@1.3.0: {} - rimraf@2.7.1: - dependencies: - glob: 7.2.3 - rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -14976,12 +15237,14 @@ snapshots: sprintf-js: 1.1.3 optional: true - rollup-plugin-visualizer@5.9.2: + rollup-plugin-visualizer@5.9.2(rollup@3.29.4): dependencies: open: 8.4.2 picomatch: 2.3.1 source-map: 0.7.4 yargs: 17.7.2 + optionalDependencies: + rollup: 3.29.4 rollup@2.77.3: optionalDependencies: @@ -15483,8 +15746,8 @@ snapshots: dependencies: '@effect/data': 0.17.1 '@effect/io': 0.38.0(@effect/data@0.17.1) - '@effect/match': 0.32.0(@effect/data@0.17.1)(@effect/schema@0.33.1) - '@effect/schema': 0.33.1(@effect/data@0.17.1)(@effect/io@0.38.0) + '@effect/match': 0.32.0(@effect/data@0.17.1)(@effect/schema@0.33.1(@effect/data@0.17.1)(@effect/io@0.38.0(@effect/data@0.17.1))) + '@effect/schema': 0.33.1(@effect/data@0.17.1)(@effect/io@0.38.0(@effect/data@0.17.1)) chalk: 4.1.2 commander: 11.0.0 cosmiconfig: 8.2.0 @@ -15691,24 +15954,6 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node-dev@2.0.0(@types/node@20.9.1)(typescript@5.2.2): - dependencies: - chokidar: 3.5.3 - dynamic-dedupe: 0.3.0 - minimist: 1.2.8 - mkdirp: 1.0.4 - resolve: 1.22.8 - rimraf: 2.7.1 - source-map-support: 0.5.21 - tree-kill: 1.2.2 - ts-node: 10.9.1(@types/node@20.9.1)(typescript@5.2.2) - tsconfig: 7.0.0 - typescript: 5.2.2 - transitivePeerDependencies: - - '@swc/core' - - '@swc/wasm' - - '@types/node' - ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -15726,6 +15971,7 @@ snapshots: typescript: 5.2.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optional: true ts-toolbelt@9.6.0: {} @@ -15744,16 +15990,9 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsconfig@7.0.0: - dependencies: - '@types/strip-bom': 3.0.0 - '@types/strip-json-comments': 0.0.30 - strip-bom: 3.0.0 - strip-json-comments: 2.0.1 - tslib@2.6.2: {} - tsup@7.2.0(postcss@8.4.31)(ts-node@10.9.1)(typescript@5.2.2): + tsup@7.2.0(postcss@8.4.31)(ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2))(typescript@5.2.2): dependencies: bundle-require: 4.0.2(esbuild@0.18.20) cac: 6.7.14 @@ -15763,18 +16002,26 @@ snapshots: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss: 8.4.31 - postcss-load-config: 4.0.1(postcss@8.4.31)(ts-node@10.9.1) + postcss-load-config: 4.0.1(postcss@8.4.31)(ts-node@10.9.1(@types/node@20.9.1)(typescript@5.2.2)) resolve-from: 5.0.0 rollup: 3.29.4 source-map: 0.8.0-beta.0 sucrase: 3.34.0 tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.4.31 typescript: 5.2.2 transitivePeerDependencies: - supports-color - ts-node + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 @@ -15872,9 +16119,9 @@ snapshots: unilogr@0.0.27: {} - unimport@1.3.0: + unimport@1.3.0(rollup@3.29.4): dependencies: - '@rollup/pluginutils': 5.0.5 + '@rollup/pluginutils': 5.0.5(rollup@3.29.4) escape-string-regexp: 5.0.0 fast-glob: 3.3.2 local-pkg: 0.4.3 @@ -15908,22 +16155,23 @@ snapshots: unpipe@1.0.0: {} - unplugin-auto-import@0.11.5(@vueuse/core@10.5.0): + unplugin-auto-import@0.11.5(@vueuse/core@10.5.0(vue@3.2.47))(rollup@3.29.4): dependencies: '@antfu/utils': 0.7.6 - '@rollup/pluginutils': 5.0.5 - '@vueuse/core': 10.5.0(vue@3.2.47) + '@rollup/pluginutils': 5.0.5(rollup@3.29.4) local-pkg: 0.4.3 magic-string: 0.26.7 - unimport: 1.3.0 + unimport: 1.3.0(rollup@3.29.4) unplugin: 1.5.0 + optionalDependencies: + '@vueuse/core': 10.5.0(vue@3.2.47) transitivePeerDependencies: - rollup - unplugin-vue-components@0.22.12(vue@3.2.47): + unplugin-vue-components@0.22.12(@babel/parser@7.23.0)(rollup@3.29.4)(vue@3.2.47): dependencies: '@antfu/utils': 0.7.6 - '@rollup/pluginutils': 5.0.5 + '@rollup/pluginutils': 5.0.5(rollup@3.29.4) chokidar: 3.5.3 debug: 4.3.4 fast-glob: 3.3.2 @@ -15933,6 +16181,8 @@ snapshots: resolve: 1.22.8 unplugin: 1.5.0 vue: 3.2.47 + optionalDependencies: + '@babel/parser': 7.23.0 transitivePeerDependencies: - rollup - supports-color @@ -16012,7 +16262,8 @@ snapshots: uuid@3.4.0: {} - v8-compile-cache-lib@3.0.1: {} + v8-compile-cache-lib@3.0.1: + optional: true validate-npm-package-license@3.0.4: dependencies: @@ -16032,14 +16283,14 @@ snapshots: extsprintf: 1.4.1 optional: true - vite-node@0.34.6(@types/node@20.8.10): + vite-node@0.34.6(@types/node@20.8.10)(sass@1.32.12): dependencies: cac: 6.7.14 debug: 4.3.4 mlly: 1.4.2 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.5.0(@types/node@20.8.10) + vite: 4.5.0(@types/node@20.8.10)(sass@1.32.12) transitivePeerDependencies: - '@types/node' - less @@ -16056,20 +16307,21 @@ snapshots: postcss: 8.4.31 resolve: 1.22.8 rollup: 2.77.3 - sass: 1.32.12 optionalDependencies: fsevents: 2.3.3 + sass: 1.32.12 - vite@4.5.0(@types/node@20.8.10): + vite@4.5.0(@types/node@20.8.10)(sass@1.32.12): dependencies: - '@types/node': 20.8.10 esbuild: 0.18.20 postcss: 8.4.31 rollup: 3.29.4 optionalDependencies: + '@types/node': 20.8.10 fsevents: 2.3.3 + sass: 1.32.12 - vitest@0.34.6: + vitest@0.34.6(sass@1.32.12): dependencies: '@types/chai': 4.3.9 '@types/chai-subset': 1.3.4 @@ -16092,8 +16344,8 @@ snapshots: strip-literal: 1.3.0 tinybench: 2.5.1 tinypool: 0.7.0 - vite: 4.5.0(@types/node@20.8.10) - vite-node: 0.34.6(@types/node@20.8.10) + vite: 4.5.0(@types/node@20.8.10)(sass@1.32.12) + vite-node: 0.34.6(@types/node@20.8.10)(sass@1.32.12) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -16247,7 +16499,7 @@ snapshots: typedarray-to-buffer: 3.1.5 ws@8.11.0(utf-8-validate@5.0.10): - dependencies: + optionalDependencies: utf-8-validate: 5.0.10 xdg-basedir@5.1.0: {} @@ -16281,7 +16533,7 @@ snapshots: xtend@4.0.2: {} - y-prosemirror@1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6)(yjs@13.6.8): + y-prosemirror@1.0.20(prosemirror-model@1.18.3)(prosemirror-state@1.4.3)(prosemirror-view@1.29.2)(y-protocols@1.0.6(yjs@13.6.8))(yjs@13.6.8): dependencies: lib0: 0.2.87 prosemirror-model: 1.18.3 @@ -16365,7 +16617,8 @@ snapshots: dependencies: lib0: 0.2.87 - yn@3.1.1: {} + yn@3.1.1: + optional: true yocto-queue@0.1.0: {} From ad6b9209c7aef475f765e47bb9795e96269dfb1e Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sat, 14 Dec 2024 23:22:12 -0300 Subject: [PATCH 004/243] refactor(client): move auth to areas --- apps/client/src/boot/auth.client.ts | 2 +- apps/client/src/code/{ => areas}/auth/demo.ts | 4 ++-- apps/client/src/code/{ => areas}/auth/login.ts | 2 +- apps/client/src/code/{ => areas}/auth/logout.ts | 6 +++--- apps/client/src/code/{ => areas}/auth/refresh.ts | 4 ++-- apps/client/src/code/{ => areas}/auth/register.ts | 0 apps/client/src/code/{ => areas}/auth/tokens.ts | 2 +- .../layouts/HomeLayout/Header/RightButtons/RightMenu.vue | 2 +- .../src/layouts/PagesLayout/MainToolbar/AccountPopup.vue | 2 +- apps/client/src/pages/home/Account/General/ChangeEmail.vue | 2 +- .../client/src/pages/home/Account/General/DeleteAccount.vue | 2 +- .../src/pages/home/Account/Security/ChangePassword.vue | 2 +- apps/client/src/pages/home/Account/Security/RotateKeys.vue | 2 +- apps/client/src/pages/home/Login/Authenticator.vue | 2 +- apps/client/src/pages/home/Login/Recovery.vue | 2 +- apps/client/src/pages/home/Login/Standard.vue | 4 ++-- apps/client/src/pages/home/Register.vue | 4 ++-- 17 files changed, 22 insertions(+), 22 deletions(-) rename apps/client/src/code/{ => areas}/auth/demo.ts (88%) rename apps/client/src/code/{ => areas}/auth/login.ts (98%) rename apps/client/src/code/{ => areas}/auth/logout.ts (87%) rename apps/client/src/code/{ => areas}/auth/refresh.ts (97%) rename apps/client/src/code/{ => areas}/auth/register.ts (100%) rename apps/client/src/code/{ => areas}/auth/tokens.ts (97%) diff --git a/apps/client/src/boot/auth.client.ts b/apps/client/src/boot/auth.client.ts index 0465e986..c9e37272 100644 --- a/apps/client/src/boot/auth.client.ts +++ b/apps/client/src/boot/auth.client.ts @@ -1,6 +1,6 @@ import { sleep } from '@stdlib/misc'; import { boot } from 'quasar/wrappers'; -import { tryRefreshTokens } from 'src/code/auth/refresh'; +import { tryRefreshTokens } from 'src/code/areas/auth/refresh'; const _moduleLogger = mainLogger.sub('boot/auth.client.ts'); diff --git a/apps/client/src/code/auth/demo.ts b/apps/client/src/code/areas/auth/demo.ts similarity index 88% rename from apps/client/src/code/auth/demo.ts rename to apps/client/src/code/areas/auth/demo.ts index a6f13ba8..f08f0074 100644 --- a/apps/client/src/code/auth/demo.ts +++ b/apps/client/src/code/areas/auth/demo.ts @@ -1,8 +1,8 @@ import { wrapSymmetricKey } from '@stdlib/crypto'; import sodium from 'libsodium-wrappers-sumo'; -import type { deriveUserValues } from '../crypto'; -import { trpcClient } from '../trpc'; +import type { deriveUserValues } from '../../crypto'; +import { trpcClient } from '../../trpc'; import { login } from './login'; import { getRegistrationValues } from './register'; diff --git a/apps/client/src/code/auth/login.ts b/apps/client/src/code/areas/auth/login.ts similarity index 98% rename from apps/client/src/code/auth/login.ts rename to apps/client/src/code/areas/auth/login.ts index 905950b3..000b6ca1 100644 --- a/apps/client/src/code/auth/login.ts +++ b/apps/client/src/code/areas/auth/login.ts @@ -3,7 +3,7 @@ import type { SymmetricKey } from '@stdlib/crypto'; import { createPrivateKeyring } from '@stdlib/crypto'; import { createSymmetricKeyring, wrapSymmetricKey } from '@stdlib/crypto'; -import { multiModePath } from '../utils/misc'; +import { multiModePath } from '../../utils/misc'; import { storeClientTokenExpirations } from './tokens'; export async function login(input: { diff --git a/apps/client/src/code/auth/logout.ts b/apps/client/src/code/areas/auth/logout.ts similarity index 87% rename from apps/client/src/code/auth/logout.ts rename to apps/client/src/code/areas/auth/logout.ts index 6ab83eb4..28b76b54 100644 --- a/apps/client/src/code/auth/logout.ts +++ b/apps/client/src/code/areas/auth/logout.ts @@ -1,6 +1,6 @@ -import { clearCookie } from '../cookies'; -import { trpcClient } from '../trpc'; -import { multiModePath } from '../utils/misc'; +import { clearCookie } from '../../cookies'; +import { trpcClient } from '../../trpc'; +import { multiModePath } from '../../utils/misc'; import { clearClientTokenExpirations } from './tokens'; export async function logout() { diff --git a/apps/client/src/code/auth/refresh.ts b/apps/client/src/code/areas/auth/refresh.ts similarity index 97% rename from apps/client/src/code/auth/refresh.ts rename to apps/client/src/code/areas/auth/refresh.ts index 929aaa71..dde6a31a 100644 --- a/apps/client/src/code/auth/refresh.ts +++ b/apps/client/src/code/areas/auth/refresh.ts @@ -12,8 +12,8 @@ import { wrapSymmetricKey, } from '@stdlib/crypto'; -import { redirectIfNecessary } from '../routing'; -import { trpcClient } from '../trpc'; +import { redirectIfNecessary } from '../../routing'; +import { trpcClient } from '../../trpc'; import { logout } from './logout'; import { areClientTokensExpiring, diff --git a/apps/client/src/code/auth/register.ts b/apps/client/src/code/areas/auth/register.ts similarity index 100% rename from apps/client/src/code/auth/register.ts rename to apps/client/src/code/areas/auth/register.ts diff --git a/apps/client/src/code/auth/tokens.ts b/apps/client/src/code/areas/auth/tokens.ts similarity index 97% rename from apps/client/src/code/auth/tokens.ts rename to apps/client/src/code/areas/auth/tokens.ts index a9b4dde5..cf9ac06f 100644 --- a/apps/client/src/code/auth/tokens.ts +++ b/apps/client/src/code/areas/auth/tokens.ts @@ -3,7 +3,7 @@ import { getRefreshTokenExpiration, } from '@deeplib/misc'; -import { shouldRememberSession } from '../utils/misc'; +import { shouldRememberSession } from '../../utils/misc'; export function getClientTokenExpirationDate( token: 'access' | 'refresh', diff --git a/apps/client/src/layouts/HomeLayout/Header/RightButtons/RightMenu.vue b/apps/client/src/layouts/HomeLayout/Header/RightButtons/RightMenu.vue index 090fdf29..d76b1914 100644 --- a/apps/client/src/layouts/HomeLayout/Header/RightButtons/RightMenu.vue +++ b/apps/client/src/layouts/HomeLayout/Header/RightButtons/RightMenu.vue @@ -124,7 +124,7 @@ + + diff --git a/new-deepnotes/apps/web/package.json b/new-deepnotes/apps/web/package.json new file mode 100644 index 00000000..db9bd204 --- /dev/null +++ b/new-deepnotes/apps/web/package.json @@ -0,0 +1,23 @@ +{ + "name": "@deepnotes/web", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite", + "lint": "eslint vite.config.ts \"src/**/*.ts\"", + "test": "node -e \"process.exit(0)\"", + "typecheck": "vue-tsc --noEmit -p tsconfig.app.json", + "preview": "vite preview" + }, + "dependencies": { + "vue": "^3.5.13" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.3", + "typescript": "^5.8.3", + "vite": "^6.3.3", + "vue-tsc": "^2.2.10" + } +} diff --git a/new-deepnotes/apps/web/src/App.vue b/new-deepnotes/apps/web/src/App.vue new file mode 100644 index 00000000..4c23bdea --- /dev/null +++ b/new-deepnotes/apps/web/src/App.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/new-deepnotes/apps/web/src/main.ts b/new-deepnotes/apps/web/src/main.ts new file mode 100644 index 00000000..45573079 --- /dev/null +++ b/new-deepnotes/apps/web/src/main.ts @@ -0,0 +1,5 @@ +import { createApp } from "vue"; + +import App from "./App.vue"; + +createApp(App).mount("#app"); diff --git a/new-deepnotes/apps/web/src/vite-env.d.ts b/new-deepnotes/apps/web/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/new-deepnotes/apps/web/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/new-deepnotes/apps/web/tsconfig.app.json b/new-deepnotes/apps/web/tsconfig.app.json new file mode 100644 index 00000000..4ff66ac8 --- /dev/null +++ b/new-deepnotes/apps/web/tsconfig.app.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "verbatimModuleSyntax": true, + "noUncheckedIndexedAccess": true, + "useDefineForClassFields": true, + "types": [] + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/new-deepnotes/apps/web/tsconfig.json b/new-deepnotes/apps/web/tsconfig.json new file mode 100644 index 00000000..1ffef600 --- /dev/null +++ b/new-deepnotes/apps/web/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/new-deepnotes/apps/web/tsconfig.node.json b/new-deepnotes/apps/web/tsconfig.node.json new file mode 100644 index 00000000..529867cf --- /dev/null +++ b/new-deepnotes/apps/web/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "verbatimModuleSyntax": true + }, + "include": ["vite.config.ts"] +} diff --git a/new-deepnotes/apps/web/vite.config.ts b/new-deepnotes/apps/web/vite.config.ts new file mode 100644 index 00000000..a3505235 --- /dev/null +++ b/new-deepnotes/apps/web/vite.config.ts @@ -0,0 +1,9 @@ +import vue from "@vitejs/plugin-vue"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [vue()], + server: { + port: 5174, + }, +}); diff --git a/new-deepnotes/docker-compose.yml b/new-deepnotes/docker-compose.yml new file mode 100644 index 00000000..1c13c2b5 --- /dev/null +++ b/new-deepnotes/docker-compose.yml @@ -0,0 +1,33 @@ +services: + postgres: + image: postgres:16-alpine + ports: + - "5433:5432" + environment: + POSTGRES_USER: deepnotes + POSTGRES_PASSWORD: deepnotes + POSTGRES_DB: deepnotes + volumes: + - new-deepnotes-pg:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U deepnotes -d deepnotes"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + ports: + - "6380:6379" + command: ["redis-server", "--appendonly", "yes"] + volumes: + - new-deepnotes-redis:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + +volumes: + new-deepnotes-pg: + new-deepnotes-redis: diff --git a/new-deepnotes/eslint.config.js b/new-deepnotes/eslint.config.js new file mode 100644 index 00000000..90d6ee17 --- /dev/null +++ b/new-deepnotes/eslint.config.js @@ -0,0 +1,10 @@ +// @ts-check +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + ...tseslint.configs.recommended, + { + ignores: ["**/dist/**", "**/.output/**", "**/node_modules/**", "**/migrations/meta/**"], + }, +); diff --git a/new-deepnotes/package.json b/new-deepnotes/package.json new file mode 100644 index 00000000..4e01ca6e --- /dev/null +++ b/new-deepnotes/package.json @@ -0,0 +1,27 @@ +{ + "name": "new-deepnotes", + "private": true, + "type": "module", + "packageManager": "pnpm@9.15.4", + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "turbo run build", + "dev": "turbo run dev --parallel", + "lint": "turbo run lint", + "typecheck": "turbo run typecheck", + "test": "turbo run test", + "db:generate": "pnpm --filter @deepnotes/db exec drizzle-kit generate", + "db:migrate": "pnpm --filter @deepnotes/db exec drizzle-kit migrate", + "db:studio": "pnpm --filter @deepnotes/db exec drizzle-kit studio", + "db:check": "pnpm --filter @deepnotes/db exec drizzle-kit check" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "eslint": "^9.25.0", + "turbo": "^2.5.0", + "typescript": "^5.8.3", + "typescript-eslint": "^8.31.0" + } +} diff --git a/new-deepnotes/packages/api/eslint.config.js b/new-deepnotes/packages/api/eslint.config.js new file mode 100644 index 00000000..f6c5b17b --- /dev/null +++ b/new-deepnotes/packages/api/eslint.config.js @@ -0,0 +1,3 @@ +import base from "../../eslint.config.js"; + +export default [...base]; diff --git a/new-deepnotes/packages/api/package.json b/new-deepnotes/packages/api/package.json new file mode 100644 index 00000000..3519286d --- /dev/null +++ b/new-deepnotes/packages/api/package.json @@ -0,0 +1,31 @@ +{ + "name": "@deepnotes/api", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./openapi": { + "types": "./src/openapi.ts", + "default": "./src/openapi.ts" + } + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "lint": "eslint .", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@asteasolutions/zod-to-openapi": "^7.3.0", + "openapi3-ts": "^4.4.0", + "zod": "^3.24.3" + }, + "devDependencies": { + "typescript": "^5.8.3", + "vitest": "^3.1.1" + } +} diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts new file mode 100644 index 00000000..dc84e72f --- /dev/null +++ b/new-deepnotes/packages/api/src/index.ts @@ -0,0 +1,5 @@ +export { getOpenApiDocument } from "./openapi.js"; +export { + healthResponseSchema, + type HealthResponse, +} from "./schemas/health.js"; diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts new file mode 100644 index 00000000..c9129374 --- /dev/null +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -0,0 +1,10 @@ +import { describe, expect, it } from "vitest"; + +import { getOpenApiDocument } from "./openapi.js"; + +describe("getOpenApiDocument", () => { + it("includes /api/health", () => { + const doc = getOpenApiDocument(); + expect(doc.paths?.["/api/health"]?.get).toBeDefined(); + }); +}); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts new file mode 100644 index 00000000..b2bfdc40 --- /dev/null +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -0,0 +1,40 @@ +import { + OpenApiGeneratorV3, + OpenAPIRegistry, +} from "@asteasolutions/zod-to-openapi"; +import type { OpenAPIObject } from "openapi3-ts/oas30"; + +import { healthResponseSchema } from "./schemas/health.js"; + +const registry = new OpenAPIRegistry(); + +registry.registerPath({ + method: "get", + path: "/api/health", + summary: "Health check", + responses: { + 200: { + description: "API is reachable", + content: { + "application/json": { + schema: healthResponseSchema, + }, + }, + }, + }, +}); + +const generator = new OpenApiGeneratorV3(registry.definitions); + +export function getOpenApiDocument(): OpenAPIObject { + return generator.generateDocument({ + openapi: "3.0.0", + info: { + title: "DeepNotes API", + version: "0.0.0", + description: + "Greenfield HTTP API (REST + OpenAPI). Legacy /trpc is not a compatibility target.", + }, + servers: [{ url: "/" }], + }); +} diff --git a/new-deepnotes/packages/api/src/schemas/health.ts b/new-deepnotes/packages/api/src/schemas/health.ts new file mode 100644 index 00000000..58ec9954 --- /dev/null +++ b/new-deepnotes/packages/api/src/schemas/health.ts @@ -0,0 +1,13 @@ +import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; +import { z } from "zod"; + +extendZodWithOpenApi(z); + +export const healthResponseSchema = z + .object({ + status: z.literal("ok"), + service: z.string(), + }) + .openapi("HealthResponse"); + +export type HealthResponse = z.infer; diff --git a/new-deepnotes/packages/api/tsconfig.build.json b/new-deepnotes/packages/api/tsconfig.build.json new file mode 100644 index 00000000..b60b5c2e --- /dev/null +++ b/new-deepnotes/packages/api/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"], + "exclude": ["**/*.test.ts"] +} diff --git a/new-deepnotes/packages/api/tsconfig.json b/new-deepnotes/packages/api/tsconfig.json new file mode 100644 index 00000000..129f731c --- /dev/null +++ b/new-deepnotes/packages/api/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["src/**/*.ts"] +} diff --git a/new-deepnotes/packages/api/vitest.config.ts b/new-deepnotes/packages/api/vitest.config.ts new file mode 100644 index 00000000..c1433e6e --- /dev/null +++ b/new-deepnotes/packages/api/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + }, +}); diff --git a/new-deepnotes/packages/db/drizzle.config.ts b/new-deepnotes/packages/db/drizzle.config.ts new file mode 100644 index 00000000..bf788799 --- /dev/null +++ b/new-deepnotes/packages/db/drizzle.config.ts @@ -0,0 +1,16 @@ +import { config as loadEnv } from "dotenv"; +import { defineConfig } from "drizzle-kit"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = fileURLToPath(new URL("../..", import.meta.url)); +loadEnv({ path: resolve(root, ".env") }); + +export default defineConfig({ + dialect: "postgresql", + schema: "./src/schema.ts", + out: "./migrations", + dbCredentials: { + url: process.env.DATABASE_URL ?? "postgresql://deepnotes:deepnotes@127.0.0.1:5433/deepnotes", + }, +}); diff --git a/new-deepnotes/packages/db/eslint.config.js b/new-deepnotes/packages/db/eslint.config.js new file mode 100644 index 00000000..f6c5b17b --- /dev/null +++ b/new-deepnotes/packages/db/eslint.config.js @@ -0,0 +1,3 @@ +import base from "../../eslint.config.js"; + +export default [...base]; diff --git a/new-deepnotes/packages/db/migrations/0000_init.sql b/new-deepnotes/packages/db/migrations/0000_init.sql new file mode 100644 index 00000000..353459d8 --- /dev/null +++ b/new-deepnotes/packages/db/migrations/0000_init.sql @@ -0,0 +1,5 @@ +CREATE TABLE "app_meta" ( + "key" text PRIMARY KEY NOT NULL, + "value" text NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); diff --git a/new-deepnotes/packages/db/migrations/meta/0000_snapshot.json b/new-deepnotes/packages/db/migrations/meta/0000_snapshot.json new file mode 100644 index 00000000..bf3b577e --- /dev/null +++ b/new-deepnotes/packages/db/migrations/meta/0000_snapshot.json @@ -0,0 +1,51 @@ +{ + "id": "596f6d22-f0ba-49e1-81a9-42a641e73269", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.app_meta": { + "name": "app_meta", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/new-deepnotes/packages/db/migrations/meta/_journal.json b/new-deepnotes/packages/db/migrations/meta/_journal.json new file mode 100644 index 00000000..3fa46dfa --- /dev/null +++ b/new-deepnotes/packages/db/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1777252387392, + "tag": "0000_init", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/new-deepnotes/packages/db/package.json b/new-deepnotes/packages/db/package.json new file mode 100644 index 00000000..f6b68ab6 --- /dev/null +++ b/new-deepnotes/packages/db/package.json @@ -0,0 +1,33 @@ +{ + "name": "@deepnotes/db", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + "./schema": { + "types": "./src/schema.ts", + "default": "./src/schema.ts" + }, + "./client": { + "types": "./src/client.ts", + "default": "./src/client.ts" + } + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "lint": "eslint .", + "typecheck": "tsc -p tsconfig.json --noEmit", + "db:check": "drizzle-kit check", + "test": "node -e \"process.exit(0)\"" + }, + "dependencies": { + "drizzle-orm": "^0.41.0", + "postgres": "^3.4.5" + }, + "devDependencies": { + "@types/node": "^22.14.1", + "dotenv": "^16.5.0", + "drizzle-kit": "^0.31.0", + "typescript": "^5.8.3" + } +} diff --git a/new-deepnotes/packages/db/src/client.ts b/new-deepnotes/packages/db/src/client.ts new file mode 100644 index 00000000..7c8ebab7 --- /dev/null +++ b/new-deepnotes/packages/db/src/client.ts @@ -0,0 +1,11 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; + +import * as schema from "./schema.js"; + +export type DeepnotesDb = ReturnType; + +export function createDb(connectionString: string) { + const client = postgres(connectionString, { max: 10 }); + return drizzle(client, { schema }); +} diff --git a/new-deepnotes/packages/db/src/schema.ts b/new-deepnotes/packages/db/src/schema.ts new file mode 100644 index 00000000..2707b013 --- /dev/null +++ b/new-deepnotes/packages/db/src/schema.ts @@ -0,0 +1,13 @@ +import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; + +/** + * Bootstrap table so the migration chain is non-empty. + * Replace or extend when importing the legacy schema from postgres-init.sql (RESTART_PLAN §4.5). + */ +export const appMeta = pgTable("app_meta", { + key: text("key").primaryKey(), + value: text("value").notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }) + .notNull() + .defaultNow(), +}); diff --git a/new-deepnotes/packages/db/tsconfig.build.json b/new-deepnotes/packages/db/tsconfig.build.json new file mode 100644 index 00000000..b60b5c2e --- /dev/null +++ b/new-deepnotes/packages/db/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"], + "exclude": ["**/*.test.ts"] +} diff --git a/new-deepnotes/packages/db/tsconfig.json b/new-deepnotes/packages/db/tsconfig.json new file mode 100644 index 00000000..563dceb7 --- /dev/null +++ b/new-deepnotes/packages/db/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["src/**/*.ts", "drizzle.config.ts"] +} diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml new file mode 100644 index 00000000..2291eee5 --- /dev/null +++ b/new-deepnotes/pnpm-lock.yaml @@ -0,0 +1,3874 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@eslint/js': + specifier: ^9.25.0 + version: 9.39.4 + eslint: + specifier: ^9.25.0 + version: 9.39.4 + turbo: + specifier: ^2.5.0 + version: 2.9.6 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.31.0 + version: 8.59.0(eslint@9.39.4)(typescript@5.9.3) + + apps/api-worker: + dependencies: + '@deepnotes/api': + specifier: workspace:* + version: link:../../packages/api + hono: + specifier: ^4.7.7 + version: 4.12.15 + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20250426.0 + version: 4.20260426.1 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vitest: + specifier: ^3.1.1 + version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + wrangler: + specifier: ^4.12.0 + version: 4.85.0(@cloudflare/workers-types@4.20260426.1) + + apps/web: + dependencies: + vue: + specifier: ^3.5.13 + version: 3.5.33(typescript@5.9.3) + devDependencies: + '@vitejs/plugin-vue': + specifier: ^5.2.3 + version: 5.2.4(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.33(typescript@5.9.3)) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vite: + specifier: ^6.3.3 + version: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + vue-tsc: + specifier: ^2.2.10 + version: 2.2.12(typescript@5.9.3) + + packages/api: + dependencies: + '@asteasolutions/zod-to-openapi': + specifier: ^7.3.0 + version: 7.3.4(zod@3.25.76) + openapi3-ts: + specifier: ^4.4.0 + version: 4.5.0 + zod: + specifier: ^3.24.3 + version: 3.25.76 + devDependencies: + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vitest: + specifier: ^3.1.1 + version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + + packages/db: + dependencies: + drizzle-orm: + specifier: ^0.41.0 + version: 0.41.0(@cloudflare/workers-types@4.20260426.1)(gel@2.2.0)(postgres@3.4.9) + postgres: + specifier: ^3.4.5 + version: 3.4.9 + devDependencies: + '@types/node': + specifier: ^22.14.1 + version: 22.19.17 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + drizzle-kit: + specifier: ^0.31.0 + version: 0.31.10 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + +packages: + + '@asteasolutions/zod-to-openapi@7.3.4': + resolution: {integrity: sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA==} + peerDependencies: + zod: ^3.20.2 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260424.1': + resolution: {integrity: sha512-yFR1XaJbSDLg/qbwtrYaU2xwFXatIPKR5nrMQCN1q/m6+Qe/j6r+kCnFEvOJjMZOm9iCKsE6Qly5clgl4u32qw==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260424.1': + resolution: {integrity: sha512-LqWKcE7x/9KyC2iQvKPeb20hKST3dYXDZlYTvFymgR1DfLS0OFOCzVGTloVNd7WqvK4SkdzBYfxo7QMIAeBK0w==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260424.1': + resolution: {integrity: sha512-YlEBFbAYZHe/ylzl8WEYQEU/jr+0XMqXaST2oBk5oVjksdb1NGuJaggluCdZAzuJJ8UqdTmyhY5u/qrasbiFWA==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260424.1': + resolution: {integrity: sha512-qJ0X0m6cL8fWDUPDg8K4IxYZXNJI6XbeOihqjnqKbAClrjdPDn8VUSd+z2XiCQ5NylMtMrpa/skC9UfaR6mh8g==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260424.1': + resolution: {integrity: sha512-tZ7Z9qmYNAP6z1/+8r/zKbk8F8DZmpmwNzMeN+zkde2Wnhfr3FBqOkJXT/5zmli8HPoWrIXxSiyqcNDMy8V2Zg==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@4.20260426.1': + resolution: {integrity: sha512-cBYeQaWwv/jFV8ualmwp6wIxmAf0rDe2DPPQwPbslKmPHqgv861YpAvm45r05K40QboZgxNQVIPgNkmtHqZeJQ==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@petamoriken/float16@3.9.3': + resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} + + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + + '@rollup/rollup-android-arm-eabi@4.60.2': + resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.2': + resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.2': + resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.2': + resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.2': + resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.2': + resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.2': + resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.2': + resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.2': + resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.2': + resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.2': + resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.2': + resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.2': + resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.2': + resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.2': + resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.2': + resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.2': + resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.2': + resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.2': + resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.2': + resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.2': + resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} + cpu: [x64] + os: [win32] + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.15': + resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} + + '@turbo/darwin-64@2.9.6': + resolution: {integrity: sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.9.6': + resolution: {integrity: sha512-aalBeSl4agT/QtYGDyf/XLajedWzUC9Vg/pm/YO6QQ93vkQ91Vz5uK1ta5RbVRDozQSz4njxUNqRNmOXDzW+qw==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.9.6': + resolution: {integrity: sha512-YKi05jnNHaD7vevgYwahpzGwbsNNTwzU2c7VZdmdFm7+cGDP4oREUWSsainiMfRqjRuolQxBwRn8wf1jmu+YZA==} + cpu: [x64] + os: [linux] + + '@turbo/linux-arm64@2.9.6': + resolution: {integrity: sha512-02o/ZS69cOYEDczXvOB2xmyrtzjQ2hVFtWZK1iqxXUfzMmTjZK4UumrfNnjckSg+gqeBfnPRHa0NstA173Ik3g==} + cpu: [arm64] + os: [linux] + + '@turbo/windows-64@2.9.6': + resolution: {integrity: sha512-wVdQjvnBI15wB6JrA+43CtUtagjIMmX6XYO758oZHAsCNSxqRlJtdyujih0D8OCnwCRWiGWGI63zAxR0hO6s9g==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.9.6': + resolution: {integrity: sha512-1XUUyWW0W6FTSqGEhU8RHVqb2wP1SPkr7hIvBlMEwH9jr+sJQK5kqeosLJ/QaUv4ecSAd1ZhIrLoW7qslAzT4A==} + cpu: [arm64] + os: [win32] + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.19.17': + resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + + '@typescript-eslint/eslint-plugin@8.59.0': + resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.0': + resolution: {integrity: sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.0': + resolution: {integrity: sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.0': + resolution: {integrity: sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.0': + resolution: {integrity: sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.0': + resolution: {integrity: sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.0': + resolution: {integrity: sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.0': + resolution: {integrity: sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.0': + resolution: {integrity: sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.0': + resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + '@volar/language-core@2.4.15': + resolution: {integrity: sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==} + + '@volar/source-map@2.4.15': + resolution: {integrity: sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==} + + '@volar/typescript@2.4.15': + resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} + + '@vue/compiler-core@3.5.33': + resolution: {integrity: sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==} + + '@vue/compiler-dom@3.5.33': + resolution: {integrity: sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==} + + '@vue/compiler-sfc@3.5.33': + resolution: {integrity: sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==} + + '@vue/compiler-ssr@3.5.33': + resolution: {integrity: sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==} + + '@vue/compiler-vue2@2.7.16': + resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + + '@vue/language-core@2.2.12': + resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.33': + resolution: {integrity: sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==} + + '@vue/runtime-core@3.5.33': + resolution: {integrity: sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==} + + '@vue/runtime-dom@3.5.33': + resolution: {integrity: sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==} + + '@vue/server-renderer@3.5.33': + resolution: {integrity: sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==} + peerDependencies: + vue: 3.5.33 + + '@vue/shared@3.5.33': + resolution: {integrity: sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + alien-signals@1.0.13: + resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@2.1.0: + resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + de-indent@1.0.2: + resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + drizzle-kit@0.31.10: + resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} + hasBin: true + + drizzle-orm@0.41.0: + resolution: {integrity: sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gel@2.2.0: + resolution: {integrity: sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==} + engines: {node: '>= 18.0.0'} + hasBin: true + + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + hono@4.12.15: + resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==} + engines: {node: '>=16.9.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + miniflare@4.20260424.0: + resolution: {integrity: sha512-B6MKBBd5TJ19daUc3Ae9rWctn1nDA/VCXykXfCsp9fTxyfGxnZY27tJs1caxgE9MWEMMKGbGHouqVtgKbKGxmw==} + engines: {node: '>=18.0.0'} + hasBin: true + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + openapi3-ts@4.5.0: + resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.12: + resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} + engines: {node: ^10 || ^12 || >=14} + + postgres@3.4.9: + resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} + engines: {node: '>=12'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rollup@4.60.2: + resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + turbo@2.9.6: + resolution: {integrity: sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg==} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.59.0: + resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.24.8: + resolution: {integrity: sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@6.4.2: + resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-tsc@2.2.12: + resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.33: + resolution: {integrity: sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + workerd@1.20260424.1: + resolution: {integrity: sha512-oKsB0Xo/mfkYMdSACoS06XZg09VUK4rXwHfF/1t3P++sMbwzf4UHQvMO57+zxpEB2nVrY/ZkW0bYFGq4GdAFSQ==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.85.0: + resolution: {integrity: sha512-93cwt2RPb1qdcmEgPzH7ybiLN4BIKoWpscIX6SywjHrQOeIZrQk2haoc3XMLKtQTmzapxza9OuDD+kMHpsuuhg==} + engines: {node: '>=20.3.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260424.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + 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 + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@asteasolutions/zod-to-openapi@7.3.4(zod@3.25.76)': + dependencies: + openapi3-ts: 4.5.0 + zod: 3.25.76 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260424.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260424.1 + + '@cloudflare/workerd-darwin-64@1.20260424.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260424.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260424.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260424.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260424.1': + optional: true + + '@cloudflare/workers-types@4.20260426.1': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@drizzle-team/brocli@0.10.2': {} + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.14.0 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.3': + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.3': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.3': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.3': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.3': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.3': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.3': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.3': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.3': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.3': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.3': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.3': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.3': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.3': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.3': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.3': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.3': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.3': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.3': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.3': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.3': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.3': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.3': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.3': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.10.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@petamoriken/float16@3.9.3': + optional: true + + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + + '@rollup/rollup-android-arm-eabi@4.60.2': + optional: true + + '@rollup/rollup-android-arm64@4.60.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.2': + optional: true + + '@rollup/rollup-darwin-x64@4.60.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.2': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.2': + optional: true + + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.15': {} + + '@turbo/darwin-64@2.9.6': + optional: true + + '@turbo/darwin-arm64@2.9.6': + optional: true + + '@turbo/linux-64@2.9.6': + optional: true + + '@turbo/linux-arm64@2.9.6': + optional: true + + '@turbo/windows-64@2.9.6': + optional: true + + '@turbo/windows-arm64@2.9.6': + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@22.19.17': + dependencies: + undici-types: 6.21.0 + + '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/type-utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.0 + eslint: 9.39.4 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3 + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + + '@typescript-eslint/tsconfig-utils@8.59.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.59.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.0': {} + + '@typescript-eslint/typescript-estree@8.59.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/visitor-keys': 8.59.0 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.0(eslint@9.39.4)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.59.0 + '@typescript-eslint/types': 8.59.0 + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.0': + dependencies: + '@typescript-eslint/types': 8.59.0 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.33(typescript@5.9.3))': + dependencies: + vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + vue: 3.5.33(typescript@5.9.3) + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + '@volar/language-core@2.4.15': + dependencies: + '@volar/source-map': 2.4.15 + + '@volar/source-map@2.4.15': {} + + '@volar/typescript@2.4.15': + dependencies: + '@volar/language-core': 2.4.15 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.33': + dependencies: + '@babel/parser': 7.29.2 + '@vue/shared': 3.5.33 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.33': + dependencies: + '@vue/compiler-core': 3.5.33 + '@vue/shared': 3.5.33 + + '@vue/compiler-sfc@3.5.33': + dependencies: + '@babel/parser': 7.29.2 + '@vue/compiler-core': 3.5.33 + '@vue/compiler-dom': 3.5.33 + '@vue/compiler-ssr': 3.5.33 + '@vue/shared': 3.5.33 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.12 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.33': + dependencies: + '@vue/compiler-dom': 3.5.33 + '@vue/shared': 3.5.33 + + '@vue/compiler-vue2@2.7.16': + dependencies: + de-indent: 1.0.2 + he: 1.2.0 + + '@vue/language-core@2.2.12(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.15 + '@vue/compiler-dom': 3.5.33 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.33 + alien-signals: 1.0.13 + minimatch: 9.0.9 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + + '@vue/reactivity@3.5.33': + dependencies: + '@vue/shared': 3.5.33 + + '@vue/runtime-core@3.5.33': + dependencies: + '@vue/reactivity': 3.5.33 + '@vue/shared': 3.5.33 + + '@vue/runtime-dom@3.5.33': + dependencies: + '@vue/reactivity': 3.5.33 + '@vue/runtime-core': 3.5.33 + '@vue/shared': 3.5.33 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.33(vue@3.5.33(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.33 + '@vue/shared': 3.5.33 + vue: 3.5.33(typescript@5.9.3) + + '@vue/shared@3.5.33': {} + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + alien-signals@1.0.13: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + blake3-wasm@2.1.5: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.0: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + buffer-from@1.1.2: {} + + cac@6.7.14: {} + + callsites@3.1.0: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cookie@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + de-indent@1.0.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + detect-libc@2.1.2: {} + + dotenv@16.6.1: {} + + drizzle-kit@0.31.10: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.12 + tsx: 4.21.0 + + drizzle-orm@0.41.0(@cloudflare/workers-types@4.20260426.1)(gel@2.2.0)(postgres@3.4.9): + optionalDependencies: + '@cloudflare/workers-types': 4.20260426.1 + gel: 2.2.0 + postgres: 3.4.9 + + entities@7.0.1: {} + + env-paths@3.0.0: + optional: true + + error-stack-parser-es@1.0.5: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.3: + optional: true + + gel@2.2.0: + dependencies: + '@petamoriken/float16': 3.9.3 + debug: 4.4.3 + env-paths: 3.0.0 + semver: 7.7.4 + shell-quote: 1.8.3 + which: 4.0.0 + transitivePeerDependencies: + - supports-color + optional: true + + get-tsconfig@4.14.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + has-flag@4.0.0: {} + + he@1.2.0: {} + + hono@4.12.15: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + isexe@3.1.5: + optional: true + + js-tokens@9.0.1: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@4.1.5: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + miniflare@4.20260424.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.24.8 + workerd: 1.20260424.1 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.0 + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + openapi3-ts@4.5.0: + dependencies: + yaml: 2.8.3 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-to-regexp@6.3.0: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.12: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres@3.4.9: {} + + prelude-ls@1.2.1: {} + + punycode@2.3.1: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + rollup@4.60.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.2 + '@rollup/rollup-android-arm64': 4.60.2 + '@rollup/rollup-darwin-arm64': 4.60.2 + '@rollup/rollup-darwin-x64': 4.60.2 + '@rollup/rollup-freebsd-arm64': 4.60.2 + '@rollup/rollup-freebsd-x64': 4.60.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.2 + '@rollup/rollup-linux-arm-musleabihf': 4.60.2 + '@rollup/rollup-linux-arm64-gnu': 4.60.2 + '@rollup/rollup-linux-arm64-musl': 4.60.2 + '@rollup/rollup-linux-loong64-gnu': 4.60.2 + '@rollup/rollup-linux-loong64-musl': 4.60.2 + '@rollup/rollup-linux-ppc64-gnu': 4.60.2 + '@rollup/rollup-linux-ppc64-musl': 4.60.2 + '@rollup/rollup-linux-riscv64-gnu': 4.60.2 + '@rollup/rollup-linux-riscv64-musl': 4.60.2 + '@rollup/rollup-linux-s390x-gnu': 4.60.2 + '@rollup/rollup-linux-x64-gnu': 4.60.2 + '@rollup/rollup-linux-x64-musl': 4.60.2 + '@rollup/rollup-openbsd-x64': 4.60.2 + '@rollup/rollup-openharmony-arm64': 4.60.2 + '@rollup/rollup-win32-arm64-msvc': 4.60.2 + '@rollup/rollup-win32-ia32-msvc': 4.60.2 + '@rollup/rollup-win32-x64-gnu': 4.60.2 + '@rollup/rollup-win32-x64-msvc': 4.60.2 + fsevents: 2.3.3 + + semver@7.7.4: {} + + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: + optional: true + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@10.2.2: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@2.8.1: + optional: true + + tsx@4.21.0: + dependencies: + esbuild: 0.27.7 + get-tsconfig: 4.14.0 + optionalDependencies: + fsevents: 2.3.3 + + turbo@2.9.6: + optionalDependencies: + '@turbo/darwin-64': 2.9.6 + '@turbo/darwin-arm64': 2.9.6 + '@turbo/linux-64': 2.9.6 + '@turbo/linux-arm64': 2.9.6 + '@turbo/windows-64': 2.9.6 + '@turbo/windows-arm64': 2.9.6 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.59.0(eslint@9.39.4)(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + undici@7.24.8: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite-node@3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.12 + rollup: 4.60.2 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 22.19.17 + fsevents: 2.3.3 + tsx: 4.21.0 + yaml: 2.8.3 + + vitest@3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.16 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.17 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vscode-uri@3.1.0: {} + + vue-tsc@2.2.12(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.15 + '@vue/language-core': 2.2.12(typescript@5.9.3) + typescript: 5.9.3 + + vue@3.5.33(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.33 + '@vue/compiler-sfc': 3.5.33 + '@vue/runtime-dom': 3.5.33 + '@vue/server-renderer': 3.5.33(vue@3.5.33(typescript@5.9.3)) + '@vue/shared': 3.5.33 + optionalDependencies: + typescript: 5.9.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@4.0.0: + dependencies: + isexe: 3.1.5 + optional: true + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + workerd@1.20260424.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260424.1 + '@cloudflare/workerd-darwin-arm64': 1.20260424.1 + '@cloudflare/workerd-linux-64': 1.20260424.1 + '@cloudflare/workerd-linux-arm64': 1.20260424.1 + '@cloudflare/workerd-windows-64': 1.20260424.1 + + wrangler@4.85.0(@cloudflare/workers-types@4.20260426.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260424.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260424.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260424.1 + optionalDependencies: + '@cloudflare/workers-types': 4.20260426.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.18.0: {} + + yaml@2.8.3: {} + + yocto-queue@0.1.0: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.15 + cookie: 1.1.1 + youch-core: 0.3.3 + + zod@3.25.76: {} diff --git a/new-deepnotes/pnpm-workspace.yaml b/new-deepnotes/pnpm-workspace.yaml new file mode 100644 index 00000000..3ff5faaa --- /dev/null +++ b/new-deepnotes/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "apps/*" + - "packages/*" diff --git a/new-deepnotes/template.env b/new-deepnotes/template.env new file mode 100644 index 00000000..2daa6c28 --- /dev/null +++ b/new-deepnotes/template.env @@ -0,0 +1,14 @@ +# Copy to .env for local development (see docker-compose.yml). + +# PostgreSQL — used by Drizzle CLI, integration tests, and Wrangler Hyperdrive localConnectionString +DATABASE_URL=postgresql://deepnotes:deepnotes@localhost:5433/deepnotes + +# Admin URL for template-database tests (role with CREATEDB); optional until integration tests land +# DATABASE_ADMIN_URL=postgresql://postgres:postgres@localhost:5433/postgres + +# Standard Redis (not KeyDB). TCP URL for Node; Workers may use Upstash HTTP or REDIS_URL via TCP where supported +REDIS_URL=redis://localhost:6380 + +# API worker (Wrangler secrets in production) +# JWT_SECRET= +# STRIPE_WEBHOOK_SECRET= diff --git a/new-deepnotes/tsconfig.base.json b/new-deepnotes/tsconfig.base.json new file mode 100644 index 00000000..95c08107 --- /dev/null +++ b/new-deepnotes/tsconfig.base.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true + } +} diff --git a/new-deepnotes/turbo.json b/new-deepnotes/turbo.json new file mode 100644 index 00000000..02af1308 --- /dev/null +++ b/new-deepnotes/turbo.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".output/**"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "lint": {}, + "typecheck": {}, + "test": {}, + "db:check": { + "cache": false + } + } +} From 8cf85b7d3ed657058606f718f1580c4bedc962a1 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 22:40:37 -0300 Subject: [PATCH 021/243] feat(new-deepnotes): phase 0 - legacy Drizzle schema, TRPC/REST map, and template DB tests --- .github/workflows/new-deepnotes-ci.yml | 2 + new-deepnotes/PLAN_PROGRESS.md | 21 +- new-deepnotes/docs/AUTH_AND_CORS.md | 50 + new-deepnotes/docs/CLIENT_FORKS.md | 22 + new-deepnotes/docs/DEPLOY_CLOUDFLARE.md | 45 + new-deepnotes/docs/TRPC_REST_MAP.md | 102 ++ .../packages/db/migrations/0000_init.sql | 5 - .../db/migrations/0000_legacy_baseline.sql | 225 +++ .../db/migrations/meta/0000_snapshot.json | 1366 ++++++++++++++++- .../packages/db/migrations/meta/_journal.json | 6 +- new-deepnotes/packages/db/package.json | 5 +- new-deepnotes/packages/db/src/schema.ts | 424 ++++- .../packages/db/src/template-db.test.ts | 88 ++ new-deepnotes/packages/db/src/test/db-url.ts | 13 + .../packages/db/src/test/template-db.ts | 88 ++ new-deepnotes/packages/db/vitest.config.ts | 12 + new-deepnotes/pnpm-lock.yaml | 3 + new-deepnotes/template.env | 4 +- 18 files changed, 2441 insertions(+), 40 deletions(-) create mode 100644 new-deepnotes/docs/AUTH_AND_CORS.md create mode 100644 new-deepnotes/docs/CLIENT_FORKS.md create mode 100644 new-deepnotes/docs/DEPLOY_CLOUDFLARE.md create mode 100644 new-deepnotes/docs/TRPC_REST_MAP.md delete mode 100644 new-deepnotes/packages/db/migrations/0000_init.sql create mode 100644 new-deepnotes/packages/db/migrations/0000_legacy_baseline.sql create mode 100644 new-deepnotes/packages/db/src/template-db.test.ts create mode 100644 new-deepnotes/packages/db/src/test/db-url.ts create mode 100644 new-deepnotes/packages/db/src/test/template-db.ts create mode 100644 new-deepnotes/packages/db/vitest.config.ts diff --git a/.github/workflows/new-deepnotes-ci.yml b/.github/workflows/new-deepnotes-ci.yml index f3e1aff5..43872fc9 100644 --- a/.github/workflows/new-deepnotes-ci.yml +++ b/.github/workflows/new-deepnotes-ci.yml @@ -33,6 +33,8 @@ jobs: --health-retries 10 env: DATABASE_URL: postgresql://deepnotes:deepnotes@127.0.0.1:5433/deepnotes + # CREATEDB-capable catalog connection for @deepnotes/db template-clone tests (RESTART_PLAN §5.7) + DATABASE_ADMIN_URL: postgresql://deepnotes:deepnotes@127.0.0.1:5433/postgres steps: - uses: actions/checkout@v4 diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 67f96962..ebbf0d8d 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -10,9 +10,9 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | Phase | Status | Notes | |-------|--------|--------| -| **0** — OpenAPI + Drizzle inventory | **In progress** | Minimal OpenAPI (`/api/health`) and Drizzle chain exist; full tRPC→REST map, `postgres-init.sql` transcription, auth env doc still open. | +| **0** — OpenAPI + Drizzle inventory | **In progress** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | -| **2** — Repo bootstrap | **Mostly done** | pnpm + Turbo 2, Node ≥22, Docker Postgres/Redis, `template.env`, Hono worker + Wrangler + Hyperdrive stub, Vue+Vite web shell, root CI (`new-deepnotes-ci.yml`). Missing: Pages/preview env doc, **CREATEDB** + template-DB integration tests (§5.7). | +| **2** — Repo bootstrap | **Mostly done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). Optional: Wrangler deploy job. | | **3** — REST + Drizzle features | **Not started** | Auth/sessions, pages/groups, realtime/collab, Stripe (no RevenueCat). | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -21,11 +21,11 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ ## Phase 0 checklist (exit: OpenAPI v0 + Drizzle in repo + feature checklist) -- [ ] Map legacy **tRPC** procedures + **WebSocket** handlers → proposed REST/WS names (skeleton routes may return `501`). +- [x] Map legacy **tRPC** procedures + **WebSocket** handlers → proposed REST/WS names (skeleton routes may return `501`). - [x] **OpenAPI** published from code (v0: health + spec endpoint); expand paths as features land. -- [ ] Transcribe **`postgres-init.sql`** → Drizzle schema + follow-on migrations (bootstrap `app_meta` / `0000` is only a placeholder). -- [ ] Document **cookie names**, **JWT** claims, **CORS** origins. -- [ ] List **`@deepnotes/*` forks** the new client will not use (exception list with owners if any remain). +- [x] Transcribe **`postgres-init.sql`** → Drizzle schema + baseline migration (`0000_legacy_baseline`: `pgcrypto`, `nanoid()`, core tables, FKs aligned with Drizzle; legacy `NOT VALID` FKs omitted for fresh installs). +- [x] Document **cookie names**, **JWT** claims, **CORS** origins → [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md). +- [x] List **`@deepnotes/*` forks** the new client will not use (exception list with owners if any remain) → [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). --- @@ -34,9 +34,9 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] pnpm + Turborepo 2, Node 22+. - [x] Docker Compose: Postgres + Redis (`REDIS_URL`-style in `template.env`). - [x] Cloudflare: `wrangler.toml`, Hyperdrive binding (replace placeholder `id` before prod). -- [ ] Document **Pages** / preview vs production env vars; optional deploy job to CF preview. +- [x] Document **Pages** / preview vs production env vars; optional deploy job to CF preview → [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). - [x] CI: lint, typecheck, tests, `drizzle-kit check`, build (Postgres service present for future migrate/tests). -- [ ] CI: Postgres role with **CREATEDB** + **template DB** integration tests (RESTART_PLAN §5.7). +- [x] CI: Postgres role with **CREATEDB** + **template DB** integration tests (RESTART_PLAN §5.7) — `DATABASE_ADMIN_URL` + `src/template-db.test.ts`. --- @@ -46,10 +46,10 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [ ] Drizzle migrations from empty DB documented for production upgrades. - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). -- [ ] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7). +- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` template test. - [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. -- [ ] Client: zero undocumented forks, or a short owned exception list. +- [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. --- @@ -58,6 +58,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | Date | Change | |------|--------| +| 2026-04-26 | Phase 0 docs (TRPC_REST_MAP, AUTH_AND_CORS, CLIENT_FORKS); Phase 2 deploy doc; Drizzle legacy baseline from `postgres-init.sql`; Vitest template-DB integration test + CI `DATABASE_ADMIN_URL`. | | 2026-04-26 | Initial `new-deepnotes` monorepo: `@deepnotes/api`, `@deepnotes/db`, `@deepnotes/api-worker`, `@deepnotes/web`, CI workflow. | Add a row here for meaningful milestones (e.g. “auth MVP”, “first Drizzle migration from legacy schema”). diff --git a/new-deepnotes/docs/AUTH_AND_CORS.md b/new-deepnotes/docs/AUTH_AND_CORS.md new file mode 100644 index 00000000..c4fa58a2 --- /dev/null +++ b/new-deepnotes/docs/AUTH_AND_CORS.md @@ -0,0 +1,50 @@ +# Auth cookies, JWT claims, and CORS (legacy → new stack) + +This document captures **current legacy** behavior from `apps/app-server` so the new API can align deliberately (names may stay; paths and bodies are new per RESTART_PLAN). + +## HTTP cookies + +| Cookie | Purpose | Notes | +|--------|---------|--------| +| `accessToken` | JWT access token | `httpOnly`, `secure` when not `DEV`, `sameSite: 'strict'`, path `/` | +| `refreshToken` | JWT refresh token | same defaults as access | +| `loggedIn` | Client hint (`"true"`) | **`httpOnly: false`** so the SPA can branch UI; sameSite/path as above | + +Clearing session clears all three (`src/utils/cookies.ts`). + +## JWT signing + +| Token | Env secret | Durations (from `@deeplib/misc`) | +|-------|------------|-----------------------------------| +| Access | `ACCESS_SECRET` | 30 minutes | +| Refresh (short session) | `REFRESH_SECRET` | 1 hour | +| Refresh (“remember me”) | `REFRESH_SECRET` | 7 days | + +## Access token payload (`AccessTokenPayload`) + +| Claim | Meaning | +|-------|---------| +| `uid` | User id (nanoid-style string) | +| `sid` | Session id | + +## Refresh token payload (`RefreshTokenPayload`) + +| Claim | Meaning | +|-------|---------| +| `sid` | Session id | +| `rfc` | Refresh code (server-side validation) | +| `rms` | Remember-session flag | + +## New stack env (target) + +Use **`JWT_SECRET`** (or split **`ACCESS_SECRET`** / **`REFRESH_SECRET`**) in Wrangler secrets and local `.env`; document final names when the auth package lands. Do not ship secrets to the client bundle. + +## CORS (legacy reference) + +Legacy `@fastify/cors` (`apps/app-server/src/fastify/server.ts`): + +- `credentials: true` +- Allowed origins when **not** `DEV`: `process.env.CLIENT_URL`, `capacitor://deepnotes.app`, `http://localhost`, or `undefined` (non-browser) +- In `DEV`, all origins allowed + +The new stack should set an explicit allowlist for production: **web app origin** (e.g. Pages URL), **API subdomain** if distinct, and any **Capacitor** scheme you still support. Stripe dashboard webhook URL is server-to-server (no browser CORS). diff --git a/new-deepnotes/docs/CLIENT_FORKS.md b/new-deepnotes/docs/CLIENT_FORKS.md new file mode 100644 index 00000000..5392ddea --- /dev/null +++ b/new-deepnotes/docs/CLIENT_FORKS.md @@ -0,0 +1,22 @@ +# Legacy `@deepnotes/*` forks — new client stance + +The greenfield SPA under `apps/web` uses **stock Vite 6 + Vue 3** with **no** workspace-scoped Quasar/Vite forks. This list records what the **legacy** client depended on so we do **not** reintroduce them by accident. + +## Not used in `new-deepnotes` (default) + +| Legacy package / area | Role in old client | New approach | +|----------------------|-------------------|----------------| +| `@deepnotes/quasar`, `@deepnotes/quasar-app-vite` | UI shell, build | Plain Vue + Vite; no Quasar | +| `@deepnotes/app-server` / `AppRouter` | tRPC types | OpenAPI + `fetch` / generated client | +| `superjson` | tRPC serialization | JSON + explicit schemas | +| Forked `ioredis`, `html2canvas`, Tiptap collaboration cursor, `dotenv-expand` | Various | Use upstream npm unless a **documented** exception is required | + +## Exception process + +If a fork is unavoidable, add a row here with **owner**, **reason**, and **upgrade plan**. + +| Package | Owner | Reason | Plan | +|---------|-------|--------|------| +| — | — | — | — | + +Crypto and domain libraries (`@stdlib/crypto`, `@deeplib/misc`, etc.) are **separate** from Quasar/Vite forks; port only what Phase 4 needs, as shared packages or vendored modules with tests. diff --git a/new-deepnotes/docs/DEPLOY_CLOUDFLARE.md b/new-deepnotes/docs/DEPLOY_CLOUDFLARE.md new file mode 100644 index 00000000..7e51f78f --- /dev/null +++ b/new-deepnotes/docs/DEPLOY_CLOUDFLARE.md @@ -0,0 +1,45 @@ +# Cloudflare deploy — preview vs production + +High-level runbook aligned with [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN.md). + +## Components + +| Piece | Target | Config | +|-------|--------|--------| +| HTTP API | Cloudflare Workers | `apps/api-worker/wrangler.toml` | +| SPA | Cloudflare Pages (or Workers static assets) | Build `apps/web` `dist/` | +| Postgres | Managed Postgres (external) | **Hyperdrive** binding → same logical DB as local | +| Redis | Upstash / Redis Cloud / TCP-capable provider | `REDIS_URL` (or vendor HTTP API) via secrets | + +Replace the **Hyperdrive id placeholder** in `wrangler.toml` before production. + +## Environment variables + +### Production (Workers) + +Set via **Wrangler secrets** or dashboard (never commit): + +- Database: Hyperdrive handles pooling; app reads Hyperdrive binding, not raw remote URL in Worker code paths that should use the binding. +- `JWT_SECRET` (or `ACCESS_SECRET` / `REFRESH_SECRET` if split to match legacy semantics) +- `STRIPE_WEBHOOK_SECRET` when billing is wired +- `REDIS_URL` or vendor-specific vars for rate limits / sessions + +### Preview (per PR / branch) + +Typical pattern: + +- **Preview Worker**: separate environment in Wrangler (`env.preview`) or a second Worker name; secrets scoped to a **branch database** or read-only clone. +- **Preview Pages**: branch deployments; set **environment variables** in Pages project for **public** config only (e.g. `VITE_API_URL=https://api-preview.example.com`). +- **Never** put DB passwords or signing keys in `VITE_*` client variables. + +### Local + +See `template.env` and `docker-compose.yml`: Postgres on `5433`, Redis on `6380`, `DATABASE_URL`, optional `DATABASE_ADMIN_URL` for template DB tests. + +## CI + +GitHub Actions (`.github/workflows/new-deepnotes-ci.yml`) runs lint, typecheck, tests (including Postgres-backed `@deepnotes/db` tests), `drizzle-kit check`, and build. Optional follow-up: add a **deploy** job that runs `wrangler deploy` with Cloudflare API token stored as a repo secret. + +## DNS and cookies + +Serve the API from a stable host (e.g. `api.deepnotes.example`) and the SPA from `app.` or apex; set cookie **`Domain`** / **`Secure`** / **`SameSite`** to match that split. Document the chosen pairing in `AUTH_AND_CORS.md` when auth ships. diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md new file mode 100644 index 00000000..38a3495d --- /dev/null +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -0,0 +1,102 @@ +# Legacy tRPC / WebSocket → new HTTP map + +Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN.md). Proposed paths are **suggestions**; wire `501 Not Implemented` until handlers exist. **Out of scope** for the new product: user/group **rotate keys** (WebSocket) and **RevenueCat**. + +## Sessions (`sessionsRouter`) + +| Legacy procedure | Proposed REST / notes | +|------------------|----------------------| +| `sessions.startDemo` | `POST /api/sessions/demo` | +| `sessions.login` | `POST /api/sessions/login` | +| `sessions.refresh` | `POST /api/sessions/refresh` | +| `sessions.logout` | `POST /api/sessions/logout` | + +## Users — account (`users.account`) + +| Legacy procedure | Proposed REST / notes | +|------------------|----------------------| +| `users.account.register` | `POST /api/users` | +| `users.account.resendVerificationEmail` | `POST /api/users/me/email-verification/resend` | +| `users.account.verifyEmail` | `POST /api/users/me/email-verification/confirm` | +| `users.account.emailChange.request` | `POST /api/users/me/email-change` | +| `users.account.twoFactorAuth.enable.request` | `POST /api/users/me/2fa/enable/request` | +| `users.account.twoFactorAuth.enable.finish` | `POST /api/users/me/2fa/enable/finish` | +| `users.account.twoFactorAuth.load` | `GET /api/users/me/2fa` | +| `users.account.twoFactorAuth.generateRecoveryCodes` | `POST /api/users/me/2fa/recovery-codes` | +| `users.account.twoFactorAuth.forgetTrustedDevices` | `POST /api/users/me/2fa/devices/forget` | +| `users.account.twoFactorAuth.disable` | `POST /api/users/me/2fa/disable` | +| `users.account.stripe.createCheckoutSession` | `POST /api/billing/stripe/checkout-session` | +| `users.account.stripe.createPortalSession` | `POST /api/billing/stripe/portal-session` | +| `users.account.delete` | `DELETE /api/users/me` | + +## Users — pages (`users.pages`) + +| Legacy procedure | Proposed REST / notes | +|------------------|----------------------| +| `users.pages.notifications.load` | `GET /api/users/me/notifications` | +| `users.pages.notifications.markAsRead` | `POST /api/users/me/notifications/read` | +| `users.pages.getStartingPageId` | `GET /api/users/me/pages/starting` | +| `users.pages.getCurrentPath` | `GET /api/users/me/pages/path` | +| `users.pages.removeRecentPages` | `POST /api/users/me/pages/recent/remove` | +| `users.pages.clearRecentPages` | `POST /api/users/me/pages/recent/clear` | +| `users.pages.addFavoritePages` | `POST /api/users/me/pages/favorites` | +| `users.pages.removeFavoritePages` | `POST /api/users/me/pages/favorites/remove` | +| `users.pages.clearFavoritePages` | `POST /api/users/me/pages/favorites/clear` | +| `users.pages.setEncryptedDefaultNote` | `PATCH /api/users/me/defaults/note` | +| `users.pages.setEncryptedDefaultArrow` | `PATCH /api/users/me/defaults/arrow` | +| `users.pages.getGroupIds` | `GET /api/users/me/groups` | + +## Groups (`groupsRouter`) + +| Legacy procedure | Proposed REST / notes | +|------------------|----------------------| +| `groups.getMainPageId` | `GET /api/groups/:groupId/main-page` | +| `groups.getUserIds` | `GET /api/groups/:groupId/members` (ids / minimal DTO) | +| `groups.getPages` | `GET /api/groups/:groupId/pages` | +| `groups.password.enable` | `POST /api/groups/:groupId/password` | +| `groups.password.change` | `PATCH /api/groups/:groupId/password` | +| `groups.password.disable` | `DELETE /api/groups/:groupId/password` | +| `groups.privacy.makePublic` | `POST /api/groups/:groupId/privacy/public` | +| `groups.privacy.setJoinRequestsAllowed` | `PATCH /api/groups/:groupId/privacy/join-requests` | +| `groups.deletion.delete` | `DELETE /api/groups/:groupId` (soft) | +| `groups.deletion.restore` | `POST /api/groups/:groupId/restore` | +| `groups.deletion.deletePermanently` | `POST /api/groups/:groupId/purge` | + +## Pages (`pagesRouter`) + +| Legacy procedure | Proposed REST / notes | +|------------------|----------------------| +| `pages.create` | `POST /api/groups/:groupId/pages` | +| `pages.bump` | `POST /api/pages/:pageId/bump` | +| `pages.backlinks.create` | `POST /api/pages/:pageId/backlinks` | +| `pages.backlinks.delete` | `DELETE /api/pages/:pageId/backlinks/:targetPageId` | +| `pages.snapshots.save` | `POST /api/pages/:pageId/snapshots` | +| `pages.snapshots.load` | `GET /api/pages/:pageId/snapshots/:snapshotId` | +| `pages.snapshots.delete` | `DELETE /api/pages/:pageId/snapshots/:snapshotId` | +| `pages.deletion.delete` | `DELETE /api/pages/:pageId` (soft) | +| `pages.deletion.restore` | `POST /api/pages/:pageId/restore` | +| `pages.deletion.deletePermanently` | `POST /api/pages/:pageId/purge` | + +## Legacy app-server WebSocket → target + +| Legacy handler | New surface | Notes | +|----------------|-------------|--------| +| `websocket/groups/join-invitations/*` | `WS /api/ws/groups/...` or REST for low-frequency | send / accept / reject / cancel | +| `websocket/groups/join-requests/*` | same | send / accept / reject / cancel | +| `websocket/groups/change-user-role` | `PATCH /api/groups/:groupId/members/:userId` | prefer REST if acceptable | +| `websocket/groups/remove-user` | `DELETE /api/groups/:groupId/members/:userId` | | +| `websocket/groups/privacy/make-private` | `POST /api/groups/:groupId/privacy/private` | | +| `websocket/groups/rotate-keys` | — | **removed** per RESTART_PLAN | +| `websocket/pages/move` | `POST /api/pages/:pageId/move` | | +| `websocket/users/account/change-password` | `POST /api/users/me/password` | | +| `websocket/users/account/email-change/finish` | `POST /api/users/me/email-change/confirm` | | +| `websocket/users/account/rotate-keys` | — | **removed** | + +## Webhooks (not tRPC) + +| Legacy | New | +|--------|-----| +| Stripe webhook (Fastify) | `POST /api/webhooks/stripe` | +| RevenueCat webhook | **not implemented** | + +Reference routers: `apps/app-server/src/trpc/router.ts`, `apps/app-server/src/trpc/api/**`, `apps/app-server/src/websocket/**`. diff --git a/new-deepnotes/packages/db/migrations/0000_init.sql b/new-deepnotes/packages/db/migrations/0000_init.sql deleted file mode 100644 index 353459d8..00000000 --- a/new-deepnotes/packages/db/migrations/0000_init.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE "app_meta" ( - "key" text PRIMARY KEY NOT NULL, - "value" text NOT NULL, - "updated_at" timestamp with time zone DEFAULT now() NOT NULL -); diff --git a/new-deepnotes/packages/db/migrations/0000_legacy_baseline.sql b/new-deepnotes/packages/db/migrations/0000_legacy_baseline.sql new file mode 100644 index 00000000..cae65ca0 --- /dev/null +++ b/new-deepnotes/packages/db/migrations/0000_legacy_baseline.sql @@ -0,0 +1,225 @@ +CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "public"; +--> statement-breakpoint +CREATE FUNCTION "public"."nanoid"("size" integer DEFAULT 21, "alphabet" text DEFAULT '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'::text) RETURNS text + LANGUAGE plpgsql + AS $$ +DECLARE + idBuilder text := ''; + i int := 0; + bytes bytea; + alphabetIndex int; + mask int; + step int; +BEGIN + mask := (2 << cast(floor(log(length(alphabet) - 1) / log(2)) as int)) - 1; + step := cast(ceil(1.6 * mask * size / length(alphabet)) AS int); + + while true + loop + bytes := gen_random_bytes(size); + while i < size + loop + alphabetIndex := (get_byte(bytes, i) & mask) + 1; + if alphabetIndex <= length(alphabet) then + idBuilder := idBuilder || substr(alphabet, alphabetIndex, 1); + if length(idBuilder) = size then + return idBuilder; + end if; + end if; + i = i + 1; + end loop; + + i := 0; + end loop; +END +$$; +--> statement-breakpoint +CREATE TABLE "devices" ( + "id" char(21) PRIMARY KEY DEFAULT public.nanoid() NOT NULL, + "user_id" char(21) NOT NULL, + "trusted" boolean DEFAULT false NOT NULL, + "hash" bytea NOT NULL +); +--> statement-breakpoint +CREATE TABLE "group_join_invitations" ( + "group_id" char(21) NOT NULL, + "user_id" char(21) NOT NULL, + "inviter_id" char(21) NOT NULL, + "role" text NOT NULL, + "encrypted_access_keyring" bytea, + "encrypted_internal_keyring" bytea NOT NULL, + "encrypted_name" bytea NOT NULL, + "creation_date" timestamp with time zone DEFAULT now() NOT NULL, + "encrypted_name_for_user" bytea, + CONSTRAINT "group_join_invitations_pkey" PRIMARY KEY("group_id","user_id") +); +--> statement-breakpoint +CREATE TABLE "group_join_requests" ( + "group_id" char(21) NOT NULL, + "user_id" char(21) NOT NULL, + "rejected" boolean DEFAULT false NOT NULL, + "encrypted_name" bytea NOT NULL, + "creation_date" timestamp with time zone DEFAULT now() NOT NULL, + "encrypted_name_for_user" bytea NOT NULL, + CONSTRAINT "group_join_requests_pkey" PRIMARY KEY("group_id","user_id") +); +--> statement-breakpoint +CREATE TABLE "group_members" ( + "user_id" char(21) NOT NULL, + "group_id" char(21) NOT NULL, + "encrypted_access_keyring" bytea, + "role" text NOT NULL, + "encrypted_internal_keyring" bytea NOT NULL, + "last_activity_date" timestamp with time zone DEFAULT now() NOT NULL, + "encrypted_name" bytea, + "encrypted_name_for_user" bytea, + CONSTRAINT "groups_users_pkey" PRIMARY KEY("group_id","user_id") +); +--> statement-breakpoint +CREATE TABLE "groups" ( + "id" char(21) PRIMARY KEY DEFAULT public.nanoid() NOT NULL, + "main_page_id" char(21) NOT NULL, + "creation_date" timestamp with time zone DEFAULT now() NOT NULL, + "user_id" char(21), + "encrypted_name" bytea NOT NULL, + "public_keyring" bytea NOT NULL, + "encrypted_private_keyring" bytea NOT NULL, + "access_keyring" bytea, + "encrypted_content_keyring" bytea NOT NULL, + "permanent_deletion_date" timestamp with time zone, + "encrypted_rehashed_password_hash" bytea, + "are_join_requests_allowed" boolean DEFAULT true NOT NULL +); +--> statement-breakpoint +CREATE TABLE "notifications" ( + "type" text NOT NULL, + "datetime" timestamp with time zone DEFAULT now() NOT NULL, + "encrypted_content" bytea NOT NULL, + "id" bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "notifications_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1) +); +--> statement-breakpoint +CREATE TABLE "page_links" ( + "target_page_id" char(21) NOT NULL, + "source_page_id" char(21) NOT NULL, + "last_activity_date" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "page_links_pkey" PRIMARY KEY("source_page_id","target_page_id") +); +--> statement-breakpoint +CREATE TABLE "page_snapshots" ( + "page_id" char(21) NOT NULL, + "creation_date" timestamp with time zone DEFAULT now() NOT NULL, + "encrypted_data" bytea NOT NULL, + "author_id" char(21), + "type" text NOT NULL, + "encrypted_symmetric_key" bytea, + "id" char(21) PRIMARY KEY DEFAULT public.nanoid() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "page_updates" ( + "page_id" char(21) NOT NULL, + "index" bigint NOT NULL, + "encrypted_data" bytea NOT NULL, + CONSTRAINT "pages_updates_pkey" PRIMARY KEY("page_id","index") +); +--> statement-breakpoint +CREATE TABLE "pages" ( + "id" char(21) PRIMARY KEY DEFAULT public.nanoid() NOT NULL, + "creation_date" timestamp with time zone DEFAULT now() NOT NULL, + "last_activity_date" timestamp with time zone DEFAULT now() NOT NULL, + "group_id" char(21) NOT NULL, + "encrypted_relative_title" bytea NOT NULL, + "encrypted_symmetric_keyring" bytea NOT NULL, + "free" boolean, + "next_snapshot_update_index" bigint DEFAULT 100 NOT NULL, + "next_snapshot_date" timestamp with time zone DEFAULT (now() + '00:15:00'::interval) NOT NULL, + "next_key_rotation_date" timestamp with time zone DEFAULT (now() + '7 days'::interval) NOT NULL, + "permanent_deletion_date" timestamp with time zone, + "encrypted_absolute_title" bytea NOT NULL +); +--> statement-breakpoint +CREATE TABLE "sessions" ( + "id" char(21) PRIMARY KEY DEFAULT public.nanoid() NOT NULL, + "user_id" char(21) NOT NULL, + "creation_date" timestamp with time zone DEFAULT now() NOT NULL, + "invalidated" boolean DEFAULT false NOT NULL, + "device_id" char(21) NOT NULL, + "last_refresh_date" timestamp with time zone DEFAULT now() NOT NULL, + "expiration_date" timestamp with time zone NOT NULL, + "encryption_key" bytea NOT NULL, + "refresh_code" char(21) NOT NULL +); +--> statement-breakpoint +CREATE TABLE "users" ( + "id" char(21) PRIMARY KEY DEFAULT public.nanoid() NOT NULL, + "creation_date" timestamp with time zone DEFAULT now() NOT NULL, + "starting_page_id" char(21) NOT NULL, + "recent_page_ids" char(21)[] DEFAULT '{}'::character(21)[] NOT NULL, + "personal_group_id" char(21) NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "public_keyring" bytea NOT NULL, + "encrypted_private_keyring" bytea NOT NULL, + "encrypted_symmetric_keyring" bytea NOT NULL, + "encrypted_default_arrow" bytea NOT NULL, + "encrypted_default_note" bytea NOT NULL, + "two_factor_auth_enabled" boolean DEFAULT false NOT NULL, + "email_verification_expiration_date" timestamp with time zone, + "email_verification_code" text, + "recent_group_ids" char(21)[] DEFAULT '{}'::character(21)[] NOT NULL, + "last_notification_read" bigint, + "customer_id" text, + "plan" text DEFAULT 'basic' NOT NULL, + "subscription_id" text, + "encrypted_name" bytea, + "num_free_pages" integer DEFAULT 0 NOT NULL, + "encrypted_authenticator_secret" bytea, + "encrypted_email" bytea NOT NULL, + "encrypted_new_email" bytea, + "encrypted_recovery_codes" bytea, + "demo" boolean, + "email_hash" bytea NOT NULL, + "encrypted_rehashed_login_hash" bytea NOT NULL, + "new" boolean DEFAULT true NOT NULL +); +--> statement-breakpoint +CREATE TABLE "users_notifications" ( + "user_id" char(21) NOT NULL, + "encrypted_symmetric_key" bytea NOT NULL, + "notification_id" bigint NOT NULL, + CONSTRAINT "users_notifications_pkey" PRIMARY KEY("user_id","notification_id") +); +--> statement-breakpoint +CREATE TABLE "users_pages" ( + "user_id" char(21) NOT NULL, + "page_id" char(21) NOT NULL, + "last_parent_id" char(21), + CONSTRAINT "users_pages_pkey" PRIMARY KEY("user_id","page_id") +); +--> statement-breakpoint +ALTER TABLE "devices" ADD CONSTRAINT "devices_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "group_join_invitations" ADD CONSTRAINT "group_join_invitations_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "group_join_invitations" ADD CONSTRAINT "group_join_invitations_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "group_join_requests" ADD CONSTRAINT "group_join_requests_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "group_join_requests" ADD CONSTRAINT "group_join_requests_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "group_members" ADD CONSTRAINT "group_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "group_members" ADD CONSTRAINT "group_members_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "groups" ADD CONSTRAINT "groups_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "page_links" ADD CONSTRAINT "page_links_target_page_id_pages_id_fk" FOREIGN KEY ("target_page_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "page_links" ADD CONSTRAINT "page_links_source_page_id_pages_id_fk" FOREIGN KEY ("source_page_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "page_snapshots" ADD CONSTRAINT "page_snapshots_page_id_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "page_updates" ADD CONSTRAINT "page_updates_page_id_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."pages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "pages" ADD CONSTRAINT "pages_group_id_groups_id_fk" FOREIGN KEY ("group_id") REFERENCES "public"."groups"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "sessions" ADD CONSTRAINT "sessions_device_id_devices_id_fk" FOREIGN KEY ("device_id") REFERENCES "public"."devices"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "users_notifications" ADD CONSTRAINT "users_notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "users_notifications" ADD CONSTRAINT "users_notifications_notification_id_notifications_id_fk" FOREIGN KEY ("notification_id") REFERENCES "public"."notifications"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "users_pages" ADD CONSTRAINT "users_pages_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "group_join_invitations_user_id_idx" ON "group_join_invitations" USING btree ("user_id","creation_date" DESC);--> statement-breakpoint +CREATE INDEX "group_join_requests_user_id_idx" ON "group_join_requests" USING btree ("user_id","creation_date" DESC);--> statement-breakpoint +CREATE INDEX "group_members_user_id_idx" ON "group_members" USING btree ("user_id","last_activity_date" DESC);--> statement-breakpoint +CREATE INDEX "page_links_target_page_id_idx" ON "page_links" USING btree ("target_page_id","last_activity_date" DESC);--> statement-breakpoint +CREATE INDEX "sessions_refresh_code_idx" ON "sessions" USING btree ("refresh_code");--> statement-breakpoint +CREATE UNIQUE INDEX "users_encrypted_email_key" ON "users" USING btree ("encrypted_email");--> statement-breakpoint +CREATE UNIQUE INDEX "users_email_hash_idx" ON "users" USING btree ("email_hash");--> statement-breakpoint +CREATE INDEX "users_creation_date_idx" ON "users" USING btree ("creation_date" DESC);--> statement-breakpoint +CREATE INDEX "users_customer_id_idx" ON "users" USING btree ("customer_id");--> statement-breakpoint +CREATE INDEX "users_pages_page_id_idx" ON "users_pages" USING btree ("page_id"); diff --git a/new-deepnotes/packages/db/migrations/meta/0000_snapshot.json b/new-deepnotes/packages/db/migrations/meta/0000_snapshot.json index bf3b577e..fd6adc83 100644 --- a/new-deepnotes/packages/db/migrations/meta/0000_snapshot.json +++ b/new-deepnotes/packages/db/migrations/meta/0000_snapshot.json @@ -1,40 +1,1388 @@ { - "id": "596f6d22-f0ba-49e1-81a9-42a641e73269", + "id": "e2275e57-a99e-45e4-8228-df7ecbd085bf", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", "tables": { - "public.app_meta": { - "name": "app_meta", + "public.devices": { + "name": "devices", "schema": "", "columns": { - "key": { - "name": "key", + "id": { + "name": "id", + "type": "char(21)", + "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + }, + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "trusted": { + "name": "trusted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hash": { + "name": "hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "devices_user_id_users_id_fk": { + "name": "devices_user_id_users_id_fk", + "tableFrom": "devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_join_invitations": { + "name": "group_join_invitations", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_access_keyring": { + "name": "encrypted_access_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "encrypted_internal_keyring": { + "name": "encrypted_internal_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "encrypted_name_for_user": { + "name": "encrypted_name_for_user", + "type": "bytea", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "group_join_invitations_user_id_idx": { + "name": "group_join_invitations_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"creation_date\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_join_invitations_group_id_groups_id_fk": { + "name": "group_join_invitations_group_id_groups_id_fk", + "tableFrom": "group_join_invitations", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_join_invitations_user_id_users_id_fk": { + "name": "group_join_invitations_user_id_users_id_fk", + "tableFrom": "group_join_invitations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_join_invitations_pkey": { + "name": "group_join_invitations_pkey", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_join_requests": { + "name": "group_join_requests", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "rejected": { + "name": "rejected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "encrypted_name_for_user": { + "name": "encrypted_name_for_user", + "type": "bytea", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "group_join_requests_user_id_idx": { + "name": "group_join_requests_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"creation_date\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_join_requests_group_id_groups_id_fk": { + "name": "group_join_requests_group_id_groups_id_fk", + "tableFrom": "group_join_requests", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_join_requests_user_id_users_id_fk": { + "name": "group_join_requests_user_id_users_id_fk", + "tableFrom": "group_join_requests", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_join_requests_pkey": { + "name": "group_join_requests_pkey", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_members": { + "name": "group_members", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "encrypted_access_keyring": { + "name": "encrypted_access_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_internal_keyring": { + "name": "encrypted_internal_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "last_activity_date": { + "name": "last_activity_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "encrypted_name_for_user": { + "name": "encrypted_name_for_user", + "type": "bytea", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "group_members_user_id_idx": { + "name": "group_members_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"last_activity_date\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_members_user_id_users_id_fk": { + "name": "group_members_user_id_users_id_fk", + "tableFrom": "group_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_members_group_id_groups_id_fk": { + "name": "group_members_group_id_groups_id_fk", + "tableFrom": "group_members", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groups_users_pkey": { + "name": "groups_users_pkey", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groups": { + "name": "groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(21)", "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + }, + "main_page_id": { + "name": "main_page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": false + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "public_keyring": { + "name": "public_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_private_keyring": { + "name": "encrypted_private_keyring", + "type": "bytea", + "primaryKey": false, "notNull": true }, - "value": { - "name": "value", + "access_keyring": { + "name": "access_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "encrypted_content_keyring": { + "name": "encrypted_content_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "permanent_deletion_date": { + "name": "permanent_deletion_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "encrypted_rehashed_password_hash": { + "name": "encrypted_rehashed_password_hash", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "are_join_requests_allowed": { + "name": "are_join_requests_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "groups_user_id_users_id_fk": { + "name": "groups_user_id_users_id_fk", + "tableFrom": "groups", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "type": { + "name": "type", "type": "text", "primaryKey": false, "notNull": true }, - "updated_at": { - "name": "updated_at", + "datetime": { + "name": "datetime", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "encrypted_content": { + "name": "encrypted_content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "notifications_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_links": { + "name": "page_links", + "schema": "", + "columns": { + "target_page_id": { + "name": "target_page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "source_page_id": { + "name": "source_page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "last_activity_date": { + "name": "last_activity_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "page_links_target_page_id_idx": { + "name": "page_links_target_page_id_idx", + "columns": [ + { + "expression": "target_page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"last_activity_date\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_links_target_page_id_pages_id_fk": { + "name": "page_links_target_page_id_pages_id_fk", + "tableFrom": "page_links", + "tableTo": "pages", + "columnsFrom": [ + "target_page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_links_source_page_id_pages_id_fk": { + "name": "page_links_source_page_id_pages_id_fk", + "tableFrom": "page_links", + "tableTo": "pages", + "columnsFrom": [ + "source_page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "page_links_pkey": { + "name": "page_links_pkey", + "columns": [ + "source_page_id", + "target_page_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_snapshots": { + "name": "page_snapshots", + "schema": "", + "columns": { + "page_id": { + "name": "page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "encrypted_data": { + "name": "encrypted_data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "char(21)", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_symmetric_key": { + "name": "encrypted_symmetric_key", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "char(21)", + "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + } + }, + "indexes": {}, + "foreignKeys": { + "page_snapshots_page_id_pages_id_fk": { + "name": "page_snapshots_page_id_pages_id_fk", + "tableFrom": "page_snapshots", + "tableTo": "pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_updates": { + "name": "page_updates", + "schema": "", + "columns": { + "page_id": { + "name": "page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "encrypted_data": { + "name": "encrypted_data", + "type": "bytea", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "page_updates_page_id_pages_id_fk": { + "name": "page_updates_page_id_pages_id_fk", + "tableFrom": "page_updates", + "tableTo": "pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "pages_updates_pkey": { + "name": "pages_updates_pkey", + "columns": [ + "page_id", + "index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pages": { + "name": "pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(21)", + "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + }, + "creation_date": { + "name": "creation_date", "type": "timestamp with time zone", "primaryKey": false, "notNull": true, "default": "now()" + }, + "last_activity_date": { + "name": "last_activity_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "group_id": { + "name": "group_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "encrypted_relative_title": { + "name": "encrypted_relative_title", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_symmetric_keyring": { + "name": "encrypted_symmetric_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "free": { + "name": "free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "next_snapshot_update_index": { + "name": "next_snapshot_update_index", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "next_snapshot_date": { + "name": "next_snapshot_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "(now() + '00:15:00'::interval)" + }, + "next_key_rotation_date": { + "name": "next_key_rotation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "(now() + '7 days'::interval)" + }, + "permanent_deletion_date": { + "name": "permanent_deletion_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "encrypted_absolute_title": { + "name": "encrypted_absolute_title", + "type": "bytea", + "primaryKey": false, + "notNull": true } }, "indexes": {}, + "foreignKeys": { + "pages_group_id_groups_id_fk": { + "name": "pages_group_id_groups_id_fk", + "tableFrom": "pages", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(21)", + "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + }, + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "invalidated": { + "name": "invalidated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "device_id": { + "name": "device_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "last_refresh_date": { + "name": "last_refresh_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "encryption_key": { + "name": "encryption_key", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "refresh_code": { + "name": "refresh_code", + "type": "char(21)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sessions_refresh_code_idx": { + "name": "sessions_refresh_code_idx", + "columns": [ + { + "expression": "refresh_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_device_id_devices_id_fk": { + "name": "sessions_device_id_devices_id_fk", + "tableFrom": "sessions", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(21)", + "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "starting_page_id": { + "name": "starting_page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "recent_page_ids": { + "name": "recent_page_ids", + "type": "char(21)[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::character(21)[]" + }, + "personal_group_id": { + "name": "personal_group_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "public_keyring": { + "name": "public_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_private_keyring": { + "name": "encrypted_private_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_symmetric_keyring": { + "name": "encrypted_symmetric_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_default_arrow": { + "name": "encrypted_default_arrow", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_default_note": { + "name": "encrypted_default_note", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "two_factor_auth_enabled": { + "name": "two_factor_auth_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_verification_expiration_date": { + "name": "email_verification_expiration_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_verification_code": { + "name": "email_verification_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recent_group_ids": { + "name": "recent_group_ids", + "type": "char(21)[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::character(21)[]" + }, + "last_notification_read": { + "name": "last_notification_read", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'basic'" + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "num_free_pages": { + "name": "num_free_pages", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "encrypted_authenticator_secret": { + "name": "encrypted_authenticator_secret", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "encrypted_email": { + "name": "encrypted_email", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_new_email": { + "name": "encrypted_new_email", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "encrypted_recovery_codes": { + "name": "encrypted_recovery_codes", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "demo": { + "name": "demo", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email_hash": { + "name": "email_hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_rehashed_login_hash": { + "name": "encrypted_rehashed_login_hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "new": { + "name": "new", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "users_encrypted_email_key": { + "name": "users_encrypted_email_key", + "columns": [ + { + "expression": "encrypted_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_hash_idx": { + "name": "users_email_hash_idx", + "columns": [ + { + "expression": "email_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_creation_date_idx": { + "name": "users_creation_date_idx", + "columns": [ + { + "expression": "\"creation_date\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_customer_id_idx": { + "name": "users_customer_id_idx", + "columns": [ + { + "expression": "customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, "policies": {}, "checkConstraints": {}, "isRLSEnabled": false + }, + "public.users_notifications": { + "name": "users_notifications", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "encrypted_symmetric_key": { + "name": "encrypted_symmetric_key", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_notifications_user_id_users_id_fk": { + "name": "users_notifications_user_id_users_id_fk", + "tableFrom": "users_notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_notifications_notification_id_notifications_id_fk": { + "name": "users_notifications_notification_id_notifications_id_fk", + "tableFrom": "users_notifications", + "tableTo": "notifications", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_notifications_pkey": { + "name": "users_notifications_pkey", + "columns": [ + "user_id", + "notification_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_pages": { + "name": "users_pages", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "last_parent_id": { + "name": "last_parent_id", + "type": "char(21)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_pages_page_id_idx": { + "name": "users_pages_page_id_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_pages_user_id_users_id_fk": { + "name": "users_pages_user_id_users_id_fk", + "tableFrom": "users_pages", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_pages_pkey": { + "name": "users_pages_pkey", + "columns": [ + "user_id", + "page_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false } }, "enums": {}, diff --git a/new-deepnotes/packages/db/migrations/meta/_journal.json b/new-deepnotes/packages/db/migrations/meta/_journal.json index 3fa46dfa..6b813454 100644 --- a/new-deepnotes/packages/db/migrations/meta/_journal.json +++ b/new-deepnotes/packages/db/migrations/meta/_journal.json @@ -5,9 +5,9 @@ { "idx": 0, "version": "7", - "when": 1777252387392, - "tag": "0000_init", + "when": 1777253675226, + "tag": "0000_legacy_baseline", "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/new-deepnotes/packages/db/package.json b/new-deepnotes/packages/db/package.json index f6b68ab6..a2d64826 100644 --- a/new-deepnotes/packages/db/package.json +++ b/new-deepnotes/packages/db/package.json @@ -18,7 +18,7 @@ "lint": "eslint .", "typecheck": "tsc -p tsconfig.json --noEmit", "db:check": "drizzle-kit check", - "test": "node -e \"process.exit(0)\"" + "test": "vitest run" }, "dependencies": { "drizzle-orm": "^0.41.0", @@ -28,6 +28,7 @@ "@types/node": "^22.14.1", "dotenv": "^16.5.0", "drizzle-kit": "^0.31.0", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^3.2.4" } } diff --git a/new-deepnotes/packages/db/src/schema.ts b/new-deepnotes/packages/db/src/schema.ts index 2707b013..f1ec3b18 100644 --- a/new-deepnotes/packages/db/src/schema.ts +++ b/new-deepnotes/packages/db/src/schema.ts @@ -1,13 +1,419 @@ -import { pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { relations, sql } from "drizzle-orm"; +import { + bigint, + boolean, + char, + customType, + index, + integer, + pgTable, + primaryKey, + text, + timestamp, + uniqueIndex, +} from "drizzle-orm/pg-core"; -/** - * Bootstrap table so the migration chain is non-empty. - * Replace or extend when importing the legacy schema from postgres-init.sql (RESTART_PLAN §4.5). - */ -export const appMeta = pgTable("app_meta", { - key: text("key").primaryKey(), - value: text("value").notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }) +/** Binary columns in the legacy DB (`postgres-init.sql`). */ +export const bytea = customType<{ data: Buffer; driverData: Buffer }>({ + dataType() { + return "bytea"; + }, +}); + +export const users = pgTable( + "users", + { + id: char("id", { length: 21 }).primaryKey().default(sql`public.nanoid()`), + creationDate: timestamp("creation_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + startingPageId: char("starting_page_id", { length: 21 }).notNull(), + recentPageIds: char("recent_page_ids", { length: 21 }) + .array() + .notNull() + .default(sql`'{}'::character(21)[]`), + personalGroupId: char("personal_group_id", { length: 21 }).notNull(), + emailVerified: boolean("email_verified").notNull().default(false), + publicKeyring: bytea("public_keyring").notNull(), + encryptedPrivateKeyring: bytea("encrypted_private_keyring").notNull(), + encryptedSymmetricKeyring: bytea("encrypted_symmetric_keyring").notNull(), + encryptedDefaultArrow: bytea("encrypted_default_arrow").notNull(), + encryptedDefaultNote: bytea("encrypted_default_note").notNull(), + twoFactorAuthEnabled: boolean("two_factor_auth_enabled") + .notNull() + .default(false), + emailVerificationExpirationDate: timestamp( + "email_verification_expiration_date", + { withTimezone: true, mode: "string" }, + ), + emailVerificationCode: text("email_verification_code"), + recentGroupIds: char("recent_group_ids", { length: 21 }) + .array() + .notNull() + .default(sql`'{}'::character(21)[]`), + lastNotificationRead: bigint("last_notification_read", { + mode: "number", + }), + customerId: text("customer_id"), + plan: text("plan").notNull().default("basic"), + subscriptionId: text("subscription_id"), + encryptedName: bytea("encrypted_name"), + numFreePages: integer("num_free_pages").notNull().default(0), + encryptedAuthenticatorSecret: bytea("encrypted_authenticator_secret"), + encryptedEmail: bytea("encrypted_email").notNull(), + encryptedNewEmail: bytea("encrypted_new_email"), + encryptedRecoveryCodes: bytea("encrypted_recovery_codes"), + demo: boolean("demo"), + emailHash: bytea("email_hash").notNull(), + encryptedRehashedLoginHash: bytea("encrypted_rehashed_login_hash").notNull(), + isNew: boolean("new").notNull().default(true), + }, + (t) => [ + uniqueIndex("users_encrypted_email_key").on(t.encryptedEmail), + uniqueIndex("users_email_hash_idx").on(t.emailHash), + index("users_creation_date_idx").on(sql`${t.creationDate} DESC`), + index("users_customer_id_idx").on(t.customerId), + ], +); + +export const groups = pgTable("groups", { + id: char("id", { length: 21 }).primaryKey().default(sql`public.nanoid()`), + mainPageId: char("main_page_id", { length: 21 }).notNull(), + creationDate: timestamp("creation_date", { + withTimezone: true, + mode: "string", + }) .notNull() .defaultNow(), + userId: char("user_id", { length: 21 }).references(() => users.id, { + onDelete: "cascade", + }), + encryptedName: bytea("encrypted_name").notNull(), + publicKeyring: bytea("public_keyring").notNull(), + encryptedPrivateKeyring: bytea("encrypted_private_keyring").notNull(), + accessKeyring: bytea("access_keyring"), + encryptedContentKeyring: bytea("encrypted_content_keyring").notNull(), + permanentDeletionDate: timestamp("permanent_deletion_date", { + withTimezone: true, + mode: "string", + }), + encryptedRehashedPasswordHash: bytea("encrypted_rehashed_password_hash"), + areJoinRequestsAllowed: boolean("are_join_requests_allowed") + .notNull() + .default(true), +}); + +export const pages = pgTable("pages", { + id: char("id", { length: 21 }).primaryKey().default(sql`public.nanoid()`), + creationDate: timestamp("creation_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + lastActivityDate: timestamp("last_activity_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + groupId: char("group_id", { length: 21 }) + .notNull() + .references(() => groups.id, { onDelete: "cascade" }), + encryptedRelativeTitle: bytea("encrypted_relative_title").notNull(), + encryptedSymmetricKeyring: bytea("encrypted_symmetric_keyring").notNull(), + free: boolean("free"), + nextSnapshotUpdateIndex: bigint("next_snapshot_update_index", { + mode: "number", + }) + .notNull() + .default(100), + nextSnapshotDate: timestamp("next_snapshot_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .default(sql`(now() + '00:15:00'::interval)`), + /** Legacy column; key rotation is dropped in the new product (RESTART_PLAN). Kept for DB compatibility. */ + nextKeyRotationDate: timestamp("next_key_rotation_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .default(sql`(now() + '7 days'::interval)`), + permanentDeletionDate: timestamp("permanent_deletion_date", { + withTimezone: true, + mode: "string", + }), + encryptedAbsoluteTitle: bytea("encrypted_absolute_title").notNull(), +}); + +export const devices = pgTable("devices", { + id: char("id", { length: 21 }).primaryKey().default(sql`public.nanoid()`), + userId: char("user_id", { length: 21 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + trusted: boolean("trusted").notNull().default(false), + hash: bytea("hash").notNull(), }); + +export const sessions = pgTable( + "sessions", + { + id: char("id", { length: 21 }).primaryKey().default(sql`public.nanoid()`), + userId: char("user_id", { length: 21 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + creationDate: timestamp("creation_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + invalidated: boolean("invalidated").notNull().default(false), + deviceId: char("device_id", { length: 21 }) + .notNull() + .references(() => devices.id, { onDelete: "cascade" }), + lastRefreshDate: timestamp("last_refresh_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + expirationDate: timestamp("expiration_date", { + withTimezone: true, + mode: "string", + }).notNull(), + encryptionKey: bytea("encryption_key").notNull(), + refreshCode: char("refresh_code", { length: 21 }).notNull(), + }, + (t) => [index("sessions_refresh_code_idx").on(t.refreshCode)], +); + +export const notifications = pgTable("notifications", { + type: text("type").notNull(), + datetime: timestamp("datetime", { withTimezone: true, mode: "string" }) + .notNull() + .defaultNow(), + encryptedContent: bytea("encrypted_content").notNull(), + id: bigint("id", { mode: "number" }) + .primaryKey() + .generatedAlwaysAsIdentity({ name: "notifications_id_seq" }), +}); + +export const groupMembers = pgTable( + "group_members", + { + userId: char("user_id", { length: 21 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + groupId: char("group_id", { length: 21 }) + .notNull() + .references(() => groups.id, { onDelete: "cascade" }), + encryptedAccessKeyring: bytea("encrypted_access_keyring"), + role: text("role").notNull(), + encryptedInternalKeyring: bytea("encrypted_internal_keyring").notNull(), + lastActivityDate: timestamp("last_activity_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + encryptedName: bytea("encrypted_name"), + encryptedNameForUser: bytea("encrypted_name_for_user"), + }, + (t) => [ + primaryKey({ columns: [t.groupId, t.userId], name: "groups_users_pkey" }), + index("group_members_user_id_idx").on( + t.userId, + sql`${t.lastActivityDate} DESC`, + ), + ], +); + +export const groupJoinInvitations = pgTable( + "group_join_invitations", + { + groupId: char("group_id", { length: 21 }) + .notNull() + .references(() => groups.id, { onDelete: "cascade" }), + userId: char("user_id", { length: 21 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + inviterId: char("inviter_id", { length: 21 }).notNull(), + role: text("role").notNull(), + encryptedAccessKeyring: bytea("encrypted_access_keyring"), + encryptedInternalKeyring: bytea("encrypted_internal_keyring").notNull(), + encryptedName: bytea("encrypted_name").notNull(), + creationDate: timestamp("creation_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + encryptedNameForUser: bytea("encrypted_name_for_user"), + }, + (t) => [ + primaryKey({ + columns: [t.groupId, t.userId], + name: "group_join_invitations_pkey", + }), + index("group_join_invitations_user_id_idx").on( + t.userId, + sql`${t.creationDate} DESC`, + ), + ], +); + +export const groupJoinRequests = pgTable( + "group_join_requests", + { + groupId: char("group_id", { length: 21 }) + .notNull() + .references(() => groups.id, { onDelete: "cascade" }), + userId: char("user_id", { length: 21 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + rejected: boolean("rejected").notNull().default(false), + encryptedName: bytea("encrypted_name").notNull(), + creationDate: timestamp("creation_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + encryptedNameForUser: bytea("encrypted_name_for_user").notNull(), + }, + (t) => [ + primaryKey({ + columns: [t.groupId, t.userId], + name: "group_join_requests_pkey", + }), + index("group_join_requests_user_id_idx").on( + t.userId, + sql`${t.creationDate} DESC`, + ), + ], +); + +export const pageLinks = pgTable( + "page_links", + { + targetPageId: char("target_page_id", { length: 21 }) + .notNull() + .references(() => pages.id, { onDelete: "cascade" }), + sourcePageId: char("source_page_id", { length: 21 }) + .notNull() + .references(() => pages.id, { onDelete: "cascade" }), + lastActivityDate: timestamp("last_activity_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + }, + (t) => [ + primaryKey({ + columns: [t.sourcePageId, t.targetPageId], + name: "page_links_pkey", + }), + index("page_links_target_page_id_idx").on( + t.targetPageId, + sql`${t.lastActivityDate} DESC`, + ), + ], +); + +export const pageSnapshots = pgTable( + "page_snapshots", + { + pageId: char("page_id", { length: 21 }) + .notNull() + .references(() => pages.id, { onDelete: "cascade" }), + creationDate: timestamp("creation_date", { + withTimezone: true, + mode: "string", + }) + .notNull() + .defaultNow(), + encryptedData: bytea("encrypted_data").notNull(), + authorId: char("author_id", { length: 21 }), + type: text("type").notNull(), + encryptedSymmetricKey: bytea("encrypted_symmetric_key"), + id: char("id", { length: 21 }).primaryKey().default(sql`public.nanoid()`), + }, + () => [], +); + +export const pageUpdates = pgTable( + "page_updates", + { + pageId: char("page_id", { length: 21 }) + .notNull() + .references(() => pages.id, { onDelete: "cascade" }), + index: bigint("index", { mode: "number" }).notNull(), + encryptedData: bytea("encrypted_data").notNull(), + }, + (t) => [ + primaryKey({ + columns: [t.pageId, t.index], + name: "pages_updates_pkey", + }), + ], +); + +export const usersNotifications = pgTable( + "users_notifications", + { + userId: char("user_id", { length: 21 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + encryptedSymmetricKey: bytea("encrypted_symmetric_key").notNull(), + notificationId: bigint("notification_id", { mode: "number" }) + .notNull() + .references(() => notifications.id, { onDelete: "cascade" }), + }, + (t) => [ + primaryKey({ + columns: [t.userId, t.notificationId], + name: "users_notifications_pkey", + }), + ], +); + +export const usersPages = pgTable( + "users_pages", + { + userId: char("user_id", { length: 21 }) + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + pageId: char("page_id", { length: 21 }).notNull(), + lastParentId: char("last_parent_id", { length: 21 }), + }, + (t) => [ + primaryKey({ columns: [t.userId, t.pageId], name: "users_pages_pkey" }), + index("users_pages_page_id_idx").on(t.pageId), + ], +); + +export const usersRelations = relations(users, ({ many, one }) => ({ + devices: many(devices), + sessions: many(sessions), + personalGroup: one(groups, { + fields: [users.personalGroupId], + references: [groups.id], + }), +})); + +export const groupsRelations = relations(groups, ({ one, many }) => ({ + owner: one(users, { fields: [groups.userId], references: [users.id] }), + pages: many(pages), + members: many(groupMembers), +})); + +export const pagesRelations = relations(pages, ({ one }) => ({ + group: one(groups, { fields: [pages.groupId], references: [groups.id] }), +})); diff --git a/new-deepnotes/packages/db/src/template-db.test.ts b/new-deepnotes/packages/db/src/template-db.test.ts new file mode 100644 index 00000000..ac45b0b1 --- /dev/null +++ b/new-deepnotes/packages/db/src/template-db.test.ts @@ -0,0 +1,88 @@ +import { randomBytes } from "node:crypto"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { config as loadEnv } from "dotenv"; +import { eq, sql } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import * as schema from "./schema.js"; +import { users } from "./schema.js"; +import { withDatabaseName } from "./test/db-url.js"; +import { + createDatabaseFromTemplate, + dropDatabaseIfExists, + ensureTemplateDatabase, + resolveTemplateContext, +} from "./test/template-db.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +loadEnv({ path: join(__dirname, "../../../.env") }); + +const ctx = resolveTemplateContext(); + +describe.skipIf(ctx == null)("postgres template database (§5.7)", () => { + const templateName = ctx!.templateName; + const adminUrl = ctx!.adminUrl; + const appBaseUrl = ctx!.appBaseUrl; + + beforeAll(async () => { + await ensureTemplateDatabase(ctx!); + }); + + afterAll(async () => { + const admin = postgres(adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin, templateName); + } finally { + await admin.end({ timeout: 5 }); + } + }); + + it("clones template and sees isolated empty users", async () => { + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const countRows = await db + .select({ n: sql`count(*)::int` }) + .from(users); + expect(countRows[0]?.n).toBe(0); + + const uid = "012345678901234567890"; + await db.insert(users).values({ + id: uid, + startingPageId: uid, + personalGroupId: uid, + publicKeyring: Buffer.alloc(1), + encryptedPrivateKeyring: Buffer.alloc(1), + encryptedSymmetricKeyring: Buffer.alloc(1), + encryptedDefaultArrow: Buffer.alloc(1), + encryptedDefaultNote: Buffer.alloc(1), + encryptedEmail: Buffer.alloc(1), + emailHash: Buffer.alloc(1), + encryptedRehashedLoginHash: Buffer.alloc(1), + }); + + const row = await db.select().from(users).where(eq(users.id, uid)); + expect(row).toHaveLength(1); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); +}); diff --git a/new-deepnotes/packages/db/src/test/db-url.ts b/new-deepnotes/packages/db/src/test/db-url.ts new file mode 100644 index 00000000..d9fb9e63 --- /dev/null +++ b/new-deepnotes/packages/db/src/test/db-url.ts @@ -0,0 +1,13 @@ +/** + * Parse `postgresql://user:pass@host:port/dbname` and swap the database segment. + */ +export function withDatabaseName(connectionString: string, databaseName: string): string { + const u = new URL(connectionString); + u.pathname = `/${databaseName}`; + return u.toString(); +} + +/** Admin catalog URL (role must be able to CREATE DATABASE). */ +export function defaultAdminUrlFromAppUrl(appUrl: string): string { + return withDatabaseName(appUrl, "postgres"); +} diff --git a/new-deepnotes/packages/db/src/test/template-db.ts b/new-deepnotes/packages/db/src/test/template-db.ts new file mode 100644 index 00000000..cc41693e --- /dev/null +++ b/new-deepnotes/packages/db/src/test/template-db.ts @@ -0,0 +1,88 @@ +import { drizzle } from "drizzle-orm/postgres-js"; +import { migrate } from "drizzle-orm/postgres-js/migrator"; +import postgres from "postgres"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { defaultAdminUrlFromAppUrl, withDatabaseName } from "./db-url.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +const MIGRATIONS_FOLDER = join(__dirname, "../../migrations"); + +/** Only generated names — never pass request input here. */ +function assertSafeDbIdentifier(name: string): void { + if (!/^[a-z][a-z0-9_]{0,62}$/i.test(name)) { + throw new Error(`Refusing unsafe database identifier: ${name}`); + } +} + +export async function terminateConnections( + admin: ReturnType, + dbName: string, +): Promise { + assertSafeDbIdentifier(dbName); + await admin.unsafe( + `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${dbName.replace(/'/g, "''")}' AND pid <> pg_backend_pid()`, + ); +} + +export async function dropDatabaseIfExists( + admin: ReturnType, + dbName: string, +): Promise { + assertSafeDbIdentifier(dbName); + await terminateConnections(admin, dbName); + await admin.unsafe(`DROP DATABASE IF EXISTS ${dbName} WITH (FORCE)`); +} + +export async function createDatabaseFromTemplate( + admin: ReturnType, + newDbName: string, + templateName: string, +): Promise { + assertSafeDbIdentifier(newDbName); + assertSafeDbIdentifier(templateName); + await admin.unsafe(`CREATE DATABASE ${newDbName} TEMPLATE ${templateName}`); +} + +export async function migrateFreshDatabase(connectionString: string): Promise { + const client = postgres(connectionString, { max: 1 }); + const db = drizzle(client); + try { + await migrate(db, { migrationsFolder: MIGRATIONS_FOLDER }); + } finally { + await client.end({ timeout: 5 }); + } +} + +export type TemplateDbContext = { + adminUrl: string; + appBaseUrl: string; + templateName: string; +}; + +export function resolveTemplateContext(): TemplateDbContext | null { + const appBaseUrl = process.env.DATABASE_URL; + if (!appBaseUrl) return null; + const adminUrl = + process.env.DATABASE_ADMIN_URL ?? defaultAdminUrlFromAppUrl(appBaseUrl); + const templateName = process.env.TEST_DB_TEMPLATE_NAME ?? "dn_test_tpl_deepnotes"; + return { adminUrl, appBaseUrl, templateName }; +} + +/** + * Build a migrated template database (RESTART_PLAN §5.7). Caller must drop when done. + */ +export async function ensureTemplateDatabase(ctx: TemplateDbContext): Promise { + assertSafeDbIdentifier(ctx.templateName); + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin, ctx.templateName); + await admin.unsafe(`CREATE DATABASE ${ctx.templateName}`); + } finally { + await admin.end({ timeout: 5 }); + } + const templateUrl = withDatabaseName(ctx.appBaseUrl, ctx.templateName); + await migrateFreshDatabase(templateUrl); +} diff --git a/new-deepnotes/packages/db/vitest.config.ts b/new-deepnotes/packages/db/vitest.config.ts new file mode 100644 index 00000000..7c83d6c2 --- /dev/null +++ b/new-deepnotes/packages/db/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["src/**/*.test.ts"], + fileParallelism: false, + poolOptions: { + threads: { singleThread: true }, + }, + }, +}); diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml index 2291eee5..a2829607 100644 --- a/new-deepnotes/pnpm-lock.yaml +++ b/new-deepnotes/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: typescript: specifier: ^5.8.3 version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) packages: diff --git a/new-deepnotes/template.env b/new-deepnotes/template.env index 2daa6c28..01c618ca 100644 --- a/new-deepnotes/template.env +++ b/new-deepnotes/template.env @@ -3,8 +3,8 @@ # PostgreSQL — used by Drizzle CLI, integration tests, and Wrangler Hyperdrive localConnectionString DATABASE_URL=postgresql://deepnotes:deepnotes@localhost:5433/deepnotes -# Admin URL for template-database tests (role with CREATEDB); optional until integration tests land -# DATABASE_ADMIN_URL=postgresql://postgres:postgres@localhost:5433/postgres +# Admin catalog URL (CREATEDB) for @deepnotes/db template-clone tests — defaults to …/postgres from DATABASE_URL if unset +# DATABASE_ADMIN_URL=postgresql://deepnotes:deepnotes@localhost:5433/postgres # Standard Redis (not KeyDB). TCP URL for Node; Workers may use Upstash HTTP or REDIS_URL via TCP where supported REDIS_URL=redis://localhost:6380 From faa93f7dc78061a9da39f2f4b6a2199a5d21fdc0 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 22:46:10 -0300 Subject: [PATCH 022/243] feat(new-deepnotes): OpenAPI session routes and 501 API worker stubs --- new-deepnotes/PLAN_PROGRESS.md | 15 +++- .../apps/api-worker/src/index.test.ts | 13 ++++ new-deepnotes/apps/api-worker/src/index.ts | 17 +++++ new-deepnotes/packages/api/src/index.ts | 10 +++ .../packages/api/src/openapi.test.ts | 8 ++ new-deepnotes/packages/api/src/openapi.ts | 75 +++++++++++++++++++ .../packages/api/src/schemas/errors.ts | 14 ++++ .../packages/api/src/schemas/sessions.ts | 45 +++++++++++ 8 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 new-deepnotes/packages/api/src/schemas/errors.ts create mode 100644 new-deepnotes/packages/api/src/schemas/sessions.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index ebbf0d8d..e3653da3 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -10,10 +10,10 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | Phase | Status | Notes | |-------|--------|--------| -| **0** — OpenAPI + Drizzle inventory | **In progress** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | +| **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Mostly done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **Not started** | Auth/sessions, pages/groups, realtime/collab, Stripe (no RevenueCat). | +| **3** — REST + Drizzle features | **In progress** | Session contract in OpenAPI + `501` stubs on worker (`/api/sessions/*`). Next: wire login/refresh/logout/demo with crypto, Redis, Drizzle + cookies per [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md). | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -29,6 +29,16 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ --- +## Phase 3 checklist (REST + Drizzle) + +- [x] Document **sessions** REST paths + request schemas in OpenAPI; worker returns **501** until handlers exist. +- [ ] Implement **sessions.login** / refresh / logout / start-demo against Drizzle + Redis + legacy crypto semantics. +- [ ] **JWT + httpOnly cookies** (`accessToken`, `refreshToken`, `loggedIn`) matching [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md). +- [ ] **Users** registration + `GET /api/users/me` (and remaining TRPC_REST_MAP slices as needed). +- [ ] Pages/groups CRUD, realtime/collab, Stripe webhook (no RevenueCat). + +--- + ## Phase 2 checklist (bootstrap) - [x] pnpm + Turborepo 2, Node 22+. @@ -58,6 +68,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | Date | Change | |------|--------| +| 2026-04-26 | Phase 3 start: OpenAPI + Zod for `POST /api/sessions/login|refresh|logout|demo`; api-worker `501` stubs; Phase 0 marked done in snapshot. | | 2026-04-26 | Phase 0 docs (TRPC_REST_MAP, AUTH_AND_CORS, CLIENT_FORKS); Phase 2 deploy doc; Drizzle legacy baseline from `postgres-init.sql`; Vitest template-DB integration test + CI `DATABASE_ADMIN_URL`. | | 2026-04-26 | Initial `new-deepnotes` monorepo: `@deepnotes/api`, `@deepnotes/db`, `@deepnotes/api-worker`, `@deepnotes/web`, CI workflow. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 66564f4e..d1df81b0 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -21,4 +21,17 @@ describe("api-worker", () => { info: { title: "DeepNotes API" }, }); }); + + it.each([ + ["POST", "/api/sessions/login"], + ["POST", "/api/sessions/refresh"], + ["POST", "/api/sessions/logout"], + ["POST", "/api/sessions/demo"], + ] as const)("returns 501 for %s %s until implemented", async (method, path) => { + const res = await app.request(`http://test${path}`, { method }); + expect(res.status).toBe(501); + await expect(res.json()).resolves.toMatchObject({ + code: "NOT_IMPLEMENTED", + }); + }); }); diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 1b978286..764ceb7c 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -8,6 +8,12 @@ type Bindings = { const app = new Hono<{ Bindings: Bindings }>(); +const sessionNotImplementedBody = { + code: "NOT_IMPLEMENTED" as const, + message: + "Session handlers are not wired yet (crypto, Redis, Drizzle). See OpenAPI for the contract.", +}; + app.get("/api/openapi.json", (c) => c.json(getOpenApiDocument())); app.get("/api/health", (c) => { @@ -19,4 +25,15 @@ app.get("/api/health", (c) => { return c.json(parsed.data); }); +app.post("/api/sessions/login", (c) => + c.json(sessionNotImplementedBody, 501), +); +app.post("/api/sessions/refresh", (c) => + c.json(sessionNotImplementedBody, 501), +); +app.post("/api/sessions/logout", (c) => + c.json(sessionNotImplementedBody, 501), +); +app.post("/api/sessions/demo", (c) => c.json(sessionNotImplementedBody, 501)); + export default app; diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index dc84e72f..cfe3e5a3 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -1,5 +1,15 @@ export { getOpenApiDocument } from "./openapi.js"; +export { + notImplementedResponseSchema, + type NotImplementedResponse, +} from "./schemas/errors.js"; export { healthResponseSchema, type HealthResponse, } from "./schemas/health.js"; +export { + sessionDemoRequestSchema, + sessionLoginEmailSchema, + sessionLoginRequestSchema, + type SessionLoginRequest, +} from "./schemas/sessions.js"; diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index c9129374..19ed387f 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -7,4 +7,12 @@ describe("getOpenApiDocument", () => { const doc = getOpenApiDocument(); expect(doc.paths?.["/api/health"]?.get).toBeDefined(); }); + + it("includes session routes (Phase 3 contract)", () => { + const doc = getOpenApiDocument(); + expect(doc.paths?.["/api/sessions/login"]?.post).toBeDefined(); + expect(doc.paths?.["/api/sessions/refresh"]?.post).toBeDefined(); + expect(doc.paths?.["/api/sessions/logout"]?.post).toBeDefined(); + expect(doc.paths?.["/api/sessions/demo"]?.post).toBeDefined(); + }); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index b2bfdc40..e2a0a66f 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -4,10 +4,25 @@ import { } from "@asteasolutions/zod-to-openapi"; import type { OpenAPIObject } from "openapi3-ts/oas30"; +import { notImplementedResponseSchema } from "./schemas/errors.js"; import { healthResponseSchema } from "./schemas/health.js"; +import { + sessionDemoRequestSchema, + sessionLoginRequestSchema, +} from "./schemas/sessions.js"; const registry = new OpenAPIRegistry(); +const sessionNotImplemented501 = { + description: + "Not implemented yet; path and schemas are stable for codegen (Phase 3).", + content: { + "application/json": { + schema: notImplementedResponseSchema, + }, + }, +} as const; + registry.registerPath({ method: "get", path: "/api/health", @@ -24,6 +39,66 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/sessions/login", + summary: "Create session (email + login hash)", + description: + "Replaces legacy `sessions.login`. Sets httpOnly cookies when implemented.", + request: { + body: { + content: { + "application/json": { + schema: sessionLoginRequestSchema, + }, + }, + }, + }, + responses: { + 501: sessionNotImplemented501, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/sessions/refresh", + summary: "Rotate access token using refresh cookie", + description: "Replaces legacy `sessions.refresh`.", + responses: { + 501: sessionNotImplemented501, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/sessions/logout", + summary: "Invalidate session and clear cookies", + description: "Replaces legacy `sessions.logout`.", + responses: { + 501: sessionNotImplemented501, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/sessions/demo", + summary: "Create demo user and session", + description: + "Replaces legacy `sessions.startDemo`. Request body will match registration key material once defined.", + request: { + body: { + content: { + "application/json": { + schema: sessionDemoRequestSchema, + }, + }, + }, + }, + responses: { + 501: sessionNotImplemented501, + }, +}); + const generator = new OpenApiGeneratorV3(registry.definitions); export function getOpenApiDocument(): OpenAPIObject { diff --git a/new-deepnotes/packages/api/src/schemas/errors.ts b/new-deepnotes/packages/api/src/schemas/errors.ts new file mode 100644 index 00000000..b19f8f0f --- /dev/null +++ b/new-deepnotes/packages/api/src/schemas/errors.ts @@ -0,0 +1,14 @@ +import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; +import { z } from "zod"; + +extendZodWithOpenApi(z); + +/** Returned while a route is documented but not yet implemented (Phase 3+). */ +export const notImplementedResponseSchema = z + .object({ + code: z.literal("NOT_IMPLEMENTED"), + message: z.string(), + }) + .openapi("NotImplementedResponse"); + +export type NotImplementedResponse = z.infer; diff --git a/new-deepnotes/packages/api/src/schemas/sessions.ts b/new-deepnotes/packages/api/src/schemas/sessions.ts new file mode 100644 index 00000000..1add0342 --- /dev/null +++ b/new-deepnotes/packages/api/src/schemas/sessions.ts @@ -0,0 +1,45 @@ +import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; +import { z } from "zod"; + +extendZodWithOpenApi(z); + +/** + * Login email: normal address or literal `demo` (legacy `sessions.login` parity). + */ +export const sessionLoginEmailSchema = z.union([ + z.string().email(), + z.literal("demo"), +]); + +/** + * JSON body for `POST /api/sessions/login`. + * Legacy tRPC sent `loginHash` as raw bytes via superjson; REST uses base64 (OAS `format: byte`). + */ +export const sessionLoginRequestSchema = z + .object({ + email: sessionLoginEmailSchema, + loginHash: z.string().openapi({ + description: + "Base64-encoded login hash (legacy wire used binary; prefer standard base64 in JSON).", + format: "byte", + }), + rememberSession: z.boolean(), + authenticatorToken: z.string().optional(), + rememberDevice: z.boolean().optional(), + recoveryCode: z + .string() + .regex(/^[a-f0-9]{32}$/) + .optional(), + }) + .openapi("SessionLoginRequest"); + +export type SessionLoginRequest = z.infer; + +/** + * Demo session creation mirrors legacy `sessions.startDemo` input (crypto material + ids). + * Shape will align with `POST /api/users` once registration is implemented; `additionalProperties` keeps codegen honest until then. + */ +export const sessionDemoRequestSchema = z + .object({}) + .catchall(z.unknown()) + .openapi("SessionDemoRequest"); From d985f9f1f9a95ee28869ed84244290a8ebb51103 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 23:03:59 -0300 Subject: [PATCH 023/243] feat(new-deepnotes): @deepnotes/session and wired login, refresh, logout --- .github/workflows/new-deepnotes-ci.yml | 6 + new-deepnotes/PLAN_PROGRESS.md | 8 +- new-deepnotes/apps/api-worker/package.json | 2 + new-deepnotes/apps/api-worker/src/cookies.ts | 20 ++ new-deepnotes/apps/api-worker/src/db-pool.ts | 14 ++ .../apps/api-worker/src/index.test.ts | 13 +- new-deepnotes/apps/api-worker/src/index.ts | 188 +++++++++++++- .../apps/api-worker/src/session-env.ts | 50 ++++ new-deepnotes/apps/api-worker/wrangler.toml | 1 + new-deepnotes/package.json | 7 + new-deepnotes/packages/api/src/index.ts | 6 + new-deepnotes/packages/api/src/openapi.ts | 54 +++- .../api/src/schemas/session-responses.ts | 40 +++ .../packages/session/eslint.config.js | 3 + new-deepnotes/packages/session/package.json | 36 +++ new-deepnotes/packages/session/src/cookies.ts | 89 +++++++ .../packages/session/src/datetime.ts | 5 + .../packages/session/src/device-hash.ts | 16 ++ .../packages/session/src/email-hash.ts | 24 ++ new-deepnotes/packages/session/src/env.ts | 21 ++ new-deepnotes/packages/session/src/errors.ts | 10 + new-deepnotes/packages/session/src/index.ts | 7 + new-deepnotes/packages/session/src/jwt.ts | 99 ++++++++ .../packages/session/src/legacy-crypto.ts | 112 +++++++++ new-deepnotes/packages/session/src/login.ts | 214 ++++++++++++++++ new-deepnotes/packages/session/src/logout.ts | 33 +++ new-deepnotes/packages/session/src/refresh.ts | 92 +++++++ .../packages/session/src/session-lifecycle.ts | 120 +++++++++ new-deepnotes/packages/session/src/tokens.ts | 15 ++ .../packages/session/src/two-factor.ts | 85 +++++++ new-deepnotes/packages/session/tsconfig.json | 8 + new-deepnotes/pnpm-lock.yaml | 230 ++++++++++++++++++ new-deepnotes/template.env | 15 +- 33 files changed, 1621 insertions(+), 22 deletions(-) create mode 100644 new-deepnotes/apps/api-worker/src/cookies.ts create mode 100644 new-deepnotes/apps/api-worker/src/db-pool.ts create mode 100644 new-deepnotes/apps/api-worker/src/session-env.ts create mode 100644 new-deepnotes/packages/api/src/schemas/session-responses.ts create mode 100644 new-deepnotes/packages/session/eslint.config.js create mode 100644 new-deepnotes/packages/session/package.json create mode 100644 new-deepnotes/packages/session/src/cookies.ts create mode 100644 new-deepnotes/packages/session/src/datetime.ts create mode 100644 new-deepnotes/packages/session/src/device-hash.ts create mode 100644 new-deepnotes/packages/session/src/email-hash.ts create mode 100644 new-deepnotes/packages/session/src/env.ts create mode 100644 new-deepnotes/packages/session/src/errors.ts create mode 100644 new-deepnotes/packages/session/src/index.ts create mode 100644 new-deepnotes/packages/session/src/jwt.ts create mode 100644 new-deepnotes/packages/session/src/legacy-crypto.ts create mode 100644 new-deepnotes/packages/session/src/login.ts create mode 100644 new-deepnotes/packages/session/src/logout.ts create mode 100644 new-deepnotes/packages/session/src/refresh.ts create mode 100644 new-deepnotes/packages/session/src/session-lifecycle.ts create mode 100644 new-deepnotes/packages/session/src/tokens.ts create mode 100644 new-deepnotes/packages/session/src/two-factor.ts create mode 100644 new-deepnotes/packages/session/tsconfig.json diff --git a/.github/workflows/new-deepnotes-ci.yml b/.github/workflows/new-deepnotes-ci.yml index 43872fc9..069e0ecb 100644 --- a/.github/workflows/new-deepnotes-ci.yml +++ b/.github/workflows/new-deepnotes-ci.yml @@ -51,6 +51,12 @@ jobs: - name: Install run: pnpm install --frozen-lockfile + - name: Build linked @stdlib/crypto (parent monorepo) + working-directory: ${{ github.workspace }} + run: | + pnpm install --frozen-lockfile --filter @stdlib/crypto... + pnpm --filter @stdlib/crypto run build + - name: Lint run: pnpm lint diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index e3653da3..cfa52283 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Mostly done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Session contract in OpenAPI + `501` stubs on worker (`/api/sessions/*`). Next: wire login/refresh/logout/demo with crypto, Redis, Drizzle + cookies per [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md). | +| **3** — REST + Drizzle features | **In progress** | `POST /api/sessions/login|refresh|logout` wired via `@deepnotes/session` (Drizzle + legacy crypto + `jose` JWT + cookies). `POST /api/sessions/demo` still `501`. Next: Redis-backed login rate limits, `start-demo` / registration, `GET /api/users/me`. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -32,8 +32,9 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ ## Phase 3 checklist (REST + Drizzle) - [x] Document **sessions** REST paths + request schemas in OpenAPI; worker returns **501** until handlers exist. -- [ ] Implement **sessions.login** / refresh / logout / start-demo against Drizzle + Redis + legacy crypto semantics. -- [ ] **JWT + httpOnly cookies** (`accessToken`, `refreshToken`, `loggedIn`) matching [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md). +- [x] Implement **sessions.login** / refresh / logout against Drizzle + legacy crypto semantics (JWT via `jose`; **Redis** rate limits not wired yet—parity with legacy `login` lockouts). +- [ ] Implement **sessions.start-demo** (registration path) + **Redis** for failed-login / optional session cache. +- [x] **JWT + httpOnly cookies** (`accessToken`, `refreshToken`, `loggedIn`) matching [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md). - [ ] **Users** registration + `GET /api/users/me` (and remaining TRPC_REST_MAP slices as needed). - [ ] Pages/groups CRUD, realtime/collab, Stripe webhook (no RevenueCat). @@ -68,6 +69,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | Date | Change | |------|--------| +| 2026-04-26 | Phase 3: `@deepnotes/session` (login/refresh/logout + 2FA TOTP/recovery), api-worker Hyperdrive + dynamic import for Workers bundle; OpenAPI 200/401/503 for session routes; demo remains `501`; CI builds parent `@stdlib/crypto`; `libsodium-wrappers-sumo@^0.8` override for Wrangler. | | 2026-04-26 | Phase 3 start: OpenAPI + Zod for `POST /api/sessions/login|refresh|logout|demo`; api-worker `501` stubs; Phase 0 marked done in snapshot. | | 2026-04-26 | Phase 0 docs (TRPC_REST_MAP, AUTH_AND_CORS, CLIENT_FORKS); Phase 2 deploy doc; Drizzle legacy baseline from `postgres-init.sql`; Vitest template-DB integration test + CI `DATABASE_ADMIN_URL`. | | 2026-04-26 | Initial `new-deepnotes` monorepo: `@deepnotes/api`, `@deepnotes/db`, `@deepnotes/api-worker`, `@deepnotes/web`, CI workflow. | diff --git a/new-deepnotes/apps/api-worker/package.json b/new-deepnotes/apps/api-worker/package.json index 7b23efd4..2a32a647 100644 --- a/new-deepnotes/apps/api-worker/package.json +++ b/new-deepnotes/apps/api-worker/package.json @@ -12,6 +12,8 @@ }, "dependencies": { "@deepnotes/api": "workspace:*", + "@deepnotes/db": "workspace:*", + "@deepnotes/session": "workspace:*", "hono": "^4.7.7" }, "devDependencies": { diff --git a/new-deepnotes/apps/api-worker/src/cookies.ts b/new-deepnotes/apps/api-worker/src/cookies.ts new file mode 100644 index 00000000..b51f9cda --- /dev/null +++ b/new-deepnotes/apps/api-worker/src/cookies.ts @@ -0,0 +1,20 @@ +/** Parse `Cookie` header (no dependency on `hono/cookie` runtime shape). */ +export function readCookieHeader( + cookieHeader: string | undefined, + name: string, +): string | undefined { + if (cookieHeader == null || cookieHeader === "") return; + const parts = cookieHeader.split(";"); + const prefix = `${name}=`; + for (const part of parts) { + const t = part.trim(); + if (t.startsWith(prefix)) { + try { + return decodeURIComponent(t.slice(prefix.length)); + } catch { + return t.slice(prefix.length); + } + } + } + return; +} diff --git a/new-deepnotes/apps/api-worker/src/db-pool.ts b/new-deepnotes/apps/api-worker/src/db-pool.ts new file mode 100644 index 00000000..c53732c6 --- /dev/null +++ b/new-deepnotes/apps/api-worker/src/db-pool.ts @@ -0,0 +1,14 @@ +import { createDb, type DeepnotesDb } from "@deepnotes/db/client"; + +let cachedConn: string | undefined; +let cachedDb: DeepnotesDb | undefined; + +/** One Drizzle instance per isolate; Hyperdrive URL is stable for the binding. */ +export function getDbForConnectionString(connectionString: string): DeepnotesDb { + if (cachedDb != null && cachedConn === connectionString) { + return cachedDb; + } + cachedConn = connectionString; + cachedDb = createDb(connectionString); + return cachedDb; +} diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index d1df81b0..0b8241d2 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -26,9 +26,18 @@ describe("api-worker", () => { ["POST", "/api/sessions/login"], ["POST", "/api/sessions/refresh"], ["POST", "/api/sessions/logout"], - ["POST", "/api/sessions/demo"], - ] as const)("returns 501 for %s %s until implemented", async (method, path) => { + ] as const)("returns 503 for %s %s when auth env is not configured", async (method, path) => { const res = await app.request(`http://test${path}`, { method }); + expect(res.status).toBe(503); + await expect(res.json()).resolves.toMatchObject({ + code: "SERVICE_UNAVAILABLE", + }); + }); + + it("POST /api/sessions/demo returns 501 until registration is wired", async () => { + const res = await app.request("http://test/api/sessions/demo", { + method: "POST", + }); expect(res.status).toBe(501); await expect(res.json()).resolves.toMatchObject({ code: "NOT_IMPLEMENTED", diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 764ceb7c..c7c261ab 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -1,7 +1,16 @@ -import { getOpenApiDocument, healthResponseSchema } from "@deepnotes/api"; +import { + getOpenApiDocument, + healthResponseSchema, + sessionLoginRequestSchema, +} from "@deepnotes/api"; +import type { ContentfulStatusCode } from "hono/utils/http-status"; import { Hono } from "hono"; -type Bindings = { +import { getDbForConnectionString } from "./db-pool.js"; +import { readCookieHeader } from "./cookies.js"; +import { getSessionEnv, type WorkerSessionBindings } from "./session-env.js"; + +type Bindings = WorkerSessionBindings & { /** Wired in `wrangler.toml`; optional in unit tests that do not pass `env`. */ HYPERDRIVE?: Hyperdrive; }; @@ -11,9 +20,21 @@ const app = new Hono<{ Bindings: Bindings }>(); const sessionNotImplementedBody = { code: "NOT_IMPLEMENTED" as const, message: - "Session handlers are not wired yet (crypto, Redis, Drizzle). See OpenAPI for the contract.", + "Demo registration is not implemented yet. See OpenAPI for the contract.", }; +const serviceUnavailableBody = { + code: "SERVICE_UNAVAILABLE" as const, + message: + "Session routes require ACCESS_SECRET, REFRESH_SECRET, USER_EMAIL_SECRET, USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, and USER_RECOVERY_CODES_ENCRYPTION_KEY (e.g. Wrangler secrets / .dev.vars).", +}; + +function appendSetCookies(res: Response, lines: string[]): void { + for (const line of lines) { + res.headers.append("Set-Cookie", line); + } +} + app.get("/api/openapi.json", (c) => c.json(getOpenApiDocument())); app.get("/api/health", (c) => { @@ -25,15 +46,160 @@ app.get("/api/health", (c) => { return c.json(parsed.data); }); -app.post("/api/sessions/login", (c) => - c.json(sessionNotImplementedBody, 501), -); -app.post("/api/sessions/refresh", (c) => - c.json(sessionNotImplementedBody, 501), -); -app.post("/api/sessions/logout", (c) => +app.post("/api/sessions/login", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = sessionLoginRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + let loginHash: Uint8Array; + try { + loginHash = new Uint8Array( + Buffer.from(parsed.data.loginHash, "base64"), + ); + } catch { + return c.json( + { code: "VALIDATION_ERROR", message: "loginHash must be valid base64." }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + + try { + const { performSessionLogin } = await import("@deepnotes/session"); + const { json, cookieLines } = await performSessionLogin({ + db, + env: sessionEnv, + body: { + email: parsed.data.email, + loginHash, + rememberSession: parsed.data.rememberSession, + authenticatorToken: parsed.data.authenticatorToken, + rememberDevice: parsed.data.rememberDevice, + recoveryCode: parsed.data.recoveryCode, + }, + clientIp: c.req.header("CF-Connecting-IP") ?? "127.0.0.1", + userAgent: c.req.header("User-Agent") ?? "", + }); + const res = c.json(json, 200); + appendSetCookies(res, cookieLines); + return res; + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/sessions/refresh", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performSessionRefresh } = await import("@deepnotes/session"); + const { json, cookieLines } = await performSessionRefresh({ + db, + env: sessionEnv, + refreshCookie: readCookieHeader(cookieHeader, "refreshToken"), + loggedInCookie: readCookieHeader(cookieHeader, "loggedIn"), + }); + const res = c.json(json, 200); + appendSetCookies(res, cookieLines); + return res; + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/sessions/logout", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + const { performSessionLogout } = await import("@deepnotes/session"); + const { cookieLines } = await performSessionLogout({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + }); + + const res = c.body(null, 204); + appendSetCookies(res, cookieLines); + return res; +}); + +app.post("/api/sessions/demo", (c) => c.json(sessionNotImplementedBody, 501), ); -app.post("/api/sessions/demo", (c) => c.json(sessionNotImplementedBody, 501)); export default app; diff --git a/new-deepnotes/apps/api-worker/src/session-env.ts b/new-deepnotes/apps/api-worker/src/session-env.ts new file mode 100644 index 00000000..3dbec199 --- /dev/null +++ b/new-deepnotes/apps/api-worker/src/session-env.ts @@ -0,0 +1,50 @@ +import type { SessionEnv } from "@deepnotes/session"; + +export type WorkerSessionBindings = { + ACCESS_SECRET?: string; + REFRESH_SECRET?: string; + USER_EMAIL_SECRET?: string; + USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY?: string; + USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY?: string; + USER_RECOVERY_CODES_ENCRYPTION_KEY?: string; + DEV?: string; + COOKIE_DOMAIN?: string; + EMAIL_CASE_SENSITIVITY_EXCEPTIONS?: string; +}; + +export function getSessionEnv( + env: WorkerSessionBindings | undefined, +): SessionEnv | null { + if (env == null) { + return null; + } + const { + ACCESS_SECRET, + REFRESH_SECRET, + USER_EMAIL_SECRET, + USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, + USER_RECOVERY_CODES_ENCRYPTION_KEY, + } = env; + if ( + !ACCESS_SECRET || + !REFRESH_SECRET || + !USER_EMAIL_SECRET || + !USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY || + !USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY || + !USER_RECOVERY_CODES_ENCRYPTION_KEY + ) { + return null; + } + return { + ACCESS_SECRET, + REFRESH_SECRET, + USER_EMAIL_SECRET, + USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, + USER_RECOVERY_CODES_ENCRYPTION_KEY, + DEV: env.DEV, + COOKIE_DOMAIN: env.COOKIE_DOMAIN, + EMAIL_CASE_SENSITIVITY_EXCEPTIONS: env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS, + }; +} diff --git a/new-deepnotes/apps/api-worker/wrangler.toml b/new-deepnotes/apps/api-worker/wrangler.toml index 5f785d90..aea4e2a4 100644 --- a/new-deepnotes/apps/api-worker/wrangler.toml +++ b/new-deepnotes/apps/api-worker/wrangler.toml @@ -1,6 +1,7 @@ name = "deepnotes-api" main = "src/index.ts" compatibility_date = "2025-04-01" +compatibility_flags = ["nodejs_compat"] # Replace `id` with a real Hyperdrive config in Cloudflare before production deploy. # Local dev uses `localConnectionString` (see RESTART_PLAN.md §Hosting). diff --git a/new-deepnotes/package.json b/new-deepnotes/package.json index 4e01ca6e..0171df7b 100644 --- a/new-deepnotes/package.json +++ b/new-deepnotes/package.json @@ -17,6 +17,13 @@ "db:studio": "pnpm --filter @deepnotes/db exec drizzle-kit studio", "db:check": "pnpm --filter @deepnotes/db exec drizzle-kit check" }, + "pnpm": { + "overrides": { + "@stdlib/base64": "link:../packages/@stdlib/base64", + "@stdlib/misc": "link:../packages/@stdlib/misc", + "libsodium-wrappers-sumo": "^0.8.0" + } + }, "devDependencies": { "@eslint/js": "^9.25.0", "eslint": "^9.25.0", diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index cfe3e5a3..783cb7d7 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -7,6 +7,12 @@ export { healthResponseSchema, type HealthResponse, } from "./schemas/health.js"; +export { + serviceUnavailableResponseSchema, + sessionErrorResponseSchema, + sessionLoginSuccessSchema, + sessionRefreshSuccessSchema, +} from "./schemas/session-responses.js"; export { sessionDemoRequestSchema, sessionLoginEmailSchema, diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index e2a0a66f..c9036d0c 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -6,6 +6,12 @@ import type { OpenAPIObject } from "openapi3-ts/oas30"; import { notImplementedResponseSchema } from "./schemas/errors.js"; import { healthResponseSchema } from "./schemas/health.js"; +import { + serviceUnavailableResponseSchema, + sessionErrorResponseSchema, + sessionLoginSuccessSchema, + sessionRefreshSuccessSchema, +} from "./schemas/session-responses.js"; import { sessionDemoRequestSchema, sessionLoginRequestSchema, @@ -15,7 +21,7 @@ const registry = new OpenAPIRegistry(); const sessionNotImplemented501 = { description: - "Not implemented yet; path and schemas are stable for codegen (Phase 3).", + "Demo registration is not implemented on this route yet (Phase 3+).", content: { "application/json": { schema: notImplementedResponseSchema, @@ -23,6 +29,25 @@ const sessionNotImplemented501 = { }, } as const; +const sessionServiceUnavailable503 = { + description: + "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + content: { + "application/json": { + schema: serviceUnavailableResponseSchema, + }, + }, +} as const; + +const sessionUnauthorized401 = { + description: "Invalid credentials, token, or session state.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, +} as const; + registry.registerPath({ method: "get", path: "/api/health", @@ -55,7 +80,16 @@ registry.registerPath({ }, }, responses: { - 501: sessionNotImplemented501, + 200: { + description: "Login succeeded; cookies set.", + content: { + "application/json": { + schema: sessionLoginSuccessSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 503: sessionServiceUnavailable503, }, }); @@ -65,7 +99,16 @@ registry.registerPath({ summary: "Rotate access token using refresh cookie", description: "Replaces legacy `sessions.refresh`.", responses: { - 501: sessionNotImplemented501, + 200: { + description: "New session key and cookies.", + content: { + "application/json": { + schema: sessionRefreshSuccessSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 503: sessionServiceUnavailable503, }, }); @@ -75,7 +118,10 @@ registry.registerPath({ summary: "Invalidate session and clear cookies", description: "Replaces legacy `sessions.logout`.", responses: { - 501: sessionNotImplemented501, + 204: { + description: "Logged out (cookies cleared).", + }, + 503: sessionServiceUnavailable503, }, }); diff --git a/new-deepnotes/packages/api/src/schemas/session-responses.ts b/new-deepnotes/packages/api/src/schemas/session-responses.ts new file mode 100644 index 00000000..9190660b --- /dev/null +++ b/new-deepnotes/packages/api/src/schemas/session-responses.ts @@ -0,0 +1,40 @@ +import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; +import { z } from "zod"; + +extendZodWithOpenApi(z); + +export const sessionErrorResponseSchema = z + .object({ + code: z.string(), + message: z.string(), + }) + .openapi("SessionErrorResponse"); + +export const sessionLoginSuccessSchema = z + .object({ + userId: z.string(), + sessionId: z.string(), + sessionKey: z.string().openapi({ + description: "Base64-encoded session symmetric key.", + format: "byte", + }), + personalGroupId: z.string(), + publicKeyring: z.string().openapi({ format: "byte" }), + encryptedPrivateKeyring: z.string().openapi({ format: "byte" }), + encryptedSymmetricKeyring: z.string().openapi({ format: "byte" }), + }) + .openapi("SessionLoginSuccess"); + +export const sessionRefreshSuccessSchema = z + .object({ + oldSessionKey: z.string().openapi({ format: "byte" }), + newSessionKey: z.string().openapi({ format: "byte" }), + }) + .openapi("SessionRefreshSuccess"); + +export const serviceUnavailableResponseSchema = z + .object({ + code: z.literal("SERVICE_UNAVAILABLE"), + message: z.string(), + }) + .openapi("ServiceUnavailableResponse"); diff --git a/new-deepnotes/packages/session/eslint.config.js b/new-deepnotes/packages/session/eslint.config.js new file mode 100644 index 00000000..f6c5b17b --- /dev/null +++ b/new-deepnotes/packages/session/eslint.config.js @@ -0,0 +1,3 @@ +import base from "../../eslint.config.js"; + +export default [...base]; diff --git a/new-deepnotes/packages/session/package.json b/new-deepnotes/packages/session/package.json new file mode 100644 index 00000000..9e619529 --- /dev/null +++ b/new-deepnotes/packages/session/package.json @@ -0,0 +1,36 @@ +{ + "name": "@deepnotes/session", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "scripts": { + "lint": "eslint .", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run --passWithNoTests" + }, + "dependencies": { + "@deepnotes/db": "workspace:*", + "@stdlib/base64": "link:../../../packages/@stdlib/base64", + "@stdlib/crypto": "link:../../../packages/@stdlib/crypto", + "@stdlib/misc": "link:../../../packages/@stdlib/misc", + "crypto-js": "^4.2.0", + "drizzle-orm": "^0.41.0", + "jose": "^5.10.0", + "libsodium-wrappers-sumo": "^0.8.0", + "msgpackr": "^1.11.2", + "nanoid": "^5.1.5", + "otplib": "^12.0.1" + }, + "devDependencies": { + "@types/crypto-js": "^4.2.2", + "@types/node": "^22.14.1", + "typescript": "^5.8.3", + "vitest": "^3.2.4" + } +} diff --git a/new-deepnotes/packages/session/src/cookies.ts b/new-deepnotes/packages/session/src/cookies.ts new file mode 100644 index 00000000..f8823188 --- /dev/null +++ b/new-deepnotes/packages/session/src/cookies.ts @@ -0,0 +1,89 @@ +import { isDev } from "./env.js"; +import type { SessionEnv } from "./env.js"; +import { + ACCESS_TOKEN_DURATION_MS, + REFRESH_TOKEN_LONG_DURATION_MS, +} from "./tokens.js"; + +export type CookieBuildOptions = { + secure: boolean; + domain?: string; +}; + +export function cookieOptionsFromEnv(env: SessionEnv): CookieBuildOptions { + return { + secure: !isDev(env), + domain: env.COOKIE_DOMAIN || undefined, + }; +} + +function appendPart( + line: string, + condition: boolean, + part: string, +): string { + return condition ? `${line}; ${part}` : line; +} + +function serializeCookie( + name: string, + value: string, + opts: CookieBuildOptions & { + httpOnly: boolean; + expires?: Date; + maxAgeSec?: number; + }, +): string { + let line = `${name}=${encodeURIComponent(value)}; Path=/; SameSite=Strict`; + line = appendPart(line, !!opts.domain, `Domain=${opts.domain}`); + line = appendPart(line, opts.secure, "Secure"); + line = appendPart(line, opts.httpOnly, "HttpOnly"); + if (opts.expires) { + line = appendPart(line, true, `Expires=${opts.expires.toUTCString()}`); + } + if (opts.maxAgeSec != null) { + line = appendPart(line, true, `Max-Age=${String(opts.maxAgeSec)}`); + } + return line; +} + +export function buildSessionCookies(input: { + opts: CookieBuildOptions; + accessToken: string; + refreshToken: string; + rememberSession: boolean; +}): string[] { + const rememberExpires = input.rememberSession + ? new Date(Date.now() + REFRESH_TOKEN_LONG_DURATION_MS) + : undefined; + const accessExpires = input.rememberSession + ? new Date(Date.now() + ACCESS_TOKEN_DURATION_MS) + : undefined; + + return [ + serializeCookie("accessToken", input.accessToken, { + ...input.opts, + httpOnly: true, + expires: accessExpires, + }), + serializeCookie("refreshToken", input.refreshToken, { + ...input.opts, + httpOnly: true, + expires: rememberExpires, + }), + serializeCookie("loggedIn", "true", { + ...input.opts, + httpOnly: false, + expires: rememberExpires, + }), + ]; +} + +export function buildClearSessionCookies(opts: CookieBuildOptions): string[] { + const cleared = { ...opts, maxAgeSec: 0 }; + return [ + serializeCookie("accessToken", "", { ...cleared, httpOnly: true }), + serializeCookie("refreshToken", "", { ...cleared, httpOnly: true }), + serializeCookie("loggedIn", "", { ...cleared, httpOnly: false }), + ]; +} diff --git a/new-deepnotes/packages/session/src/datetime.ts b/new-deepnotes/packages/session/src/datetime.ts new file mode 100644 index 00000000..3e35b945 --- /dev/null +++ b/new-deepnotes/packages/session/src/datetime.ts @@ -0,0 +1,5 @@ +export function addDays(date: Date, days: number): Date { + const d = new Date(date); + d.setUTCDate(d.getUTCDate() + days); + return d; +} diff --git a/new-deepnotes/packages/session/src/device-hash.ts b/new-deepnotes/packages/session/src/device-hash.ts new file mode 100644 index 00000000..2c281d0d --- /dev/null +++ b/new-deepnotes/packages/session/src/device-hash.ts @@ -0,0 +1,16 @@ +import { nanoidToBytes } from "@stdlib/misc"; +import sodium from "libsodium-wrappers-sumo"; + +export function getDeviceHash(input: { + ip: string; + userAgent: string; + userId: string; +}): Buffer { + return Buffer.from( + sodium.crypto_generichash( + 16, + `${input.ip} ${input.userAgent}`, + nanoidToBytes(input.userId), + ), + ); +} diff --git a/new-deepnotes/packages/session/src/email-hash.ts b/new-deepnotes/packages/session/src/email-hash.ts new file mode 100644 index 00000000..46d0cab2 --- /dev/null +++ b/new-deepnotes/packages/session/src/email-hash.ts @@ -0,0 +1,24 @@ +function normalizeEmail(email: string, exceptions: string): string { + return exceptions.split(";").includes(email) ? email : email.toLowerCase(); +} + +/** + * Legacy-compatible HMAC-SHA256 over normalized email (see `@deeplib/data` hashUserEmail). + */ +export async function hashUserEmail( + email: string, + userEmailSecret: string, + exceptions: string, +): Promise { + const normalized = normalizeEmail(email, exceptions); + const enc = new TextEncoder(); + const key = await crypto.subtle.importKey( + "raw", + enc.encode(userEmailSecret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + const sig = await crypto.subtle.sign("HMAC", key, enc.encode(normalized)); + return new Uint8Array(sig); +} diff --git a/new-deepnotes/packages/session/src/env.ts b/new-deepnotes/packages/session/src/env.ts new file mode 100644 index 00000000..a6073f36 --- /dev/null +++ b/new-deepnotes/packages/session/src/env.ts @@ -0,0 +1,21 @@ +/** + * Secrets and flags required for session routes (see docs/AUTH_AND_CORS.md). + */ +export type SessionEnv = { + ACCESS_SECRET: string; + REFRESH_SECRET: string; + USER_EMAIL_SECRET: string; + USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY: string; + USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY: string; + USER_RECOVERY_CODES_ENCRYPTION_KEY: string; + /** When `"true"`, cookies omit `Secure` (local HTTP). */ + DEV?: string; + /** Optional `Domain=` attribute (legacy `HOST`). */ + COOKIE_DOMAIN?: string; + /** Semicolon-separated emails that skip lowercasing (legacy). */ + EMAIL_CASE_SENSITIVITY_EXCEPTIONS?: string; +}; + +export function isDev(env: Pick): boolean { + return env.DEV === "true" || env.DEV === "1"; +} diff --git a/new-deepnotes/packages/session/src/errors.ts b/new-deepnotes/packages/session/src/errors.ts new file mode 100644 index 00000000..05a397cc --- /dev/null +++ b/new-deepnotes/packages/session/src/errors.ts @@ -0,0 +1,10 @@ +export class SessionError extends Error { + constructor( + public readonly status: number, + public readonly code: string, + message: string, + ) { + super(message); + this.name = "SessionError"; + } +} diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts new file mode 100644 index 00000000..56ba7561 --- /dev/null +++ b/new-deepnotes/packages/session/src/index.ts @@ -0,0 +1,7 @@ +export type { SessionEnv } from "./env.js"; +export { isDev } from "./env.js"; +export { SessionError } from "./errors.js"; +export { performSessionLogin } from "./login.js"; +export type { SessionLoginBody } from "./login.js"; +export { performSessionLogout } from "./logout.js"; +export { performSessionRefresh } from "./refresh.js"; diff --git a/new-deepnotes/packages/session/src/jwt.ts b/new-deepnotes/packages/session/src/jwt.ts new file mode 100644 index 00000000..a9e7da05 --- /dev/null +++ b/new-deepnotes/packages/session/src/jwt.ts @@ -0,0 +1,99 @@ +import { decodeJwt, jwtVerify, SignJWT } from "jose"; + +import type { AccessTokenPayload, RefreshTokenPayload } from "./tokens.js"; + +function accessKey(secret: string) { + return new TextEncoder().encode(secret); +} + +function refreshKey(secret: string) { + return new TextEncoder().encode(secret); +} + +export async function signAccessToken(input: { + secret: string; + userId: string; + sessionId: string; +}): Promise { + return await new SignJWT({ + uid: input.userId, + sid: input.sessionId, + } satisfies AccessTokenPayload) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("30m") + .sign(accessKey(input.secret)); +} + +export async function signRefreshToken(input: { + secret: string; + sessionId: string; + refreshCode: string; + rememberSession: boolean; +}): Promise { + const payload = { + sid: input.sessionId, + rfc: input.refreshCode, + rms: input.rememberSession, + } satisfies RefreshTokenPayload; + + const exp = input.rememberSession ? "7d" : "60m"; + + return await new SignJWT(payload) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime(exp) + .sign(refreshKey(input.secret)); +} + +export async function verifyAccessToken( + token: string, + secret: string, +): Promise { + try { + const { payload } = await jwtVerify(token, accessKey(secret), { + algorithms: ["HS256"], + }); + const uid = payload.uid; + const sid = payload.sid; + if (typeof uid !== "string" || typeof sid !== "string") return null; + return { uid, sid }; + } catch { + return null; + } +} + +export async function verifyRefreshToken( + token: string, + secret: string, +): Promise { + try { + const { payload } = await jwtVerify(token, refreshKey(secret), { + algorithms: ["HS256"], + }); + const sid = payload.sid; + const rfc = payload.rfc; + const rms = payload.rms; + if (typeof sid !== "string" || typeof rfc !== "string" || typeof rms !== "boolean") + return null; + return { sid, rfc, rms }; + } catch { + return null; + } +} + +export function decodeRefreshTokenUnsafe( + token: string, +): RefreshTokenPayload | null { + try { + const payload = decodeJwt(token); + const sid = payload.sid; + const rfc = payload.rfc; + const rms = payload.rms; + if (typeof sid !== "string" || typeof rfc !== "string" || typeof rms !== "boolean") + return null; + return { sid, rfc, rms }; + } catch { + return null; + } +} diff --git a/new-deepnotes/packages/session/src/legacy-crypto.ts b/new-deepnotes/packages/session/src/legacy-crypto.ts new file mode 100644 index 00000000..431c0de6 --- /dev/null +++ b/new-deepnotes/packages/session/src/legacy-crypto.ts @@ -0,0 +1,112 @@ +import { base64ToBytes } from "@stdlib/base64"; +import { + cryptoJsWordArrayToUint8Array, + getPasswordHashValues, + wrapSymmetricKey, +} from "@stdlib/crypto"; +import { bytesToText, concatUint8Arrays } from "@stdlib/misc"; +import CryptoJS from "crypto-js"; +import sodium from "libsodium-wrappers-sumo"; +import { pack, unpack } from "msgpackr"; + +export async function ensureSodiumReady(): Promise { + await sodium.ready; +} + +export function derivePasswordValues(input: { + password: Uint8Array; + salt?: Uint8Array; +}) { + input.salt ??= sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES); + + const derivedKey = sodium.crypto_pwhash( + 32 + 64, + input.password, + input.salt, + 2, + 32 * 1048576, + sodium.crypto_pwhash_ALG_ARGON2ID13, + ); + + return { + key: wrapSymmetricKey(derivedKey.slice(0, 32)), + hash: derivedKey.slice(32), + salt: input.salt, + }; +} + +export type PasswordValues = ReturnType; + +export function decryptUserRehashedLoginHash( + userEncryptedRehashedLoginHash: Uint8Array, + encryptionKeyB64: string, +): string { + const key = wrapSymmetricKey(base64ToBytes(encryptionKeyB64)); + return bytesToText( + key.decrypt(userEncryptedRehashedLoginHash, { + associatedData: { context: "UserRehashedLoginHash" }, + }), + ); +} + +export { getPasswordHashValues }; + +export function decryptUserAuthenticatorSecret( + userEncryptedAuthenticatorSecret: Uint8Array, + encryptionKeyB64: string, +): string { + const key = wrapSymmetricKey(base64ToBytes(encryptionKeyB64)); + return bytesToText( + key.decrypt(userEncryptedAuthenticatorSecret, { + associatedData: { context: "UserAuthenticatorSecret" }, + }), + ); +} + +export function decryptRecoveryCodes( + userEncryptedRecoveryCodes: Uint8Array, + encryptionKeyB64: string, +): Uint8Array[] { + const key = wrapSymmetricKey(base64ToBytes(encryptionKeyB64)); + return unpack( + key.decrypt(userEncryptedRecoveryCodes, { + associatedData: { context: "UserRecoveryCodes" }, + }), + ); +} + +export function encryptRecoveryCodes( + userRecoveryCodes: Uint8Array[], + encryptionKeyB64: string, +): Uint8Array { + const key = wrapSymmetricKey(base64ToBytes(encryptionKeyB64)); + return key.encrypt(pack(userRecoveryCodes), { + associatedData: { context: "UserRecoveryCodes" }, + }); +} + +export function hashRecoveryCode( + recoveryCode: string, + salt?: Uint8Array, +): Uint8Array { + salt ??= sodium.randombytes_buf(16); + + return concatUint8Arrays( + salt, + cryptoJsWordArrayToUint8Array( + CryptoJS.SHA256(sodium.to_hex(salt) + recoveryCode), + ), + ); +} + +export function verifyRecoveryCode( + recoveryCode: string, + hashedRecoveryCode: Uint8Array, +): boolean { + const salt = hashedRecoveryCode.slice(0, 16); + + return sodium.memcmp( + hashedRecoveryCode.slice(16), + hashRecoveryCode(recoveryCode, salt).slice(16), + ); +} diff --git a/new-deepnotes/packages/session/src/login.ts b/new-deepnotes/packages/session/src/login.ts new file mode 100644 index 00000000..e5a56c21 --- /dev/null +++ b/new-deepnotes/packages/session/src/login.ts @@ -0,0 +1,214 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { and, eq, gt, or } from "drizzle-orm"; +import { + createPrivateKeyring, + createSymmetricKeyring, +} from "@stdlib/crypto"; +import sodium from "libsodium-wrappers-sumo"; +import { nanoid } from "nanoid"; + +import { devices, users } from "@deepnotes/db/schema"; + +import { cookieOptionsFromEnv } from "./cookies.js"; +import { getDeviceHash } from "./device-hash.js"; +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { hashUserEmail } from "./email-hash.js"; +import { + decryptUserRehashedLoginHash, + derivePasswordValues, + ensureSodiumReady, + getPasswordHashValues, +} from "./legacy-crypto.js"; +import { createSessionRowAndCookies } from "./session-lifecycle.js"; +import { assertTwoFactorOk } from "./two-factor.js"; + +export type SessionLoginBody = { + email: string; + /** Raw login hash bytes (decoded from request base64). */ + loginHash: Uint8Array; + rememberSession: boolean; + authenticatorToken?: string; + rememberDevice?: boolean; + recoveryCode?: string; +}; + +function toB64(u: Uint8Array): string { + return Buffer.from(u).toString("base64"); +} + +export async function performSessionLogin(input: { + db: DeepnotesDb; + env: SessionEnv; + body: SessionLoginBody; + clientIp: string; + userAgent: string; +}): Promise<{ json: Record; cookieLines: string[] }> { + await ensureSodiumReady(); + + const exceptions = input.env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS ?? ""; + const emailHashBuf = Buffer.from( + await hashUserEmail( + input.body.email, + input.env.USER_EMAIL_SECRET, + exceptions, + ), + ); + + const rows = await input.db + .select({ + id: users.id, + emailVerified: users.emailVerified, + encryptedRehashedLoginHash: users.encryptedRehashedLoginHash, + publicKeyring: users.publicKeyring, + encryptedPrivateKeyring: users.encryptedPrivateKeyring, + encryptedSymmetricKeyring: users.encryptedSymmetricKeyring, + personalGroupId: users.personalGroupId, + twoFactorAuthEnabled: users.twoFactorAuthEnabled, + encryptedAuthenticatorSecret: users.encryptedAuthenticatorSecret, + encryptedRecoveryCodes: users.encryptedRecoveryCodes, + }) + .from(users) + .where( + and( + eq(users.emailHash, emailHashBuf), + or( + eq(users.emailVerified, true), + gt(users.emailVerificationExpirationDate, new Date().toISOString()), + ), + ), + ) + .limit(1); + + const user = rows[0]; + if (user == null) { + throw new SessionError(401, "UNAUTHORIZED", "Incorrect email or password."); + } + + const passwordHashValues = getPasswordHashValues( + decryptUserRehashedLoginHash( + new Uint8Array(user.encryptedRehashedLoginHash), + input.env.USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + ), + ); + + const passwordValues = derivePasswordValues({ + password: input.body.loginHash, + salt: passwordHashValues.saltBytes, + }); + + const passwordOk = sodium.memcmp(passwordValues.hash, passwordHashValues.hashBytes); + if (!passwordOk) { + throw new SessionError(401, "UNAUTHORIZED", "Incorrect email or password."); + } + + if (!user.emailVerified) { + throw new SessionError( + 401, + "UNAUTHORIZED", + "Email awaiting verification. New email sent.", + ); + } + + const cookieOpts = cookieOptionsFromEnv(input.env); + + return await input.db.transaction(async (tx) => { + const deviceHash = getDeviceHash({ + ip: input.clientIp, + userAgent: input.userAgent, + userId: user.id, + }); + + const existingDevice = await tx + .select({ id: devices.id, trusted: devices.trusted }) + .from(devices) + .where( + and(eq(devices.userId, user.id), eq(devices.hash, deviceHash)), + ) + .limit(1); + + let deviceId: string; + let deviceTrusted: boolean; + if (existingDevice[0]) { + deviceId = existingDevice[0].id; + deviceTrusted = existingDevice[0].trusted; + } else { + deviceId = nanoid(); + await tx.insert(devices).values({ + id: deviceId, + userId: user.id, + hash: deviceHash, + trusted: false, + }); + deviceTrusted = false; + } + + if (user.twoFactorAuthEnabled) { + if (user.encryptedAuthenticatorSecret == null) { + throw new SessionError( + 500, + "SERVER_MISCONFIG", + "Two-factor enabled but authenticator secret is missing.", + ); + } + await assertTwoFactorOk({ + tx: tx as unknown as DeepnotesDb, + user: { + id: user.id, + encryptedAuthenticatorSecret: user.encryptedAuthenticatorSecret, + encryptedRecoveryCodes: user.encryptedRecoveryCodes, + }, + device: { id: deviceId, trusted: deviceTrusted }, + authenticatorToken: input.body.authenticatorToken, + recoveryCode: input.body.recoveryCode, + rememberDevice: input.body.rememberDevice, + userAuthenticatorKeyB64: input.env.USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, + userRecoveryCodesKeyB64: input.env.USER_RECOVERY_CODES_ENCRYPTION_KEY, + }); + } + + const sessionId = nanoid(); + const { sessionKey, cookieLines } = await createSessionRowAndCookies({ + db: tx as unknown as DeepnotesDb, + sessionId, + userId: user.id, + deviceId, + rememberSession: input.body.rememberSession, + env: input.env, + cookieOpts, + }); + + const encPriv = createPrivateKeyring( + new Uint8Array(user.encryptedPrivateKeyring), + ) + .unwrapSymmetric(passwordValues.key, { + associatedData: { + context: "UserEncryptedPrivateKeyring", + userId: user.id, + }, + }).wrappedValue; + + const encSym = createSymmetricKeyring( + new Uint8Array(user.encryptedSymmetricKeyring), + ) + .unwrapSymmetric(passwordValues.key, { + associatedData: { + context: "UserEncryptedSymmetricKeyring", + userId: user.id, + }, + }).wrappedValue; + + return { + json: { + userId: user.id, + sessionId, + sessionKey: toB64(sessionKey), + personalGroupId: user.personalGroupId, + publicKeyring: toB64(new Uint8Array(user.publicKeyring)), + encryptedPrivateKeyring: toB64(encPriv), + encryptedSymmetricKeyring: toB64(encSym), + }, + cookieLines, + }; + }); +} diff --git a/new-deepnotes/packages/session/src/logout.ts b/new-deepnotes/packages/session/src/logout.ts new file mode 100644 index 00000000..4cfce64e --- /dev/null +++ b/new-deepnotes/packages/session/src/logout.ts @@ -0,0 +1,33 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { eq } from "drizzle-orm"; + +import { sessions } from "@deepnotes/db/schema"; + +import { buildClearSessionCookies, cookieOptionsFromEnv } from "./cookies.js"; +import type { SessionEnv } from "./env.js"; +import { verifyAccessToken } from "./jwt.js"; + +export async function performSessionLogout(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; +}): Promise<{ cookieLines: string[] }> { + const cookieOpts = cookieOptionsFromEnv(input.env); + + if (input.accessCookie) { + const payload = await verifyAccessToken( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + if (payload != null) { + await input.db + .update(sessions) + .set({ invalidated: true }) + .where(eq(sessions.id, payload.sid)); + } + } + + return { + cookieLines: buildClearSessionCookies(cookieOpts), + }; +} diff --git a/new-deepnotes/packages/session/src/refresh.ts b/new-deepnotes/packages/session/src/refresh.ts new file mode 100644 index 00000000..e97a5ef9 --- /dev/null +++ b/new-deepnotes/packages/session/src/refresh.ts @@ -0,0 +1,92 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { and, eq } from "drizzle-orm"; + +import { sessions } from "@deepnotes/db/schema"; + +import { cookieOptionsFromEnv } from "./cookies.js"; +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { + decodeRefreshTokenUnsafe, + verifyRefreshToken, +} from "./jwt.js"; +import { ensureSodiumReady } from "./legacy-crypto.js"; +import { rotateSessionRowAndCookies } from "./session-lifecycle.js"; + +export async function performSessionRefresh(input: { + db: DeepnotesDb; + env: SessionEnv; + refreshCookie: string | undefined; + loggedInCookie: string | undefined; +}): Promise<{ json: Record; cookieLines: string[] }> { + await ensureSodiumReady(); + + if (input.loggedInCookie !== "true") { + throw new SessionError(401, "UNAUTHORIZED", "User not logged in."); + } + if (input.refreshCookie == null || input.refreshCookie === "") { + throw new SessionError(401, "UNAUTHORIZED", "No refresh token received."); + } + + const payload = await verifyRefreshToken( + input.refreshCookie, + input.env.REFRESH_SECRET, + ); + + if (payload == null) { + const decoded = decodeRefreshTokenUnsafe(input.refreshCookie); + if (decoded != null) { + await input.db + .update(sessions) + .set({ invalidated: true }) + .where(eq(sessions.id, decoded.sid)); + } + throw new SessionError(401, "UNAUTHORIZED", "Invalid refresh token."); + } + + const sessionRows = await input.db + .select({ + id: sessions.id, + userId: sessions.userId, + invalidated: sessions.invalidated, + expirationDate: sessions.expirationDate, + encryptionKey: sessions.encryptionKey, + }) + .from(sessions) + .where( + and( + eq(sessions.refreshCode, payload.rfc), + eq(sessions.id, payload.sid), + ), + ) + .limit(1); + + const session = sessionRows[0]; + const now = new Date().toISOString(); + if ( + session == null || + session.invalidated || + session.expirationDate < now + ) { + throw new SessionError(401, "UNAUTHORIZED", "Session was invalidated."); + } + + const cookieOpts = cookieOptionsFromEnv(input.env); + const { sessionKey, cookieLines, oldSessionKey } = + await rotateSessionRowAndCookies({ + db: input.db, + sessionId: session.id, + userId: session.userId, + rememberSession: payload.rms, + env: input.env, + cookieOpts, + }); + + return { + json: { + oldSessionKey: Buffer.from(oldSessionKey).toString("base64"), + newSessionKey: Buffer.from(sessionKey).toString("base64"), + }, + cookieLines, + }; +} diff --git a/new-deepnotes/packages/session/src/session-lifecycle.ts b/new-deepnotes/packages/session/src/session-lifecycle.ts new file mode 100644 index 00000000..5dd55d76 --- /dev/null +++ b/new-deepnotes/packages/session/src/session-lifecycle.ts @@ -0,0 +1,120 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { eq } from "drizzle-orm"; +import sodium from "libsodium-wrappers-sumo"; +import { nanoid } from "nanoid"; + +import { sessions } from "@deepnotes/db/schema"; + +import { buildSessionCookies } from "./cookies.js"; +import type { CookieBuildOptions } from "./cookies.js"; +import type { SessionEnv } from "./env.js"; +import { signAccessToken, signRefreshToken } from "./jwt.js"; +import { addDays } from "./datetime.js"; + +export async function createSessionRowAndCookies(input: { + db: DeepnotesDb; + sessionId: string; + userId: string; + deviceId: string; + rememberSession: boolean; + env: SessionEnv; + cookieOpts: CookieBuildOptions; +}): Promise<{ + sessionKey: Uint8Array; + refreshCode: string; + cookieLines: string[]; +}> { + const sessionKey = sodium.crypto_aead_xchacha20poly1305_ietf_keygen(); + const refreshCode = nanoid(); + const expirationDate = addDays(new Date(), 7).toISOString(); + + await input.db.insert(sessions).values({ + id: input.sessionId, + userId: input.userId, + deviceId: input.deviceId, + encryptionKey: Buffer.from(sessionKey), + refreshCode, + expirationDate, + invalidated: false, + }); + + const accessToken = await signAccessToken({ + secret: input.env.ACCESS_SECRET, + userId: input.userId, + sessionId: input.sessionId, + }); + const refreshToken = await signRefreshToken({ + secret: input.env.REFRESH_SECRET, + sessionId: input.sessionId, + refreshCode, + rememberSession: input.rememberSession, + }); + + const cookieLines = buildSessionCookies({ + opts: input.cookieOpts, + accessToken, + refreshToken, + rememberSession: input.rememberSession, + }); + + return { sessionKey, refreshCode, cookieLines }; +} + +export async function rotateSessionRowAndCookies(input: { + db: DeepnotesDb; + sessionId: string; + userId: string; + rememberSession: boolean; + env: SessionEnv; + cookieOpts: CookieBuildOptions; +}): Promise<{ + sessionKey: Uint8Array; + cookieLines: string[]; + oldSessionKey: Uint8Array; +}> { + const sessionKey = sodium.crypto_aead_xchacha20poly1305_ietf_keygen(); + const refreshCode = nanoid(); + const expirationDate = addDays(new Date(), 7).toISOString(); + + const existing = await input.db + .select({ encryptionKey: sessions.encryptionKey }) + .from(sessions) + .where(eq(sessions.id, input.sessionId)) + .limit(1); + const oldRow = existing[0]; + if (oldRow?.encryptionKey == null) { + throw new Error("Session row missing for refresh."); + } + const oldSessionKey = new Uint8Array(oldRow.encryptionKey); + + await input.db + .update(sessions) + .set({ + encryptionKey: Buffer.from(sessionKey), + refreshCode, + lastRefreshDate: new Date().toISOString(), + expirationDate, + }) + .where(eq(sessions.id, input.sessionId)); + + const accessToken = await signAccessToken({ + secret: input.env.ACCESS_SECRET, + userId: input.userId, + sessionId: input.sessionId, + }); + const refreshToken = await signRefreshToken({ + secret: input.env.REFRESH_SECRET, + sessionId: input.sessionId, + refreshCode, + rememberSession: input.rememberSession, + }); + + const cookieLines = buildSessionCookies({ + opts: input.cookieOpts, + accessToken, + refreshToken, + rememberSession: input.rememberSession, + }); + + return { sessionKey, cookieLines, oldSessionKey }; +} diff --git a/new-deepnotes/packages/session/src/tokens.ts b/new-deepnotes/packages/session/src/tokens.ts new file mode 100644 index 00000000..049d43f1 --- /dev/null +++ b/new-deepnotes/packages/session/src/tokens.ts @@ -0,0 +1,15 @@ +/** Access token TTL (legacy `@deeplib/misc`). */ +export const ACCESS_TOKEN_DURATION_MS = 30 * 60 * 1000; +export const REFRESH_TOKEN_SHORT_DURATION_MS = 60 * 60 * 1000; +export const REFRESH_TOKEN_LONG_DURATION_MS = 7 * 24 * 60 * 60 * 1000; + +export type AccessTokenPayload = { + uid: string; + sid: string; +}; + +export type RefreshTokenPayload = { + sid: string; + rfc: string; + rms: boolean; +}; diff --git a/new-deepnotes/packages/session/src/two-factor.ts b/new-deepnotes/packages/session/src/two-factor.ts new file mode 100644 index 00000000..8fcb9203 --- /dev/null +++ b/new-deepnotes/packages/session/src/two-factor.ts @@ -0,0 +1,85 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { eq } from "drizzle-orm"; +import { authenticator } from "otplib"; + +import { devices, users } from "@deepnotes/db/schema"; + +import { SessionError } from "./errors.js"; +import { + decryptRecoveryCodes, + decryptUserAuthenticatorSecret, + encryptRecoveryCodes, + verifyRecoveryCode, +} from "./legacy-crypto.js"; + +type User2faRow = { + id: string; + /** Required when caller enables the 2FA branch. */ + encryptedAuthenticatorSecret: Buffer; + encryptedRecoveryCodes: Buffer | null; +}; + +export async function assertTwoFactorOk(input: { + tx: DeepnotesDb; + user: User2faRow; + device: { id: string; trusted: boolean }; + authenticatorToken: string | undefined; + recoveryCode: string | undefined; + rememberDevice: boolean | undefined; + userAuthenticatorKeyB64: string; + userRecoveryCodesKeyB64: string; +}): Promise { + if (input.device.trusted) { + return; + } + + if (input.authenticatorToken != null) { + const secret = decryptUserAuthenticatorSecret( + new Uint8Array(input.user.encryptedAuthenticatorSecret), + input.userAuthenticatorKeyB64, + ); + if (authenticator.check(input.authenticatorToken, secret)) { + if (input.rememberDevice) { + await input.tx + .update(devices) + .set({ trusted: true }) + .where(eq(devices.id, input.device.id)); + } + return; + } + throw new SessionError(401, "UNAUTHORIZED", "Invalid authenticator token."); + } + + if (input.recoveryCode != null) { + if (input.user.encryptedRecoveryCodes == null) { + throw new SessionError(401, "UNAUTHORIZED", "Invalid recovery code."); + } + const recoveryCodes = decryptRecoveryCodes( + new Uint8Array(input.user.encryptedRecoveryCodes), + input.userRecoveryCodesKeyB64, + ); + + for (let i = 0; i < recoveryCodes.length; i++) { + if (verifyRecoveryCode(input.recoveryCode, recoveryCodes[i]!)) { + recoveryCodes.splice(i, 1); + await input.tx + .update(users) + .set({ + encryptedRecoveryCodes: Buffer.from( + encryptRecoveryCodes(recoveryCodes, input.userRecoveryCodesKeyB64), + ), + }) + .where(eq(users.id, input.user.id)); + return; + } + } + + throw new SessionError(401, "UNAUTHORIZED", "Invalid recovery code."); + } + + throw new SessionError( + 401, + "UNAUTHORIZED", + "Requires two-factor authentication.", + ); +} diff --git a/new-deepnotes/packages/session/tsconfig.json b/new-deepnotes/packages/session/tsconfig.json new file mode 100644 index 00000000..129f731c --- /dev/null +++ b/new-deepnotes/packages/session/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "." + }, + "include": ["src/**/*.ts"] +} diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml index a2829607..8c9e1769 100644 --- a/new-deepnotes/pnpm-lock.yaml +++ b/new-deepnotes/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@stdlib/base64': link:../packages/@stdlib/base64 + '@stdlib/misc': link:../packages/@stdlib/misc + libsodium-wrappers-sumo: ^0.8.0 + importers: .: @@ -29,6 +34,12 @@ importers: '@deepnotes/api': specifier: workspace:* version: link:../../packages/api + '@deepnotes/db': + specifier: workspace:* + version: link:../../packages/db + '@deepnotes/session': + specifier: workspace:* + version: link:../../packages/session hono: specifier: ^4.7.7 version: 4.12.15 @@ -109,6 +120,55 @@ importers: specifier: ^3.2.4 version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + packages/session: + dependencies: + '@deepnotes/db': + specifier: workspace:* + version: link:../db + '@stdlib/base64': + specifier: link:../../../packages/@stdlib/base64 + version: link:../../../packages/@stdlib/base64 + '@stdlib/crypto': + specifier: link:../../../packages/@stdlib/crypto + version: link:../../../packages/@stdlib/crypto + '@stdlib/misc': + specifier: link:../../../packages/@stdlib/misc + version: link:../../../packages/@stdlib/misc + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + drizzle-orm: + specifier: ^0.41.0 + version: 0.41.0(@cloudflare/workers-types@4.20260426.1)(gel@2.2.0)(postgres@3.4.9) + jose: + specifier: ^5.10.0 + version: 5.10.0 + libsodium-wrappers-sumo: + specifier: ^0.8.0 + version: 0.8.4 + msgpackr: + specifier: ^1.11.2 + version: 1.11.10 + nanoid: + specifier: ^5.1.5 + version: 5.1.9 + otplib: + specifier: ^12.0.1 + version: 12.0.1 + devDependencies: + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 + '@types/node': + specifier: ^22.14.1 + version: 22.19.17 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + packages: '@asteasolutions/zod-to-openapi@7.3.4': @@ -1002,6 +1062,54 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + + '@otplib/core@12.0.1': + resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} + + '@otplib/plugin-crypto@12.0.1': + resolution: {integrity: sha512-qPuhN3QrT7ZZLcLCyKOSNhuijUi9G5guMRVrxq63r9YNOxxQjPm59gVxLM+7xGnHnM6cimY57tuKsjK7y9LM1g==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/plugin-thirty-two@12.0.1': + resolution: {integrity: sha512-MtT+uqRso909UkbrrYpJ6XFjj9D+x2Py7KjTO9JDPhL0bJUYVu5kFP4TFZW4NFAywrAtFRxOVY261u0qwb93gA==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/preset-default@12.0.1': + resolution: {integrity: sha512-xf1v9oOJRyXfluBhMdpOkr+bsE+Irt+0D5uHtvg6x1eosfmHCsCC6ej/m7FXiWqdo0+ZUI6xSKDhJwc8yfiOPQ==} + deprecated: Please upgrade to v13 of otplib. Refer to otplib docs for migration paths + + '@otplib/preset-v11@12.0.1': + resolution: {integrity: sha512-9hSetMI7ECqbFiKICrNa4w70deTUfArtwXykPUvSHWOdzOlfa9ajglu7mNCntlvxycTiOAXkQGwjQCzzDEMRMg==} + '@petamoriken/float16@3.9.3': resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} @@ -1179,6 +1287,9 @@ packages: '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1423,6 +1534,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1737,6 +1851,9 @@ packages: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -1764,6 +1881,12 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + libsodium-sumo@0.8.4: + resolution: {integrity: sha512-TMtHShQfVVsaxDygyapvUC3o7YsPgXa/hRWeIgzyFz6w5k/1hirGptCxp1U7XwW3rCskaTTYKgV10v86UiGgNw==} + + libsodium-wrappers-sumo@0.8.4: + resolution: {integrity: sha512-ql7hcgulKZ3ekfa2DGAogcCKsWU0diA/0nArz1CFzh93WQdb46/Kj18ka/Hifq6uA3Ush34Pc6vU/6HXeRwUkg==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1796,6 +1919,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.10: + resolution: {integrity: sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==} + muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} @@ -1804,9 +1934,18 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.9: + resolution: {integrity: sha512-ZUvP7KeBLe3OZ1ypw6dI/TzYJuvHP77IM4Ry73waSQTLn8/g8rpdjfyVAh7t1/+FjBtG4lCP42MEbDxOsRpBMw==} + engines: {node: ^18 || >=20} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + openapi3-ts@4.5.0: resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==} @@ -1814,6 +1953,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + otplib@12.0.1: + resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -1938,6 +2080,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + thirty-two@1.0.2: + resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} + engines: {node: '>=0.2.6'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -2694,6 +2840,47 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + + '@otplib/core@12.0.1': {} + + '@otplib/plugin-crypto@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + + '@otplib/plugin-thirty-two@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + thirty-two: 1.0.2 + + '@otplib/preset-default@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + '@otplib/plugin-crypto': 12.0.1 + '@otplib/plugin-thirty-two': 12.0.1 + + '@otplib/preset-v11@12.0.1': + dependencies: + '@otplib/core': 12.0.1 + '@otplib/plugin-crypto': 12.0.1 + '@otplib/plugin-thirty-two': 12.0.1 + '@petamoriken/float16@3.9.3': optional: true @@ -2811,6 +2998,8 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/crypto-js@4.2.2': {} + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -3122,6 +3311,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + csstype@3.2.3: {} de-indent@1.0.2: {} @@ -3429,6 +3620,8 @@ snapshots: isexe@3.1.5: optional: true + jose@5.10.0: {} + js-tokens@9.0.1: {} js-yaml@4.1.1: @@ -3452,6 +3645,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + libsodium-sumo@0.8.4: {} + + libsodium-wrappers-sumo@0.8.4: + dependencies: + libsodium-sumo: 0.8.4 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -3490,12 +3689,35 @@ snapshots: ms@2.1.3: {} + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.10: + optionalDependencies: + msgpackr-extract: 3.0.3 + muggle-string@0.4.1: {} nanoid@3.3.11: {} + nanoid@5.1.9: {} + natural-compare@1.4.0: {} + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + openapi3-ts@4.5.0: dependencies: yaml: 2.8.3 @@ -3509,6 +3731,12 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + otplib@12.0.1: + dependencies: + '@otplib/core': 12.0.1 + '@otplib/preset-default': 12.0.1 + '@otplib/preset-v11': 12.0.1 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -3653,6 +3881,8 @@ snapshots: dependencies: has-flag: 4.0.0 + thirty-two@1.0.2: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} diff --git a/new-deepnotes/template.env b/new-deepnotes/template.env index 01c618ca..7060f0d9 100644 --- a/new-deepnotes/template.env +++ b/new-deepnotes/template.env @@ -9,6 +9,17 @@ DATABASE_URL=postgresql://deepnotes:deepnotes@localhost:5433/deepnotes # Standard Redis (not KeyDB). TCP URL for Node; Workers may use Upstash HTTP or REDIS_URL via TCP where supported REDIS_URL=redis://localhost:6380 -# API worker (Wrangler secrets in production) -# JWT_SECRET= +# API worker — match legacy names (docs/AUTH_AND_CORS.md). Use Wrangler secrets / .dev.vars in dev. +# ACCESS_SECRET= +# REFRESH_SECRET= +# USER_EMAIL_SECRET= +# USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY= # base64, 32-byte symmetric key material +# USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY= # base64 +# USER_RECOVERY_CODES_ENCRYPTION_KEY= # base64 +# COOKIE_DOMAIN= # optional; legacy used HOST +# DEV=true # local HTTP without Secure cookies # STRIPE_WEBHOOK_SECRET= + +# Before `pnpm build` / CI in this repo: build parent `@stdlib/crypto` once (linked by @deepnotes/session): +# pnpm install --filter @stdlib/crypto... && pnpm --filter @stdlib/crypto run build +# (from the DeepNotes monorepo root, not only new-deepnotes). From c5ac4bf81272ef5b77ebd8aa3ff31c2f7722e512 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 23:09:41 -0300 Subject: [PATCH 024/243] refactor: remove legacy links --- .github/workflows/new-deepnotes-ci.yml | 6 - new-deepnotes/PLAN_PROGRESS.md | 2 +- new-deepnotes/package.json | 2 - new-deepnotes/packages/session/package.json | 3 - .../packages/session/src/crypto/bytes.ts | 38 ++++ .../session/src/crypto/crypto-js-wordarray.ts | 40 ++++ .../packages/session/src/crypto/index.ts | 11 ++ .../packages/session/src/crypto/key-pair.ts | 35 ++++ .../packages/session/src/crypto/keyring.ts | 187 ++++++++++++++++++ .../session/src/crypto/nanoid-bytes.ts | 35 ++++ .../session/src/crypto/password-hashing.ts | 38 ++++ .../session/src/crypto/private-key.ts | 62 ++++++ .../session/src/crypto/private-keyring.ts | 133 +++++++++++++ .../packages/session/src/crypto/public-key.ts | 7 + .../session/src/crypto/symmetric-key.ts | 97 +++++++++ .../session/src/crypto/symmetric-keyring.ts | 138 +++++++++++++ .../session/src/crypto/wrapped-data.ts | 158 +++++++++++++++ .../packages/session/src/device-hash.ts | 2 +- .../packages/session/src/legacy-crypto.ts | 16 +- new-deepnotes/packages/session/src/login.ts | 2 +- new-deepnotes/pnpm-lock.yaml | 11 -- new-deepnotes/template.env | 4 - 22 files changed, 990 insertions(+), 37 deletions(-) create mode 100644 new-deepnotes/packages/session/src/crypto/bytes.ts create mode 100644 new-deepnotes/packages/session/src/crypto/crypto-js-wordarray.ts create mode 100644 new-deepnotes/packages/session/src/crypto/index.ts create mode 100644 new-deepnotes/packages/session/src/crypto/key-pair.ts create mode 100644 new-deepnotes/packages/session/src/crypto/keyring.ts create mode 100644 new-deepnotes/packages/session/src/crypto/nanoid-bytes.ts create mode 100644 new-deepnotes/packages/session/src/crypto/password-hashing.ts create mode 100644 new-deepnotes/packages/session/src/crypto/private-key.ts create mode 100644 new-deepnotes/packages/session/src/crypto/private-keyring.ts create mode 100644 new-deepnotes/packages/session/src/crypto/public-key.ts create mode 100644 new-deepnotes/packages/session/src/crypto/symmetric-key.ts create mode 100644 new-deepnotes/packages/session/src/crypto/symmetric-keyring.ts create mode 100644 new-deepnotes/packages/session/src/crypto/wrapped-data.ts diff --git a/.github/workflows/new-deepnotes-ci.yml b/.github/workflows/new-deepnotes-ci.yml index 069e0ecb..43872fc9 100644 --- a/.github/workflows/new-deepnotes-ci.yml +++ b/.github/workflows/new-deepnotes-ci.yml @@ -51,12 +51,6 @@ jobs: - name: Install run: pnpm install --frozen-lockfile - - name: Build linked @stdlib/crypto (parent monorepo) - working-directory: ${{ github.workspace }} - run: | - pnpm install --frozen-lockfile --filter @stdlib/crypto... - pnpm --filter @stdlib/crypto run build - - name: Lint run: pnpm lint diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index cfa52283..a8c549c8 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -69,7 +69,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | Date | Change | |------|--------| -| 2026-04-26 | Phase 3: `@deepnotes/session` (login/refresh/logout + 2FA TOTP/recovery), api-worker Hyperdrive + dynamic import for Workers bundle; OpenAPI 200/401/503 for session routes; demo remains `501`; CI builds parent `@stdlib/crypto`; `libsodium-wrappers-sumo@^0.8` override for Wrangler. | +| 2026-04-26 | Phase 3: `@deepnotes/session` (login/refresh/logout + 2FA TOTP/recovery), api-worker Hyperdrive + dynamic import for Workers bundle; OpenAPI 200/401/503 for session routes; demo remains `501`; session crypto vendored in-package (no parent `@stdlib` links); `libsodium-wrappers-sumo@^0.8` override for Wrangler. | | 2026-04-26 | Phase 3 start: OpenAPI + Zod for `POST /api/sessions/login|refresh|logout|demo`; api-worker `501` stubs; Phase 0 marked done in snapshot. | | 2026-04-26 | Phase 0 docs (TRPC_REST_MAP, AUTH_AND_CORS, CLIENT_FORKS); Phase 2 deploy doc; Drizzle legacy baseline from `postgres-init.sql`; Vitest template-DB integration test + CI `DATABASE_ADMIN_URL`. | | 2026-04-26 | Initial `new-deepnotes` monorepo: `@deepnotes/api`, `@deepnotes/db`, `@deepnotes/api-worker`, `@deepnotes/web`, CI workflow. | diff --git a/new-deepnotes/package.json b/new-deepnotes/package.json index 0171df7b..ad711175 100644 --- a/new-deepnotes/package.json +++ b/new-deepnotes/package.json @@ -19,8 +19,6 @@ }, "pnpm": { "overrides": { - "@stdlib/base64": "link:../packages/@stdlib/base64", - "@stdlib/misc": "link:../packages/@stdlib/misc", "libsodium-wrappers-sumo": "^0.8.0" } }, diff --git a/new-deepnotes/packages/session/package.json b/new-deepnotes/packages/session/package.json index 9e619529..24b051e7 100644 --- a/new-deepnotes/packages/session/package.json +++ b/new-deepnotes/packages/session/package.json @@ -16,9 +16,6 @@ }, "dependencies": { "@deepnotes/db": "workspace:*", - "@stdlib/base64": "link:../../../packages/@stdlib/base64", - "@stdlib/crypto": "link:../../../packages/@stdlib/crypto", - "@stdlib/misc": "link:../../../packages/@stdlib/misc", "crypto-js": "^4.2.0", "drizzle-orm": "^0.41.0", "jose": "^5.10.0", diff --git a/new-deepnotes/packages/session/src/crypto/bytes.ts b/new-deepnotes/packages/session/src/crypto/bytes.ts new file mode 100644 index 00000000..6ce1e2ef --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/bytes.ts @@ -0,0 +1,38 @@ +export function concatUint8Arrays(...arrays: Uint8Array[]): Uint8Array { + let totalLength = 0; + for (const arr of arrays) { + totalLength += arr.length; + } + const result = new Uint8Array(totalLength); + let offset = 0; + for (const arr of arrays) { + result.set(arr, offset); + offset += arr.length; + } + return result; +} + +export function bytesToText(bytes: Uint8Array): string { + return new TextDecoder().decode(bytes); +} + +export function textToBytes(text: string): Uint8Array { + return new TextEncoder().encode(text); +} + +/** Standard base64 decode (DeepNotes password-hash strings use non–URL-safe base64). */ +export function base64ToBytes(input: string): Uint8Array { + return new Uint8Array(Buffer.from(input, "base64")); +} + +/** Mirrors legacy `@stdlib/base64` `bytesToBase64` (Argon2 PHC strings). */ +export function bytesToBase64( + input: Uint8Array, + params?: { urlSafe?: boolean; padding?: boolean }, +): string { + const b64 = Buffer.from(input).toString("base64"); + if (!params?.urlSafe && !params?.padding) { + return b64.replace(/=+$/, ""); + } + return b64; +} diff --git a/new-deepnotes/packages/session/src/crypto/crypto-js-wordarray.ts b/new-deepnotes/packages/session/src/crypto/crypto-js-wordarray.ts new file mode 100644 index 00000000..a2b708a7 --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/crypto-js-wordarray.ts @@ -0,0 +1,40 @@ +import type CryptoJS from "crypto-js"; + +/** Port of `@stdlib/crypto` `cryptoJsWordArrayToUint8Array` for recovery-code hashing. */ +export function cryptoJsWordArrayToUint8Array( + wordArray: CryptoJS.lib.WordArray, +): Uint8Array { + const totalLength = wordArray.sigBytes; + const words = wordArray.words; + const result = new Uint8Array(totalLength); + + let i = 0; + let j = 0; + + while (true) { + if (i === totalLength) { + break; + } + + const w = words[j++]!; + + result[i++] = (w & 0xff000000) >>> 24; + + if (i === totalLength) { + break; + } + result[i++] = (w & 0x00ff0000) >>> 16; + + if (i === totalLength) { + break; + } + result[i++] = (w & 0x0000ff00) >>> 8; + + if (i === totalLength) { + break; + } + result[i++] = w & 0x000000ff; + } + + return result; +} diff --git a/new-deepnotes/packages/session/src/crypto/index.ts b/new-deepnotes/packages/session/src/crypto/index.ts new file mode 100644 index 00000000..f3a1e610 --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/index.ts @@ -0,0 +1,11 @@ +/** + * In-repo crypto primitives required for session/login parity with stored user blobs. + * (Selective port of former `@stdlib/crypto` keyring + hashing wire shapes — no workspace link.) + */ +export { createPrivateKeyring } from "./private-keyring.js"; +export type { PrivateKeyring } from "./private-keyring.js"; +export { createSymmetricKeyring } from "./symmetric-keyring.js"; +export type { SymmetricKeyring } from "./symmetric-keyring.js"; +export { getPasswordHashValues } from "./password-hashing.js"; +export { wrapSymmetricKey } from "./symmetric-key.js"; +export type { SymmetricKey } from "./symmetric-key.js"; diff --git a/new-deepnotes/packages/session/src/crypto/key-pair.ts b/new-deepnotes/packages/session/src/crypto/key-pair.ts new file mode 100644 index 00000000..aa7ffdc9 --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/key-pair.ts @@ -0,0 +1,35 @@ +import type { PrivateKey } from "./private-key.js"; +import type { PublicKey } from "./public-key.js"; + +export function wrapKeyPair(publicKey: PublicKey, privateKey: PrivateKey) { + return new (class KeyPair { + get publicKey() { + return publicKey; + } + get privateKey() { + return privateKey; + } + + encrypt( + plaintext: Uint8Array, + recipientsPublicKey: PublicKey, + params?: { padding?: boolean }, + ): Uint8Array { + return privateKey.encrypt( + plaintext, + recipientsPublicKey, + publicKey, + params, + ); + } + + decrypt( + ciphertext: Uint8Array, + params?: { padding?: boolean }, + ): Uint8Array { + return privateKey.decrypt(ciphertext, params); + } + })(); +} + +export type KeyPair = ReturnType; diff --git a/new-deepnotes/packages/session/src/crypto/keyring.ts b/new-deepnotes/packages/session/src/crypto/keyring.ts new file mode 100644 index 00000000..f157ca52 --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/keyring.ts @@ -0,0 +1,187 @@ +import { addDays } from "../datetime.js"; + +import type { KeyPair } from "./key-pair.js"; +import type { PrivateKey } from "./private-key.js"; +import type { PublicKey } from "./public-key.js"; +import type { SymmetricKey } from "./symmetric-key.js"; +import type { Wrappable } from "./wrapped-data.js"; +import { DataLayer, WrappedData } from "./wrapped-data.js"; + +export interface KeyMetadata { + rotationDate: Date; +} + +export interface IKeyring extends Wrappable { + get keyMetadata(): KeyMetadata[]; + + get content(): Uint8Array; + get wrappedValue(): Uint8Array; + + addKey(key: Uint8Array): IKeyring; +} + +/** Legacy-style factory; return type is intentionally loose to avoid circular `IKeyring` inference. */ +export function createKeyring( + value: Uint8Array, + params?: { raw?: boolean; locked?: boolean }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- factory object matches legacy `createKeyring` shape +): any { + const raw = params?.raw ?? value.length <= 32; + + const _wrappedData = new WrappedData(value, { + raw, + }); + + return new (class Keyring implements Wrappable, IKeyring { + keys: Uint8Array[] = []; + + constructor() { + if (_wrappedData.topLayer !== DataLayer.Raw) { + return; + } + + this.keys = decodeKeys(_wrappedData.content); + + const meta = _wrappedData.metadata as { keys?: KeyMetadata[] }; + if (meta.keys == null) { + meta.keys = new Array(this.keys.length).fill({ + rotationDate: new Date(), + }); + } + + this.trimExpiredKeys(); + } + + trimExpiredKeys() { + if (_wrappedData.topLayer !== DataLayer.Raw) { + throw new Error("Cannot trim keys on non-raw keyring."); + } + + const currentDate = new Date(); + const meta = _wrappedData.metadata as { keys: KeyMetadata[] }; + + const unexpiredIndexes = this.keys + .map((_, index) => index) + .filter((index) => { + return ( + index === 0 || + addDays(meta.keys[index]!.rotationDate, 1) > currentDate + ); + }); + + this.keys = this.keys.filter((_, index) => + unexpiredIndexes.includes(index), + ); + _wrappedData.content = encodeKeys(this.keys); + + meta.keys = meta.keys.filter((_, index) => + unexpiredIndexes.includes(index), + ); + } + + get layers() { + return _wrappedData.layers; + } + get topLayer() { + return _wrappedData.topLayer; + } + hasLayer(layer: DataLayer) { + return _wrappedData.hasLayer(layer); + } + countLayerType(layer: DataLayer) { + return _wrappedData.countLayerType(layer); + } + + get topKey() { + return this.keys[0]; + } + + get keyMetadata(): KeyMetadata[] { + return (_wrappedData.metadata as { keys: KeyMetadata[] }).keys; + } + + clone() { + return createKeyring(_wrappedData.value); + } + + addKey(key: Uint8Array) { + if (_wrappedData.topLayer !== DataLayer.Raw) { + throw new Error("Cannot add key to non-raw keyring."); + } + + const result = this.clone(); + result.keyMetadata[0]!.rotationDate = new Date(); + result.keys.unshift(key); + result.keyMetadata.unshift({ rotationDate: new Date() }); + result.trimExpiredKeys(); + return result; + } + + get content() { + return _wrappedData.content; + } + get wrappedValue() { + return _wrappedData.value; + } + get value() { + return this.topKey!; + } + + wrapSymmetric( + symmetricKey: SymmetricKey, + params?: { + nonce?: Uint8Array; + includeNonce?: boolean; + associatedData?: object; + padding?: boolean; + }, + ) { + return createKeyring( + _wrappedData.wrapSymmetric(symmetricKey, params).value, + ); + } + unwrapSymmetric( + symmetricKey: SymmetricKey, + params?: { + nonce?: Uint8Array; + associatedData?: object; + padding?: boolean; + }, + ) { + return createKeyring( + _wrappedData.unwrapSymmetric(symmetricKey, params).value, + ); + } + + wrapAsymmetric( + keyPair: KeyPair, + recipientsPublicKey: PublicKey, + params?: { padding?: boolean }, + ) { + return createKeyring( + _wrappedData.wrapAsymmetric(keyPair, recipientsPublicKey, params).value, + ); + } + unwrapAsymmetric(privateKey: PrivateKey, params?: { padding?: boolean }) { + return createKeyring( + _wrappedData.unwrapAsymmetric(privateKey, params).value, + ); + } + })(); +} + +export type Keyring = ReturnType; + +export function decodeKeys(input: Uint8Array): Uint8Array[] { + const keys: Uint8Array[] = []; + for (let i = 0; i < input.length / 32; ++i) { + keys.push(input.slice(i * 32, i * 32 + 32)); + } + return keys; +} + +export function encodeKeys(keys: Uint8Array[]): Uint8Array { + return Uint8Array.from( + keys.reduce((acc, key) => acc.concat(Array.from(key)), []), + ); +} diff --git a/new-deepnotes/packages/session/src/crypto/nanoid-bytes.ts b/new-deepnotes/packages/session/src/crypto/nanoid-bytes.ts new file mode 100644 index 00000000..f5392aaf --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/nanoid-bytes.ts @@ -0,0 +1,35 @@ +/** Legacy nanoid alphabet (must match `@stdlib/misc` / client). */ +const alphabet = + "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; + +const charMap = new Map(); +for (let i = 0; i < alphabet.length; i++) { + charMap.set(alphabet[i]!, i); +} + +export const nanoidLength = 21; + +export function nanoidToBytes(input: string): Uint8Array { + const bytes = new Uint8Array(16); + + let bitPos = 0; + let bytePos = 0; + + let oldBitOffset = 0; + + for (let i = 0; i < nanoidLength; i++) { + bytes[bytePos]! |= charMap.get(input[i]!)! << oldBitOffset; + + bitPos += 6; + bytePos = bitPos >>> 3; + const newBitOffset = bitPos % 8; + + if (oldBitOffset > newBitOffset) { + bytes[bytePos]! |= charMap.get(input[i]!)! >>> (8 - oldBitOffset); + } + + oldBitOffset = newBitOffset; + } + + return bytes; +} diff --git a/new-deepnotes/packages/session/src/crypto/password-hashing.ts b/new-deepnotes/packages/session/src/crypto/password-hashing.ts new file mode 100644 index 00000000..b52e5b48 --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/password-hashing.ts @@ -0,0 +1,38 @@ +import { base64ToBytes, bytesToBase64 } from "./bytes.js"; + +export function encodePasswordHash( + passwordHash: Uint8Array, + salt: Uint8Array, + timeCost: number, + memoryCost: number, +): string { + return `$argon2id$v=19$m=${String(memoryCost)},t=${String(timeCost)},p=1$${bytesToBase64(salt, { urlSafe: false })}$${bytesToBase64(passwordHash, { urlSafe: false })}`; +} + +export function getPasswordHashValues(encodedPasswordHash: string) { + const result = + /^\$(?.+?)\$v=(?\d+?)\$m=(?\d+?),t=(?\d+?),p=(?\d+?)\$(?.+?)\$(?.+?)$/.exec( + encodedPasswordHash, + ); + + if (result?.groups == null) { + throw new Error("Invalid password hash."); + } + + const g = result.groups; + + return { + algorithm: g.algorithm!, + version: parseInt(g.version!, 10), + + memoryCost: parseInt(g.memoryCost!, 10), + timeCost: parseInt(g.timeCost!, 10), + parallelism: parseInt(g.parallelism!, 10), + + saltBase64: g.saltBase64!, + hashBase64: g.hashBase64!, + + saltBytes: base64ToBytes(g.saltBase64!), + hashBytes: base64ToBytes(g.hashBase64!), + }; +} diff --git a/new-deepnotes/packages/session/src/crypto/private-key.ts b/new-deepnotes/packages/session/src/crypto/private-key.ts new file mode 100644 index 00000000..c0139e17 --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/private-key.ts @@ -0,0 +1,62 @@ +import sodium from "libsodium-wrappers-sumo"; + +import { concatUint8Arrays } from "./bytes.js"; + +import type { PublicKey } from "./public-key.js"; + +export function wrapPrivateKey(value: Uint8Array) { + return new (class PrivateKey { + get value() { + return value; + } + + encrypt( + plaintext: Uint8Array, + recipientsPublicKey: PublicKey, + sendersPublicKey: PublicKey, + params?: { padding?: boolean }, + ): Uint8Array { + if (params?.padding) { + plaintext = sodium.pad(plaintext, 8); + } + + const nonce = sodium.randombytes_buf(sodium.crypto_box_NONCEBYTES); + + const ciphertext = sodium.crypto_box_easy( + plaintext, + nonce, + recipientsPublicKey.value, + value, + ); + + return concatUint8Arrays(sendersPublicKey.value, nonce, ciphertext); + } + + decrypt(message: Uint8Array, params?: { padding?: boolean }): Uint8Array { + const sendersPublicKey = message.slice( + 0, + sodium.crypto_box_PUBLICKEYBYTES, + ); + const nonce = message.slice( + sendersPublicKey.length, + sendersPublicKey.length + sodium.crypto_box_NONCEBYTES, + ); + const ciphertext = message.slice(sendersPublicKey.length + nonce.length); + + let plaintext = sodium.crypto_box_open_easy( + ciphertext, + nonce, + sendersPublicKey, + value, + ); + + if (params?.padding) { + plaintext = sodium.unpad(plaintext, 8); + } + + return plaintext; + } + })(); +} + +export type PrivateKey = ReturnType; diff --git a/new-deepnotes/packages/session/src/crypto/private-keyring.ts b/new-deepnotes/packages/session/src/crypto/private-keyring.ts new file mode 100644 index 00000000..bab6f52b --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/private-keyring.ts @@ -0,0 +1,133 @@ +import type { KeyPair } from "./key-pair.js"; +import { wrapPrivateKey, type PrivateKey } from "./private-key.js"; +import type { PublicKey } from "./public-key.js"; +import type { SymmetricKey } from "./symmetric-key.js"; +import type { IKeyring, KeyMetadata } from "./keyring.js"; +import { createKeyring } from "./keyring.js"; +import { DataLayer } from "./wrapped-data.js"; + +export function createPrivateKeyring( + value: Uint8Array, + params?: { raw?: boolean }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + const _keyring = createKeyring(value, params); + + return new (class PrivateKeyring implements IKeyring { + keys: PrivateKey[] = []; + + get topKey(): PrivateKey { + return this.keys[0]!; + } + + addKey(key: Uint8Array) { + return createPrivateKeyring(_keyring.addKey(key).wrappedValue); + } + + constructor() { + this.keys = _keyring.keys.map((key: Uint8Array) => wrapPrivateKey(key)); + } + + get keyMetadata(): KeyMetadata[] { + return _keyring.keyMetadata; + } + get content(): Uint8Array { + return _keyring.content; + } + get wrappedValue(): Uint8Array { + return _keyring.wrappedValue; + } + get value(): Uint8Array { + return _keyring.value; + } + + get layers() { + return _keyring.layers; + } + get topLayer() { + return _keyring.topLayer; + } + hasLayer(layer: DataLayer) { + return _keyring.hasLayer(layer); + } + countLayerType(layer: DataLayer) { + return _keyring.countLayerType(layer); + } + + wrapSymmetric( + symmetricKey: SymmetricKey, + params?: { + nonce?: Uint8Array; + includeNonce?: boolean; + associatedData?: object; + padding?: boolean; + }, + ) { + return createPrivateKeyring( + _keyring.wrapSymmetric(symmetricKey, params).wrappedValue, + ); + } + unwrapSymmetric( + symmetricKey: SymmetricKey, + params?: { + nonce?: Uint8Array; + associatedData?: object; + padding?: boolean; + }, + ) { + return createPrivateKeyring( + _keyring.unwrapSymmetric(symmetricKey, params).wrappedValue, + ); + } + + wrapAsymmetric( + keyPair: KeyPair, + recipientsPublicKey: PublicKey, + params?: { padding?: boolean }, + ) { + return createPrivateKeyring( + _keyring.wrapAsymmetric(keyPair, recipientsPublicKey, params) + .wrappedValue, + ); + } + unwrapAsymmetric(privateKeyArg: PrivateKey, params?: { padding?: boolean }) { + return createPrivateKeyring( + _keyring.unwrapAsymmetric(privateKeyArg, params).wrappedValue, + ); + } + + encrypt( + plaintext: Uint8Array, + recipientsPublicKey: PublicKey, + sendersPublicKey: PublicKey, + params?: { padding?: boolean }, + ): Uint8Array { + if (_keyring.topLayer !== DataLayer.Raw) { + throw new Error("Cannot encrypt with non-raw keyring."); + } + return this.topKey.encrypt( + plaintext, + recipientsPublicKey, + sendersPublicKey, + params, + ); + } + + decrypt(ciphertext: Uint8Array, params?: { padding?: boolean }): Uint8Array { + if (_keyring.topLayer !== DataLayer.Raw) { + throw new Error("Cannot decrypt with non-raw keyring."); + } + let lastError: unknown; + for (let i = 0; i < this.keys.length; i++) { + try { + return this.keys[i]!.decrypt(ciphertext, params); + } catch (error) { + lastError = error; + } + } + throw lastError; + } + })(); +} + +export type PrivateKeyring = ReturnType; diff --git a/new-deepnotes/packages/session/src/crypto/public-key.ts b/new-deepnotes/packages/session/src/crypto/public-key.ts new file mode 100644 index 00000000..ca7dfa20 --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/public-key.ts @@ -0,0 +1,7 @@ +export interface PublicKey { + value: Uint8Array; +} + +export function wrapPublicKey(value: Uint8Array): PublicKey { + return { value }; +} diff --git a/new-deepnotes/packages/session/src/crypto/symmetric-key.ts b/new-deepnotes/packages/session/src/crypto/symmetric-key.ts new file mode 100644 index 00000000..e5be8e60 --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/symmetric-key.ts @@ -0,0 +1,97 @@ +import sodium from "libsodium-wrappers-sumo"; + +import { concatUint8Arrays } from "./bytes.js"; + +export function wrapSymmetricKey( + value = sodium.crypto_aead_xchacha20poly1305_ietf_keygen(), +) { + return new (class SymmetricKey { + get value() { + return value; + } + + encrypt( + plaintext: Uint8Array, + params?: { + nonce?: Uint8Array; + includeNonce?: boolean; + associatedData?: object; + padding?: boolean; + }, + ): Uint8Array { + if (params?.padding) { + plaintext = sodium.pad(plaintext, 8); + } + + const nonce = + params?.nonce ?? + sodium.randombytes_buf( + sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + ); + + const associatedData = JSON.stringify({ + app: "DeepNotes", + extra: params?.associatedData ?? {}, + }); + + const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt( + plaintext, + associatedData, + null, + nonce, + value, + ); + + if (params?.includeNonce === false) { + return ciphertext; + } + return concatUint8Arrays(nonce, ciphertext); + } + + decrypt( + nonceAndCiphertext: Uint8Array, + params?: { + nonce?: Uint8Array; + associatedData?: object; + padding?: boolean; + }, + ): Uint8Array { + let nonce: Uint8Array; + let ciphertext: Uint8Array; + + if (params?.nonce != null) { + nonce = params.nonce; + ciphertext = nonceAndCiphertext; + } else { + nonce = nonceAndCiphertext.slice( + 0, + sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + ); + ciphertext = nonceAndCiphertext.slice( + sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES, + ); + } + + const associatedData = JSON.stringify({ + app: "DeepNotes", + extra: params?.associatedData ?? {}, + }); + + let plaintext = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt( + null, + ciphertext, + associatedData, + nonce, + value, + ); + + if (params?.padding) { + plaintext = sodium.unpad(plaintext, 8); + } + + return plaintext; + } + })(); +} + +export type SymmetricKey = ReturnType; diff --git a/new-deepnotes/packages/session/src/crypto/symmetric-keyring.ts b/new-deepnotes/packages/session/src/crypto/symmetric-keyring.ts new file mode 100644 index 00000000..ab9c5a3b --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/symmetric-keyring.ts @@ -0,0 +1,138 @@ +import sodium from "libsodium-wrappers-sumo"; + +import type { KeyPair } from "./key-pair.js"; +import type { PrivateKey } from "./private-key.js"; +import type { PublicKey } from "./public-key.js"; +import type { SymmetricKey } from "./symmetric-key.js"; +import { wrapSymmetricKey } from "./symmetric-key.js"; +import type { IKeyring, KeyMetadata } from "./keyring.js"; +import { createKeyring } from "./keyring.js"; +import { DataLayer } from "./wrapped-data.js"; + +export function createSymmetricKeyring( + value = sodium.crypto_aead_xchacha20poly1305_ietf_keygen(), + params?: { raw?: boolean; locked?: boolean }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): any { + const _keyring = createKeyring(value, params); + + return new (class SymmetricKeyring implements IKeyring { + keys: SymmetricKey[] = []; + + get topKey(): SymmetricKey { + return this.keys[0]!; + } + + addKey(key = sodium.crypto_aead_xchacha20poly1305_ietf_keygen()) { + return createSymmetricKeyring(_keyring.addKey(key).wrappedValue); + } + + constructor() { + this.keys = _keyring.keys.map((key: Uint8Array) => + wrapSymmetricKey(key), + ); + } + + get keyMetadata(): KeyMetadata[] { + return _keyring.keyMetadata; + } + get content(): Uint8Array { + return _keyring.content; + } + get wrappedValue(): Uint8Array { + return _keyring.wrappedValue; + } + get value(): Uint8Array { + return _keyring.value; + } + + get layers() { + return _keyring.layers; + } + get topLayer() { + return _keyring.topLayer; + } + hasLayer(layer: DataLayer) { + return _keyring.hasLayer(layer); + } + countLayerType(layer: DataLayer) { + return _keyring.countLayerType(layer); + } + + wrapSymmetric( + symmetricKey: SymmetricKey, + params?: { + nonce?: Uint8Array; + includeNonce?: boolean; + associatedData?: object; + padding?: boolean; + }, + ) { + return createSymmetricKeyring( + _keyring.wrapSymmetric(symmetricKey, params).wrappedValue, + ); + } + unwrapSymmetric( + symmetricKey: SymmetricKey, + params?: { + nonce?: Uint8Array; + associatedData?: object; + padding?: boolean; + }, + ) { + return createSymmetricKeyring( + _keyring.unwrapSymmetric(symmetricKey, params).wrappedValue, + ); + } + + wrapAsymmetric( + keyPair: KeyPair, + recipientsPublicKey: PublicKey, + params?: { padding?: boolean }, + ) { + return createSymmetricKeyring( + _keyring.wrapAsymmetric(keyPair, recipientsPublicKey, params) + .wrappedValue, + ); + } + unwrapAsymmetric(privateKey: PrivateKey, params?: { padding?: boolean }) { + return createSymmetricKeyring( + _keyring.unwrapAsymmetric(privateKey, params).wrappedValue, + ); + } + + encrypt( + plaintext: Uint8Array, + params?: { + nonce?: Uint8Array; + includeNonce?: boolean; + associatedData?: object; + padding?: boolean; + }, + ): Uint8Array { + return this.topKey.encrypt(plaintext, params); + } + + decrypt( + ciphertext: Uint8Array, + params?: { + nonce?: Uint8Array; + includeNonce?: boolean; + associatedData?: object; + padding?: boolean; + }, + ): Uint8Array { + let lastError: unknown; + for (let i = 0; i < this.keys.length; i++) { + try { + return this.keys[i]!.decrypt(ciphertext, params); + } catch (error) { + lastError = error; + } + } + throw lastError; + } + })(); +} + +export type SymmetricKeyring = ReturnType; diff --git a/new-deepnotes/packages/session/src/crypto/wrapped-data.ts b/new-deepnotes/packages/session/src/crypto/wrapped-data.ts new file mode 100644 index 00000000..c217dd15 --- /dev/null +++ b/new-deepnotes/packages/session/src/crypto/wrapped-data.ts @@ -0,0 +1,158 @@ +import { pack, unpack } from "msgpackr"; + +import type { KeyPair } from "./key-pair.js"; +import type { PrivateKey } from "./private-key.js"; +import type { PublicKey } from "./public-key.js"; +import type { SymmetricKey } from "./symmetric-key.js"; + +export enum DataLayer { + Raw, + Symmetric, + Asymmetric, +} + +export interface Wrappable { + get layers(): DataLayer[]; + get topLayer(): unknown; + hasLayer(layer: DataLayer): boolean; + countLayerType(layer: DataLayer): number; + + wrapSymmetric( + symmetricKey: SymmetricKey, + params?: { + nonce?: Uint8Array; + includeNonce?: boolean; + associatedData?: object; + padding?: boolean; + }, + ): Wrappable; + unwrapSymmetric( + symmetricKey: SymmetricKey, + params?: { + nonce?: Uint8Array; + associatedData?: object; + padding?: boolean; + }, + ): Wrappable; + + wrapAsymmetric( + keyPair: KeyPair, + recipientsPublicKey: PublicKey, + params?: { padding?: boolean }, + ): Wrappable; + unwrapAsymmetric( + privateKey: PrivateKey, + params?: { padding?: boolean }, + ): Wrappable; +} + +export class WrappedData implements Wrappable { + layers: DataLayer[] = []; + metadata: Record = {}; + content: Uint8Array = new Uint8Array(); + + constructor(value: Uint8Array, params?: { raw?: boolean; metadata?: unknown }) { + this.metadata = (params?.metadata as Record) ?? {}; + + if (params?.raw) { + this.layers.push(DataLayer.Raw); + this.content = value; + } else if (value[0]! >= 3) { + const obj = unpack(value.slice(1)) as { + layers: DataLayer[]; + metadata: Record; + content: Uint8Array; + }; + this.layers = obj.layers; + this.metadata = obj.metadata; + this.content = obj.content; + } else { + let index = 0; + while (value[index] !== DataLayer.Raw) { + this.layers.push(value[index++]!); + } + this.layers.push(value[index++]!); + this.content = value.slice(index); + } + } + + get topLayer() { + return this.layers[0]; + } + + hasLayer(layer: DataLayer) { + return this.layers.includes(layer); + } + + countLayerType(layer: DataLayer) { + return this.layers.filter((l) => l === layer).length; + } + + get value() { + return new Uint8Array([ + 3, + ...pack({ + layers: this.layers, + metadata: this.metadata, + content: this.content, + }), + ]); + } + + clone() { + return new WrappedData(this.value); + } + + wrapSymmetric( + symmetricKey: SymmetricKey, + params?: { + nonce?: Uint8Array; + includeNonce?: boolean; + associatedData?: object; + padding?: boolean; + }, + ) { + const result = this.clone(); + result.layers.unshift(DataLayer.Symmetric); + result.content = symmetricKey.encrypt(result.content, params); + return result; + } + + unwrapSymmetric( + symmetricKey: SymmetricKey, + params?: { + nonce?: Uint8Array; + associatedData?: object; + padding?: boolean; + }, + ) { + if (this.topLayer !== DataLayer.Symmetric) { + throw new Error("Cannot decrypt non-symmetric keyring."); + } + const result = this.clone(); + result.layers.shift(); + result.content = symmetricKey.decrypt(result.content, params); + return result; + } + + wrapAsymmetric( + keyPair: KeyPair, + recipientsPublicKey: PublicKey, + params?: { padding?: boolean }, + ) { + const result = this.clone(); + result.layers.unshift(DataLayer.Asymmetric); + result.content = keyPair.encrypt(result.content, recipientsPublicKey, params); + return result; + } + + unwrapAsymmetric(privateKey: PrivateKey, params?: { padding?: boolean }) { + if (this.topLayer !== DataLayer.Asymmetric) { + throw new Error("Cannot decrypt non-asymmetric keyring."); + } + const result = this.clone(); + result.layers.shift(); + result.content = privateKey.decrypt(result.content, params); + return result; + } +} diff --git a/new-deepnotes/packages/session/src/device-hash.ts b/new-deepnotes/packages/session/src/device-hash.ts index 2c281d0d..b0a7cdf4 100644 --- a/new-deepnotes/packages/session/src/device-hash.ts +++ b/new-deepnotes/packages/session/src/device-hash.ts @@ -1,4 +1,4 @@ -import { nanoidToBytes } from "@stdlib/misc"; +import { nanoidToBytes } from "./crypto/nanoid-bytes.js"; import sodium from "libsodium-wrappers-sumo"; export function getDeviceHash(input: { diff --git a/new-deepnotes/packages/session/src/legacy-crypto.ts b/new-deepnotes/packages/session/src/legacy-crypto.ts index 431c0de6..7a50feb6 100644 --- a/new-deepnotes/packages/session/src/legacy-crypto.ts +++ b/new-deepnotes/packages/session/src/legacy-crypto.ts @@ -1,13 +1,13 @@ -import { base64ToBytes } from "@stdlib/base64"; -import { - cryptoJsWordArrayToUint8Array, - getPasswordHashValues, - wrapSymmetricKey, -} from "@stdlib/crypto"; -import { bytesToText, concatUint8Arrays } from "@stdlib/misc"; import CryptoJS from "crypto-js"; import sodium from "libsodium-wrappers-sumo"; import { pack, unpack } from "msgpackr"; +import { + base64ToBytes, + bytesToText, + concatUint8Arrays, +} from "./crypto/bytes.js"; +import { cryptoJsWordArrayToUint8Array } from "./crypto/crypto-js-wordarray.js"; +import { wrapSymmetricKey } from "./crypto/symmetric-key.js"; export async function ensureSodiumReady(): Promise { await sodium.ready; @@ -49,7 +49,7 @@ export function decryptUserRehashedLoginHash( ); } -export { getPasswordHashValues }; +export { getPasswordHashValues } from "./crypto/password-hashing.js"; export function decryptUserAuthenticatorSecret( userEncryptedAuthenticatorSecret: Uint8Array, diff --git a/new-deepnotes/packages/session/src/login.ts b/new-deepnotes/packages/session/src/login.ts index e5a56c21..745ad323 100644 --- a/new-deepnotes/packages/session/src/login.ts +++ b/new-deepnotes/packages/session/src/login.ts @@ -3,7 +3,7 @@ import { and, eq, gt, or } from "drizzle-orm"; import { createPrivateKeyring, createSymmetricKeyring, -} from "@stdlib/crypto"; +} from "./crypto/index.js"; import sodium from "libsodium-wrappers-sumo"; import { nanoid } from "nanoid"; diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml index 8c9e1769..d4fe7a16 100644 --- a/new-deepnotes/pnpm-lock.yaml +++ b/new-deepnotes/pnpm-lock.yaml @@ -5,8 +5,6 @@ settings: excludeLinksFromLockfile: false overrides: - '@stdlib/base64': link:../packages/@stdlib/base64 - '@stdlib/misc': link:../packages/@stdlib/misc libsodium-wrappers-sumo: ^0.8.0 importers: @@ -125,15 +123,6 @@ importers: '@deepnotes/db': specifier: workspace:* version: link:../db - '@stdlib/base64': - specifier: link:../../../packages/@stdlib/base64 - version: link:../../../packages/@stdlib/base64 - '@stdlib/crypto': - specifier: link:../../../packages/@stdlib/crypto - version: link:../../../packages/@stdlib/crypto - '@stdlib/misc': - specifier: link:../../../packages/@stdlib/misc - version: link:../../../packages/@stdlib/misc crypto-js: specifier: ^4.2.0 version: 4.2.0 diff --git a/new-deepnotes/template.env b/new-deepnotes/template.env index 7060f0d9..0fc72b8f 100644 --- a/new-deepnotes/template.env +++ b/new-deepnotes/template.env @@ -19,7 +19,3 @@ REDIS_URL=redis://localhost:6380 # COOKIE_DOMAIN= # optional; legacy used HOST # DEV=true # local HTTP without Secure cookies # STRIPE_WEBHOOK_SECRET= - -# Before `pnpm build` / CI in this repo: build parent `@stdlib/crypto` once (linked by @deepnotes/session): -# pnpm install --filter @stdlib/crypto... && pnpm --filter @stdlib/crypto run build -# (from the DeepNotes monorepo root, not only new-deepnotes). From 6e476c29627f8b419f217f0b251b9f1c0fcc3694 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 23:12:29 -0300 Subject: [PATCH 025/243] refactor: rename file --- new-deepnotes/packages/session/src/crypto/index.ts | 4 ++-- .../{legacy-crypto.ts => crypto/session-crypto.ts} | 14 ++++++++++---- new-deepnotes/packages/session/src/login.ts | 2 +- new-deepnotes/packages/session/src/refresh.ts | 2 +- new-deepnotes/packages/session/src/two-factor.ts | 2 +- 5 files changed, 15 insertions(+), 9 deletions(-) rename new-deepnotes/packages/session/src/{legacy-crypto.ts => crypto/session-crypto.ts} (86%) diff --git a/new-deepnotes/packages/session/src/crypto/index.ts b/new-deepnotes/packages/session/src/crypto/index.ts index f3a1e610..531ba9e3 100644 --- a/new-deepnotes/packages/session/src/crypto/index.ts +++ b/new-deepnotes/packages/session/src/crypto/index.ts @@ -1,6 +1,6 @@ /** - * In-repo crypto primitives required for session/login parity with stored user blobs. - * (Selective port of former `@stdlib/crypto` keyring + hashing wire shapes — no workspace link.) + * Keyring + PHC helpers for stored user blobs. Higher-level login glue lives in + * `./session-crypto.ts` (password derivation, encrypted user fields, recovery codes). */ export { createPrivateKeyring } from "./private-keyring.js"; export type { PrivateKeyring } from "./private-keyring.js"; diff --git a/new-deepnotes/packages/session/src/legacy-crypto.ts b/new-deepnotes/packages/session/src/crypto/session-crypto.ts similarity index 86% rename from new-deepnotes/packages/session/src/legacy-crypto.ts rename to new-deepnotes/packages/session/src/crypto/session-crypto.ts index 7a50feb6..be2a9f8b 100644 --- a/new-deepnotes/packages/session/src/legacy-crypto.ts +++ b/new-deepnotes/packages/session/src/crypto/session-crypto.ts @@ -1,13 +1,19 @@ +/** + * Session/login helpers: password derivation, server-encrypted user fields, + * and recovery codes. Uses primitives in this folder (`wrapSymmetricKey`, etc.) + * so stored Postgres blobs remain compatible. + */ import CryptoJS from "crypto-js"; import sodium from "libsodium-wrappers-sumo"; import { pack, unpack } from "msgpackr"; + import { base64ToBytes, bytesToText, concatUint8Arrays, -} from "./crypto/bytes.js"; -import { cryptoJsWordArrayToUint8Array } from "./crypto/crypto-js-wordarray.js"; -import { wrapSymmetricKey } from "./crypto/symmetric-key.js"; +} from "./bytes.js"; +import { cryptoJsWordArrayToUint8Array } from "./crypto-js-wordarray.js"; +import { wrapSymmetricKey } from "./symmetric-key.js"; export async function ensureSodiumReady(): Promise { await sodium.ready; @@ -49,7 +55,7 @@ export function decryptUserRehashedLoginHash( ); } -export { getPasswordHashValues } from "./crypto/password-hashing.js"; +export { getPasswordHashValues } from "./password-hashing.js"; export function decryptUserAuthenticatorSecret( userEncryptedAuthenticatorSecret: Uint8Array, diff --git a/new-deepnotes/packages/session/src/login.ts b/new-deepnotes/packages/session/src/login.ts index 745ad323..96de5d91 100644 --- a/new-deepnotes/packages/session/src/login.ts +++ b/new-deepnotes/packages/session/src/login.ts @@ -19,7 +19,7 @@ import { derivePasswordValues, ensureSodiumReady, getPasswordHashValues, -} from "./legacy-crypto.js"; +} from "./crypto/session-crypto.js"; import { createSessionRowAndCookies } from "./session-lifecycle.js"; import { assertTwoFactorOk } from "./two-factor.js"; diff --git a/new-deepnotes/packages/session/src/refresh.ts b/new-deepnotes/packages/session/src/refresh.ts index e97a5ef9..75f351e3 100644 --- a/new-deepnotes/packages/session/src/refresh.ts +++ b/new-deepnotes/packages/session/src/refresh.ts @@ -10,7 +10,7 @@ import { decodeRefreshTokenUnsafe, verifyRefreshToken, } from "./jwt.js"; -import { ensureSodiumReady } from "./legacy-crypto.js"; +import { ensureSodiumReady } from "./crypto/session-crypto.js"; import { rotateSessionRowAndCookies } from "./session-lifecycle.js"; export async function performSessionRefresh(input: { diff --git a/new-deepnotes/packages/session/src/two-factor.ts b/new-deepnotes/packages/session/src/two-factor.ts index 8fcb9203..6f1a62c8 100644 --- a/new-deepnotes/packages/session/src/two-factor.ts +++ b/new-deepnotes/packages/session/src/two-factor.ts @@ -10,7 +10,7 @@ import { decryptUserAuthenticatorSecret, encryptRecoveryCodes, verifyRecoveryCode, -} from "./legacy-crypto.js"; +} from "./crypto/session-crypto.js"; type User2faRow = { id: string; From c3ba952563653012a7ed4a903bba8a2f86c4fc9f Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 23:13:22 -0300 Subject: [PATCH 026/243] refactor: removing unecessary re-export --- new-deepnotes/packages/session/src/crypto/session-crypto.ts | 2 -- new-deepnotes/packages/session/src/login.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/new-deepnotes/packages/session/src/crypto/session-crypto.ts b/new-deepnotes/packages/session/src/crypto/session-crypto.ts index be2a9f8b..b7c9e5b2 100644 --- a/new-deepnotes/packages/session/src/crypto/session-crypto.ts +++ b/new-deepnotes/packages/session/src/crypto/session-crypto.ts @@ -55,8 +55,6 @@ export function decryptUserRehashedLoginHash( ); } -export { getPasswordHashValues } from "./password-hashing.js"; - export function decryptUserAuthenticatorSecret( userEncryptedAuthenticatorSecret: Uint8Array, encryptionKeyB64: string, diff --git a/new-deepnotes/packages/session/src/login.ts b/new-deepnotes/packages/session/src/login.ts index 96de5d91..2b08e0f5 100644 --- a/new-deepnotes/packages/session/src/login.ts +++ b/new-deepnotes/packages/session/src/login.ts @@ -3,6 +3,7 @@ import { and, eq, gt, or } from "drizzle-orm"; import { createPrivateKeyring, createSymmetricKeyring, + getPasswordHashValues, } from "./crypto/index.js"; import sodium from "libsodium-wrappers-sumo"; import { nanoid } from "nanoid"; @@ -18,7 +19,6 @@ import { decryptUserRehashedLoginHash, derivePasswordValues, ensureSodiumReady, - getPasswordHashValues, } from "./crypto/session-crypto.js"; import { createSessionRowAndCookies } from "./session-lifecycle.js"; import { assertTwoFactorOk } from "./two-factor.js"; From 916133b9e6cdf9ae3b40706e76d308eddbcd2e78 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 23:18:09 -0300 Subject: [PATCH 027/243] docs: improve restart plan --- docs/RESTART_PLAN.md | 49 +++++++++++++++++++++++++++++----- new-deepnotes/PLAN_PROGRESS.md | 45 +++++++++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 8 deletions(-) diff --git a/docs/RESTART_PLAN.md b/docs/RESTART_PLAN.md index 5dac9eaf..0c3e1ff6 100644 --- a/docs/RESTART_PLAN.md +++ b/docs/RESTART_PLAN.md @@ -4,7 +4,7 @@ This document is a **technical and delivery plan** for recreating DeepNotes in a **Audience:** engineers and technical leads who will scope work, own compatibility, and sequence migration. -**Non-goals here:** detailed UI redesign, pricing, or product roadmap—only what is required to **restart the implementation safely**. +**Non-goals here:** detailed visual redesign, pricing, or product roadmap. **In scope:** how the **new** SPA is **structured**, **decoupled** from the server, and **tested** so UI work does not recreate the legacy coupling and manual-only verification story. --- @@ -93,6 +93,18 @@ The existing stack is already described accurately in [TECHNICAL_OVERVIEW.md](./ - **Migrations:** without a **repeatable, ordered migration** story, any new service that shares the same DB is gambling on one-off DBA steps. - **Multi-service deployment:** **five** long-running entrypoints (client builds aside) increase coordination cost; a restart is an opportunity to **document** and eventually **consolidate** (only after contracts are clear). +### 3.5 Frontend and UI (legacy): coupling, scale, and almost no automated tests + +The restart plan’s **backend and contracts** story is necessary but not sufficient: most user-visible risk and churn lives in **`apps/client`** (~400+ files under `src/` alone), and that surface was **not** treated as a first-class test target. + +- **Hard type coupling to the server:** the SPA imports **`AppRouter`** from `@deepnotes/app-server` (`src/code/trpc.ts`) and **deep paths** into the server for **WebSocket message types** and helpers (e.g. `src/code/areas/api-interface/**` importing `@deepnotes/app-server/src/websocket/...`). Any server refactor becomes a **client compile** problem; the new app must depend only on **published HTTP/OpenAPI** (and documented WS appendices), not on server source trees. +- **Framework coupling and implicit wiring:** **Quasar** + **unplugin-auto-import** registers **`trpcClient`**, **`internals`**, **Pinia stores**, and **router helpers** as globals (`quasar.config.js`). That speeds authoring but **obscures dependency edges** and encourages large “god” objects (e.g. **`DeepNotesInternals`** in `src/boot/internals.universal.ts` tying storage, crypto, router, realtime, Tiptap, and `Pages`). +- **Feature logic mixed with UI shells:** domain-heavy classes (e.g. **`Pages`** in `src/code/pages/pages.ts` using **`trpcClient`**) sit beside Vue layouts under `src/layouts/` and `src/pages/`, without a stable **inner boundary** between “call API / apply crypto” and “render Vue”. +- **Sparse automated UI tests:** there are **no** `*.test.*` / `*.spec.*` files under `apps/client` in the current tree; verification for UI and integration flows is largely **manual**. The greenfield **`@deepnotes/web`** app currently wires **`test`** to a **no-op** exit in `package.json`—so CI can be “green” while the client layer has **zero** regression signal. +- **Build and typecheck split:** the legacy client sits **outside** the root TypeScript project references (see §3.2), so IDE and CI feedback for UI code is **weaker** than for `packages/*`. + +**Implication for the new repo:** treat **frontend architecture and testing** as a **parallel delivery track** (package layout, API client generation, Vitest + DOM environment, optional E2E)—not an afterthought folded only into “Phase 4 — client MVP.” + --- ## 4. Compatibility and migration surface @@ -151,6 +163,7 @@ The restart is **not** a permission to **copy-paste** the legacy layout into new | **Decoupling** | Prefer **feature-oriented** or **vertical slices** (auth, pages, groups, billing) with **narrow imports** between packages: **HTTP handlers** depend on **application services** and **typed DTOs**, not on each other’s internals. Shared **Drizzle schema** and **OpenAPI** types live in dedicated packages; avoid cycles between “everything imports `@stdlib/data`.” | | **Services without repository pattern** | Use **application services** (or use-cases) that orchestrate validation, Redis, and **Drizzle queries**. **Do not** introduce a generic **repository** layer whose main job is wrapping CRUD—you have **one** database (Postgres). Where query logic repeats, extract **small typed query helpers** or **SQL modules** next to the feature, not a parallel “repository” hierarchy. | | **Thorough testing** | **Unit tests** for pure logic (crypto, mapping, auth helpers). **Integration tests** against a **real Postgres** (and Redis where behavior matters) for migrations, constraints, and route-level flows. Coverage expectations are highest for **auth**, **crypto**, **payments**, and **data migrations**—see §5.7. | +| **Frontend boundaries** | The browser app **never** imports server apps or Drizzle; it depends on a **small typed HTTP client** (from OpenAPI or shared Zod IO types) and optional **client-only** packages (crypto, formatting). **Vue** components stay thin; **composables** and **feature modules** own orchestration. | ### 5.1–5.6 Target components (numbered) @@ -168,8 +181,10 @@ The restart is **not** a permission to **copy-paste** the legacy layout into new 5. **New client application** - **Vite 6+** + **Vue 3.5+** as a standard SPA (using `vite-ssg` for marketing page SEO). **Nuxt SSR is explicitly rejected** because DeepNotes is end-to-end encrypted; the server cannot decrypt user content to render it for SEO anyway. - - **Feature-based folder structure** (e.g., `src/features/auth`, `src/features/editor`) to co-locate components, API clients, and tests for better maintainability. - - `fetch` + **openapi-typescript** (or **hey-api** client) generated from the spec—**no** `trpc` client, **no** `superjson`, **no** Quasar. + - **Feature-based folder structure** (e.g., `src/features/auth`, `src/features/editor`) to co-locate components, composables, and **tests**; keep **shared** presentational pieces under something like `src/shared/ui` (tokens, primitives) so features do not copy-paste styles. + - **Decouple transport from UI:** a dedicated **API surface** (package or `src/api/`) that wraps `fetch` with credentials, base URL, and error mapping; types from **openapi-typescript** / **hey-api** or Zod schemas exported from `@deepnotes/api` **without** pulling Worker or DB code into the bundle. **Do not** import `@deepnotes/app-server` or any `drizzle-*` module from the web app. + - **State:** prefer **explicit composables** and small Pinia stores (if used) over a single mega-`internals` object; inject test doubles at boundaries. + - `fetch` + generated client—**no** `trpc` client, **no** `superjson`, **no** Quasar. - **Tiptap + Yjs** for the editor if you want to cap risk; **collab-server** either forked to strip rotation or rewritten against the same Yjs wire. - **Capacitor** for mobile and **Tauri v2** (or Electron) for desktop after the web app is solid. Decoupling the UI from the native wrappers avoids the heavy Quasar build matrix. @@ -201,6 +216,20 @@ Running **full Drizzle migrations from an empty database for every test** is cor - **Parallelism:** if tests run **in parallel**, each worker can own a **template clone naming prefix** or use **one DB per worker** instead of per test—tune for speed vs isolation. - **Testcontainers** (or a single long-lived local Postgres) remain valid **fallbacks** when CI cannot expose Postgres; templates are the **preferred** strategy **when Postgres is already there**. +### 5.8 Frontend: testing layers, tooling, and CI + +Backend integration tests do **not** replace **UI** and **end-to-end** confidence. Plan three layers from the first meaningful UI commit: + +| Layer | Purpose | Typical stack | +|-------|---------|----------------| +| **Unit / component** | Presentational components, composables, parsers, mappers | **Vitest** with **`environment: 'jsdom'`** or **`happy-dom`** (install the DOM lib explicitly per Vitest docs), **`@vue/test-utils`** for `mount`/`shallowMount`, same **Vite** pipeline as the app (`vitest/config` + `@vitejs/plugin-vue`). Prefer **`@vitest-environment jsdom`** on specific files if only a subset needs DOM. | +| **API / contract** | Client `fetch` wrapper respects paths, cookies, error shapes | Tests against **handlers** (e.g. **MSW** 2.x) or a short-lived **local Worker**; fixtures generated from **OpenAPI** examples so UI does not depend on a running DB for every run. | +| **E2E smoke** | Cookie auth, navigation, “open page / type / sync” happy paths | **Playwright** (or Cypress) against **preview** or **docker-compose** stack; keep the suite **small** and fast—defer broad visual regression unless product asks for it. | + +**CI:** `@deepnotes/web` (or equivalent) must run **real** `vitest` (and later Playwright) in **Turbo** `test`, not a **no-op** script—otherwise UI refactors ship with **zero** automated signal (the current legacy client pattern). + +**Organization:** co-locate `*.spec.ts` / `*.test.ts` next to features or under `src/__tests__/` consistently; forbid **new** deep imports from server packages into the web bundle (enforce with **ESLint** `import/no-restricted-paths` or **dependency-cruiser** if needed). + --- ## 6. Phased work plan (recommended order) @@ -211,6 +240,7 @@ Running **full Drizzle migrations from an empty database for every test** is cor - Transcribe `postgres-init.sql` into a **Drizzle schema** and generate **migration 0001** (or squash later—goal is a **repeatable** chain). - Document **cookie names**, **JWT** claims, and **CORS** origins. - List which **`@deepnotes/*`** forks the **new** client can avoid entirely. +- **Optional but valuable:** sketch **`apps/web`** (or `packages/web`) **folder conventions** and **forbidden imports** (no server, no Drizzle) in a short `README` or ADR so the first feature PRs do not invent incompatible layouts. **Exit:** OpenAPI v0 + Drizzle schema in source control; feature checklist derived from the old tRPC tree. @@ -226,6 +256,7 @@ Only if you still touch the old monorepo: remove default **`--inspect-brk`**, ad - **Docker compose:** **Postgres** + **Redis** (not KeyDB). New env file with **`REDIS_URL`**-style settings. - **Cloudflare:** `wrangler.toml` (or Wrangler JSON), **Hyperdrive** config pointing at the same Postgres URL used locally (or a branch DB), **Pages** project for the client build output; document preview vs production env vars. - **CI** green: lint, typecheck, `drizzle-kit check`, unit smoke, and **Postgres-backed** integration tests where a **GitHub Actions `services: postgres`** (or equivalent) supplies a DB user with **`CREATEDB`** for **template clones** (§5.7); optional **deploy** job to a **Cloudflare preview** environment. +- **Client CI is real:** replace placeholder **`test`** scripts on **`@deepnotes/web`** with **Vitest** (see §5.8) so the SPA is typechecked and unit-tested in the same pipeline as the API packages. ### Phase 3 — backend features on REST + Drizzle @@ -235,7 +266,8 @@ Only if you still touch the old monorepo: remove default **`--inspect-brk`**, ad ### Phase 4 — new client MVP -- Feature slice: **auth** → **page list** → **single page** → **Yjs collab** → **groups** subset. +- **Foundation (before heavy screens):** OpenAPI-driven **typed client** (or hand wrapper + generated types), **`src/features/*`** layout, and **Vitest + jsdom/happy-dom** running in CI (§5.8). +- Feature slice: **auth** → **page list** → **single page** → **Yjs collab** → **groups** subset—each slice ships with **at least** composable or API-layer tests where logic is non-trivial; auth and session flows additionally covered by **E2E smoke** when cookies and redirects are involved. - Reuse or port **`@stdlib/crypto`**, `@deeplib/misc` where domain-stable; delete dead code as you go. - **Electron** and **Capacitor** after web parity (they multiply CI cost). @@ -260,6 +292,8 @@ Only if you still touch the old monorepo: remove default **`--inspect-brk`**, ad | Mobile and desktop matrices | Defer **Capacitor/Tauri** matrix; get **web** SPA (with `vite-ssg` for SEO) solid first. | | **Worker** CPU time and **DO** costs under collab load | Load-test **Durable Object** fan-out and Hyperdrive early; model worst-case concurrent pages and websocket churn. | | **Framework** assumes full **Node** | Prefer **Hono** on Workers; gate **Fastify** (or heavy native deps) behind a verified Workers profile or a non-CF deployment path. | +| **UI regressions** and **tight UI↔server coupling** repeat | **No** server imports in the web package; **component + contract tests** from the first auth UI; **small Playwright** suite on preview; optional **Storybook** only if the team will maintain it. | +| **God-object state** (`internals`-style) | Cap composable surface area; document **dependency injection** patterns for crypto and API clients in tests. | --- @@ -274,6 +308,8 @@ Only if you still touch the old monorepo: remove default **`--inspect-brk`**, ad - [ ] **No tRPC** and **no** `superjson` in the new default stack. **No** RevenueCat. **Key rotation** code paths are **absent** and the team signed off on **IAP** / **Stripe** user handling. - [ ] **Zero** undocumented framework forks in the new default client, or a short exception list with an owner. - [ ] **Cloudflare:** API + static/SSG deploy documented; **Hyperdrive** + external **Postgres** + **Redis** proven in staging; **collab/realtime** path chosen (**DO** vs separate service) and load-tested. +- [ ] **Web app:** **Vitest** (DOM environment) runs in CI on every change to `@deepnotes/web`; **no** dependency from web source onto **app-server** / **Drizzle** packages. +- [ ] **E2E:** at least one **automated** smoke path for **login/session cookies** (or equivalent) against a **preview** or **compose** stack before declaring client MVP “done.” --- @@ -284,11 +320,12 @@ Only if you still touch the old monorepo: remove default **`--inspect-brk`**, ad - `template.env` — **legacy** env names; the new app introduces **`REDIS_*`**, drops **KeyDB-** specific names, and does not add **RevenueCat** variables. - `apps/app-server/src/trpc/router.ts` and `apps/app-server/src/trpc/api/**` — **legacy** procedure checklist for feature parity, not a wire spec. - `apps/app-server/src/websocket/**` — **legacy** WS; **user/group rotate-keys** are **out of scope** for the new product. -- `postgres-init.sql` — import baseline for **Drizzle** `schema.ts`. +- `apps/client/**` — **legacy** Quasar/Vue SPA (**tRPC** + **`@deepnotes/app-server`** imports, large `internals` / `Pages` classes); use as **UX and behavior** reference, not as a layout or testing model for the new app. +- `postgres-init.sql` — import baseline for **Drizzle** `schema.ts`. - Up-to-date **Drizzle** (schema + migrations) documentation for the version you pin (e.g. via the Context7 MCP in Cursor if available). --- ## 10. Summary -This restart is **intentionally not tRPC- or KeyDB-compatible** on the wire. Success depends on **OpenAPI + REST**, **Drizzle** migrations, **vanilla Redis**, a **simpler crypto story** (no key rotation, no **RevenueCat**), and a **coordinated** rollout of the new **HTTP** stack with **realtime**/**collab** and clients that no longer expect `/trpc` or scheduled re-keying. **Production** targets **Cloudflare** (**Workers** + **Pages**, **Hyperdrive** to Postgres, external **Redis**, **Durable Objects** where stateful WebSockets need them). Treat the old monorepo as a **behavioral reference** and a **one-time** source of schema and test vectors—**reorganize** into **decoupled** features and **services** (no **repository** pattern), prove behavior with **thorough tests** including **Postgres template–based** integration isolation (§5.7), then retire the legacy repo when parity and data checks are proven. +This restart is **intentionally not tRPC- or KeyDB-compatible** on the wire. Success depends on **OpenAPI + REST**, **Drizzle** migrations, **vanilla Redis**, a **simpler crypto story** (no key rotation, no **RevenueCat**), and a **coordinated** rollout of the new **HTTP** stack with **realtime**/**collab** and clients that no longer expect `/trpc` or scheduled re-keying. **Production** targets **Cloudflare** (**Workers** + **Pages**, **Hyperdrive** to Postgres, external **Redis**, **Durable Objects** where stateful WebSockets need them). Treat the old monorepo as a **behavioral reference** and a **one-time** source of schema and test vectors—**reorganize** into **decoupled** features and **services** (no **repository** pattern), prove behavior with **thorough tests** including **Postgres template–based** integration isolation (§5.7), and treat the **SPA** as its own product: **typed HTTP client**, **feature-based UI structure**, and **Vitest + (optional) E2E** in CI (§5.8)—not a thin shell with manual-only verification. Retire the legacy repo when parity, **UI** smoke, and data checks are proven. diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index a8c549c8..bdfe59f8 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -12,9 +12,9 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ |-------|--------|--------| | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | -| **2** — Repo bootstrap | **Mostly done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). Optional: Wrangler deploy job. | +| **2** — Repo bootstrap | **Mostly done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). Optional: Wrangler deploy job. **Gap:** `apps/web` tests still no-op—see Phase 2 checklist + [Frontend / UI track](#frontend--ui-track). | | **3** — REST + Drizzle features | **In progress** | `POST /api/sessions/login|refresh|logout` wired via `@deepnotes/session` (Drizzle + legacy crypto + `jose` JWT + cookies). `POST /api/sessions/demo` still `501`. Next: Redis-backed login rate limits, `start-demo` / registration, `GET /api/users/me`. | -| **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. | +| **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, Vitest+DOM in CI, small E2E smoke—see [Frontend / UI track](#frontend--ui-track) (not deferred to “when MVP is done”). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | --- @@ -40,6 +40,17 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ --- +## Phase 4 checklist (client MVP) + +- [ ] **Tooling:** Vitest + `jsdom` or `happy-dom` + `@vue/test-utils`; `@vitejs/plugin-vue` in Vitest config (same as [Frontend / UI track](#frontend--ui-track)). +- [ ] **API client:** consume **OpenAPI** (generated types + `fetch`, or hey-api) from `@deepnotes/api` / published spec—**no** workspace dependency on Worker or DB packages from web source. +- [ ] **Routing + auth UI:** login / refresh / logout / 2FA flows aligned with [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md); composable or component tests + **E2E smoke** for cookie session. +- [ ] **Pages:** list → open editor shell → integrate **Yjs** / collab when API is ready. +- [ ] **Groups** subset and notifications UX as mapped from [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). +- [ ] **Native wrappers** (Capacitor / Tauri): only after web MVP and CI stable. + +--- + ## Phase 2 checklist (bootstrap) - [x] pnpm + Turborepo 2, Node 22+. @@ -48,6 +59,34 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] Document **Pages** / preview vs production env vars; optional deploy job to CF preview → [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). - [x] CI: lint, typecheck, tests, `drizzle-kit check`, build (Postgres service present for future migrate/tests). - [x] CI: Postgres role with **CREATEDB** + **template DB** integration tests (RESTART_PLAN §5.7) — `DATABASE_ADMIN_URL` + `src/template-db.test.ts`. +- [ ] **Web package tests are real:** `apps/web` currently uses a **no-op** `test` script; replace with **Vitest** + `jsdom` or `happy-dom` + `@vue/test-utils` and wire into root `pnpm test` / CI (RESTART_PLAN §5.8). + +--- + +## Frontend / UI track + +Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patterns: **tRPC + `AppRouter`**, **deep `@deepnotes/app-server` imports** for WS types, **auto-imported globals** (`trpcClient`, `internals`, stores), **~400+** mixed layout/code files, and **no** `*.test.*` / `*.spec.*` under the legacy client tree. + +### Decoupling and layout (`@deepnotes/web`) + +- [ ] **Forbidden imports:** no `@deepnotes/api-worker`, `@deepnotes/db`, or Drizzle from `apps/web` source; HTTP only via a small **API layer** (generated OpenAPI client or `fetch` + shared types from `@deepnotes/api`). +- [ ] **Feature folders:** e.g. `src/features/auth`, `src/features/pages`, `src/shared/ui`—document the convention in `apps/web/README.md` (or link from repo root README). +- [ ] **Thin Vue, fat composables:** session and crypto orchestration live in testable modules, not only in `.vue` files. + +### Testing (see RESTART_PLAN §5.8) + +- [ ] **Vitest** in `apps/web` with DOM environment and `@vitejs/plugin-vue` aligned with Vite 6. +- [ ] **Component or composable tests** for the first **auth** / session flows (forms, validation, error mapping from API). +- [ ] **Contract tests** for the fetch wrapper (MSW or recorded OpenAPI fixtures)—optional until multiple features consume the API. +- [ ] **E2E smoke** (Playwright recommended): login or session refresh with **httpOnly cookies** against **local compose** or **Cloudflare preview**—add CI job when stable enough (can start `manual`/`workflow_dispatch` if cost is a concern). + +### Progress vs legacy (reference only) + +| Legacy (`apps/client`) | New (`new-deepnotes/apps/web`) | +|------------------------|--------------------------------| +| Quasar + Vite 2, 4GB heap builds | Vite 6 + Vue 3.5, minimal app shell today | +| Imports `AppRouter`, server websocket paths | Must use **OpenAPI** + documented WS only | +| No automated UI tests | **To do:** real `test` script + CI | --- @@ -62,6 +101,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. +- [ ] Web: Vitest + DOM env in CI; no server/db imports from web source; E2E smoke for session cookies (RESTART_PLAN §8 extended items). --- @@ -69,6 +109,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | Date | Change | |------|--------| +| 2026-04-26 | Docs: [docs/RESTART_PLAN.md](../docs/RESTART_PLAN.md) §3.5 legacy frontend pain points, §5.8 frontend testing/CI, phased updates; this file: **Frontend / UI track** + Phase 2/4 notes on real web tests. | | 2026-04-26 | Phase 3: `@deepnotes/session` (login/refresh/logout + 2FA TOTP/recovery), api-worker Hyperdrive + dynamic import for Workers bundle; OpenAPI 200/401/503 for session routes; demo remains `501`; session crypto vendored in-package (no parent `@stdlib` links); `libsodium-wrappers-sumo@^0.8` override for Wrangler. | | 2026-04-26 | Phase 3 start: OpenAPI + Zod for `POST /api/sessions/login|refresh|logout|demo`; api-worker `501` stubs; Phase 0 marked done in snapshot. | | 2026-04-26 | Phase 0 docs (TRPC_REST_MAP, AUTH_AND_CORS, CLIENT_FORKS); Phase 2 deploy doc; Drizzle legacy baseline from `postgres-init.sql`; Vitest template-DB integration test + CI `DATABASE_ADMIN_URL`. | From 0ff52c298a40a3c0e98d03a8fb199dad6d7e0424 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 23:28:09 -0300 Subject: [PATCH 028/243] feat(new-deepnotes): Redis, users API, and encrypted email metadata --- new-deepnotes/PLAN_PROGRESS.md | 10 +- new-deepnotes/apps/api-worker/package.json | 1 + .../apps/api-worker/src/index.test.ts | 11 +- new-deepnotes/apps/api-worker/src/index.ts | 113 +++++++- .../apps/api-worker/src/redis-port.ts | 29 +++ .../apps/api-worker/src/session-env.ts | 7 + new-deepnotes/packages/api/src/index.ts | 5 + .../packages/api/src/openapi.test.ts | 1 + new-deepnotes/packages/api/src/openapi.ts | 58 ++++- .../packages/api/src/schemas/sessions.ts | 54 +++- .../packages/api/src/schemas/users.ts | 15 ++ new-deepnotes/packages/session/package.json | 2 +- .../session/src/encrypt-user-email.ts | 22 ++ new-deepnotes/packages/session/src/env.ts | 2 + new-deepnotes/packages/session/src/index.ts | 9 + .../session/src/login-rate-limit.test.ts | 74 ++++++ .../packages/session/src/login-rate-limit.ts | 58 +++++ new-deepnotes/packages/session/src/login.ts | 44 ++++ .../packages/session/src/start-demo.ts | 241 ++++++++++++++++++ .../packages/session/src/two-factor.ts | 29 +++ new-deepnotes/packages/session/src/user-me.ts | 56 ++++ new-deepnotes/pnpm-lock.yaml | 15 ++ new-deepnotes/template.env | 4 + 23 files changed, 822 insertions(+), 38 deletions(-) create mode 100644 new-deepnotes/apps/api-worker/src/redis-port.ts create mode 100644 new-deepnotes/packages/api/src/schemas/users.ts create mode 100644 new-deepnotes/packages/session/src/encrypt-user-email.ts create mode 100644 new-deepnotes/packages/session/src/login-rate-limit.test.ts create mode 100644 new-deepnotes/packages/session/src/login-rate-limit.ts create mode 100644 new-deepnotes/packages/session/src/start-demo.ts create mode 100644 new-deepnotes/packages/session/src/user-me.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index bdfe59f8..d62cb1a3 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Mostly done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). Optional: Wrangler deploy job. **Gap:** `apps/web` tests still no-op—see Phase 2 checklist + [Frontend / UI track](#frontend--ui-track). | -| **3** — REST + Drizzle features | **In progress** | `POST /api/sessions/login|refresh|logout` wired via `@deepnotes/session` (Drizzle + legacy crypto + `jose` JWT + cookies). `POST /api/sessions/demo` still `501`. Next: Redis-backed login rate limits, `start-demo` / registration, `GET /api/users/me`. | +| **3** — REST + Drizzle features | **In progress** | `POST /api/sessions/login|refresh|logout` + **`POST /api/sessions/demo`** + **`GET /api/users/me`** via `@deepnotes/session`. Optional **Upstash** (`UPSTASH_REDIS_REST_*`) wires legacy-style **failed-login rate limits**; without it, limits are skipped (local dev). **New secret:** `USER_EMAIL_ENCRYPTION_KEY` (legacy email encryption). Next: `POST /api/users` registration, pages/groups CRUD, Stripe. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, Vitest+DOM in CI, small E2E smoke—see [Frontend / UI track](#frontend--ui-track) (not deferred to “when MVP is done”). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -31,11 +31,12 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ ## Phase 3 checklist (REST + Drizzle) -- [x] Document **sessions** REST paths + request schemas in OpenAPI; worker returns **501** until handlers exist. +- [x] Document **sessions** REST paths + request schemas in OpenAPI; demo + `users/me` contracts updated. - [x] Implement **sessions.login** / refresh / logout against Drizzle + legacy crypto semantics (JWT via `jose`; **Redis** rate limits not wired yet—parity with legacy `login` lockouts). -- [ ] Implement **sessions.start-demo** (registration path) + **Redis** for failed-login / optional session cache. +- [x] Implement **sessions.start-demo** (`POST /api/sessions/demo`) + **Redis** for failed-login when Upstash env is set. - [x] **JWT + httpOnly cookies** (`accessToken`, `refreshToken`, `loggedIn`) matching [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md). -- [ ] **Users** registration + `GET /api/users/me` (and remaining TRPC_REST_MAP slices as needed). +- [x] **`GET /api/users/me`** (minimal summary from `accessToken` cookie). +- [ ] **Users** `POST /api/users` registration + remaining TRPC_REST_MAP slices as needed. - [ ] Pages/groups CRUD, realtime/collab, Stripe webhook (no RevenueCat). --- @@ -109,6 +110,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-26 | Phase 3: `POST /api/sessions/demo` (`performSessionStartDemo`), `GET /api/users/me`, Redis failed-login limits (`SessionRedisPort` + optional Upstash), `USER_EMAIL_ENCRYPTION_KEY` on `SessionEnv`; OpenAPI 200/400 for demo, 429 for login, `userMeResponseSchema`; Vitest `login-rate-limit.test.ts`. | | 2026-04-26 | Docs: [docs/RESTART_PLAN.md](../docs/RESTART_PLAN.md) §3.5 legacy frontend pain points, §5.8 frontend testing/CI, phased updates; this file: **Frontend / UI track** + Phase 2/4 notes on real web tests. | | 2026-04-26 | Phase 3: `@deepnotes/session` (login/refresh/logout + 2FA TOTP/recovery), api-worker Hyperdrive + dynamic import for Workers bundle; OpenAPI 200/401/503 for session routes; demo remains `501`; session crypto vendored in-package (no parent `@stdlib` links); `libsodium-wrappers-sumo@^0.8` override for Wrangler. | | 2026-04-26 | Phase 3 start: OpenAPI + Zod for `POST /api/sessions/login|refresh|logout|demo`; api-worker `501` stubs; Phase 0 marked done in snapshot. | diff --git a/new-deepnotes/apps/api-worker/package.json b/new-deepnotes/apps/api-worker/package.json index 2a32a647..ae0d163f 100644 --- a/new-deepnotes/apps/api-worker/package.json +++ b/new-deepnotes/apps/api-worker/package.json @@ -14,6 +14,7 @@ "@deepnotes/api": "workspace:*", "@deepnotes/db": "workspace:*", "@deepnotes/session": "workspace:*", + "@upstash/redis": "^1.34.8", "hono": "^4.7.7" }, "devDependencies": { diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 0b8241d2..dca8a1db 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -26,6 +26,8 @@ describe("api-worker", () => { ["POST", "/api/sessions/login"], ["POST", "/api/sessions/refresh"], ["POST", "/api/sessions/logout"], + ["POST", "/api/sessions/demo"], + ["GET", "/api/users/me"], ] as const)("returns 503 for %s %s when auth env is not configured", async (method, path) => { const res = await app.request(`http://test${path}`, { method }); expect(res.status).toBe(503); @@ -34,13 +36,4 @@ describe("api-worker", () => { }); }); - it("POST /api/sessions/demo returns 501 until registration is wired", async () => { - const res = await app.request("http://test/api/sessions/demo", { - method: "POST", - }); - expect(res.status).toBe(501); - await expect(res.json()).resolves.toMatchObject({ - code: "NOT_IMPLEMENTED", - }); - }); }); diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index c7c261ab..89db1bde 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -1,6 +1,7 @@ import { getOpenApiDocument, healthResponseSchema, + sessionDemoRequestSchema, sessionLoginRequestSchema, } from "@deepnotes/api"; import type { ContentfulStatusCode } from "hono/utils/http-status"; @@ -8,6 +9,7 @@ import { Hono } from "hono"; import { getDbForConnectionString } from "./db-pool.js"; import { readCookieHeader } from "./cookies.js"; +import { getSessionRedisPort } from "./redis-port.js"; import { getSessionEnv, type WorkerSessionBindings } from "./session-env.js"; type Bindings = WorkerSessionBindings & { @@ -17,16 +19,10 @@ type Bindings = WorkerSessionBindings & { const app = new Hono<{ Bindings: Bindings }>(); -const sessionNotImplementedBody = { - code: "NOT_IMPLEMENTED" as const, - message: - "Demo registration is not implemented yet. See OpenAPI for the contract.", -}; - const serviceUnavailableBody = { code: "SERVICE_UNAVAILABLE" as const, message: - "Session routes require ACCESS_SECRET, REFRESH_SECRET, USER_EMAIL_SECRET, USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, and USER_RECOVERY_CODES_ENCRYPTION_KEY (e.g. Wrangler secrets / .dev.vars).", + "Session routes require ACCESS_SECRET, REFRESH_SECRET, USER_EMAIL_SECRET, USER_EMAIL_ENCRYPTION_KEY, USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, and USER_RECOVERY_CODES_ENCRYPTION_KEY (e.g. Wrangler secrets / .dev.vars).", }; function appendSetCookies(res: Response, lines: string[]): void { @@ -93,6 +89,7 @@ app.post("/api/sessions/login", async (c) => { } const db = getDbForConnectionString(hyper.connectionString); + const redis = getSessionRedisPort(c.env); try { const { performSessionLogin } = await import("@deepnotes/session"); @@ -109,6 +106,7 @@ app.post("/api/sessions/login", async (c) => { }, clientIp: c.req.header("CF-Connecting-IP") ?? "127.0.0.1", userAgent: c.req.header("User-Agent") ?? "", + redis, }); const res = c.json(json, 200); appendSetCookies(res, cookieLines); @@ -198,8 +196,103 @@ app.post("/api/sessions/logout", async (c) => { return res; }); -app.post("/api/sessions/demo", (c) => - c.json(sessionNotImplementedBody, 501), -); +app.post("/api/sessions/demo", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = sessionDemoRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + + try { + const { performSessionStartDemo } = await import("@deepnotes/session"); + const { json, cookieLines } = await performSessionStartDemo({ + db, + env: sessionEnv, + body: parsed.data, + clientIp: c.req.header("CF-Connecting-IP") ?? "127.0.0.1", + userAgent: c.req.header("User-Agent") ?? "", + }); + const res = c.json(json, 200); + appendSetCookies(res, cookieLines); + return res; + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.get("/api/users/me", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { getAuthenticatedUserSummary } = await import("@deepnotes/session"); + const summary = await getAuthenticatedUserSummary({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + }); + return c.json(summary, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); export default app; diff --git a/new-deepnotes/apps/api-worker/src/redis-port.ts b/new-deepnotes/apps/api-worker/src/redis-port.ts new file mode 100644 index 00000000..460edfa6 --- /dev/null +++ b/new-deepnotes/apps/api-worker/src/redis-port.ts @@ -0,0 +1,29 @@ +import { Redis } from "@upstash/redis"; + +import type { SessionRedisPort } from "@deepnotes/session"; + +import type { WorkerSessionBindings } from "./session-env.js"; + +export function getSessionRedisPort( + env: Pick, +): SessionRedisPort | undefined { + const url = env.UPSTASH_REDIS_REST_URL; + const token = env.UPSTASH_REDIS_REST_TOKEN; + if (url == null || url === "" || token == null || token === "") { + return undefined; + } + + const redis = new Redis({ url, token }); + return { + async get(key: string) { + const v = await redis.get(key); + if (v == null) return null; + return typeof v === "string" ? v : String(v); + }, + ttl: (key) => redis.ttl(key), + incr: (key) => redis.incr(key), + expire: async (key, seconds) => { + await redis.expire(key, seconds); + }, + }; +} diff --git a/new-deepnotes/apps/api-worker/src/session-env.ts b/new-deepnotes/apps/api-worker/src/session-env.ts index 3dbec199..57c02fe4 100644 --- a/new-deepnotes/apps/api-worker/src/session-env.ts +++ b/new-deepnotes/apps/api-worker/src/session-env.ts @@ -4,12 +4,16 @@ export type WorkerSessionBindings = { ACCESS_SECRET?: string; REFRESH_SECRET?: string; USER_EMAIL_SECRET?: string; + USER_EMAIL_ENCRYPTION_KEY?: string; USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY?: string; USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY?: string; USER_RECOVERY_CODES_ENCRYPTION_KEY?: string; DEV?: string; COOKIE_DOMAIN?: string; EMAIL_CASE_SENSITIVITY_EXCEPTIONS?: string; + /** Optional; when set with token, failed-login rate limits use Upstash REST Redis. */ + UPSTASH_REDIS_REST_URL?: string; + UPSTASH_REDIS_REST_TOKEN?: string; }; export function getSessionEnv( @@ -22,6 +26,7 @@ export function getSessionEnv( ACCESS_SECRET, REFRESH_SECRET, USER_EMAIL_SECRET, + USER_EMAIL_ENCRYPTION_KEY, USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, USER_RECOVERY_CODES_ENCRYPTION_KEY, @@ -30,6 +35,7 @@ export function getSessionEnv( !ACCESS_SECRET || !REFRESH_SECRET || !USER_EMAIL_SECRET || + !USER_EMAIL_ENCRYPTION_KEY || !USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY || !USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY || !USER_RECOVERY_CODES_ENCRYPTION_KEY @@ -40,6 +46,7 @@ export function getSessionEnv( ACCESS_SECRET, REFRESH_SECRET, USER_EMAIL_SECRET, + USER_EMAIL_ENCRYPTION_KEY, USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, USER_RECOVERY_CODES_ENCRYPTION_KEY, diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index 783cb7d7..9958b13a 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -17,5 +17,10 @@ export { sessionDemoRequestSchema, sessionLoginEmailSchema, sessionLoginRequestSchema, + type SessionDemoRequest, type SessionLoginRequest, } from "./schemas/sessions.js"; +export { + userMeResponseSchema, + type UserMeResponse, +} from "./schemas/users.js"; diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 19ed387f..c41a9272 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -14,5 +14,6 @@ describe("getOpenApiDocument", () => { expect(doc.paths?.["/api/sessions/refresh"]?.post).toBeDefined(); expect(doc.paths?.["/api/sessions/logout"]?.post).toBeDefined(); expect(doc.paths?.["/api/sessions/demo"]?.post).toBeDefined(); + expect(doc.paths?.["/api/users/me"]?.get).toBeDefined(); }); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index c9036d0c..611f6323 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -4,7 +4,6 @@ import { } from "@asteasolutions/zod-to-openapi"; import type { OpenAPIObject } from "openapi3-ts/oas30"; -import { notImplementedResponseSchema } from "./schemas/errors.js"; import { healthResponseSchema } from "./schemas/health.js"; import { serviceUnavailableResponseSchema, @@ -16,31 +15,31 @@ import { sessionDemoRequestSchema, sessionLoginRequestSchema, } from "./schemas/sessions.js"; +import { userMeResponseSchema } from "./schemas/users.js"; const registry = new OpenAPIRegistry(); -const sessionNotImplemented501 = { +const sessionServiceUnavailable503 = { description: - "Demo registration is not implemented on this route yet (Phase 3+).", + "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", content: { "application/json": { - schema: notImplementedResponseSchema, + schema: serviceUnavailableResponseSchema, }, }, } as const; -const sessionServiceUnavailable503 = { - description: - "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", +const sessionUnauthorized401 = { + description: "Invalid credentials, token, or session state.", content: { "application/json": { - schema: serviceUnavailableResponseSchema, + schema: sessionErrorResponseSchema, }, }, } as const; -const sessionUnauthorized401 = { - description: "Invalid credentials, token, or session state.", +const sessionTooManyRequests429 = { + description: "Too many failed login attempts (rate limited).", content: { "application/json": { schema: sessionErrorResponseSchema, @@ -89,6 +88,27 @@ registry.registerPath({ }, }, 401: sessionUnauthorized401, + 429: sessionTooManyRequests429, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "get", + path: "/api/users/me", + summary: "Current user (from access cookie)", + description: + "Minimal account summary for the authenticated user (`accessToken` cookie).", + responses: { + 200: { + description: "Authenticated user.", + content: { + "application/json": { + schema: userMeResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, 503: sessionServiceUnavailable503, }, }); @@ -141,7 +161,23 @@ registry.registerPath({ }, }, responses: { - 501: sessionNotImplemented501, + 200: { + description: "Demo user created; same response shape as login.", + content: { + "application/json": { + schema: sessionLoginSuccessSchema, + }, + }, + }, + 400: { + description: "Validation error (e.g. unsupported group password on demo).", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 503: sessionServiceUnavailable503, }, }); diff --git a/new-deepnotes/packages/api/src/schemas/sessions.ts b/new-deepnotes/packages/api/src/schemas/sessions.ts index 1add0342..db647dd1 100644 --- a/new-deepnotes/packages/api/src/schemas/sessions.ts +++ b/new-deepnotes/packages/api/src/schemas/sessions.ts @@ -35,11 +35,59 @@ export const sessionLoginRequestSchema = z export type SessionLoginRequest = z.infer; +const nanoidId = z + .string() + .length(21) + .regex(/^[A-Za-z0-9_-]{21}$/, "expected nanoid id"); + +const byteB64 = z + .string() + .min(1) + .openapi({ + format: "byte", + description: "Standard base64-encoded binary (legacy tRPC used raw bytes).", + }) + .transform((s) => new Uint8Array(Buffer.from(s, "base64"))); + +const sessionDemoGroupCreationSchema = z + .object({ + groupEncryptedName: byteB64, + groupPasswordHash: byteB64.optional(), + groupIsPublic: z.boolean(), + groupAccessKeyring: byteB64, + groupEncryptedInternalKeyring: byteB64, + groupEncryptedContentKeyring: byteB64, + groupPublicKeyring: byteB64, + groupEncryptedPrivateKeyring: byteB64, + groupOwnerEncryptedName: byteB64, + }) + .openapi("SessionDemoGroupCreation"); + +const sessionDemoPageCreationSchema = z + .object({ + pageEncryptedSymmetricKeyring: byteB64, + pageEncryptedRelativeTitle: byteB64, + pageEncryptedAbsoluteTitle: byteB64, + }) + .openapi("SessionDemoPageCreation"); + /** * Demo session creation mirrors legacy `sessions.startDemo` input (crypto material + ids). - * Shape will align with `POST /api/users` once registration is implemented; `additionalProperties` keeps codegen honest until then. */ export const sessionDemoRequestSchema = z - .object({}) - .catchall(z.unknown()) + .object({ + userId: nanoidId, + groupId: nanoidId, + pageId: nanoidId, + userPublicKeyring: byteB64, + userEncryptedPrivateKeyring: byteB64, + userEncryptedSymmetricKeyring: byteB64, + userEncryptedName: byteB64, + userEncryptedDefaultNote: byteB64, + userEncryptedDefaultArrow: byteB64, + groupCreation: sessionDemoGroupCreationSchema, + pageCreation: sessionDemoPageCreationSchema, + }) .openapi("SessionDemoRequest"); + +export type SessionDemoRequest = z.infer; diff --git a/new-deepnotes/packages/api/src/schemas/users.ts b/new-deepnotes/packages/api/src/schemas/users.ts new file mode 100644 index 00000000..77a15244 --- /dev/null +++ b/new-deepnotes/packages/api/src/schemas/users.ts @@ -0,0 +1,15 @@ +import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; +import { z } from "zod"; + +extendZodWithOpenApi(z); + +export const userMeResponseSchema = z + .object({ + userId: z.string(), + emailVerified: z.boolean(), + demo: z.boolean(), + personalGroupId: z.string(), + }) + .openapi("UserMeResponse"); + +export type UserMeResponse = z.infer; diff --git a/new-deepnotes/packages/session/package.json b/new-deepnotes/packages/session/package.json index 24b051e7..d59d8efb 100644 --- a/new-deepnotes/packages/session/package.json +++ b/new-deepnotes/packages/session/package.json @@ -12,7 +12,7 @@ "scripts": { "lint": "eslint .", "typecheck": "tsc -p tsconfig.json --noEmit", - "test": "vitest run --passWithNoTests" + "test": "vitest run" }, "dependencies": { "@deepnotes/db": "workspace:*", diff --git a/new-deepnotes/packages/session/src/encrypt-user-email.ts b/new-deepnotes/packages/session/src/encrypt-user-email.ts new file mode 100644 index 00000000..962fb7d8 --- /dev/null +++ b/new-deepnotes/packages/session/src/encrypt-user-email.ts @@ -0,0 +1,22 @@ +import { base64ToBytes, textToBytes } from "./crypto/bytes.js"; +import { wrapSymmetricKey } from "./crypto/symmetric-key.js"; + +function normalizeEmail(email: string, exceptions: string): string { + return exceptions.split(";").includes(email) ? email : email.toLowerCase(); +} + +/** + * Legacy `@deeplib/data` `encryptUserEmail` (XChaCha20-Poly1305, padding, AAD `UserEmail`). + */ +export function encryptUserEmail( + email: string, + encryptionKeyB64: string, + exceptions: string, +): Uint8Array { + const normalized = normalizeEmail(email, exceptions); + const key = wrapSymmetricKey(base64ToBytes(encryptionKeyB64)); + return key.encrypt(textToBytes(normalized), { + padding: true, + associatedData: { context: "UserEmail" }, + }); +} diff --git a/new-deepnotes/packages/session/src/env.ts b/new-deepnotes/packages/session/src/env.ts index a6073f36..3afe98c7 100644 --- a/new-deepnotes/packages/session/src/env.ts +++ b/new-deepnotes/packages/session/src/env.ts @@ -5,6 +5,8 @@ export type SessionEnv = { ACCESS_SECRET: string; REFRESH_SECRET: string; USER_EMAIL_SECRET: string; + /** Base64 symmetric key for `encrypted_email` (legacy `USER_EMAIL_ENCRYPTION_KEY`). */ + USER_EMAIL_ENCRYPTION_KEY: string; USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY: string; USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY: string; USER_RECOVERY_CODES_ENCRYPTION_KEY: string; diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 56ba7561..9b49c96e 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -3,5 +3,14 @@ export { isDev } from "./env.js"; export { SessionError } from "./errors.js"; export { performSessionLogin } from "./login.js"; export type { SessionLoginBody } from "./login.js"; +export type { SessionRedisPort } from "./login-rate-limit.js"; export { performSessionLogout } from "./logout.js"; export { performSessionRefresh } from "./refresh.js"; +export { performSessionStartDemo } from "./start-demo.js"; +export type { + SessionStartDemoGroupCreation, + SessionStartDemoInput, + SessionStartDemoPageCreation, +} from "./start-demo.js"; +export { getAuthenticatedUserSummary } from "./user-me.js"; +export type { AuthenticatedUserSummary } from "./user-me.js"; diff --git a/new-deepnotes/packages/session/src/login-rate-limit.test.ts b/new-deepnotes/packages/session/src/login-rate-limit.test.ts new file mode 100644 index 00000000..e082e6af --- /dev/null +++ b/new-deepnotes/packages/session/src/login-rate-limit.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from "vitest"; + +import { + checkFailedLoginAttempts, + incrementFailedLoginAttempts, + type SessionRedisPort, +} from "./login-rate-limit.js"; + +function createMemoryRedis(): SessionRedisPort & { + store: Map; + now: () => number; +} { + const store = new Map(); + const t = 1_700_000_000_000; + const now = () => t; + return { + store, + now, + async get(key) { + const row = store.get(key); + if (row == null) return null; + if (row.ttlAt <= now()) { + store.delete(key); + return null; + } + return row.value; + }, + async ttl(key) { + const row = store.get(key); + if (row == null) return -2; + if (row.ttlAt <= now()) return -2; + return Math.max(0, Math.ceil((row.ttlAt - now()) / 1000)); + }, + async incr(key) { + const row = store.get(key); + const v = String((Number.parseInt(row?.value ?? "0", 10) || 0) + 1); + const ttlAt = row?.ttlAt ?? now() + 15 * 60 * 1000; + store.set(key, { value: v, ttlAt }); + return Number.parseInt(v, 10); + }, + async expire(key, seconds) { + const row = store.get(key); + if (row == null) return; + row.ttlAt = now() + seconds * 1000; + }, + }; +} + +describe("login-rate-limit", () => { + it("blocks after four failed attempts (email counter)", async () => { + const redis = createMemoryRedis(); + const email = "a@b.co"; + const ip = "1.2.3.4"; + + for (let i = 0; i < 3; i++) { + await incrementFailedLoginAttempts(redis, email, ip); + } + let s = await checkFailedLoginAttempts(redis, email, ip); + expect(s.excessive).toBe(false); + + await incrementFailedLoginAttempts(redis, email, ip); + s = await checkFailedLoginAttempts(redis, email, ip); + expect(s.excessive).toBe(true); + }); + + it("ignores email counter for literal demo login email (IP must stay low)", async () => { + const redis = createMemoryRedis(); + for (let i = 0; i < 10; i++) { + await incrementFailedLoginAttempts(redis, "demo", `10.0.0.${String(i)}`); + } + const s = await checkFailedLoginAttempts(redis, "demo", "10.0.0.99"); + expect(s.excessive).toBe(false); + }); +}); diff --git a/new-deepnotes/packages/session/src/login-rate-limit.ts b/new-deepnotes/packages/session/src/login-rate-limit.ts new file mode 100644 index 00000000..c2346401 --- /dev/null +++ b/new-deepnotes/packages/session/src/login-rate-limit.ts @@ -0,0 +1,58 @@ +/** + * Failed-login rate limiting (legacy `sessions.login` parity). + * Keys: `email-failed-login-attempts:${email}`, `ip-failed-login-attempts:${ip}`. + * Threshold 4 failures / 15 minutes; literal email `demo` skips email-side counter. + */ + +export type SessionRedisPort = { + get(key: string): Promise; + ttl(key: string): Promise; + incr(key: string): Promise; + expire(key: string, seconds: number): Promise; +}; + +const TTL_SECONDS = 15 * 60; +const MAX_ATTEMPTS = 4; + +export async function checkFailedLoginAttempts( + redis: SessionRedisPort, + email: string, + ip: string, +): Promise<{ excessive: boolean; loginBlockTTLMinutes: number }> { + const [emailStr, emailTtl, ipStr, ipTtl] = await Promise.all([ + email === "demo" + ? Promise.resolve("0") + : redis.get(`email-failed-login-attempts:${email}`), + email === "demo" ? Promise.resolve(0) : redis.ttl(`email-failed-login-attempts:${email}`), + redis.get(`ip-failed-login-attempts:${ip}`), + redis.ttl(`ip-failed-login-attempts:${ip}`), + ]); + + const numEmail = Number.parseInt(emailStr ?? "0", 10) || 0; + const numIp = Number.parseInt(ipStr ?? "0", 10) || 0; + const excessive = Math.max(numEmail, numIp) >= MAX_ATTEMPTS; + const ttlSeconds = Math.max( + emailTtl < 0 ? 0 : emailTtl, + ipTtl < 0 ? 0 : ipTtl, + ); + const loginBlockTTLMinutes = Math.ceil(ttlSeconds / 60); + + return { excessive, loginBlockTTLMinutes }; +} + +export async function incrementFailedLoginAttempts( + redis: SessionRedisPort, + email: string, + ip: string, +): Promise { + await Promise.all([ + (async () => { + await redis.incr(`email-failed-login-attempts:${email}`); + await redis.expire(`email-failed-login-attempts:${email}`, TTL_SECONDS); + })(), + (async () => { + await redis.incr(`ip-failed-login-attempts:${ip}`); + await redis.expire(`ip-failed-login-attempts:${ip}`, TTL_SECONDS); + })(), + ]); +} diff --git a/new-deepnotes/packages/session/src/login.ts b/new-deepnotes/packages/session/src/login.ts index 2b08e0f5..644a5a67 100644 --- a/new-deepnotes/packages/session/src/login.ts +++ b/new-deepnotes/packages/session/src/login.ts @@ -13,6 +13,11 @@ import { devices, users } from "@deepnotes/db/schema"; import { cookieOptionsFromEnv } from "./cookies.js"; import { getDeviceHash } from "./device-hash.js"; import type { SessionEnv } from "./env.js"; +import { + checkFailedLoginAttempts, + incrementFailedLoginAttempts, + type SessionRedisPort, +} from "./login-rate-limit.js"; import { SessionError } from "./errors.js"; import { hashUserEmail } from "./email-hash.js"; import { @@ -43,9 +48,26 @@ export async function performSessionLogin(input: { body: SessionLoginBody; clientIp: string; userAgent: string; + /** When set (e.g. Upstash in Workers), enforces legacy failed-login limits. */ + redis?: SessionRedisPort; }): Promise<{ json: Record; cookieLines: string[] }> { await ensureSodiumReady(); + if (input.redis != null) { + const { excessive, loginBlockTTLMinutes } = await checkFailedLoginAttempts( + input.redis, + input.body.email, + input.clientIp, + ); + if (excessive) { + throw new SessionError( + 429, + "TOO_MANY_REQUESTS", + `Too many failed login attempts. Try again in ${String(loginBlockTTLMinutes)} minutes.`, + ); + } + } + const exceptions = input.env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS ?? ""; const emailHashBuf = Buffer.from( await hashUserEmail( @@ -82,6 +104,13 @@ export async function performSessionLogin(input: { const user = rows[0]; if (user == null) { + if (input.redis != null) { + await incrementFailedLoginAttempts( + input.redis, + input.body.email, + input.clientIp, + ); + } throw new SessionError(401, "UNAUTHORIZED", "Incorrect email or password."); } @@ -99,6 +128,13 @@ export async function performSessionLogin(input: { const passwordOk = sodium.memcmp(passwordValues.hash, passwordHashValues.hashBytes); if (!passwordOk) { + if (input.redis != null) { + await incrementFailedLoginAttempts( + input.redis, + input.body.email, + input.clientIp, + ); + } throw new SessionError(401, "UNAUTHORIZED", "Incorrect email or password."); } @@ -164,6 +200,14 @@ export async function performSessionLogin(input: { rememberDevice: input.body.rememberDevice, userAuthenticatorKeyB64: input.env.USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, userRecoveryCodesKeyB64: input.env.USER_RECOVERY_CODES_ENCRYPTION_KEY, + failedLoginRateLimit: + input.redis != null + ? { + redis: input.redis, + email: input.body.email, + ip: input.clientIp, + } + : undefined, }); } diff --git a/new-deepnotes/packages/session/src/start-demo.ts b/new-deepnotes/packages/session/src/start-demo.ts new file mode 100644 index 00000000..d6d184ee --- /dev/null +++ b/new-deepnotes/packages/session/src/start-demo.ts @@ -0,0 +1,241 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { eq } from "drizzle-orm"; +import sodium from "libsodium-wrappers-sumo"; +import { nanoid } from "nanoid"; + +import { + devices, + groupMembers, + groups, + pages, + users, + usersPages, +} from "@deepnotes/db/schema"; + +import { + createPrivateKeyring, + createSymmetricKeyring, +} from "./crypto/index.js"; +import { ensureSodiumReady } from "./crypto/session-crypto.js"; +import { wrapSymmetricKey } from "./crypto/symmetric-key.js"; +import { cookieOptionsFromEnv } from "./cookies.js"; +import type { SessionEnv } from "./env.js"; +import { encryptUserEmail } from "./encrypt-user-email.js"; +import { hashUserEmail } from "./email-hash.js"; +import { getDeviceHash } from "./device-hash.js"; +import { SessionError } from "./errors.js"; +import { createSessionRowAndCookies } from "./session-lifecycle.js"; + +export type SessionStartDemoGroupCreation = { + groupEncryptedName: Uint8Array; + groupPasswordHash?: Uint8Array; + groupIsPublic: boolean; + groupAccessKeyring: Uint8Array; + groupEncryptedInternalKeyring: Uint8Array; + groupEncryptedContentKeyring: Uint8Array; + groupPublicKeyring: Uint8Array; + groupEncryptedPrivateKeyring: Uint8Array; + groupOwnerEncryptedName: Uint8Array; +}; + +export type SessionStartDemoPageCreation = { + pageEncryptedSymmetricKeyring: Uint8Array; + pageEncryptedRelativeTitle: Uint8Array; + pageEncryptedAbsoluteTitle: Uint8Array; +}; + +export type SessionStartDemoInput = { + userId: string; + groupId: string; + pageId: string; + userPublicKeyring: Uint8Array; + userEncryptedPrivateKeyring: Uint8Array; + userEncryptedSymmetricKeyring: Uint8Array; + userEncryptedName: Uint8Array; + userEncryptedDefaultNote: Uint8Array; + userEncryptedDefaultArrow: Uint8Array; + groupCreation: SessionStartDemoGroupCreation; + pageCreation: SessionStartDemoPageCreation; +}; + +function toBuf(u: Uint8Array): Buffer { + return Buffer.from(u); +} + +function toB64(u: Uint8Array): string { + return Buffer.from(u).toString("base64"); +} + +/** + * Replaces legacy `sessions.startDemo`: creates a **demo** user (random server-side + * wrapping key, empty password hash), personal group + page, device, session, and cookies. + */ +export async function performSessionStartDemo(input: { + db: DeepnotesDb; + env: SessionEnv; + body: SessionStartDemoInput; + clientIp: string; + userAgent: string; +}): Promise<{ json: Record; cookieLines: string[] }> { + await ensureSodiumReady(); + + const gc = input.body.groupCreation; + if ( + gc.groupPasswordHash != null && + gc.groupPasswordHash.byteLength > 0 + ) { + throw new SessionError( + 400, + "VALIDATION_ERROR", + "Demo registration with a group password is not supported yet.", + ); + } + + const email = `demo-${nanoid()}`; + const exceptions = input.env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS ?? ""; + const emailHash = Buffer.from( + await hashUserEmail(email, input.env.USER_EMAIL_SECRET, exceptions), + ); + const encryptedEmail = Buffer.from( + encryptUserEmail(email, input.env.USER_EMAIL_ENCRYPTION_KEY, exceptions), + ); + + const passwordKey = wrapSymmetricKey(sodium.randombytes_buf(32)); + + const encryptedPrivateStored = toBuf( + createPrivateKeyring(input.body.userEncryptedPrivateKeyring) + .wrapSymmetric(passwordKey, { + associatedData: { + context: "UserEncryptedPrivateKeyring", + userId: input.body.userId, + }, + }).wrappedValue, + ); + + const encryptedSymmetricStored = toBuf( + createSymmetricKeyring(input.body.userEncryptedSymmetricKeyring) + .wrapSymmetric(passwordKey, { + associatedData: { + context: "UserEncryptedSymmetricKeyring", + userId: input.body.userId, + }, + }).wrappedValue, + ); + + const pc = input.body.pageCreation; + + return await input.db.transaction(async (tx) => { + await tx.delete(users).where(eq(users.emailHash, emailHash)); + + await tx.insert(users).values({ + id: input.body.userId, + encryptedEmail, + emailHash, + encryptedRehashedLoginHash: Buffer.alloc(0), + demo: true, + emailVerified: false, + personalGroupId: input.body.groupId, + startingPageId: input.body.pageId, + recentPageIds: [input.body.pageId], + recentGroupIds: [input.body.groupId], + publicKeyring: toBuf(input.body.userPublicKeyring), + encryptedPrivateKeyring: encryptedPrivateStored, + encryptedSymmetricKeyring: encryptedSymmetricStored, + encryptedName: toBuf(input.body.userEncryptedName), + encryptedDefaultNote: toBuf(input.body.userEncryptedDefaultNote), + encryptedDefaultArrow: toBuf(input.body.userEncryptedDefaultArrow), + }); + + await tx.insert(groups).values({ + id: input.body.groupId, + mainPageId: input.body.pageId, + encryptedName: toBuf(gc.groupEncryptedName), + userId: input.body.userId, + publicKeyring: toBuf(gc.groupPublicKeyring), + encryptedPrivateKeyring: toBuf(gc.groupEncryptedPrivateKeyring), + encryptedContentKeyring: toBuf(gc.groupEncryptedContentKeyring), + accessKeyring: gc.groupIsPublic ? toBuf(gc.groupAccessKeyring) : null, + }); + + await tx.insert(groupMembers).values({ + groupId: input.body.groupId, + userId: input.body.userId, + role: "owner", + encryptedAccessKeyring: gc.groupIsPublic + ? null + : toBuf(gc.groupAccessKeyring), + encryptedInternalKeyring: toBuf(gc.groupEncryptedInternalKeyring), + encryptedName: toBuf(gc.groupOwnerEncryptedName), + }); + + await tx.insert(pages).values({ + id: input.body.pageId, + groupId: input.body.groupId, + encryptedRelativeTitle: toBuf(pc.pageEncryptedRelativeTitle), + encryptedSymmetricKeyring: toBuf(pc.pageEncryptedSymmetricKeyring), + encryptedAbsoluteTitle: toBuf(pc.pageEncryptedAbsoluteTitle), + free: true, + }); + + await tx.insert(usersPages).values({ + userId: input.body.userId, + pageId: input.body.pageId, + lastParentId: null, + }); + + const deviceHash = getDeviceHash({ + ip: input.clientIp, + userAgent: input.userAgent, + userId: input.body.userId, + }); + + const deviceId = nanoid(); + await tx.insert(devices).values({ + id: deviceId, + userId: input.body.userId, + hash: deviceHash, + trusted: false, + }); + + const sessionId = nanoid(); + const cookieOpts = cookieOptionsFromEnv(input.env); + const { sessionKey, cookieLines } = await createSessionRowAndCookies({ + db: tx as unknown as DeepnotesDb, + sessionId, + userId: input.body.userId, + deviceId, + rememberSession: false, + env: input.env, + cookieOpts, + }); + + const encPriv = createPrivateKeyring(new Uint8Array(encryptedPrivateStored)) + .unwrapSymmetric(passwordKey, { + associatedData: { + context: "UserEncryptedPrivateKeyring", + userId: input.body.userId, + }, + }).wrappedValue; + + const encSym = createSymmetricKeyring(new Uint8Array(encryptedSymmetricStored)) + .unwrapSymmetric(passwordKey, { + associatedData: { + context: "UserEncryptedSymmetricKeyring", + userId: input.body.userId, + }, + }).wrappedValue; + + return { + json: { + userId: input.body.userId, + sessionId, + sessionKey: toB64(sessionKey), + personalGroupId: input.body.groupId, + publicKeyring: toB64(input.body.userPublicKeyring), + encryptedPrivateKeyring: toB64(encPriv), + encryptedSymmetricKeyring: toB64(encSym), + }, + cookieLines, + }; + }); +} diff --git a/new-deepnotes/packages/session/src/two-factor.ts b/new-deepnotes/packages/session/src/two-factor.ts index 6f1a62c8..e6c2ffae 100644 --- a/new-deepnotes/packages/session/src/two-factor.ts +++ b/new-deepnotes/packages/session/src/two-factor.ts @@ -4,6 +4,8 @@ import { authenticator } from "otplib"; import { devices, users } from "@deepnotes/db/schema"; +import { incrementFailedLoginAttempts } from "./login-rate-limit.js"; +import type { SessionRedisPort } from "./login-rate-limit.js"; import { SessionError } from "./errors.js"; import { decryptRecoveryCodes, @@ -28,6 +30,12 @@ export async function assertTwoFactorOk(input: { rememberDevice: boolean | undefined; userAuthenticatorKeyB64: string; userRecoveryCodesKeyB64: string; + /** When set, increments failed-login counters on bad token / recovery (legacy `sessions.login`). */ + failedLoginRateLimit?: { + redis: SessionRedisPort; + email: string; + ip: string; + }; }): Promise { if (input.device.trusted) { return; @@ -47,11 +55,25 @@ export async function assertTwoFactorOk(input: { } return; } + if (input.failedLoginRateLimit != null) { + await incrementFailedLoginAttempts( + input.failedLoginRateLimit.redis, + input.failedLoginRateLimit.email, + input.failedLoginRateLimit.ip, + ); + } throw new SessionError(401, "UNAUTHORIZED", "Invalid authenticator token."); } if (input.recoveryCode != null) { if (input.user.encryptedRecoveryCodes == null) { + if (input.failedLoginRateLimit != null) { + await incrementFailedLoginAttempts( + input.failedLoginRateLimit.redis, + input.failedLoginRateLimit.email, + input.failedLoginRateLimit.ip, + ); + } throw new SessionError(401, "UNAUTHORIZED", "Invalid recovery code."); } const recoveryCodes = decryptRecoveryCodes( @@ -74,6 +96,13 @@ export async function assertTwoFactorOk(input: { } } + if (input.failedLoginRateLimit != null) { + await incrementFailedLoginAttempts( + input.failedLoginRateLimit.redis, + input.failedLoginRateLimit.email, + input.failedLoginRateLimit.ip, + ); + } throw new SessionError(401, "UNAUTHORIZED", "Invalid recovery code."); } diff --git a/new-deepnotes/packages/session/src/user-me.ts b/new-deepnotes/packages/session/src/user-me.ts new file mode 100644 index 00000000..b79c4a15 --- /dev/null +++ b/new-deepnotes/packages/session/src/user-me.ts @@ -0,0 +1,56 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { eq } from "drizzle-orm"; + +import { users } from "@deepnotes/db/schema"; + +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { verifyAccessToken } from "./jwt.js"; + +export type AuthenticatedUserSummary = { + userId: string; + emailVerified: boolean; + demo: boolean; + personalGroupId: string; +}; + +export async function getAuthenticatedUserSummary(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; +}): Promise { + if (input.accessCookie == null || input.accessCookie === "") { + throw new SessionError(401, "UNAUTHORIZED", "No access token."); + } + + const payload = await verifyAccessToken( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + if (payload == null) { + throw new SessionError(401, "UNAUTHORIZED", "Invalid access token."); + } + + const rows = await input.db + .select({ + id: users.id, + emailVerified: users.emailVerified, + demo: users.demo, + personalGroupId: users.personalGroupId, + }) + .from(users) + .where(eq(users.id, payload.uid)) + .limit(1); + + const row = rows[0]; + if (row == null) { + throw new SessionError(401, "UNAUTHORIZED", "User not found."); + } + + return { + userId: row.id, + emailVerified: row.emailVerified, + demo: row.demo ?? false, + personalGroupId: row.personalGroupId, + }; +} diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml index d4fe7a16..973bd7de 100644 --- a/new-deepnotes/pnpm-lock.yaml +++ b/new-deepnotes/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: '@deepnotes/session': specifier: workspace:* version: link:../../packages/session + '@upstash/redis': + specifier: ^1.34.8 + version: 1.37.0 hono: specifier: ^4.7.7 version: 4.12.15 @@ -1350,6 +1353,9 @@ packages: resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@upstash/redis@1.37.0': + resolution: {integrity: sha512-LqOJ3+XWPLSZ2rGSed5DYG3ixybxb8EhZu3yQqF7MdZX1wLBG/FRcI6xcUZXHy/SS7mmXWyadrud0HJHkOc+uw==} + '@vitejs/plugin-vue@5.2.4': resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2129,6 +2135,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -3090,6 +3099,10 @@ snapshots: '@typescript-eslint/types': 8.59.0 eslint-visitor-keys: 5.0.1 + '@upstash/redis@1.37.0': + dependencies: + uncrypto: 0.1.3 + '@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.33(typescript@5.9.3))': dependencies: vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) @@ -3927,6 +3940,8 @@ snapshots: typescript@5.9.3: {} + uncrypto@0.1.3: {} + undici-types@6.21.0: {} undici@7.24.8: {} diff --git a/new-deepnotes/template.env b/new-deepnotes/template.env index 0fc72b8f..37b6013a 100644 --- a/new-deepnotes/template.env +++ b/new-deepnotes/template.env @@ -13,6 +13,10 @@ REDIS_URL=redis://localhost:6380 # ACCESS_SECRET= # REFRESH_SECRET= # USER_EMAIL_SECRET= +# USER_EMAIL_ENCRYPTION_KEY= # base64, 32-byte symmetric key for encrypted_email (legacy USER_EMAIL_ENCRYPTION_KEY) +# Upstash (optional; Workers login rate limits when both are set) +# UPSTASH_REDIS_REST_URL= +# UPSTASH_REDIS_REST_TOKEN= # USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY= # base64, 32-byte symmetric key material # USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY= # base64 # USER_RECOVERY_CODES_ENCRYPTION_KEY= # base64 From dd6c8c0b7d2c204598c9cd0db737ca3acbeb7720 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 23:33:32 -0300 Subject: [PATCH 029/243] feat(new-deepnotes): user registration and register OpenAPI --- new-deepnotes/PLAN_PROGRESS.md | 8 +- .../apps/api-worker/src/index.test.ts | 1 + new-deepnotes/apps/api-worker/src/index.ts | 71 +++++ .../apps/api-worker/src/session-env.ts | 3 + new-deepnotes/packages/api/src/index.ts | 4 + .../packages/api/src/openapi.test.ts | 1 + new-deepnotes/packages/api/src/openapi.ts | 54 +++- .../packages/api/src/schemas/sessions.ts | 15 + .../packages/api/src/schemas/users.ts | 9 + .../session/src/crypto/session-crypto.ts | 11 + .../packages/session/src/datetime.ts | 6 + new-deepnotes/packages/session/src/env.ts | 5 + new-deepnotes/packages/session/src/index.ts | 2 + .../packages/session/src/register-user.ts | 281 ++++++++++++++++++ new-deepnotes/template.env | 1 + 15 files changed, 468 insertions(+), 4 deletions(-) create mode 100644 new-deepnotes/packages/session/src/register-user.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index d62cb1a3..c5b34a57 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Mostly done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). Optional: Wrangler deploy job. **Gap:** `apps/web` tests still no-op—see Phase 2 checklist + [Frontend / UI track](#frontend--ui-track). | -| **3** — REST + Drizzle features | **In progress** | `POST /api/sessions/login|refresh|logout` + **`POST /api/sessions/demo`** + **`GET /api/users/me`** via `@deepnotes/session`. Optional **Upstash** (`UPSTASH_REDIS_REST_*`) wires legacy-style **failed-login rate limits**; without it, limits are skipped (local dev). **New secret:** `USER_EMAIL_ENCRYPTION_KEY` (legacy email encryption). Next: `POST /api/users` registration, pages/groups CRUD, Stripe. | +| **3** — REST + Drizzle features | **In progress** | `POST /api/sessions/login|refresh|logout` + **`POST /api/sessions/demo`** + **`GET /api/users/me`** + **`POST /api/users`** (registration) via `@deepnotes/session`. Optional **Upstash** (`UPSTASH_REDIS_REST_*`) for failed-login limits; **`SEND_EMAILS=false`** auto-verifies new users (no mailer yet). Next: email verification resend/confirm REST, pages/groups CRUD, Stripe. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, Vitest+DOM in CI, small E2E smoke—see [Frontend / UI track](#frontend--ui-track) (not deferred to “when MVP is done”). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -32,11 +32,12 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ ## Phase 3 checklist (REST + Drizzle) - [x] Document **sessions** REST paths + request schemas in OpenAPI; demo + `users/me` contracts updated. -- [x] Implement **sessions.login** / refresh / logout against Drizzle + legacy crypto semantics (JWT via `jose`; **Redis** rate limits not wired yet—parity with legacy `login` lockouts). +- [x] Implement **sessions.login** / refresh / logout against Drizzle + legacy crypto semantics (JWT via `jose`; optional **Redis** failed-login limits when Upstash env is set). - [x] Implement **sessions.start-demo** (`POST /api/sessions/demo`) + **Redis** for failed-login when Upstash env is set. - [x] **JWT + httpOnly cookies** (`accessToken`, `refreshToken`, `loggedIn`) matching [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md). - [x] **`GET /api/users/me`** (minimal summary from `accessToken` cookie). -- [ ] **Users** `POST /api/users` registration + remaining TRPC_REST_MAP slices as needed. +- [x] **Users** `POST /api/users` registration (crypto payload aligned with demo; conflict / unverified parity; optional `SEND_EMAILS=false` auto-verify). +- [ ] **Users** email verification resend/confirm (`POST /api/users/me/email-verification/*`) + remaining TRPC_REST_MAP slices as needed. - [ ] Pages/groups CRUD, realtime/collab, Stripe webhook (no RevenueCat). --- @@ -110,6 +111,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-26 | Phase 3: **`POST /api/users`** (`performUserRegister`), `encryptUserRehashedLoginHash`, `addHours`, OpenAPI 201/400/401/409; optional **`SEND_EMAILS`** on session env (auto-verify when `false`); group password on register still rejected (same as demo). | | 2026-04-26 | Phase 3: `POST /api/sessions/demo` (`performSessionStartDemo`), `GET /api/users/me`, Redis failed-login limits (`SessionRedisPort` + optional Upstash), `USER_EMAIL_ENCRYPTION_KEY` on `SessionEnv`; OpenAPI 200/400 for demo, 429 for login, `userMeResponseSchema`; Vitest `login-rate-limit.test.ts`. | | 2026-04-26 | Docs: [docs/RESTART_PLAN.md](../docs/RESTART_PLAN.md) §3.5 legacy frontend pain points, §5.8 frontend testing/CI, phased updates; this file: **Frontend / UI track** + Phase 2/4 notes on real web tests. | | 2026-04-26 | Phase 3: `@deepnotes/session` (login/refresh/logout + 2FA TOTP/recovery), api-worker Hyperdrive + dynamic import for Workers bundle; OpenAPI 200/401/503 for session routes; demo remains `501`; session crypto vendored in-package (no parent `@stdlib` links); `libsodium-wrappers-sumo@^0.8` override for Wrangler. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index dca8a1db..18644072 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -28,6 +28,7 @@ describe("api-worker", () => { ["POST", "/api/sessions/logout"], ["POST", "/api/sessions/demo"], ["GET", "/api/users/me"], + ["POST", "/api/users"], ] as const)("returns 503 for %s %s when auth env is not configured", async (method, path) => { const res = await app.request(`http://test${path}`, { method }); expect(res.status).toBe(503); diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 89db1bde..a36e73bd 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -3,6 +3,7 @@ import { healthResponseSchema, sessionDemoRequestSchema, sessionLoginRequestSchema, + userRegisterRequestSchema, } from "@deepnotes/api"; import type { ContentfulStatusCode } from "hono/utils/http-status"; import { Hono } from "hono"; @@ -256,6 +257,76 @@ app.post("/api/sessions/demo", async (c) => { } }); +app.post("/api/users", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = userRegisterRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + + try { + const { performUserRegister } = await import("@deepnotes/session"); + const result = await performUserRegister({ + db, + env: sessionEnv, + body: { + email: parsed.data.email, + loginHash: parsed.data.loginHash, + userId: parsed.data.userId, + groupId: parsed.data.groupId, + pageId: parsed.data.pageId, + userPublicKeyring: parsed.data.userPublicKeyring, + userEncryptedPrivateKeyring: parsed.data.userEncryptedPrivateKeyring, + userEncryptedSymmetricKeyring: parsed.data.userEncryptedSymmetricKeyring, + userEncryptedName: parsed.data.userEncryptedName, + userEncryptedDefaultNote: parsed.data.userEncryptedDefaultNote, + userEncryptedDefaultArrow: parsed.data.userEncryptedDefaultArrow, + groupCreation: parsed.data.groupCreation, + pageCreation: parsed.data.pageCreation, + }, + }); + return c.json(result, 201); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.get("/api/users/me", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/apps/api-worker/src/session-env.ts b/new-deepnotes/apps/api-worker/src/session-env.ts index 57c02fe4..755be871 100644 --- a/new-deepnotes/apps/api-worker/src/session-env.ts +++ b/new-deepnotes/apps/api-worker/src/session-env.ts @@ -11,6 +11,8 @@ export type WorkerSessionBindings = { DEV?: string; COOKIE_DOMAIN?: string; EMAIL_CASE_SENSITIVITY_EXCEPTIONS?: string; + /** When `"false"`, new registrations are email-verified without sending mail (local/CI). */ + SEND_EMAILS?: string; /** Optional; when set with token, failed-login rate limits use Upstash REST Redis. */ UPSTASH_REDIS_REST_URL?: string; UPSTASH_REDIS_REST_TOKEN?: string; @@ -53,5 +55,6 @@ export function getSessionEnv( DEV: env.DEV, COOKIE_DOMAIN: env.COOKIE_DOMAIN, EMAIL_CASE_SENSITIVITY_EXCEPTIONS: env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS, + SEND_EMAILS: env.SEND_EMAILS, }; } diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index 9958b13a..8ad2599b 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -17,10 +17,14 @@ export { sessionDemoRequestSchema, sessionLoginEmailSchema, sessionLoginRequestSchema, + userRegisterRequestSchema, type SessionDemoRequest, type SessionLoginRequest, + type UserRegisterRequest, } from "./schemas/sessions.js"; export { userMeResponseSchema, + userRegisterResponseSchema, type UserMeResponse, + type UserRegisterResponse, } from "./schemas/users.js"; diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index c41a9272..3638e568 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -15,5 +15,6 @@ describe("getOpenApiDocument", () => { expect(doc.paths?.["/api/sessions/logout"]?.post).toBeDefined(); expect(doc.paths?.["/api/sessions/demo"]?.post).toBeDefined(); expect(doc.paths?.["/api/users/me"]?.get).toBeDefined(); + expect(doc.paths?.["/api/users"]?.post).toBeDefined(); }); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index 611f6323..69578f2b 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -14,8 +14,12 @@ import { import { sessionDemoRequestSchema, sessionLoginRequestSchema, + userRegisterRequestSchema, } from "./schemas/sessions.js"; -import { userMeResponseSchema } from "./schemas/users.js"; +import { + userMeResponseSchema, + userRegisterResponseSchema, +} from "./schemas/users.js"; const registry = new OpenAPIRegistry(); @@ -47,6 +51,15 @@ const sessionTooManyRequests429 = { }, } as const; +const sessionConflict409 = { + description: "Resource already exists (e.g. email already registered).", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, +} as const; + registry.registerPath({ method: "get", path: "/api/health", @@ -93,6 +106,45 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/users", + summary: "Register a new account", + description: + "Replaces legacy `users.account.register`. Creates user, personal group, and first page; sets email verification unless `SEND_EMAILS=false` (then verifies immediately, legacy parity).", + request: { + body: { + content: { + "application/json": { + schema: userRegisterRequestSchema, + }, + }, + }, + }, + responses: { + 201: { + description: + "User created. `emailVerified` is true when outbound mail is disabled (`SEND_EMAILS=false`).", + content: { + "application/json": { + schema: userRegisterResponseSchema, + }, + }, + }, + 400: { + description: "Validation error.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 409: sessionConflict409, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "get", path: "/api/users/me", diff --git a/new-deepnotes/packages/api/src/schemas/sessions.ts b/new-deepnotes/packages/api/src/schemas/sessions.ts index db647dd1..50dfe4f7 100644 --- a/new-deepnotes/packages/api/src/schemas/sessions.ts +++ b/new-deepnotes/packages/api/src/schemas/sessions.ts @@ -91,3 +91,18 @@ export const sessionDemoRequestSchema = z .openapi("SessionDemoRequest"); export type SessionDemoRequest = z.infer; + +/** + * `POST /api/users` — same crypto payload as demo registration plus email and login hash. + */ +export const userRegisterRequestSchema = sessionDemoRequestSchema + .extend({ + email: z + .string() + .email() + .transform((e) => e.trim().toLowerCase()), + loginHash: byteB64, + }) + .openapi("UserRegisterRequest"); + +export type UserRegisterRequest = z.infer; diff --git a/new-deepnotes/packages/api/src/schemas/users.ts b/new-deepnotes/packages/api/src/schemas/users.ts index 77a15244..3ee106af 100644 --- a/new-deepnotes/packages/api/src/schemas/users.ts +++ b/new-deepnotes/packages/api/src/schemas/users.ts @@ -13,3 +13,12 @@ export const userMeResponseSchema = z .openapi("UserMeResponse"); export type UserMeResponse = z.infer; + +export const userRegisterResponseSchema = z + .object({ + userId: z.string(), + emailVerified: z.boolean(), + }) + .openapi("UserRegisterResponse"); + +export type UserRegisterResponse = z.infer; diff --git a/new-deepnotes/packages/session/src/crypto/session-crypto.ts b/new-deepnotes/packages/session/src/crypto/session-crypto.ts index b7c9e5b2..3293423c 100644 --- a/new-deepnotes/packages/session/src/crypto/session-crypto.ts +++ b/new-deepnotes/packages/session/src/crypto/session-crypto.ts @@ -11,6 +11,7 @@ import { base64ToBytes, bytesToText, concatUint8Arrays, + textToBytes, } from "./bytes.js"; import { cryptoJsWordArrayToUint8Array } from "./crypto-js-wordarray.js"; import { wrapSymmetricKey } from "./symmetric-key.js"; @@ -55,6 +56,16 @@ export function decryptUserRehashedLoginHash( ); } +export function encryptUserRehashedLoginHash( + userRehashedLoginHashPhc: string, + encryptionKeyB64: string, +): Uint8Array { + const key = wrapSymmetricKey(base64ToBytes(encryptionKeyB64)); + return key.encrypt(textToBytes(userRehashedLoginHashPhc), { + associatedData: { context: "UserRehashedLoginHash" }, + }); +} + export function decryptUserAuthenticatorSecret( userEncryptedAuthenticatorSecret: Uint8Array, encryptionKeyB64: string, diff --git a/new-deepnotes/packages/session/src/datetime.ts b/new-deepnotes/packages/session/src/datetime.ts index 3e35b945..b413d8a0 100644 --- a/new-deepnotes/packages/session/src/datetime.ts +++ b/new-deepnotes/packages/session/src/datetime.ts @@ -3,3 +3,9 @@ export function addDays(date: Date, days: number): Date { d.setUTCDate(d.getUTCDate() + days); return d; } + +export function addHours(date: Date, hours: number): Date { + const d = new Date(date); + d.setUTCHours(d.getUTCHours() + hours); + return d; +} diff --git a/new-deepnotes/packages/session/src/env.ts b/new-deepnotes/packages/session/src/env.ts index 3afe98c7..6c4b2ea8 100644 --- a/new-deepnotes/packages/session/src/env.ts +++ b/new-deepnotes/packages/session/src/env.ts @@ -16,6 +16,11 @@ export type SessionEnv = { COOKIE_DOMAIN?: string; /** Semicolon-separated emails that skip lowercasing (legacy). */ EMAIL_CASE_SENSITIVITY_EXCEPTIONS?: string; + /** + * When `"false"`, skip outbound verification email and mark the account verified + * immediately after insert (legacy `SEND_EMAILS=false` / local dev). + */ + SEND_EMAILS?: string; }; export function isDev(env: Pick): boolean { diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 9b49c96e..143addff 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -12,5 +12,7 @@ export type { SessionStartDemoInput, SessionStartDemoPageCreation, } from "./start-demo.js"; +export { performUserRegister } from "./register-user.js"; +export type { UserRegisterInput } from "./register-user.js"; export { getAuthenticatedUserSummary } from "./user-me.js"; export type { AuthenticatedUserSummary } from "./user-me.js"; diff --git a/new-deepnotes/packages/session/src/register-user.ts b/new-deepnotes/packages/session/src/register-user.ts new file mode 100644 index 00000000..af4e9d2c --- /dev/null +++ b/new-deepnotes/packages/session/src/register-user.ts @@ -0,0 +1,281 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { and, eq, gt, or } from "drizzle-orm"; +import { nanoid } from "nanoid"; + +import { + groupMembers, + groups, + pages, + users, + usersPages, +} from "@deepnotes/db/schema"; + +import { + createPrivateKeyring, + createSymmetricKeyring, +} from "./crypto/index.js"; +import { encodePasswordHash } from "./crypto/password-hashing.js"; +import { + derivePasswordValues, + encryptUserRehashedLoginHash, + ensureSodiumReady, +} from "./crypto/session-crypto.js"; +import type { SessionStartDemoInput } from "./start-demo.js"; +import { addHours } from "./datetime.js"; +import type { SessionEnv } from "./env.js"; +import { encryptUserEmail } from "./encrypt-user-email.js"; +import { hashUserEmail } from "./email-hash.js"; +import { SessionError } from "./errors.js"; + +export type UserRegisterInput = SessionStartDemoInput & { + email: string; + loginHash: Uint8Array; +}; + +function toBuf(u: Uint8Array): Buffer { + return Buffer.from(u); +} + +function sendEmailsEnabled(env: SessionEnv): boolean { + return env.SEND_EMAILS !== "false"; +} + +async function markUserEmailVerifiedByCode( + tx: DeepnotesDb, + emailVerificationCode: string, +): Promise { + const rows = await tx + .select({ + id: users.id, + encryptedNewEmail: users.encryptedNewEmail, + }) + .from(users) + .where( + and( + eq(users.emailVerified, false), + eq(users.emailVerificationCode, emailVerificationCode), + gt(users.emailVerificationExpirationDate, new Date().toISOString()), + ), + ) + .limit(1); + + const row = rows[0]; + if (row == null) { + throw new SessionError( + 500, + "SERVER_MISCONFIG", + "Auto email verification failed after registration.", + ); + } + + if (row.encryptedNewEmail == null) { + throw new SessionError( + 500, + "SERVER_MISCONFIG", + "Missing encrypted_new_email for verification.", + ); + } + + const updated = await tx + .update(users) + .set({ + encryptedEmail: row.encryptedNewEmail, + encryptedNewEmail: null, + emailVerified: true, + emailVerificationCode: null, + emailVerificationExpirationDate: null, + }) + .where(eq(users.id, row.id)) + .returning({ id: users.id }); + + if (updated.length !== 1) { + throw new SessionError( + 500, + "SERVER_MISCONFIG", + "Email verification update did not apply.", + ); + } +} + +/** + * Replaces legacy `users.account.register`: creates user (password-backed keyrings), + * personal group + page, email verification state; optionally auto-verifies when + * `SEND_EMAILS=false` (local/CI parity with legacy). + */ +export async function performUserRegister(input: { + db: DeepnotesDb; + env: SessionEnv; + body: UserRegisterInput; +}): Promise<{ userId: string; emailVerified: boolean }> { + await ensureSodiumReady(); + + const gc = input.body.groupCreation; + if ( + gc.groupPasswordHash != null && + gc.groupPasswordHash.byteLength > 0 + ) { + throw new SessionError( + 400, + "VALIDATION_ERROR", + "Registration with a group password is not supported yet.", + ); + } + + const email = input.body.email.trim().toLowerCase(); + const exceptions = input.env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS ?? ""; + const emailHash = Buffer.from( + await hashUserEmail(email, input.env.USER_EMAIL_SECRET, exceptions), + ); + const encryptedEmailBuf = Buffer.from( + encryptUserEmail(email, input.env.USER_EMAIL_ENCRYPTION_KEY, exceptions), + ); + + const passwordValues = derivePasswordValues({ + password: input.body.loginHash, + }); + const encodedRehash = encodePasswordHash( + passwordValues.hash, + passwordValues.salt, + 2, + 32, + ); + const encryptedRehashedLoginHash = toBuf( + encryptUserRehashedLoginHash( + encodedRehash, + input.env.USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + ), + ); + + const encryptedPrivateStored = toBuf( + createPrivateKeyring(input.body.userEncryptedPrivateKeyring) + .wrapSymmetric(passwordValues.key, { + associatedData: { + context: "UserEncryptedPrivateKeyring", + userId: input.body.userId, + }, + }).wrappedValue, + ); + + const encryptedSymmetricStored = toBuf( + createSymmetricKeyring(input.body.userEncryptedSymmetricKeyring) + .wrapSymmetric(passwordValues.key, { + associatedData: { + context: "UserEncryptedSymmetricKeyring", + userId: input.body.userId, + }, + }).wrappedValue, + ); + + const pc = input.body.pageCreation; + const emailVerificationCode = nanoid(); + const emailVerificationExpirationDate = addHours( + new Date(), + 1, + ).toISOString(); + + const existing = await input.db + .select({ + emailVerified: users.emailVerified, + }) + .from(users) + .where( + and( + eq(users.emailHash, emailHash), + or( + eq(users.emailVerified, true), + gt(users.emailVerificationExpirationDate, new Date().toISOString()), + ), + ), + ) + .limit(1); + + const hit = existing[0]; + if (hit != null) { + if (hit.emailVerified) { + throw new SessionError( + 409, + "CONFLICT", + "Email already registered.", + ); + } + throw new SessionError( + 401, + "UNAUTHORIZED", + "Email awaiting verification. New email sent.", + ); + } + + return await input.db.transaction(async (tx) => { + await tx.delete(users).where(eq(users.emailHash, emailHash)); + + await tx.insert(users).values({ + id: input.body.userId, + encryptedEmail: encryptedEmailBuf, + emailHash, + encryptedNewEmail: encryptedEmailBuf, + emailVerificationCode, + emailVerificationExpirationDate, + encryptedRehashedLoginHash, + demo: false, + emailVerified: false, + personalGroupId: input.body.groupId, + startingPageId: input.body.pageId, + recentPageIds: [input.body.pageId], + recentGroupIds: [input.body.groupId], + publicKeyring: toBuf(input.body.userPublicKeyring), + encryptedPrivateKeyring: encryptedPrivateStored, + encryptedSymmetricKeyring: encryptedSymmetricStored, + encryptedName: toBuf(input.body.userEncryptedName), + encryptedDefaultNote: toBuf(input.body.userEncryptedDefaultNote), + encryptedDefaultArrow: toBuf(input.body.userEncryptedDefaultArrow), + }); + + await tx.insert(groups).values({ + id: input.body.groupId, + mainPageId: input.body.pageId, + encryptedName: toBuf(gc.groupEncryptedName), + userId: input.body.userId, + publicKeyring: toBuf(gc.groupPublicKeyring), + encryptedPrivateKeyring: toBuf(gc.groupEncryptedPrivateKeyring), + encryptedContentKeyring: toBuf(gc.groupEncryptedContentKeyring), + accessKeyring: gc.groupIsPublic ? toBuf(gc.groupAccessKeyring) : null, + }); + + await tx.insert(groupMembers).values({ + groupId: input.body.groupId, + userId: input.body.userId, + role: "owner", + encryptedAccessKeyring: gc.groupIsPublic + ? null + : toBuf(gc.groupAccessKeyring), + encryptedInternalKeyring: toBuf(gc.groupEncryptedInternalKeyring), + encryptedName: toBuf(gc.groupOwnerEncryptedName), + }); + + await tx.insert(pages).values({ + id: input.body.pageId, + groupId: input.body.groupId, + encryptedRelativeTitle: toBuf(pc.pageEncryptedRelativeTitle), + encryptedSymmetricKeyring: toBuf(pc.pageEncryptedSymmetricKeyring), + encryptedAbsoluteTitle: toBuf(pc.pageEncryptedAbsoluteTitle), + free: true, + }); + + await tx.insert(usersPages).values({ + userId: input.body.userId, + pageId: input.body.pageId, + lastParentId: null, + }); + + let emailVerified = false; + if (!sendEmailsEnabled(input.env)) { + await markUserEmailVerifiedByCode( + tx as unknown as DeepnotesDb, + emailVerificationCode, + ); + emailVerified = true; + } + + return { userId: input.body.userId, emailVerified }; + }); +} diff --git a/new-deepnotes/template.env b/new-deepnotes/template.env index 37b6013a..35402f18 100644 --- a/new-deepnotes/template.env +++ b/new-deepnotes/template.env @@ -22,4 +22,5 @@ REDIS_URL=redis://localhost:6380 # USER_RECOVERY_CODES_ENCRYPTION_KEY= # base64 # COOKIE_DOMAIN= # optional; legacy used HOST # DEV=true # local HTTP without Secure cookies +# SEND_EMAILS=false # optional; when false, new accounts skip mail and are verified immediately (legacy parity) # STRIPE_WEBHOOK_SECRET= From 1471cb23640491efacf3159268b8927e8bc62709 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 23:37:58 -0300 Subject: [PATCH 030/243] feat(new-deepnotes): email verification and registration email delivery --- new-deepnotes/PLAN_PROGRESS.md | 20 +++- .../apps/api-worker/src/index.test.ts | 15 +++ new-deepnotes/apps/api-worker/src/index.ts | 108 ++++++++++++++++++ .../apps/api-worker/src/session-env.ts | 6 + new-deepnotes/docs/TRPC_REST_MAP.md | 4 +- new-deepnotes/packages/api/src/index.ts | 2 + .../packages/api/src/openapi.test.ts | 6 + new-deepnotes/packages/api/src/openapi.ts | 89 +++++++++++++++ .../packages/api/src/schemas/users.ts | 17 +++ .../session/src/email-verification.ts | 106 +++++++++++++++++ new-deepnotes/packages/session/src/env.ts | 6 + new-deepnotes/packages/session/src/index.ts | 4 + .../packages/session/src/register-user.ts | 31 ++++- .../session/src/send-registration-email.ts | 71 ++++++++++++ new-deepnotes/template.env | 4 +- 15 files changed, 481 insertions(+), 8 deletions(-) create mode 100644 new-deepnotes/packages/session/src/email-verification.ts create mode 100644 new-deepnotes/packages/session/src/send-registration-email.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index c5b34a57..5694c510 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Mostly done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). Optional: Wrangler deploy job. **Gap:** `apps/web` tests still no-op—see Phase 2 checklist + [Frontend / UI track](#frontend--ui-track). | -| **3** — REST + Drizzle features | **In progress** | `POST /api/sessions/login|refresh|logout` + **`POST /api/sessions/demo`** + **`GET /api/users/me`** + **`POST /api/users`** (registration) via `@deepnotes/session`. Optional **Upstash** (`UPSTASH_REDIS_REST_*`) for failed-login limits; **`SEND_EMAILS=false`** auto-verifies new users (no mailer yet). Next: email verification resend/confirm REST, pages/groups CRUD, Stripe. | +| **3** — REST + Drizzle features | **In progress** | Auth + registration + **email resend/confirm** (Resend) below; optional **Upstash** for login rate limits. **Next (Phase 3):** pages/groups CRUD, realtime/collab, **Stripe** webhook, then Phase 2 gap (**real `apps/web` tests** in parallel is OK). | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, Vitest+DOM in CI, small E2E smoke—see [Frontend / UI track](#frontend--ui-track) (not deferred to “when MVP is done”). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -31,14 +31,25 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ ## Phase 3 checklist (REST + Drizzle) +### Sessions + account (current) + - [x] Document **sessions** REST paths + request schemas in OpenAPI; demo + `users/me` contracts updated. - [x] Implement **sessions.login** / refresh / logout against Drizzle + legacy crypto semantics (JWT via `jose`; optional **Redis** failed-login limits when Upstash env is set). - [x] Implement **sessions.start-demo** (`POST /api/sessions/demo`) + **Redis** for failed-login when Upstash env is set. - [x] **JWT + httpOnly cookies** (`accessToken`, `refreshToken`, `loggedIn`) matching [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md). - [x] **`GET /api/users/me`** (minimal summary from `accessToken` cookie). -- [x] **Users** `POST /api/users` registration (crypto payload aligned with demo; conflict / unverified parity; optional `SEND_EMAILS=false` auto-verify). -- [ ] **Users** email verification resend/confirm (`POST /api/users/me/email-verification/*`) + remaining TRPC_REST_MAP slices as needed. -- [ ] Pages/groups CRUD, realtime/collab, Stripe webhook (no RevenueCat). +- [x] **Users** `POST /api/users` registration (crypto payload aligned with demo; conflict / unverified parity; `SEND_EMAILS=false` auto-verifies; when mail is on, `RESEND_API_KEY` required and registration email is sent after commit). +- [x] **Users — email verification** + - [x] `POST /api/users/email-verification/resend` — public, `{ "email" }` (legacy `resendVerificationEmail`); 204 / 400 / 404 / 409 / 502 / 503; [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md) updated (paths are **not** under `/me/`; legacy was never cookie-based). + - [x] `POST /api/users/email-verification/confirm` — public, `{ "emailVerificationCode" }` (nanoid, legacy `verifyEmail`); 204 / 400; DB update copies `encrypted_new_email` → `encrypted_email`. + - [x] `sendRegistrationEmail` + optional **`RESEND_API_KEY`**, optional **`PUBLIC_APP_URL`** in `SessionEnv` / [template.env](./template.env); duplicate unverified registration re-sends via same helper (401 “New email sent”). + +### Not started (Phase 3 remainder) + +- [ ] **Account (remaining tRPC):** `POST /api/users/me/email-change` (+ confirm), 2FA enable/load/disable/recovery routes from [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md), `DELETE /api/users/me`, password change (see map + legacy WS). +- [ ] **Pages** (user prefs + CRUD) and **groups** CRUD / privacy / passwords per map. +- [ ] **Realtime / collab** (new or adapted protocols; no key rotation). +- [ ] **Stripe:** `POST /api/webhooks/stripe`, checkout/portal (no RevenueCat). --- @@ -111,6 +122,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-26 | Phase 3: email verification `POST /api/users/email-verification/resend` and `…/confirm`; `performResendEmailVerification` / `performConfirmEmailVerification`; Resend in `sendRegistrationEmail`; `RESEND_API_KEY` + `PUBLIC_APP_URL`; first mail on register + re-send on duplicate unverified; OpenAPI 502 on register if provider fails; `c.env?.HYPERDRIVE` on confirm for Vitest. | | 2026-04-26 | Phase 3: **`POST /api/users`** (`performUserRegister`), `encryptUserRehashedLoginHash`, `addHours`, OpenAPI 201/400/401/409; optional **`SEND_EMAILS`** on session env (auto-verify when `false`); group password on register still rejected (same as demo). | | 2026-04-26 | Phase 3: `POST /api/sessions/demo` (`performSessionStartDemo`), `GET /api/users/me`, Redis failed-login limits (`SessionRedisPort` + optional Upstash), `USER_EMAIL_ENCRYPTION_KEY` on `SessionEnv`; OpenAPI 200/400 for demo, 429 for login, `userMeResponseSchema`; Vitest `login-rate-limit.test.ts`. | | 2026-04-26 | Docs: [docs/RESTART_PLAN.md](../docs/RESTART_PLAN.md) §3.5 legacy frontend pain points, §5.8 frontend testing/CI, phased updates; this file: **Frontend / UI track** + Phase 2/4 notes on real web tests. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 18644072..3f2c3c3d 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -29,6 +29,7 @@ describe("api-worker", () => { ["POST", "/api/sessions/demo"], ["GET", "/api/users/me"], ["POST", "/api/users"], + ["POST", "/api/users/email-verification/resend"], ] as const)("returns 503 for %s %s when auth env is not configured", async (method, path) => { const res = await app.request(`http://test${path}`, { method }); expect(res.status).toBe(503); @@ -37,4 +38,18 @@ describe("api-worker", () => { }); }); + it("returns 503 for POST /api/users/email-verification/confirm when hyperdrive is not bound", async () => { + const res = await app.request( + "http://test/api/users/email-verification/confirm", + { + method: "POST", + body: JSON.stringify({ emailVerificationCode: "a".repeat(21) }), + headers: { "Content-Type": "application/json" }, + }, + ); + expect(res.status).toBe(503); + await expect(res.json()).resolves.toMatchObject({ + code: "SERVICE_UNAVAILABLE", + }); + }); }); diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index a36e73bd..1aad3f9a 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -1,4 +1,6 @@ import { + emailVerificationConfirmRequestSchema, + emailVerificationResendRequestSchema, getOpenApiDocument, healthResponseSchema, sessionDemoRequestSchema, @@ -366,4 +368,110 @@ app.get("/api/users/me", async (c) => { } }); +app.post("/api/users/email-verification/resend", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = emailVerificationResendRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + try { + const { performResendEmailVerification } = await import( + "@deepnotes/session" + ); + await performResendEmailVerification({ + db, + env: sessionEnv, + email: parsed.data.email, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/users/email-verification/confirm", async (c) => { + const hyper = c.env?.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = emailVerificationConfirmRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + try { + const { performConfirmEmailVerification } = await import( + "@deepnotes/session" + ); + await performConfirmEmailVerification({ db, body: parsed.data }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + export default app; diff --git a/new-deepnotes/apps/api-worker/src/session-env.ts b/new-deepnotes/apps/api-worker/src/session-env.ts index 755be871..d58bd78e 100644 --- a/new-deepnotes/apps/api-worker/src/session-env.ts +++ b/new-deepnotes/apps/api-worker/src/session-env.ts @@ -13,6 +13,10 @@ export type WorkerSessionBindings = { EMAIL_CASE_SENSITIVITY_EXCEPTIONS?: string; /** When `"false"`, new registrations are email-verified without sending mail (local/CI). */ SEND_EMAILS?: string; + /** Resend.com API key; required when `SEND_EMAILS` is not `false` and email is sent. */ + RESEND_API_KEY?: string; + /** Optional; default `https://deepnotes.app` for verification links. */ + PUBLIC_APP_URL?: string; /** Optional; when set with token, failed-login rate limits use Upstash REST Redis. */ UPSTASH_REDIS_REST_URL?: string; UPSTASH_REDIS_REST_TOKEN?: string; @@ -56,5 +60,7 @@ export function getSessionEnv( COOKIE_DOMAIN: env.COOKIE_DOMAIN, EMAIL_CASE_SENSITIVITY_EXCEPTIONS: env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS, SEND_EMAILS: env.SEND_EMAILS, + RESEND_API_KEY: env.RESEND_API_KEY, + PUBLIC_APP_URL: env.PUBLIC_APP_URL, }; } diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index 38a3495d..f2838edb 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -16,8 +16,8 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | Legacy procedure | Proposed REST / notes | |------------------|----------------------| | `users.account.register` | `POST /api/users` | -| `users.account.resendVerificationEmail` | `POST /api/users/me/email-verification/resend` | -| `users.account.verifyEmail` | `POST /api/users/me/email-verification/confirm` | +| `users.account.resendVerificationEmail` | `POST /api/users/email-verification/resend` (public; body `{ "email" }` — matches legacy, not an authenticated “me” call) | +| `users.account.verifyEmail` | `POST /api/users/email-verification/confirm` (public; body `{ "emailVerificationCode" }`, nanoid) | | `users.account.emailChange.request` | `POST /api/users/me/email-change` | | `users.account.twoFactorAuth.enable.request` | `POST /api/users/me/2fa/enable/request` | | `users.account.twoFactorAuth.enable.finish` | `POST /api/users/me/2fa/enable/finish` | diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index 8ad2599b..01728d83 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -23,6 +23,8 @@ export { type UserRegisterRequest, } from "./schemas/sessions.js"; export { + emailVerificationConfirmRequestSchema, + emailVerificationResendRequestSchema, userMeResponseSchema, userRegisterResponseSchema, type UserMeResponse, diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 3638e568..998ac9d9 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -16,5 +16,11 @@ describe("getOpenApiDocument", () => { expect(doc.paths?.["/api/sessions/demo"]?.post).toBeDefined(); expect(doc.paths?.["/api/users/me"]?.get).toBeDefined(); expect(doc.paths?.["/api/users"]?.post).toBeDefined(); + expect( + doc.paths?.["/api/users/email-verification/resend"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/users/email-verification/confirm"]?.post, + ).toBeDefined(); }); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index 69578f2b..ef6e81bd 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -17,6 +17,8 @@ import { userRegisterRequestSchema, } from "./schemas/sessions.js"; import { + emailVerificationConfirmRequestSchema, + emailVerificationResendRequestSchema, userMeResponseSchema, userRegisterResponseSchema, } from "./schemas/users.js"; @@ -60,6 +62,15 @@ const sessionConflict409 = { }, } as const; +const sessionNotFound404 = { + description: "Resource not found.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, +} as const; + registry.registerPath({ method: "get", path: "/api/health", @@ -141,6 +152,14 @@ registry.registerPath({ }, 401: sessionUnauthorized401, 409: sessionConflict409, + 502: { + description: "Email send failed (e.g. Resend API error after user row was created; rare).", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, 503: sessionServiceUnavailable503, }, }); @@ -165,6 +184,76 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/users/email-verification/resend", + summary: "Resend email verification (public, by email)", + description: + "Replaces legacy `users.account.resendVerificationEmail`. Uses Resend when `SEND_EMAILS` is not `false`.", + request: { + body: { + content: { + "application/json": { + schema: emailVerificationResendRequestSchema, + }, + }, + }, + }, + responses: { + 204: { + description: "Email sent (or accepted by provider).", + }, + 400: { + description: "Validation error or outbound email disabled for this environment.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 404: sessionNotFound404, + 409: sessionConflict409, + 502: { + description: "Email provider (Resend) request failed.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/email-verification/confirm", + summary: "Confirm email with nanoid code", + description: "Replaces legacy `users.account.verifyEmail` (public).", + request: { + body: { + content: { + "application/json": { + schema: emailVerificationConfirmRequestSchema, + }, + }, + }, + }, + responses: { + 204: { + description: "Email verified; account updated.", + }, + 400: { + description: "Invalid or expired code.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + }, +}); + registry.registerPath({ method: "post", path: "/api/sessions/refresh", diff --git a/new-deepnotes/packages/api/src/schemas/users.ts b/new-deepnotes/packages/api/src/schemas/users.ts index 3ee106af..0f5b7512 100644 --- a/new-deepnotes/packages/api/src/schemas/users.ts +++ b/new-deepnotes/packages/api/src/schemas/users.ts @@ -22,3 +22,20 @@ export const userRegisterResponseSchema = z .openapi("UserRegisterResponse"); export type UserRegisterResponse = z.infer; + +const nanoidVerificationCode = z + .string() + .min(1) + .regex(/^[A-Za-z0-9_-]{21}$/, "expected nanoid verification code"); + +export const emailVerificationResendRequestSchema = z + .object({ + email: z.string().email(), + }) + .openapi("EmailVerificationResendRequest"); + +export const emailVerificationConfirmRequestSchema = z + .object({ + emailVerificationCode: nanoidVerificationCode, + }) + .openapi("EmailVerificationConfirmRequest"); diff --git a/new-deepnotes/packages/session/src/email-verification.ts b/new-deepnotes/packages/session/src/email-verification.ts new file mode 100644 index 00000000..c01979a7 --- /dev/null +++ b/new-deepnotes/packages/session/src/email-verification.ts @@ -0,0 +1,106 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { and, eq, gt, isNotNull, sql } from "drizzle-orm"; +import { users } from "@deepnotes/db/schema"; + +import type { SessionEnv } from "./env.js"; +import { hashUserEmail } from "./email-hash.js"; +import { SessionError } from "./errors.js"; +import { sendRegistrationEmail } from "./send-registration-email.js"; + +/** + * Replaces legacy `users.account.resendVerificationEmail` (public, email in body). + */ +export async function performResendEmailVerification(input: { + db: DeepnotesDb; + env: SessionEnv; + email: string; +}): Promise { + if (input.env.SEND_EMAILS === "false") { + throw new SessionError( + 400, + "BAD_REQUEST", + "Email sending is disabled (SEND_EMAILS=false); cannot resend verification.", + ); + } + + const email = input.email.trim().toLowerCase(); + const exceptions = input.env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS ?? ""; + const emailHash = Buffer.from( + await hashUserEmail(email, input.env.USER_EMAIL_SECRET, exceptions), + ); + + const rows = await input.db + .select({ + emailVerified: users.emailVerified, + emailVerificationCode: users.emailVerificationCode, + }) + .from(users) + .where(eq(users.emailHash, emailHash)) + .limit(1); + + const row = rows[0]; + if (row == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + if (row.emailVerified) { + throw new SessionError( + 409, + "CONFLICT", + "Email already registered.", + ); + } + if ( + row.emailVerificationCode == null || + row.emailVerificationCode.length === 0 + ) { + throw new SessionError( + 500, + "SERVER_ERROR", + "Missing email verification state.", + ); + } + + await sendRegistrationEmail({ + env: input.env, + toEmail: email, + emailVerificationCode: row.emailVerificationCode, + }); +} + +/** + * Replaces legacy `users.account.verifyEmail` (public, nanoid code). + * Promotes `encrypted_new_email` → `encrypted_email` (same as legacy `ref` patch). + */ +export async function performConfirmEmailVerification(input: { + db: DeepnotesDb; + body: { emailVerificationCode: string }; +}): Promise { + const code = input.body.emailVerificationCode; + + const result = await input.db + .update(users) + .set({ + encryptedEmail: sql`encrypted_new_email`, + encryptedNewEmail: null, + emailVerified: true, + emailVerificationCode: null, + emailVerificationExpirationDate: null, + }) + .where( + and( + eq(users.emailVerified, false), + eq(users.emailVerificationCode, code), + gt(users.emailVerificationExpirationDate, new Date().toISOString()), + isNotNull(users.encryptedNewEmail), + ), + ) + .returning({ id: users.id }); + + if (result.length !== 1) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Invalid email verification code.", + ); + } +} diff --git a/new-deepnotes/packages/session/src/env.ts b/new-deepnotes/packages/session/src/env.ts index 6c4b2ea8..fcacc125 100644 --- a/new-deepnotes/packages/session/src/env.ts +++ b/new-deepnotes/packages/session/src/env.ts @@ -21,6 +21,12 @@ export type SessionEnv = { * immediately after insert (legacy `SEND_EMAILS=false` / local dev). */ SEND_EMAILS?: string; + /** + * When `SEND_EMAILS` is not `"false"`, used to send registration / resend email (Resend HTTP API). + */ + RESEND_API_KEY?: string; + /** Origin for the verification link in the email; defaults to `https://deepnotes.app`. */ + PUBLIC_APP_URL?: string; }; export function isDev(env: Pick): boolean { diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 143addff..9468b571 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -14,5 +14,9 @@ export type { } from "./start-demo.js"; export { performUserRegister } from "./register-user.js"; export type { UserRegisterInput } from "./register-user.js"; +export { + performConfirmEmailVerification, + performResendEmailVerification, +} from "./email-verification.js"; export { getAuthenticatedUserSummary } from "./user-me.js"; export type { AuthenticatedUserSummary } from "./user-me.js"; diff --git a/new-deepnotes/packages/session/src/register-user.ts b/new-deepnotes/packages/session/src/register-user.ts index af4e9d2c..1c7335da 100644 --- a/new-deepnotes/packages/session/src/register-user.ts +++ b/new-deepnotes/packages/session/src/register-user.ts @@ -26,6 +26,10 @@ import type { SessionEnv } from "./env.js"; import { encryptUserEmail } from "./encrypt-user-email.js"; import { hashUserEmail } from "./email-hash.js"; import { SessionError } from "./errors.js"; +import { + assertOutboundEmailConfiguredForRegistration, + sendRegistrationEmail, +} from "./send-registration-email.js"; export type UserRegisterInput = SessionStartDemoInput & { email: string; @@ -109,6 +113,10 @@ export async function performUserRegister(input: { }): Promise<{ userId: string; emailVerified: boolean }> { await ensureSodiumReady(); + if (sendEmailsEnabled(input.env)) { + assertOutboundEmailConfiguredForRegistration(input.env); + } + const gc = input.body.groupCreation; if ( gc.groupPasswordHash != null && @@ -176,6 +184,7 @@ export async function performUserRegister(input: { const existing = await input.db .select({ emailVerified: users.emailVerified, + emailVerificationCode: users.emailVerificationCode, }) .from(users) .where( @@ -198,6 +207,16 @@ export async function performUserRegister(input: { "Email already registered.", ); } + if ( + hit.emailVerificationCode != null && + hit.emailVerificationCode.length > 0 + ) { + await sendRegistrationEmail({ + env: input.env, + toEmail: email, + emailVerificationCode: hit.emailVerificationCode, + }); + } throw new SessionError( 401, "UNAUTHORIZED", @@ -205,7 +224,7 @@ export async function performUserRegister(input: { ); } - return await input.db.transaction(async (tx) => { + const result = await input.db.transaction(async (tx) => { await tx.delete(users).where(eq(users.emailHash, emailHash)); await tx.insert(users).values({ @@ -278,4 +297,14 @@ export async function performUserRegister(input: { return { userId: input.body.userId, emailVerified }; }); + + if (sendEmailsEnabled(input.env) && !result.emailVerified) { + await sendRegistrationEmail({ + env: input.env, + toEmail: email, + emailVerificationCode, + }); + } + + return result; } diff --git a/new-deepnotes/packages/session/src/send-registration-email.ts b/new-deepnotes/packages/session/src/send-registration-email.ts new file mode 100644 index 00000000..828e1d48 --- /dev/null +++ b/new-deepnotes/packages/session/src/send-registration-email.ts @@ -0,0 +1,71 @@ +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; + +function sendEmailsEnabled(env: SessionEnv): boolean { + return env.SEND_EMAILS !== "false"; +} + +export function assertOutboundEmailConfiguredForRegistration(env: SessionEnv): void { + if (!sendEmailsEnabled(env)) { + return; + } + const key = env.RESEND_API_KEY?.trim(); + if (key == null || key.length === 0) { + throw new SessionError( + 503, + "SERVICE_UNAVAILABLE", + "RESEND_API_KEY is required when outbound email is enabled (SEND_EMAILS is not false).", + ); + } +} + +/** + * Sends the legacy-style registration verification link via Resend. + * When `SEND_EMAILS=false`, is a no-op. + */ +export async function sendRegistrationEmail(input: { + env: SessionEnv; + toEmail: string; + emailVerificationCode: string; +}): Promise { + if (!sendEmailsEnabled(input.env)) { + return; + } + const key = input.env.RESEND_API_KEY?.trim(); + if (key == null || key.length === 0) { + throw new SessionError( + 503, + "SERVICE_UNAVAILABLE", + "RESEND_API_KEY is required to send verification email.", + ); + } + + const base = + (input.env.PUBLIC_APP_URL ?? "https://deepnotes.app").replace(/\/$/, ""); + const verifyUrl = `${base}/verify-email/${input.emailVerificationCode}`; + + const res = await fetch("https://api.resend.com/emails", { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + from: "DeepNotes ", + to: [input.toEmail], + subject: "Complete your registration", + html: `Visit the following link to verify your email address:
+${verifyUrl}
+The link above expires in 1 hour.`, + }), + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new SessionError( + 502, + "EMAIL_SEND_FAILED", + `Resend request failed (${res.status}): ${text.slice(0, 200)}`, + ); + } +} diff --git a/new-deepnotes/template.env b/new-deepnotes/template.env index 35402f18..ebcf41b9 100644 --- a/new-deepnotes/template.env +++ b/new-deepnotes/template.env @@ -22,5 +22,7 @@ REDIS_URL=redis://localhost:6380 # USER_RECOVERY_CODES_ENCRYPTION_KEY= # base64 # COOKIE_DOMAIN= # optional; legacy used HOST # DEV=true # local HTTP without Secure cookies -# SEND_EMAILS=false # optional; when false, new accounts skip mail and are verified immediately (legacy parity) +# SEND_EMAILS=false # when unset, outbound mail is ON and registration/resend need RESEND_API_KEY +# RESEND_API_KEY= # Resend.com; required if SEND_EMAILS is not false +# PUBLIC_APP_URL=https://deepnotes.app # optional; verification link base # STRIPE_WEBHOOK_SECRET= From 3bd309cc1231e0df0383416a23a62c193b7fdbaf Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 23:46:57 -0300 Subject: [PATCH 031/243] feat(new-deepnotes): user account deletion and web test harness --- new-deepnotes/PLAN_PROGRESS.md | 28 +- .../apps/api-worker/src/index.test.ts | 1 + new-deepnotes/apps/api-worker/src/index.ts | 73 ++++ new-deepnotes/apps/web/package.json | 5 +- new-deepnotes/apps/web/src/app.test.ts | 12 + new-deepnotes/apps/web/vite.config.ts | 6 +- new-deepnotes/docs/TRPC_REST_MAP.md | 2 +- new-deepnotes/packages/api/src/index.ts | 2 + .../packages/api/src/openapi.test.ts | 1 + new-deepnotes/packages/api/src/openapi.ts | 35 ++ .../packages/api/src/schemas/users.ts | 18 + .../session/src/delete-user-account.ts | 173 ++++++++++ new-deepnotes/packages/session/src/index.ts | 1 + new-deepnotes/pnpm-lock.yaml | 318 +++++++++++++++++- 14 files changed, 656 insertions(+), 19 deletions(-) create mode 100644 new-deepnotes/apps/web/src/app.test.ts create mode 100644 new-deepnotes/packages/session/src/delete-user-account.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 5694c510..3ab69257 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -12,9 +12,9 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ |-------|--------|--------| | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | -| **2** — Repo bootstrap | **Mostly done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). Optional: Wrangler deploy job. **Gap:** `apps/web` tests still no-op—see Phase 2 checklist + [Frontend / UI track](#frontend--ui-track). | -| **3** — REST + Drizzle features | **In progress** | Auth + registration + **email resend/confirm** (Resend) below; optional **Upstash** for login rate limits. **Next (Phase 3):** pages/groups CRUD, realtime/collab, **Stripe** webhook, then Phase 2 gap (**real `apps/web` tests** in parallel is OK). | -| **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, Vitest+DOM in CI, small E2E smoke—see [Frontend / UI track](#frontend--ui-track) (not deferred to “when MVP is done”). | +| **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | +| **3** — REST + Drizzle features | **In progress** | Sessions, register, email verify/resend/confirm, **`DELETE /api/users/me`** (`performUserAccountDelete`: password + sole-owner guard, Drizzle tx; Stripe customer hook optional on worker). **Next:** `POST /api/users/me/password`, email-change + confirm, 2FA routes, pages/groups CRUD, realtime/collab, Stripe webhook. | +| **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | --- @@ -43,19 +43,23 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] `POST /api/users/email-verification/resend` — public, `{ "email" }` (legacy `resendVerificationEmail`); 204 / 400 / 404 / 409 / 502 / 503; [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md) updated (paths are **not** under `/me/`; legacy was never cookie-based). - [x] `POST /api/users/email-verification/confirm` — public, `{ "emailVerificationCode" }` (nanoid, legacy `verifyEmail`); 204 / 400; DB update copies `encrypted_new_email` → `encrypted_email`. - [x] `sendRegistrationEmail` + optional **`RESEND_API_KEY`**, optional **`PUBLIC_APP_URL`** in `SessionEnv` / [template.env](./template.env); duplicate unverified registration re-sends via same helper (401 “New email sent”). +- [x] **`DELETE /api/users/me`** — replaces legacy `users.account.delete`: JSON `{ "loginHash" }` (base64); verifies access JWT + password (`encrypted_rehashed_login_hash`); blocks when any membership has `member_count > 1` and `owner_count <= 1`; deletes join invites/requests, solo-member groups (cascade pages), remaining `group_members`, then user row; clears session cookies; optional `deleteStripeCustomer(customerId)` hook (worker can wire Stripe later; failures swallowed like legacy). ### Not started (Phase 3 remainder) -- [ ] **Account (remaining tRPC):** `POST /api/users/me/email-change` (+ confirm), 2FA enable/load/disable/recovery routes from [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md), `DELETE /api/users/me`, password change (see map + legacy WS). +- [ ] **Account (remaining tRPC / WS parity):** + - [ ] `POST /api/users/me/email-change` + `POST /api/users/me/email-change/confirm` (WS finish → REST). + - [ ] 2FA: `POST …/2fa/enable/request|finish`, `GET …/2fa`, `POST …/2fa/recovery-codes`, `POST …/2fa/devices/forget`, `POST …/2fa/disable` (see [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md)). + - [ ] `POST /api/users/me/password` (legacy WS `change-password` → REST). - [ ] **Pages** (user prefs + CRUD) and **groups** CRUD / privacy / passwords per map. - [ ] **Realtime / collab** (new or adapted protocols; no key rotation). -- [ ] **Stripe:** `POST /api/webhooks/stripe`, checkout/portal (no RevenueCat). +- [ ] **Stripe:** `POST /api/webhooks/stripe`, checkout/portal (no RevenueCat); wire **`deleteStripeCustomer`** from account delete when keys exist. --- ## Phase 4 checklist (client MVP) -- [ ] **Tooling:** Vitest + `jsdom` or `happy-dom` + `@vue/test-utils`; `@vitejs/plugin-vue` in Vitest config (same as [Frontend / UI track](#frontend--ui-track)). +- [x] **Tooling (bootstrap):** Vitest + **happy-dom** + `@vue/test-utils` in `@deepnotes/web` (minimal `App` test); same Vite 6 pipeline via `vitest/config` `defineConfig` (RESTART_PLAN §5.8). - [ ] **API client:** consume **OpenAPI** (generated types + `fetch`, or hey-api) from `@deepnotes/api` / published spec—**no** workspace dependency on Worker or DB packages from web source. - [ ] **Routing + auth UI:** login / refresh / logout / 2FA flows aligned with [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md); composable or component tests + **E2E smoke** for cookie session. - [ ] **Pages:** list → open editor shell → integrate **Yjs** / collab when API is ready. @@ -72,7 +76,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] Document **Pages** / preview vs production env vars; optional deploy job to CF preview → [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). - [x] CI: lint, typecheck, tests, `drizzle-kit check`, build (Postgres service present for future migrate/tests). - [x] CI: Postgres role with **CREATEDB** + **template DB** integration tests (RESTART_PLAN §5.7) — `DATABASE_ADMIN_URL` + `src/template-db.test.ts`. -- [ ] **Web package tests are real:** `apps/web` currently uses a **no-op** `test` script; replace with **Vitest** + `jsdom` or `happy-dom` + `@vue/test-utils` and wire into root `pnpm test` / CI (RESTART_PLAN §5.8). +- [x] **Web package tests are real:** `@deepnotes/web` runs `vitest run` with happy-dom; `src/app.test.ts` mounts `App.vue` (RESTART_PLAN §5.8). --- @@ -88,7 +92,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ### Testing (see RESTART_PLAN §5.8) -- [ ] **Vitest** in `apps/web` with DOM environment and `@vitejs/plugin-vue` aligned with Vite 6. +- [x] **Vitest** in `apps/web` with DOM environment (`happy-dom`) and `@vue/test-utils` aligned with Vite 6. - [ ] **Component or composable tests** for the first **auth** / session flows (forms, validation, error mapping from API). - [ ] **Contract tests** for the fetch wrapper (MSW or recorded OpenAPI fixtures)—optional until multiple features consume the API. - [ ] **E2E smoke** (Playwright recommended): login or session refresh with **httpOnly cookies** against **local compose** or **Cloudflare preview**—add CI job when stable enough (can start `manual`/`workflow_dispatch` if cost is a concern). @@ -97,9 +101,9 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Legacy (`apps/client`) | New (`new-deepnotes/apps/web`) | |------------------------|--------------------------------| -| Quasar + Vite 2, 4GB heap builds | Vite 6 + Vue 3.5, minimal app shell today | +| Quasar + Vite 2, 4GB heap builds | Vite 6 + Vue 3.5, Vitest + happy-dom in CI | | Imports `AppRouter`, server websocket paths | Must use **OpenAPI** + documented WS only | -| No automated UI tests | **To do:** real `test` script + CI | +| No automated UI tests | **Done:** real `test` script + one component test | --- @@ -114,7 +118,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. -- [ ] Web: Vitest + DOM env in CI; no server/db imports from web source; E2E smoke for session cookies (RESTART_PLAN §8 extended items). +- [x] Web: Vitest + DOM env in CI (happy-dom + `@vue/test-utils` on `App.vue`). +- [ ] Web: enforce **no** server/db imports from web source (ESLint `import/no-restricted-paths` or README when features land); **E2E** smoke for session cookies (RESTART_PLAN §8). --- @@ -122,6 +127,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-26 | Phase 2 + §5.8: `@deepnotes/web` — Vitest + happy-dom + `@vue/test-utils`, `vite.config` from `vitest/config`, `src/app.test.ts`; Phase 3: `DELETE /api/users/me` + `performUserAccountDelete` (ownership guard, Drizzle tx, clear cookies); `userAccountDeleteRequestSchema` + OpenAPI; api-worker route; TRPC_REST_MAP note on delete body / Stripe hook. | | 2026-04-26 | Phase 3: email verification `POST /api/users/email-verification/resend` and `…/confirm`; `performResendEmailVerification` / `performConfirmEmailVerification`; Resend in `sendRegistrationEmail`; `RESEND_API_KEY` + `PUBLIC_APP_URL`; first mail on register + re-send on duplicate unverified; OpenAPI 502 on register if provider fails; `c.env?.HYPERDRIVE` on confirm for Vitest. | | 2026-04-26 | Phase 3: **`POST /api/users`** (`performUserRegister`), `encryptUserRehashedLoginHash`, `addHours`, OpenAPI 201/400/401/409; optional **`SEND_EMAILS`** on session env (auto-verify when `false`); group password on register still rejected (same as demo). | | 2026-04-26 | Phase 3: `POST /api/sessions/demo` (`performSessionStartDemo`), `GET /api/users/me`, Redis failed-login limits (`SessionRedisPort` + optional Upstash), `USER_EMAIL_ENCRYPTION_KEY` on `SessionEnv`; OpenAPI 200/400 for demo, 429 for login, `userMeResponseSchema`; Vitest `login-rate-limit.test.ts`. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 3f2c3c3d..f4dd7601 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -28,6 +28,7 @@ describe("api-worker", () => { ["POST", "/api/sessions/logout"], ["POST", "/api/sessions/demo"], ["GET", "/api/users/me"], + ["DELETE", "/api/users/me"], ["POST", "/api/users"], ["POST", "/api/users/email-verification/resend"], ] as const)("returns 503 for %s %s when auth env is not configured", async (method, path) => { diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 1aad3f9a..8ef32665 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -5,6 +5,7 @@ import { healthResponseSchema, sessionDemoRequestSchema, sessionLoginRequestSchema, + userAccountDeleteRequestSchema, userRegisterRequestSchema, } from "@deepnotes/api"; import type { ContentfulStatusCode } from "hono/utils/http-status"; @@ -329,6 +330,78 @@ app.post("/api/users", async (c) => { } }); +app.delete("/api/users/me", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = userAccountDeleteRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + let loginHash: Uint8Array; + try { + loginHash = new Uint8Array( + Buffer.from(parsed.data.loginHash, "base64"), + ); + } catch { + return c.json( + { code: "VALIDATION_ERROR", message: "loginHash must be valid base64." }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performUserAccountDelete } = await import("@deepnotes/session"); + const { cookieLines } = await performUserAccountDelete({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + loginHash, + }); + const res = c.body(null, 204); + appendSetCookies(res, cookieLines); + return res; + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.get("/api/users/me", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/apps/web/package.json b/new-deepnotes/apps/web/package.json index db9bd204..f28e79d1 100644 --- a/new-deepnotes/apps/web/package.json +++ b/new-deepnotes/apps/web/package.json @@ -7,7 +7,7 @@ "build": "vite build", "dev": "vite", "lint": "eslint vite.config.ts \"src/**/*.ts\"", - "test": "node -e \"process.exit(0)\"", + "test": "vitest run", "typecheck": "vue-tsc --noEmit -p tsconfig.app.json", "preview": "vite preview" }, @@ -16,8 +16,11 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", + "@vue/test-utils": "^2.4.6", + "happy-dom": "^17.4.4", "typescript": "^5.8.3", "vite": "^6.3.3", + "vitest": "^3.2.4", "vue-tsc": "^2.2.10" } } diff --git a/new-deepnotes/apps/web/src/app.test.ts b/new-deepnotes/apps/web/src/app.test.ts new file mode 100644 index 00000000..8241f1ba --- /dev/null +++ b/new-deepnotes/apps/web/src/app.test.ts @@ -0,0 +1,12 @@ +import { mount } from "@vue/test-utils"; +import { describe, expect, it } from "vitest"; + +import App from "./App.vue"; + +describe("App", () => { + it("renders shell copy", () => { + const wrapper = mount(App); + expect(wrapper.text()).toContain("DeepNotes"); + expect(wrapper.text()).toContain("Greenfield SPA"); + }); +}); diff --git a/new-deepnotes/apps/web/vite.config.ts b/new-deepnotes/apps/web/vite.config.ts index a3505235..6ab7a83c 100644 --- a/new-deepnotes/apps/web/vite.config.ts +++ b/new-deepnotes/apps/web/vite.config.ts @@ -1,9 +1,13 @@ import vue from "@vitejs/plugin-vue"; -import { defineConfig } from "vite"; +import { defineConfig } from "vitest/config"; export default defineConfig({ plugins: [vue()], server: { port: 5174, }, + test: { + environment: "happy-dom", + include: ["src/**/*.test.ts"], + }, }); diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index f2838edb..3c354b02 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -27,7 +27,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `users.account.twoFactorAuth.disable` | `POST /api/users/me/2fa/disable` | | `users.account.stripe.createCheckoutSession` | `POST /api/billing/stripe/checkout-session` | | `users.account.stripe.createPortalSession` | `POST /api/billing/stripe/portal-session` | -| `users.account.delete` | `DELETE /api/users/me` | +| `users.account.delete` | `DELETE /api/users/me` (JSON body `{ "loginHash" }` base64; clears cookies on 204; optional `deleteStripeCustomer` in worker when billing is wired) | ## Users — pages (`users.pages`) diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index 01728d83..0aaf4ffa 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -25,8 +25,10 @@ export { export { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, + userAccountDeleteRequestSchema, userMeResponseSchema, userRegisterResponseSchema, + type UserAccountDeleteRequest, type UserMeResponse, type UserRegisterResponse, } from "./schemas/users.js"; diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 998ac9d9..38500042 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -22,5 +22,6 @@ describe("getOpenApiDocument", () => { expect( doc.paths?.["/api/users/email-verification/confirm"]?.post, ).toBeDefined(); + expect(doc.paths?.["/api/users/me"]?.delete).toBeDefined(); }); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index ef6e81bd..fc263aaf 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -19,6 +19,7 @@ import { import { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, + userAccountDeleteRequestSchema, userMeResponseSchema, userRegisterResponseSchema, } from "./schemas/users.js"; @@ -184,6 +185,40 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "delete", + path: "/api/users/me", + summary: "Delete current account (password confirmation)", + description: + "Replaces legacy `users.account.delete`. Requires `accessToken` cookie and correct `loginHash` in the JSON body. Clears session cookies on success. Optional Stripe customer deletion is handled by the deployment (not part of OpenAPI).", + request: { + body: { + content: { + "application/json": { + schema: userAccountDeleteRequestSchema, + }, + }, + }, + }, + responses: { + 204: { + description: + "Account removed; session cookies cleared (same names as login).", + }, + 400: { + description: "Wrong password, ownership constraint, or validation error.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "post", path: "/api/users/email-verification/resend", diff --git a/new-deepnotes/packages/api/src/schemas/users.ts b/new-deepnotes/packages/api/src/schemas/users.ts index 0f5b7512..f1bc6184 100644 --- a/new-deepnotes/packages/api/src/schemas/users.ts +++ b/new-deepnotes/packages/api/src/schemas/users.ts @@ -39,3 +39,21 @@ export const emailVerificationConfirmRequestSchema = z emailVerificationCode: nanoidVerificationCode, }) .openapi("EmailVerificationConfirmRequest"); + +/** Body for `DELETE /api/users/me` (replaces legacy `users.account.delete` input). */ +export const userAccountDeleteRequestSchema = z + .object({ + loginHash: z + .string() + .min(1) + .openapi({ + format: "byte", + description: + "Base64-encoded login hash (same semantics as `POST /api/sessions/login`).", + }), + }) + .openapi("UserAccountDeleteRequest"); + +export type UserAccountDeleteRequest = z.infer< + typeof userAccountDeleteRequestSchema +>; diff --git a/new-deepnotes/packages/session/src/delete-user-account.ts b/new-deepnotes/packages/session/src/delete-user-account.ts new file mode 100644 index 00000000..82b00dea --- /dev/null +++ b/new-deepnotes/packages/session/src/delete-user-account.ts @@ -0,0 +1,173 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { count, eq, inArray } from "drizzle-orm"; +import sodium from "libsodium-wrappers-sumo"; + +import { + groupJoinInvitations, + groupJoinRequests, + groupMembers, + groups, + users, +} from "@deepnotes/db/schema"; + +import { getPasswordHashValues } from "./crypto/index.js"; +import { + decryptUserRehashedLoginHash, + derivePasswordValues, + ensureSodiumReady, +} from "./crypto/session-crypto.js"; +import { buildClearSessionCookies, cookieOptionsFromEnv } from "./cookies.js"; +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { verifyAccessToken } from "./jwt.js"; + +/** + * Replaces legacy `users.account.delete`: password check, sole-owner guard, + * removes join rows, deletes solo-member groups (and cascaded pages), drops + * remaining memberships, deletes the user row (sessions/devices cascade). + */ +export async function performUserAccountDelete(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + loginHash: Uint8Array; + /** Optional Stripe `customers.del` (legacy runs after DB commit; errors are logged only). */ + deleteStripeCustomer?: (customerId: string) => Promise; +}): Promise<{ cookieLines: string[] }> { + await ensureSodiumReady(); + const cookieOpts = cookieOptionsFromEnv(input.env); + + if (input.accessCookie == null || input.accessCookie === "") { + throw new SessionError(401, "UNAUTHORIZED", "No access token."); + } + + const payload = await verifyAccessToken( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + if (payload == null) { + throw new SessionError(401, "UNAUTHORIZED", "Invalid access token."); + } + const userId = payload.uid; + + const userRows = await input.db + .select({ + id: users.id, + encryptedRehashedLoginHash: users.encryptedRehashedLoginHash, + customerId: users.customerId, + }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + const userRow = userRows[0]; + if (userRow == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + + const passwordHashValues = getPasswordHashValues( + decryptUserRehashedLoginHash( + new Uint8Array(userRow.encryptedRehashedLoginHash), + input.env.USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + ), + ); + const passwordValues = derivePasswordValues({ + password: input.loginHash, + salt: passwordHashValues.saltBytes, + }); + if (!sodium.memcmp(passwordValues.hash, passwordHashValues.hashBytes)) { + throw new SessionError(400, "BAD_REQUEST", "Password is incorrect."); + } + + const memberCounts = await input.db + .select({ + groupId: groupMembers.groupId, + n: count(), + }) + .from(groupMembers) + .groupBy(groupMembers.groupId); + + const ownerCounts = await input.db + .select({ + groupId: groupMembers.groupId, + n: count(), + }) + .from(groupMembers) + .where(eq(groupMembers.role, "owner")) + .groupBy(groupMembers.groupId); + + const memMap = new Map( + memberCounts.map((r) => [r.groupId, Number(r.n)]), + ); + const ownerMap = new Map( + ownerCounts.map((r) => [r.groupId, Number(r.n)]), + ); + + const myGroupRows = await input.db + .select({ groupId: groupMembers.groupId }) + .from(groupMembers) + .where(eq(groupMembers.userId, userId)); + + const memberships = myGroupRows.map((row) => ({ + groupId: row.groupId, + memberCount: memMap.get(row.groupId) ?? 0, + ownerCount: ownerMap.get(row.groupId) ?? 0, + })); + + if ( + memberships.some((m) => m.memberCount > 1 && m.ownerCount <= 1) + ) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Some groups would be left without an owner. Transfer ownership before deleting your account.", + ); + } + + const idsOfGroupsToDelete = memberships + .filter((m) => m.memberCount <= 1) + .map((m) => m.groupId); + + const customerId = userRow.customerId; + + await input.db.transaction(async (tx) => { + await tx + .delete(groupJoinInvitations) + .where(eq(groupJoinInvitations.userId, userId)); + await tx + .delete(groupJoinRequests) + .where(eq(groupJoinRequests.userId, userId)); + + if (idsOfGroupsToDelete.length > 0) { + await tx.delete(groups).where(inArray(groups.id, idsOfGroupsToDelete)); + } + + await tx.delete(groupMembers).where(eq(groupMembers.userId, userId)); + + const removed = await tx + .delete(users) + .where(eq(users.id, userId)) + .returning({ id: users.id }); + if (removed.length !== 1) { + throw new SessionError( + 500, + "SERVER_MISCONFIG", + "User delete did not apply.", + ); + } + }); + + if ( + customerId != null && + customerId !== "" && + input.deleteStripeCustomer != null + ) { + try { + await input.deleteStripeCustomer(customerId); + } catch { + // Legacy logs and continues after DB delete. + } + } + + return { cookieLines: buildClearSessionCookies(cookieOpts) }; +} diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 9468b571..3ae98555 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -14,6 +14,7 @@ export type { } from "./start-demo.js"; export { performUserRegister } from "./register-user.js"; export type { UserRegisterInput } from "./register-user.js"; +export { performUserAccountDelete } from "./delete-user-account.js"; export { performConfirmEmailVerification, performResendEmailVerification, diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml index 973bd7de..ca9ca458 100644 --- a/new-deepnotes/pnpm-lock.yaml +++ b/new-deepnotes/pnpm-lock.yaml @@ -53,7 +53,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.1 - version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3) wrangler: specifier: ^4.12.0 version: 4.85.0(@cloudflare/workers-types@4.20260426.1) @@ -67,12 +67,21 @@ importers: '@vitejs/plugin-vue': specifier: ^5.2.3 version: 5.2.4(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.33(typescript@5.9.3)) + '@vue/test-utils': + specifier: ^2.4.6 + version: 2.4.8(@vue/compiler-dom@3.5.33)(@vue/server-renderer@3.5.33(vue@3.5.33(typescript@5.9.3)))(vue@3.5.33(typescript@5.9.3)) + happy-dom: + specifier: ^17.4.4 + version: 17.6.3 typescript: specifier: ^5.8.3 version: 5.9.3 vite: specifier: ^6.3.3 version: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3) vue-tsc: specifier: ^2.2.10 version: 2.2.12(typescript@5.9.3) @@ -94,7 +103,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.1 - version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3) packages/db: dependencies: @@ -119,7 +128,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3) packages/session: dependencies: @@ -159,7 +168,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3) packages: @@ -1044,6 +1053,10 @@ packages: cpu: [x64] os: [win32] + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1084,6 +1097,9 @@ packages: cpu: [x64] os: [win32] + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@otplib/core@12.0.1': resolution: {integrity: sha512-4sGntwbA/AC+SbPhbsziRiD+jNDdIzsZ3JUyfZwjtKyc/wufl1pnSIaG4Uqx8ymPagujub0o92kgBnB89cuAMA==} @@ -1105,6 +1121,10 @@ packages: '@petamoriken/float16@3.9.3': resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@poppinss/colors@4.1.6': resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} @@ -1441,6 +1461,20 @@ packages: '@vue/shared@3.5.33': resolution: {integrity: sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==} + '@vue/test-utils@2.4.8': + resolution: {integrity: sha512-cjAKFbSXFhtZ9Cj+ug60b21lW/BN737e+Syu2LPACIW6R0zVtj65Fnfe649KjfHor3Etx3ZavDFFBrZ+p21YNw==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1457,10 +1491,22 @@ packages: alien-signals@1.0.13: resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1518,9 +1564,16 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} @@ -1655,6 +1708,20 @@ packages: sqlite3: optional: true + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -1782,6 +1849,10 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1799,10 +1870,19 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + happy-dom@17.6.3: + resolution: {integrity: sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==} + engines: {node: '>=20.0.0'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1831,10 +1911,17 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1846,9 +1933,21 @@ packages: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -1892,6 +1991,9 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1911,6 +2013,10 @@ packages: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1941,6 +2047,11 @@ packages: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + openapi3-ts@4.5.0: resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==} @@ -1959,6 +2070,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1974,6 +2088,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -2003,6 +2121,9 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2043,6 +2164,10 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2060,6 +2185,22 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2227,6 +2368,9 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue-component-type-helpers@3.2.7: + resolution: {integrity: sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==} + vue-tsc@2.2.12: resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} hasBin: true @@ -2241,6 +2385,14 @@ packages: typescript: optional: true + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2275,6 +2427,14 @@ packages: '@cloudflare/workers-types': optional: true + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -2829,6 +2989,15 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2856,6 +3025,8 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true + '@one-ini/wasm@0.1.1': {} + '@otplib/core@12.0.1': {} '@otplib/plugin-crypto@12.0.1': @@ -2882,6 +3053,9 @@ snapshots: '@petamoriken/float16@3.9.3': optional: true + '@pkgjs/parseargs@0.11.0': + optional: true + '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 @@ -3234,6 +3408,17 @@ snapshots: '@vue/shared@3.5.33': {} + '@vue/test-utils@2.4.8(@vue/compiler-dom@3.5.33)(@vue/server-renderer@3.5.33(vue@3.5.33(typescript@5.9.3)))(vue@3.5.33(typescript@5.9.3))': + dependencies: + '@vue/compiler-dom': 3.5.33 + js-beautify: 1.15.4 + vue: 3.5.33(typescript@5.9.3) + vue-component-type-helpers: 3.2.7 + optionalDependencies: + '@vue/server-renderer': 3.5.33(vue@3.5.33(typescript@5.9.3)) + + abbrev@2.0.0: {} + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -3249,10 +3434,16 @@ snapshots: alien-signals@1.0.13: {} + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.3: {} + argparse@2.0.1: {} assertion-error@2.0.1: {} @@ -3303,8 +3494,15 @@ snapshots: color-name@1.1.4: {} + commander@10.0.1: {} + concat-map@0.0.1: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + cookie@1.1.1: {} cross-spawn@7.0.6: @@ -3344,6 +3542,19 @@ snapshots: gel: 2.2.0 postgres: 3.4.9 + eastasianwidth@0.2.0: {} + + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.7.4 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + entities@7.0.1: {} env-paths@3.0.0: @@ -3569,6 +3780,11 @@ snapshots: flatted@3.4.2: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fsevents@2.3.3: optional: true @@ -3592,8 +3808,22 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + globals@14.0.0: {} + happy-dom@17.6.3: + dependencies: + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + has-flag@4.0.0: {} he@1.2.0: {} @@ -3611,8 +3841,12 @@ snapshots: imurmurhash@0.1.4: {} + ini@1.3.8: {} + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -3622,8 +3856,24 @@ snapshots: isexe@3.1.5: optional: true + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + jose@5.10.0: {} + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.5.0 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + js-tokens@9.0.1: {} js-yaml@4.1.1: @@ -3661,6 +3911,8 @@ snapshots: loupe@3.2.1: {} + lru-cache@10.4.3: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3689,6 +3941,8 @@ snapshots: dependencies: brace-expansion: 2.1.0 + minipass@7.1.3: {} + ms@2.1.3: {} msgpackr-extract@3.0.3: @@ -3720,6 +3974,10 @@ snapshots: detect-libc: 2.1.2 optional: true + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + openapi3-ts@4.5.0: dependencies: yaml: 2.8.3 @@ -3747,6 +4005,8 @@ snapshots: dependencies: p-limit: 3.1.0 + package-json-from-dist@1.0.1: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -3757,6 +4017,11 @@ snapshots: path-key@3.1.1: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-to-regexp@6.3.0: {} pathe@2.0.3: {} @@ -3777,6 +4042,8 @@ snapshots: prelude-ls@1.2.1: {} + proto-list@1.2.4: {} + punycode@2.3.1: {} resolve-from@4.0.0: {} @@ -3858,6 +4125,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@4.1.0: {} + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -3871,6 +4140,26 @@ snapshots: std-env@3.10.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-json-comments@3.1.1: {} strip-literal@3.1.0: @@ -3989,7 +4278,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 - vitest@3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3): + vitest@3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 @@ -4016,6 +4305,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.17 + happy-dom: 17.6.3 transitivePeerDependencies: - jiti - less @@ -4032,6 +4322,8 @@ snapshots: vscode-uri@3.1.0: {} + vue-component-type-helpers@3.2.7: {} + vue-tsc@2.2.12(typescript@5.9.3): dependencies: '@volar/typescript': 2.4.15 @@ -4048,6 +4340,10 @@ snapshots: optionalDependencies: typescript: 5.9.3 + webidl-conversions@7.0.0: {} + + whatwg-mimetype@3.0.0: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -4089,6 +4385,18 @@ snapshots: - bufferutil - utf-8-validate + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + ws@8.18.0: {} yaml@2.8.3: {} From 71d83aaa28bd682a75540594f7fddc17cb8461f1 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 23:52:24 -0300 Subject: [PATCH 032/243] feat(new-deepnotes): change user password and OpenAPI --- new-deepnotes/PLAN_PROGRESS.md | 15 +- .../apps/api-worker/src/index.test.ts | 1 + new-deepnotes/apps/api-worker/src/index.ts | 64 +++++++ new-deepnotes/docs/TRPC_REST_MAP.md | 3 +- new-deepnotes/packages/api/src/index.ts | 3 + .../packages/api/src/openapi.test.ts | 1 + new-deepnotes/packages/api/src/openapi.ts | 45 +++++ .../packages/api/src/schemas/sessions.ts | 3 +- .../packages/api/src/schemas/users.ts | 20 +++ .../session/src/change-user-password.ts | 164 ++++++++++++++++++ new-deepnotes/packages/session/src/index.ts | 1 + 11 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 new-deepnotes/packages/session/src/change-user-password.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 3ab69257..9cb8e817 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Sessions, register, email verify/resend/confirm, **`DELETE /api/users/me`** (`performUserAccountDelete`: password + sole-owner guard, Drizzle tx; Stripe customer hook optional on worker). **Next:** `POST /api/users/me/password`, email-change + confirm, 2FA routes, pages/groups CRUD, realtime/collab, Stripe webhook. | +| **3** — REST + Drizzle features | **In progress** | Sessions, register, email verify/resend/confirm, account delete, **password change** (see Phase 3 checklist). **Next:** email-change + confirm, 2FA routes, pages/groups CRUD, realtime/collab, Stripe webhook. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -44,13 +44,15 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] `POST /api/users/email-verification/confirm` — public, `{ "emailVerificationCode" }` (nanoid, legacy `verifyEmail`); 204 / 400; DB update copies `encrypted_new_email` → `encrypted_email`. - [x] `sendRegistrationEmail` + optional **`RESEND_API_KEY`**, optional **`PUBLIC_APP_URL`** in `SessionEnv` / [template.env](./template.env); duplicate unverified registration re-sends via same helper (401 “New email sent”). - [x] **`DELETE /api/users/me`** — replaces legacy `users.account.delete`: JSON `{ "loginHash" }` (base64); verifies access JWT + password (`encrypted_rehashed_login_hash`); blocks when any membership has `member_count > 1` and `owner_count <= 1`; deletes join invites/requests, solo-member groups (cascade pages), remaining `group_members`, then user row; clears session cookies; optional `deleteStripeCustomer(customerId)` hook (worker can wire Stripe later; failures swallowed like legacy). +- [x] **`POST /api/users/me/password`** — replaces legacy WS `change-password` (two RPC steps → one REST call after client re-wraps keyrings). **`performUserPasswordChange`** (`packages/session/src/change-user-password.ts`): body `oldLoginHash`, `newLoginHash`, `userEncryptedPrivateKeyring`, `userEncryptedSymmetricKeyring` (base64, same semantics as `POST /api/users`); verifies current password; **403** if `users.demo === true`; updates `encrypted_rehashed_login_hash`, `encrypted_private_keyring`, `encrypted_symmetric_keyring`; sets **`sessions.invalidated`** for all user sessions; **204** + **`buildClearSessionCookies`**. Contract: `userPasswordChangeRequestSchema` in `@deepnotes/api`; map in [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). -### Not started (Phase 3 remainder) +### Account routes still to ship (Phase 3) + +- [ ] **`POST /api/users/me/email-change`** + **`POST /api/users/me/email-change/confirm`** — legacy tRPC `emailChange.request` + WS `email-change/finish` → two REST steps; Resend / `encrypted_new_email` / verification fields; invalidate sessions on confirm if legacy does (verify in `apps/app-server`). +- [ ] **2FA (HTTP surface)** — `POST /api/users/me/2fa/enable/request|finish`, `GET /api/users/me/2fa`, `POST /api/users/me/2fa/recovery-codes`, `POST /api/users/me/2fa/devices/forget`, `POST /api/users/me/2fa/disable` ([docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md)). **Note:** `@deepnotes/session` already implements TOTP/recovery verification for **`POST /api/sessions/login`**; these routes expose enable/disable/load for the SPA. + +### Not started (Phase 3 — pages, groups, infra) -- [ ] **Account (remaining tRPC / WS parity):** - - [ ] `POST /api/users/me/email-change` + `POST /api/users/me/email-change/confirm` (WS finish → REST). - - [ ] 2FA: `POST …/2fa/enable/request|finish`, `GET …/2fa`, `POST …/2fa/recovery-codes`, `POST …/2fa/devices/forget`, `POST …/2fa/disable` (see [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md)). - - [ ] `POST /api/users/me/password` (legacy WS `change-password` → REST). - [ ] **Pages** (user prefs + CRUD) and **groups** CRUD / privacy / passwords per map. - [ ] **Realtime / collab** (new or adapted protocols; no key rotation). - [ ] **Stripe:** `POST /api/webhooks/stripe`, checkout/portal (no RevenueCat); wire **`deleteStripeCustomer`** from account delete when keys exist. @@ -127,6 +129,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-26 | Phase 3: **`POST /api/users/me/password`** — `performUserPasswordChange` (`change-user-password.ts`): old password verify, demo **403**, new keyrings + PHC, invalidate all `sessions`, clear cookies **204**; `userPasswordChangeRequestSchema`, OpenAPI + worker; export **`byteB64`** from `@deepnotes/api`; TRPC_REST_MAP rows for change-password; PLAN_PROGRESS Phase 3 account section expanded. | | 2026-04-26 | Phase 2 + §5.8: `@deepnotes/web` — Vitest + happy-dom + `@vue/test-utils`, `vite.config` from `vitest/config`, `src/app.test.ts`; Phase 3: `DELETE /api/users/me` + `performUserAccountDelete` (ownership guard, Drizzle tx, clear cookies); `userAccountDeleteRequestSchema` + OpenAPI; api-worker route; TRPC_REST_MAP note on delete body / Stripe hook. | | 2026-04-26 | Phase 3: email verification `POST /api/users/email-verification/resend` and `…/confirm`; `performResendEmailVerification` / `performConfirmEmailVerification`; Resend in `sendRegistrationEmail`; `RESEND_API_KEY` + `PUBLIC_APP_URL`; first mail on register + re-send on duplicate unverified; OpenAPI 502 on register if provider fails; `c.env?.HYPERDRIVE` on confirm for Vitest. | | 2026-04-26 | Phase 3: **`POST /api/users`** (`performUserRegister`), `encryptUserRehashedLoginHash`, `addHours`, OpenAPI 201/400/401/409; optional **`SEND_EMAILS`** on session env (auto-verify when `false`); group password on register still rejected (same as demo). | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index f4dd7601..04592f85 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -28,6 +28,7 @@ describe("api-worker", () => { ["POST", "/api/sessions/logout"], ["POST", "/api/sessions/demo"], ["GET", "/api/users/me"], + ["POST", "/api/users/me/password"], ["DELETE", "/api/users/me"], ["POST", "/api/users"], ["POST", "/api/users/email-verification/resend"], diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 8ef32665..ba9c4fb8 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -6,6 +6,7 @@ import { sessionDemoRequestSchema, sessionLoginRequestSchema, userAccountDeleteRequestSchema, + userPasswordChangeRequestSchema, userRegisterRequestSchema, } from "@deepnotes/api"; import type { ContentfulStatusCode } from "hono/utils/http-status"; @@ -330,6 +331,69 @@ app.post("/api/users", async (c) => { } }); +app.post("/api/users/me/password", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = userPasswordChangeRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performUserPasswordChange } = await import("@deepnotes/session"); + const { cookieLines } = await performUserPasswordChange({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + oldLoginHash: parsed.data.oldLoginHash, + newLoginHash: parsed.data.newLoginHash, + newEncryptedPrivateKeyring: parsed.data.userEncryptedPrivateKeyring, + newEncryptedSymmetricKeyring: parsed.data.userEncryptedSymmetricKeyring, + }); + const res = c.body(null, 204); + appendSetCookies(res, cookieLines); + return res; + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.delete("/api/users/me", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index 3c354b02..4d296478 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -28,6 +28,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `users.account.stripe.createCheckoutSession` | `POST /api/billing/stripe/checkout-session` | | `users.account.stripe.createPortalSession` | `POST /api/billing/stripe/portal-session` | | `users.account.delete` | `DELETE /api/users/me` (JSON body `{ "loginHash" }` base64; clears cookies on 204; optional `deleteStripeCustomer` in worker when billing is wired) | +| (WS) `users.account.changePassword` step 1+2 | `POST /api/users/me/password` (JSON: `oldLoginHash`, `newLoginHash`, `userEncryptedPrivateKeyring`, `userEncryptedSymmetricKeyring` as base64; same keyring semantics as `POST /api/users`; 204 + clears cookies + invalidates all sessions) | ## Users — pages (`users.pages`) @@ -88,7 +89,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `websocket/groups/privacy/make-private` | `POST /api/groups/:groupId/privacy/private` | | | `websocket/groups/rotate-keys` | — | **removed** per RESTART_PLAN | | `websocket/pages/move` | `POST /api/pages/:pageId/move` | | -| `websocket/users/account/change-password` | `POST /api/users/me/password` | | +| `websocket/users/account/change-password` | `POST /api/users/me/password` | **implemented** in `@deepnotes/session` (`performUserPasswordChange`) | | `websocket/users/account/email-change/finish` | `POST /api/users/me/email-change/confirm` | | | `websocket/users/account/rotate-keys` | — | **removed** | diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index 0aaf4ffa..eac684f0 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -14,6 +14,7 @@ export { sessionRefreshSuccessSchema, } from "./schemas/session-responses.js"; export { + byteB64, sessionDemoRequestSchema, sessionLoginEmailSchema, sessionLoginRequestSchema, @@ -27,8 +28,10 @@ export { emailVerificationResendRequestSchema, userAccountDeleteRequestSchema, userMeResponseSchema, + userPasswordChangeRequestSchema, userRegisterResponseSchema, type UserAccountDeleteRequest, type UserMeResponse, + type UserPasswordChangeRequest, type UserRegisterResponse, } from "./schemas/users.js"; diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 38500042..74a38c24 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -15,6 +15,7 @@ describe("getOpenApiDocument", () => { expect(doc.paths?.["/api/sessions/logout"]?.post).toBeDefined(); expect(doc.paths?.["/api/sessions/demo"]?.post).toBeDefined(); expect(doc.paths?.["/api/users/me"]?.get).toBeDefined(); + expect(doc.paths?.["/api/users/me/password"]?.post).toBeDefined(); expect(doc.paths?.["/api/users"]?.post).toBeDefined(); expect( doc.paths?.["/api/users/email-verification/resend"]?.post, diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index fc263aaf..2aea0ff0 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -21,6 +21,7 @@ import { emailVerificationResendRequestSchema, userAccountDeleteRequestSchema, userMeResponseSchema, + userPasswordChangeRequestSchema, userRegisterResponseSchema, } from "./schemas/users.js"; @@ -72,6 +73,15 @@ const sessionNotFound404 = { }, } as const; +const sessionForbidden403 = { + description: "Action not allowed for this account (e.g. demo user).", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, +} as const; + registry.registerPath({ method: "get", path: "/api/health", @@ -185,6 +195,41 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/users/me/password", + summary: "Change password (re-wrap keyrings)", + description: + "Replaces legacy WebSocket `users.account.changePassword`. Requires `accessToken`; verifies `oldLoginHash`; stores keyrings encrypted with the new password (`userEncrypted*` are plaintext keyrings from the client, same as registration). Invalidates all sessions and clears cookies — client must log in again.", + request: { + body: { + content: { + "application/json": { + schema: userPasswordChangeRequestSchema, + }, + }, + }, + }, + responses: { + 204: { + description: + "Password updated; all sessions invalidated; session cookies cleared.", + }, + 400: { + description: "Wrong current password or invalid key material.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "delete", path: "/api/users/me", diff --git a/new-deepnotes/packages/api/src/schemas/sessions.ts b/new-deepnotes/packages/api/src/schemas/sessions.ts index 50dfe4f7..6b432388 100644 --- a/new-deepnotes/packages/api/src/schemas/sessions.ts +++ b/new-deepnotes/packages/api/src/schemas/sessions.ts @@ -40,7 +40,8 @@ const nanoidId = z .length(21) .regex(/^[A-Za-z0-9_-]{21}$/, "expected nanoid id"); -const byteB64 = z +/** Base64 JSON field decoded to `Uint8Array` (legacy tRPC used raw bytes). */ +export const byteB64 = z .string() .min(1) .openapi({ diff --git a/new-deepnotes/packages/api/src/schemas/users.ts b/new-deepnotes/packages/api/src/schemas/users.ts index f1bc6184..2478ea84 100644 --- a/new-deepnotes/packages/api/src/schemas/users.ts +++ b/new-deepnotes/packages/api/src/schemas/users.ts @@ -1,6 +1,8 @@ import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; import { z } from "zod"; +import { byteB64 } from "./sessions.js"; + extendZodWithOpenApi(z); export const userMeResponseSchema = z @@ -57,3 +59,21 @@ export const userAccountDeleteRequestSchema = z export type UserAccountDeleteRequest = z.infer< typeof userAccountDeleteRequestSchema >; + +/** + * `POST /api/users/me/password` — replaces legacy WebSocket + * `users.account.changePassword` (two-step flow collapsed: client re-wraps + * keyrings with the new password before calling). + */ +export const userPasswordChangeRequestSchema = z + .object({ + oldLoginHash: byteB64, + newLoginHash: byteB64, + userEncryptedPrivateKeyring: byteB64, + userEncryptedSymmetricKeyring: byteB64, + }) + .openapi("UserPasswordChangeRequest"); + +export type UserPasswordChangeRequest = z.infer< + typeof userPasswordChangeRequestSchema +>; diff --git a/new-deepnotes/packages/session/src/change-user-password.ts b/new-deepnotes/packages/session/src/change-user-password.ts new file mode 100644 index 00000000..d7f783cd --- /dev/null +++ b/new-deepnotes/packages/session/src/change-user-password.ts @@ -0,0 +1,164 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { eq } from "drizzle-orm"; +import sodium from "libsodium-wrappers-sumo"; + +import { sessions, users } from "@deepnotes/db/schema"; + +import { + createPrivateKeyring, + createSymmetricKeyring, + getPasswordHashValues, +} from "./crypto/index.js"; +import { encodePasswordHash } from "./crypto/password-hashing.js"; +import { + decryptUserRehashedLoginHash, + derivePasswordValues, + encryptUserRehashedLoginHash, + ensureSodiumReady, +} from "./crypto/session-crypto.js"; +import { buildClearSessionCookies, cookieOptionsFromEnv } from "./cookies.js"; +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { verifyAccessToken } from "./jwt.js"; + +function toBuf(u: Uint8Array): Buffer { + return Buffer.from(u); +} + +/** + * Replaces legacy `users.account.changePassword` (WS step 2) + session invalidation. + * Verifies `oldLoginHash`, stores PHC + keyrings for `newLoginHash`, invalidates + * all sessions, clears cookies (re-login with new password). + */ +export async function performUserPasswordChange(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + oldLoginHash: Uint8Array; + newLoginHash: Uint8Array; + newEncryptedPrivateKeyring: Uint8Array; + newEncryptedSymmetricKeyring: Uint8Array; +}): Promise<{ cookieLines: string[] }> { + await ensureSodiumReady(); + const cookieOpts = cookieOptionsFromEnv(input.env); + + if (input.accessCookie == null || input.accessCookie === "") { + throw new SessionError(401, "UNAUTHORIZED", "No access token."); + } + + const payload = await verifyAccessToken( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + if (payload == null) { + throw new SessionError(401, "UNAUTHORIZED", "Invalid access token."); + } + const userId = payload.uid; + + const userRows = await input.db + .select({ + id: users.id, + demo: users.demo, + encryptedRehashedLoginHash: users.encryptedRehashedLoginHash, + }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + const userRow = userRows[0]; + if (userRow == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + + if (userRow.demo === true) { + throw new SessionError( + 403, + "FORBIDDEN", + "This action is unavailable for demo accounts.", + ); + } + + const passwordHashValues = getPasswordHashValues( + decryptUserRehashedLoginHash( + new Uint8Array(userRow.encryptedRehashedLoginHash), + input.env.USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + ), + ); + const passwordValues = derivePasswordValues({ + password: input.oldLoginHash, + salt: passwordHashValues.saltBytes, + }); + if (!sodium.memcmp(passwordValues.hash, passwordHashValues.hashBytes)) { + throw new SessionError(400, "BAD_REQUEST", "Password is incorrect."); + } + + const newPw = derivePasswordValues({ password: input.newLoginHash }); + const encodedRehash = encodePasswordHash( + newPw.hash, + newPw.salt, + 2, + 32, + ); + const encryptedRehashedLoginHash = toBuf( + encryptUserRehashedLoginHash( + encodedRehash, + input.env.USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + ), + ); + + let encryptedPrivateStored: Buffer; + let encryptedSymmetricStored: Buffer; + try { + encryptedPrivateStored = toBuf( + createPrivateKeyring(input.newEncryptedPrivateKeyring) + .wrapSymmetric(newPw.key, { + associatedData: { + context: "UserEncryptedPrivateKeyring", + userId, + }, + }).wrappedValue, + ); + encryptedSymmetricStored = toBuf( + createSymmetricKeyring(input.newEncryptedSymmetricKeyring) + .wrapSymmetric(newPw.key, { + associatedData: { + context: "UserEncryptedSymmetricKeyring", + userId, + }, + }).wrappedValue, + ); + } catch { + throw new SessionError( + 400, + "BAD_REQUEST", + "Invalid keyring material for the new password.", + ); + } + + await input.db.transaction(async (tx) => { + const updated = await tx + .update(users) + .set({ + encryptedRehashedLoginHash, + encryptedPrivateKeyring: encryptedPrivateStored, + encryptedSymmetricKeyring: encryptedSymmetricStored, + }) + .where(eq(users.id, userId)) + .returning({ id: users.id }); + + if (updated.length !== 1) { + throw new SessionError( + 500, + "SERVER_MISCONFIG", + "Password update did not apply.", + ); + } + + await tx + .update(sessions) + .set({ invalidated: true }) + .where(eq(sessions.userId, userId)); + }); + + return { cookieLines: buildClearSessionCookies(cookieOpts) }; +} diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 3ae98555..486182a8 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -15,6 +15,7 @@ export type { export { performUserRegister } from "./register-user.js"; export type { UserRegisterInput } from "./register-user.js"; export { performUserAccountDelete } from "./delete-user-account.js"; +export { performUserPasswordChange } from "./change-user-password.js"; export { performConfirmEmailVerification, performResendEmailVerification, From 394f72e3d472471ee060aa862324ef1ea84a22e9 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Sun, 26 Apr 2026 23:56:19 -0300 Subject: [PATCH 033/243] feat(new-deepnotes): change user email, codes, and OpenAPI --- new-deepnotes/PLAN_PROGRESS.md | 14 +- new-deepnotes/apps/api-worker/src/index.ts | 131 +++++++ new-deepnotes/docs/TRPC_REST_MAP.md | 4 +- new-deepnotes/packages/api/src/index.ts | 5 + .../packages/api/src/openapi.test.ts | 6 + new-deepnotes/packages/api/src/openapi.ts | 88 +++++ .../packages/api/src/schemas/users.ts | 42 +++ .../packages/session/src/change-user-email.ts | 337 ++++++++++++++++++ .../session/src/encrypt-user-email.ts | 18 +- new-deepnotes/packages/session/src/index.ts | 4 + .../session/src/send-email-change-code.ts | 51 +++ 11 files changed, 695 insertions(+), 5 deletions(-) create mode 100644 new-deepnotes/packages/session/src/change-user-email.ts create mode 100644 new-deepnotes/packages/session/src/send-email-change-code.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 9cb8e817..67e03565 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Sessions, register, email verify/resend/confirm, account delete, **password change** (see Phase 3 checklist). **Next:** email-change + confirm, 2FA routes, pages/groups CRUD, realtime/collab, Stripe webhook. | +| **3** — REST + Drizzle features | **In progress** | Auth + account: sessions, register, public email verify, account delete, password + **email change** (see Phase 3 checklist). **Backlog (priority):** 2FA HTTP surface → pages/groups CRUD → realtime/collab → Stripe webhook. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -48,7 +48,10 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ ### Account routes still to ship (Phase 3) -- [ ] **`POST /api/users/me/email-change`** + **`POST /api/users/me/email-change/confirm`** — legacy tRPC `emailChange.request` + WS `email-change/finish` → two REST steps; Resend / `encrypted_new_email` / verification fields; invalidate sessions on confirm if legacy does (verify in `apps/app-server`). +- [x] **Email change** + - [x] `POST /api/users/me/email-change` — `performUserEmailChangeRequest`: `oldLoginHash` + `newEmail`; **403** demo, **400** bad password or “email already in use” (global `email_hash` match, same as legacy); sets `encrypted_new_email` + 6-digit `email_verification_code`; Resend (subject/body like legacy) or **200** `{ "emailVerificationCode" }` when `SEND_EMAILS=false`; **204** when emailed. + - [x] `POST /api/users/me/email-change/confirm` — `performUserEmailChangeConfirm`: one call (WS two-step collapsed); `oldLoginHash`, `emailVerificationCode` (6 digits), `newLoginHash`, `userEncrypted*Keyring` (b64, same as register/password); verifies code + password; applies new `encrypted_email` / `email_hash`, clears pending fields, PHC + rewrapped keyrings, invalidates **all** `sessions`, **204** + `buildClearSessionCookies`; optional `updateStripeCustomerEmail` in worker (matches legacy `customers.update` after commit, errors non-fatal). + - [x] **`decryptUserEmail`** in `@deepnotes/session` for confirm; **`sendEmailChangeVerificationEmail`** (Resend); OpenAPI + [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md) updated. - [ ] **2FA (HTTP surface)** — `POST /api/users/me/2fa/enable/request|finish`, `GET /api/users/me/2fa`, `POST /api/users/me/2fa/recovery-codes`, `POST /api/users/me/2fa/devices/forget`, `POST /api/users/me/2fa/disable` ([docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md)). **Note:** `@deepnotes/session` already implements TOTP/recovery verification for **`POST /api/sessions/login`**; these routes expose enable/disable/load for the SPA. ### Not started (Phase 3 — pages, groups, infra) @@ -125,10 +128,17 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte --- +## Phase 3 working order (suggested) + +Use this when resuming: **(done)** account HTTP surface through email change including password change. **(next)** 2FA CRUD on `/api/users/me/2fa*`, reusing session crypto already used at login. **(then)** pages + groups from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) (user prefs, CRUD, group privacy/password). **(then)** long pole: **realtime + collab** (protocol, Worker/DO, no key rotation) and **Stripe** webhook + billing routes + wire `updateStripeCustomerEmail` / `deleteStripeCustomer` from account flows where applicable. + +--- + ## Short log (newest first) | Date | Change | |------|--------| +| 2026-04-26 | Phase 3: **email change** — `POST /api/users/me/email-change` + `…/confirm` (`change-user-email.ts`, `decryptUserEmail`, `send-email-change-code`); `userEmailChange*Request` schemas, OpenAPI, Hono; TRPC_REST_MAP; PLAN_PROGRESS detail + suggested Phase 3 order. | | 2026-04-26 | Phase 3: **`POST /api/users/me/password`** — `performUserPasswordChange` (`change-user-password.ts`): old password verify, demo **403**, new keyrings + PHC, invalidate all `sessions`, clear cookies **204**; `userPasswordChangeRequestSchema`, OpenAPI + worker; export **`byteB64`** from `@deepnotes/api`; TRPC_REST_MAP rows for change-password; PLAN_PROGRESS Phase 3 account section expanded. | | 2026-04-26 | Phase 2 + §5.8: `@deepnotes/web` — Vitest + happy-dom + `@vue/test-utils`, `vite.config` from `vitest/config`, `src/app.test.ts`; Phase 3: `DELETE /api/users/me` + `performUserAccountDelete` (ownership guard, Drizzle tx, clear cookies); `userAccountDeleteRequestSchema` + OpenAPI; api-worker route; TRPC_REST_MAP note on delete body / Stripe hook. | | 2026-04-26 | Phase 3: email verification `POST /api/users/email-verification/resend` and `…/confirm`; `performResendEmailVerification` / `performConfirmEmailVerification`; Resend in `sendRegistrationEmail`; `RESEND_API_KEY` + `PUBLIC_APP_URL`; first mail on register + re-send on duplicate unverified; OpenAPI 502 on register if provider fails; `c.env?.HYPERDRIVE` on confirm for Vitest. | diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index ba9c4fb8..7ab13c06 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -6,6 +6,8 @@ import { sessionDemoRequestSchema, sessionLoginRequestSchema, userAccountDeleteRequestSchema, + userEmailChangeConfirmRequestSchema, + userEmailChangeRequestSchema, userPasswordChangeRequestSchema, userRegisterRequestSchema, } from "@deepnotes/api"; @@ -394,6 +396,135 @@ app.post("/api/users/me/password", async (c) => { } }); +app.post("/api/users/me/email-change", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = userEmailChangeRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performUserEmailChangeRequest } = await import("@deepnotes/session"); + const out = await performUserEmailChangeRequest({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + oldLoginHash: parsed.data.oldLoginHash, + newEmail: parsed.data.newEmail, + }); + if (out.devEmailVerificationCode != null) { + return c.json( + { emailVerificationCode: out.devEmailVerificationCode }, + 200, + ); + } + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/users/me/email-change/confirm", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = userEmailChangeConfirmRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performUserEmailChangeConfirm } = await import("@deepnotes/session"); + const { cookieLines } = await performUserEmailChangeConfirm({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + oldLoginHash: parsed.data.oldLoginHash, + emailVerificationCode: parsed.data.emailVerificationCode, + newLoginHash: parsed.data.newLoginHash, + newEncryptedPrivateKeyring: parsed.data.userEncryptedPrivateKeyring, + newEncryptedSymmetricKeyring: parsed.data.userEncryptedSymmetricKeyring, + }); + const res = c.body(null, 204); + appendSetCookies(res, cookieLines); + return res; + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.delete("/api/users/me", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index 4d296478..de7bf9df 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -18,7 +18,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `users.account.register` | `POST /api/users` | | `users.account.resendVerificationEmail` | `POST /api/users/email-verification/resend` (public; body `{ "email" }` — matches legacy, not an authenticated “me” call) | | `users.account.verifyEmail` | `POST /api/users/email-verification/confirm` (public; body `{ "emailVerificationCode" }`, nanoid) | -| `users.account.emailChange.request` | `POST /api/users/me/email-change` | +| `users.account.emailChange.request` | `POST /api/users/me/email-change` (body: `oldLoginHash` b64, `newEmail`; **204** or **200** with `{ "emailVerificationCode" }` when `SEND_EMAILS=false`) | | `users.account.twoFactorAuth.enable.request` | `POST /api/users/me/2fa/enable/request` | | `users.account.twoFactorAuth.enable.finish` | `POST /api/users/me/2fa/enable/finish` | | `users.account.twoFactorAuth.load` | `GET /api/users/me/2fa` | @@ -90,7 +90,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `websocket/groups/rotate-keys` | — | **removed** per RESTART_PLAN | | `websocket/pages/move` | `POST /api/pages/:pageId/move` | | | `websocket/users/account/change-password` | `POST /api/users/me/password` | **implemented** in `@deepnotes/session` (`performUserPasswordChange`) | -| `websocket/users/account/email-change/finish` | `POST /api/users/me/email-change/confirm` | | +| `websocket/users/account/email-change/finish` | `POST /api/users/me/email-change/confirm` | **implemented** — one call: `oldLoginHash`, `emailVerificationCode` (6 digits), `newLoginHash`, `userEncryptedPrivateKeyring`, `userEncryptedSymmetricKeyring` (b64; same as register/password); 204, clears cookies; optional Stripe in worker | | `websocket/users/account/rotate-keys` | — | **removed** | ## Webhooks (not tRPC) diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index eac684f0..f4b1c50b 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -27,10 +27,15 @@ export { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, userAccountDeleteRequestSchema, + userEmailChangeConfirmRequestSchema, + userEmailChangeRequestResponseSchema, + userEmailChangeRequestSchema, userMeResponseSchema, userPasswordChangeRequestSchema, userRegisterResponseSchema, type UserAccountDeleteRequest, + type UserEmailChangeConfirmRequest, + type UserEmailChangeRequest, type UserMeResponse, type UserPasswordChangeRequest, type UserRegisterResponse, diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 74a38c24..6706e0e2 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -16,6 +16,12 @@ describe("getOpenApiDocument", () => { expect(doc.paths?.["/api/sessions/demo"]?.post).toBeDefined(); expect(doc.paths?.["/api/users/me"]?.get).toBeDefined(); expect(doc.paths?.["/api/users/me/password"]?.post).toBeDefined(); + expect( + doc.paths?.["/api/users/me/email-change"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/users/me/email-change/confirm"]?.post, + ).toBeDefined(); expect(doc.paths?.["/api/users"]?.post).toBeDefined(); expect( doc.paths?.["/api/users/email-verification/resend"]?.post, diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index 2aea0ff0..78fe0380 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -20,6 +20,9 @@ import { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, userAccountDeleteRequestSchema, + userEmailChangeConfirmRequestSchema, + userEmailChangeRequestResponseSchema, + userEmailChangeRequestSchema, userMeResponseSchema, userPasswordChangeRequestSchema, userRegisterResponseSchema, @@ -230,6 +233,91 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/users/me/email-change", + summary: "Request account email change (6-digit code email)", + description: + "Replaces legacy `users.account.emailChange.request`. Verifies `oldLoginHash` and that the new address is not already registered. When outbound email is enabled, sends a 6-digit code. When `SEND_EMAILS=false` (e.g. local), returns 200 with `emailVerificationCode` instead of emailing.", + request: { + body: { + content: { + "application/json": { + schema: userEmailChangeRequestSchema, + }, + }, + }, + }, + responses: { + 204: { + description: "Code emailed to the new address; pending change stored on the user row.", + }, + 200: { + description: + "Out-of-band dev response when `SEND_EMAILS=false` (verification code not emailed).", + content: { + "application/json": { + schema: userEmailChangeRequestResponseSchema, + }, + }, + }, + 400: { + description: "Wrong password, address in use, or validation error.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 502: { + description: "Email send failed (e.g. Resend) after the pending state was written.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/email-change/confirm", + summary: "Confirm email change (re-wrap keyrings, new password)", + description: + "Replaces legacy WebSocket `users.account.emailChange.finish` (step 1 + 2 in one). Verifies 6-digit code and `oldLoginHash`, then applies new email + new password-encrypted keyrings, invalidates sessions, clears cookies; optional Stripe customer email update in the deployment (not in OpenAPI).", + request: { + body: { + content: { + "application/json": { + schema: userEmailChangeConfirmRequestSchema, + }, + }, + }, + }, + responses: { + 204: { + description: "Email updated; sessions cleared; re-login required.", + }, + 400: { + description: "Wrong code, wrong password, no pending change, or invalid keyrings.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "delete", path: "/api/users/me", diff --git a/new-deepnotes/packages/api/src/schemas/users.ts b/new-deepnotes/packages/api/src/schemas/users.ts index 2478ea84..3bf9eaf9 100644 --- a/new-deepnotes/packages/api/src/schemas/users.ts +++ b/new-deepnotes/packages/api/src/schemas/users.ts @@ -77,3 +77,45 @@ export const userPasswordChangeRequestSchema = z export type UserPasswordChangeRequest = z.infer< typeof userPasswordChangeRequestSchema >; + +const sixDigitCode = z + .string() + .regex(/^\d{6}$/, "expected 6-digit verification code"); + +/** + * `POST /api/users/me/email-change` — legacy `users.account.emailChange.request`. + */ +export const userEmailChangeRequestSchema = z + .object({ + oldLoginHash: byteB64, + newEmail: z.string().email(), + }) + .openapi("UserEmailChangeRequest"); + +export type UserEmailChangeRequest = z.infer; + +/** + * When `SEND_EMAILS=false`, the server returns this body (dev / local only). + */ +export const userEmailChangeRequestResponseSchema = z + .object({ + emailVerificationCode: sixDigitCode, + }) + .openapi("UserEmailChangeRequestResponse"); + +/** + * `POST /api/users/me/email-change/confirm` — legacy WS `emailChange.finish` (two steps as one call). + */ +export const userEmailChangeConfirmRequestSchema = z + .object({ + oldLoginHash: byteB64, + emailVerificationCode: sixDigitCode, + newLoginHash: byteB64, + userEncryptedPrivateKeyring: byteB64, + userEncryptedSymmetricKeyring: byteB64, + }) + .openapi("UserEmailChangeConfirmRequest"); + +export type UserEmailChangeConfirmRequest = z.infer< + typeof userEmailChangeConfirmRequestSchema +>; diff --git a/new-deepnotes/packages/session/src/change-user-email.ts b/new-deepnotes/packages/session/src/change-user-email.ts new file mode 100644 index 00000000..8acade48 --- /dev/null +++ b/new-deepnotes/packages/session/src/change-user-email.ts @@ -0,0 +1,337 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { eq } from "drizzle-orm"; +import sodium from "libsodium-wrappers-sumo"; + +import { sessions, users } from "@deepnotes/db/schema"; + +import { + createPrivateKeyring, + createSymmetricKeyring, + getPasswordHashValues, +} from "./crypto/index.js"; +import { encodePasswordHash } from "./crypto/password-hashing.js"; +import { + decryptUserRehashedLoginHash, + derivePasswordValues, + encryptUserRehashedLoginHash, + ensureSodiumReady, +} from "./crypto/session-crypto.js"; +import { buildClearSessionCookies, cookieOptionsFromEnv } from "./cookies.js"; +import { decryptUserEmail, encryptUserEmail } from "./encrypt-user-email.js"; +import { hashUserEmail } from "./email-hash.js"; +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { verifyAccessToken } from "./jwt.js"; +import { sendEmailChangeVerificationEmail } from "./send-email-change-code.js"; + +function toBuf(u: Uint8Array): Buffer { + return Buffer.from(u); +} + +/** + * `users.account.emailChange.request` (tRPC): password check, non-demo, new email + * not in use, stores `encrypted_new_email` + 6-digit `email_verification_code`, sends email. + */ +export async function performUserEmailChangeRequest(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + oldLoginHash: Uint8Array; + newEmail: string; +}): Promise<{ devEmailVerificationCode?: string }> { + await ensureSodiumReady(); + + if (input.accessCookie == null || input.accessCookie === "") { + throw new SessionError(401, "UNAUTHORIZED", "No access token."); + } + const payload = await verifyAccessToken( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + if (payload == null) { + throw new SessionError(401, "UNAUTHORIZED", "Invalid access token."); + } + const userId = payload.uid; + + const userRows = await input.db + .select({ + id: users.id, + demo: users.demo, + encryptedRehashedLoginHash: users.encryptedRehashedLoginHash, + }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + const userRow = userRows[0]; + if (userRow == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + if (userRow.demo === true) { + throw new SessionError( + 403, + "FORBIDDEN", + "This action is unavailable for demo accounts.", + ); + } + + const passwordHashValues = getPasswordHashValues( + decryptUserRehashedLoginHash( + new Uint8Array(userRow.encryptedRehashedLoginHash), + input.env.USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + ), + ); + const passwordValues = derivePasswordValues({ + password: input.oldLoginHash, + salt: passwordHashValues.saltBytes, + }); + if (!sodium.memcmp(passwordValues.hash, passwordHashValues.hashBytes)) { + throw new SessionError(400, "BAD_REQUEST", "Password is incorrect."); + } + + const exceptions = input.env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS ?? ""; + const newEmail = input.newEmail.trim(); + if (newEmail.length === 0) { + throw new SessionError(400, "BAD_REQUEST", "Invalid email."); + } + const normalized = exceptions.split(";").includes(newEmail) + ? newEmail + : newEmail.toLowerCase(); + + const newEmailHash = Buffer.from( + await hashUserEmail(normalized, input.env.USER_EMAIL_SECRET, exceptions), + ); + + // Legacy checks global `email_hash` (includes current user — re-entering the same address fails). + const inUse = await input.db + .select({ id: users.id }) + .from(users) + .where(eq(users.emailHash, newEmailHash)) + .limit(1); + if (inUse.length > 0) { + throw new SessionError(400, "BAD_REQUEST", "Email is already in use"); + } + + const u32 = crypto.getRandomValues(new Uint32Array(1))[0]!; + const codeNum = u32 % 1_000_000; + const emailVerificationCode = String(codeNum).padStart(6, "0"); + + const encNew = encryptUserEmail( + normalized, + input.env.USER_EMAIL_ENCRYPTION_KEY, + exceptions, + ); + + const updated = await input.db + .update(users) + .set({ + encryptedNewEmail: toBuf(encNew), + emailVerificationCode: emailVerificationCode, + }) + .where(eq(users.id, userId)) + .returning({ id: users.id }); + if (updated.length !== 1) { + throw new SessionError(500, "SERVER_MISCONFIG", "Failed to set email change state."); + } + + if (input.env.SEND_EMAILS === "false") { + return { devEmailVerificationCode: emailVerificationCode }; + } + + await sendEmailChangeVerificationEmail({ + env: input.env, + toEmail: normalized, + emailVerificationCode, + }); + + return {}; +} + +/** + * `users.account.emailChange.finish` (WS steps 1+2) as one call: 6-digit code, old+new + * password, keyrings re-wrapped; writes new `encrypted_email` / `email_hash`, clears pending + * change, invalidates sessions, clears cookies; optional Stripe `customers.update` email. + */ +export async function performUserEmailChangeConfirm(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + oldLoginHash: Uint8Array; + emailVerificationCode: string; + newLoginHash: Uint8Array; + newEncryptedPrivateKeyring: Uint8Array; + newEncryptedSymmetricKeyring: Uint8Array; + updateStripeCustomerEmail?: ( + customerId: string, + email: string, + ) => Promise; +}): Promise<{ cookieLines: string[] }> { + await ensureSodiumReady(); + const cookieOpts = cookieOptionsFromEnv(input.env); + + if (input.accessCookie == null || input.accessCookie === "") { + throw new SessionError(401, "UNAUTHORIZED", "No access token."); + } + const payload = await verifyAccessToken( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + if (payload == null) { + throw new SessionError(401, "UNAUTHORIZED", "Invalid access token."); + } + const userId = payload.uid; + + const userRows = await input.db + .select({ + id: users.id, + demo: users.demo, + customerId: users.customerId, + emailVerificationCode: users.emailVerificationCode, + encryptedNewEmail: users.encryptedNewEmail, + encryptedRehashedLoginHash: users.encryptedRehashedLoginHash, + }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + const u = userRows[0]; + if (u == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + if (u.demo === true) { + throw new SessionError( + 403, + "FORBIDDEN", + "This action is unavailable for demo accounts.", + ); + } + if (u.emailVerificationCode !== input.emailVerificationCode) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Invalid email verification code.", + ); + } + if (u.encryptedNewEmail == null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "No email change requested.", + ); + } + + const passwordHashValues = getPasswordHashValues( + decryptUserRehashedLoginHash( + new Uint8Array(u.encryptedRehashedLoginHash), + input.env.USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + ), + ); + const passwordCheck = derivePasswordValues({ + password: input.oldLoginHash, + salt: passwordHashValues.saltBytes, + }); + if (!sodium.memcmp(passwordCheck.hash, passwordHashValues.hashBytes)) { + throw new SessionError(400, "BAD_REQUEST", "Password is incorrect."); + } + + const exceptions = input.env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS ?? ""; + const newEmail = decryptUserEmail( + new Uint8Array(u.encryptedNewEmail), + input.env.USER_EMAIL_ENCRYPTION_KEY, + exceptions, + ); + + const newEmailHash = Buffer.from( + await hashUserEmail(newEmail, input.env.USER_EMAIL_SECRET, exceptions), + ); + + const newPw = derivePasswordValues({ password: input.newLoginHash }); + const encodedRehash = encodePasswordHash(newPw.hash, newPw.salt, 2, 32); + const encryptedRehashedLoginHash = toBuf( + encryptUserRehashedLoginHash( + encodedRehash, + input.env.USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + ), + ); + const encryptedEmailStored = toBuf( + encryptUserEmail( + newEmail, + input.env.USER_EMAIL_ENCRYPTION_KEY, + exceptions, + ), + ); + + let encryptedPrivateStored: Buffer; + let encryptedSymmetricStored: Buffer; + try { + encryptedPrivateStored = toBuf( + createPrivateKeyring(input.newEncryptedPrivateKeyring) + .wrapSymmetric(newPw.key, { + associatedData: { + context: "UserEncryptedPrivateKeyring", + userId, + }, + }).wrappedValue, + ); + encryptedSymmetricStored = toBuf( + createSymmetricKeyring(input.newEncryptedSymmetricKeyring) + .wrapSymmetric(newPw.key, { + associatedData: { + context: "UserEncryptedSymmetricKeyring", + userId, + }, + }).wrappedValue, + ); + } catch { + throw new SessionError( + 400, + "BAD_REQUEST", + "Invalid keyring material for the new password.", + ); + } + + const customerId = u.customerId; + + await input.db.transaction(async (tx) => { + const updated = await tx + .update(users) + .set({ + encryptedEmail: encryptedEmailStored, + emailHash: newEmailHash, + encryptedNewEmail: null, + emailVerificationCode: null, + encryptedRehashedLoginHash, + encryptedPrivateKeyring: encryptedPrivateStored, + encryptedSymmetricKeyring: encryptedSymmetricStored, + }) + .where(eq(users.id, userId)) + .returning({ id: users.id }); + + if (updated.length !== 1) { + throw new SessionError( + 500, + "SERVER_MISCONFIG", + "Email change did not apply.", + ); + } + await tx + .update(sessions) + .set({ invalidated: true }) + .where(eq(sessions.userId, userId)); + }); + + const stripe = input.updateStripeCustomerEmail; + if ( + customerId != null && + customerId.length > 0 && + stripe != null + ) { + try { + await stripe(customerId, newEmail); + } catch { + // match legacy: non-fatal; account email is already updated in DB + } + } + + return { cookieLines: buildClearSessionCookies(cookieOpts) }; +} diff --git a/new-deepnotes/packages/session/src/encrypt-user-email.ts b/new-deepnotes/packages/session/src/encrypt-user-email.ts index 962fb7d8..83056948 100644 --- a/new-deepnotes/packages/session/src/encrypt-user-email.ts +++ b/new-deepnotes/packages/session/src/encrypt-user-email.ts @@ -1,4 +1,4 @@ -import { base64ToBytes, textToBytes } from "./crypto/bytes.js"; +import { base64ToBytes, bytesToText, textToBytes } from "./crypto/bytes.js"; import { wrapSymmetricKey } from "./crypto/symmetric-key.js"; function normalizeEmail(email: string, exceptions: string): string { @@ -20,3 +20,19 @@ export function encryptUserEmail( associatedData: { context: "UserEmail" }, }); } + +/** + * Decrypts ciphertext from {@link encryptUserEmail} (same key and `EMAIL_CASE_SENSITIVITY_EXCEPTIONS` rules). + */ +export function decryptUserEmail( + encrypted: Uint8Array, + encryptionKeyB64: string, + exceptions: string, +): string { + const key = wrapSymmetricKey(base64ToBytes(encryptionKeyB64)); + const plaintext = key.decrypt(encrypted, { + padding: true, + associatedData: { context: "UserEmail" }, + }); + return normalizeEmail(bytesToText(plaintext), exceptions); +} diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 486182a8..65ca8880 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -16,6 +16,10 @@ export { performUserRegister } from "./register-user.js"; export type { UserRegisterInput } from "./register-user.js"; export { performUserAccountDelete } from "./delete-user-account.js"; export { performUserPasswordChange } from "./change-user-password.js"; +export { + performUserEmailChangeConfirm, + performUserEmailChangeRequest, +} from "./change-user-email.js"; export { performConfirmEmailVerification, performResendEmailVerification, diff --git a/new-deepnotes/packages/session/src/send-email-change-code.ts b/new-deepnotes/packages/session/src/send-email-change-code.ts new file mode 100644 index 00000000..996423cc --- /dev/null +++ b/new-deepnotes/packages/session/src/send-email-change-code.ts @@ -0,0 +1,51 @@ +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; + +function sendEmailsEnabled(env: SessionEnv): boolean { + return env.SEND_EMAILS !== "false"; +} + +/** + * 6-digit code email for `users.account.emailChange.request` (Resend), legacy-style copy. + */ +export async function sendEmailChangeVerificationEmail(input: { + env: SessionEnv; + toEmail: string; + emailVerificationCode: string; +}): Promise { + if (!sendEmailsEnabled(input.env)) { + return; + } + const key = input.env.RESEND_API_KEY?.trim(); + if (key == null || key.length === 0) { + throw new SessionError( + 503, + "SERVICE_UNAVAILABLE", + "RESEND_API_KEY is required to send email change verification.", + ); + } + + const res = await fetch("https://api.resend.com/emails", { + method: "POST", + headers: { + Authorization: `Bearer ${key}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + from: "DeepNotes ", + to: [input.toEmail], + subject: "Verify your email address", + html: `Use the following code to verify your email address: ${input.emailVerificationCode}.
+If you did not request this action, you can safely ignore this email.`, + }), + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new SessionError( + 502, + "EMAIL_SEND_FAILED", + `Resend request failed (${res.status}): ${text.slice(0, 200)}`, + ); + } +} From 7dd8bfb22eae17d486c77c937bd12ad477deadd0 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:00:34 -0300 Subject: [PATCH 034/243] test(new-deepnotes): unit tests for user email and session crypto helpers --- new-deepnotes/PLAN_PROGRESS.md | 24 +++- .../apps/api-worker/src/index.test.ts | 2 + .../packages/api/src/schemas/users.test.ts | 53 +++++++++ .../packages/session/src/email-hash.test.ts | 28 +++++ .../session/src/encrypt-user-email.test.ts | 35 ++++++ .../src/send-email-change-code.test.ts | 108 ++++++++++++++++++ 6 files changed, 249 insertions(+), 1 deletion(-) create mode 100644 new-deepnotes/packages/api/src/schemas/users.test.ts create mode 100644 new-deepnotes/packages/session/src/email-hash.test.ts create mode 100644 new-deepnotes/packages/session/src/encrypt-user-email.test.ts create mode 100644 new-deepnotes/packages/session/src/send-email-change-code.test.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 67e03565..d05656fd 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -31,6 +31,15 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ ## Phase 3 checklist (REST + Drizzle) +### Test coverage (Phase 3 account surface) + +- [x] **Rate limit:** failed login counters (`login-rate-limit.test.ts`). +- [x] **Email crypto:** `encryptUserEmail` / `decryptUserEmail` + `hashUserEmail` (legacy parity cases). +- [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). +- [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change byte fields (`schemas/users.test.ts`). +- [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. +- [ ] **DB integration:** register → email-change request/confirm (or password change) against **template DB** with real `perform*` + minimal fixtures. + ### Sessions + account (current) - [x] Document **sessions** REST paths + request schemas in OpenAPI; demo + `users/me` contracts updated. @@ -102,6 +111,18 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] **Contract tests** for the fetch wrapper (MSW or recorded OpenAPI fixtures)—optional until multiple features consume the API. - [ ] **E2E smoke** (Playwright recommended): login or session refresh with **httpOnly cookies** against **local compose** or **Cloudflare preview**—add CI job when stable enough (can start `manual`/`workflow_dispatch` if cost is a concern). +#### Automated tests — package matrix (maintenance) + +| Package / app | Role | What runs today | Gaps (highest value next) | +|---------------|------|------------------|---------------------------| +| **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts`: clone template DB, smoke SQL | More assertions on FKs / critical columns after schema grows | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | `login-rate-limit.test.ts` (Redis port in memory); **`encrypt-user-email.test.ts`** (round-trip, case exceptions, tamper); **`email-hash.test.ts`** (stability, secret sensitivity, exceptions); **`send-email-change-code.test.ts`** (SEND_EMAILS=false no fetch, missing key **503**, Resend **502**/OK) | **Integration:** `performUserRegister`, login, password/email change against **template DB** + mocked Redis; JWT cookie helpers | +| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + route registry); **`schemas/users.test.ts`** (email/password change bodies, 6-digit code) | Schemas for sessions + remaining routes; optional **snapshot** of OpenAPI fragment for drift | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: health, OpenAPI JSON, **503** when secrets/DB not bound (incl. email-change paths) | **200-path tests** with test `SessionEnv` + Hyperdrive stub + template DB (heavier CI job) | +| **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | + +**Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. + ### Progress vs legacy (reference only) | Legacy (`apps/client`) | New (`new-deepnotes/apps/web`) | @@ -119,7 +140,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). - [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` template test. -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto email path (`encrypt-user-email`, `email-hash`), Resend branch behavior, and **user** Zod bodies are covered in Vitest; session **perform\*** flows still need DB integration tests. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -138,6 +159,7 @@ Use this when resuming: **(done)** account HTTP surface through email change inc | Date | Change | |------|--------| +| 2026-04-26 | **Tests:** `@deepnotes/session` — `encrypt-user-email.test.ts`, `email-hash.test.ts`, `send-email-change-code.test.ts`; `@deepnotes/api` — `schemas/users.test.ts`; api-worker — email-change routes in `503` matrix; PLAN_PROGRESS — package test matrix + Phase 3 test checklist. | | 2026-04-26 | Phase 3: **email change** — `POST /api/users/me/email-change` + `…/confirm` (`change-user-email.ts`, `decryptUserEmail`, `send-email-change-code`); `userEmailChange*Request` schemas, OpenAPI, Hono; TRPC_REST_MAP; PLAN_PROGRESS detail + suggested Phase 3 order. | | 2026-04-26 | Phase 3: **`POST /api/users/me/password`** — `performUserPasswordChange` (`change-user-password.ts`): old password verify, demo **403**, new keyrings + PHC, invalidate all `sessions`, clear cookies **204**; `userPasswordChangeRequestSchema`, OpenAPI + worker; export **`byteB64`** from `@deepnotes/api`; TRPC_REST_MAP rows for change-password; PLAN_PROGRESS Phase 3 account section expanded. | | 2026-04-26 | Phase 2 + §5.8: `@deepnotes/web` — Vitest + happy-dom + `@vue/test-utils`, `vite.config` from `vitest/config`, `src/app.test.ts`; Phase 3: `DELETE /api/users/me` + `performUserAccountDelete` (ownership guard, Drizzle tx, clear cookies); `userAccountDeleteRequestSchema` + OpenAPI; api-worker route; TRPC_REST_MAP note on delete body / Stripe hook. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 04592f85..b0ed1108 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -32,6 +32,8 @@ describe("api-worker", () => { ["DELETE", "/api/users/me"], ["POST", "/api/users"], ["POST", "/api/users/email-verification/resend"], + ["POST", "/api/users/me/email-change"], + ["POST", "/api/users/me/email-change/confirm"], ] as const)("returns 503 for %s %s when auth env is not configured", async (method, path) => { const res = await app.request(`http://test${path}`, { method }); expect(res.status).toBe(503); diff --git a/new-deepnotes/packages/api/src/schemas/users.test.ts b/new-deepnotes/packages/api/src/schemas/users.test.ts new file mode 100644 index 00000000..1ce28c9d --- /dev/null +++ b/new-deepnotes/packages/api/src/schemas/users.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; + +import { + userEmailChangeConfirmRequestSchema, + userEmailChangeRequestSchema, + userPasswordChangeRequestSchema, +} from "./users.js"; + +const oneByteB64 = Buffer.from([0xab]).toString("base64"); + +describe("user request schemas (REST body validation)", () => { + it("userEmailChangeRequestSchema accepts byte fields and email", () => { + const parsed = userEmailChangeRequestSchema.parse({ + oldLoginHash: oneByteB64, + newEmail: "new@example.com", + }); + expect(parsed.oldLoginHash).toEqual(new Uint8Array([0xab])); + expect(parsed.newEmail).toBe("new@example.com"); + }); + + it("userEmailChangeConfirmRequestSchema requires six-digit code", () => { + const body = { + oldLoginHash: oneByteB64, + emailVerificationCode: "042069", + newLoginHash: Buffer.from([1]).toString("base64"), + userEncryptedPrivateKeyring: Buffer.from([2]).toString("base64"), + userEncryptedSymmetricKeyring: Buffer.from([3]).toString("base64"), + }; + const ok = userEmailChangeConfirmRequestSchema.parse(body); + expect(ok.emailVerificationCode).toBe("042069"); + expect(() => + userEmailChangeConfirmRequestSchema.parse({ + ...body, + emailVerificationCode: "12345", + }), + ).toThrow(); + }); + + it("userPasswordChangeRequestSchema decodes all byte fields", () => { + const a = Buffer.from("aa", "utf8").toString("base64"); + const b = Buffer.from("bb", "utf8").toString("base64"); + const c = Buffer.from("cc", "utf8").toString("base64"); + const d = Buffer.from("dd", "utf8").toString("base64"); + const p = userPasswordChangeRequestSchema.parse({ + oldLoginHash: a, + newLoginHash: b, + userEncryptedPrivateKeyring: c, + userEncryptedSymmetricKeyring: d, + }); + expect(new TextDecoder().decode(p.oldLoginHash)).toBe("aa"); + expect(new TextDecoder().decode(p.newLoginHash)).toBe("bb"); + }); +}); diff --git a/new-deepnotes/packages/session/src/email-hash.test.ts b/new-deepnotes/packages/session/src/email-hash.test.ts new file mode 100644 index 00000000..742e1e96 --- /dev/null +++ b/new-deepnotes/packages/session/src/email-hash.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; + +import { hashUserEmail } from "./email-hash.js"; + +describe("hashUserEmail", () => { + it("is stable for the same normalized email and secret", async () => { + const secret = "test-email-hmac-secret"; + const exceptions = ""; + const a = await hashUserEmail("MixEd@Case.com", secret, exceptions); + const b = await hashUserEmail("mixed@case.com", secret, exceptions); + expect(Buffer.from(a).equals(Buffer.from(b))).toBe(true); + }); + + it("differs when the secret differs", async () => { + const exceptions = ""; + const h1 = await hashUserEmail("same@x.co", "secret-a", exceptions); + const h2 = await hashUserEmail("same@x.co", "secret-b", exceptions); + expect(Buffer.from(h1).equals(Buffer.from(h2))).toBe(false); + }); + + it("respects case-sensitivity exceptions like encryptUserEmail", async () => { + const secret = "s"; + const ex = "Special@X.co"; + const hPreserve = await hashUserEmail("Special@X.co", secret, ex); + const hLower = await hashUserEmail("special@x.co", secret, ex); + expect(Buffer.from(hPreserve).equals(Buffer.from(hLower))).toBe(false); + }); +}); diff --git a/new-deepnotes/packages/session/src/encrypt-user-email.test.ts b/new-deepnotes/packages/session/src/encrypt-user-email.test.ts new file mode 100644 index 00000000..b1e648ea --- /dev/null +++ b/new-deepnotes/packages/session/src/encrypt-user-email.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it, beforeAll } from "vitest"; + +import { ensureSodiumReady } from "./crypto/session-crypto.js"; +import { decryptUserEmail, encryptUserEmail } from "./encrypt-user-email.js"; + +/** 32-byte XChaCha key as standard base64 */ +const TEST_KEY_B64 = Buffer.alloc(32, 9).toString("base64"); + +describe("encryptUserEmail / decryptUserEmail", () => { + beforeAll(async () => { + await ensureSodiumReady(); + }); + + it("round-trips and lowercases by default", () => { + const email = "User@Example.COM"; + const ct = encryptUserEmail(email, TEST_KEY_B64, ""); + const out = decryptUserEmail(ct, TEST_KEY_B64, ""); + expect(out).toBe("user@example.com"); + }); + + it("preserves casing for addresses in EMAIL_CASE_SENSITIVITY_EXCEPTIONS", () => { + const email = "PreserveCase@X.org"; + const exceptions = "PreserveCase@X.org"; + const ct = encryptUserEmail(email, TEST_KEY_B64, exceptions); + const out = decryptUserEmail(ct, TEST_KEY_B64, exceptions); + expect(out).toBe("PreserveCase@X.org"); + }); + + it("rejects tampered ciphertext", () => { + const ct = encryptUserEmail("a@b.co", TEST_KEY_B64, ""); + const tampered = new Uint8Array(ct); + tampered[0] = (tampered[0] ?? 0) ^ 0xff; + expect(() => decryptUserEmail(tampered, TEST_KEY_B64, "")).toThrow(); + }); +}); diff --git a/new-deepnotes/packages/session/src/send-email-change-code.test.ts b/new-deepnotes/packages/session/src/send-email-change-code.test.ts new file mode 100644 index 00000000..63072015 --- /dev/null +++ b/new-deepnotes/packages/session/src/send-email-change-code.test.ts @@ -0,0 +1,108 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import type { SessionEnv } from "./env.js"; +import { sendEmailChangeVerificationEmail } from "./send-email-change-code.js"; + +function minimalEnv( + overrides: Partial>, +): SessionEnv { + return { + ACCESS_SECRET: "a", + REFRESH_SECRET: "b", + USER_EMAIL_SECRET: "c", + USER_EMAIL_ENCRYPTION_KEY: Buffer.alloc(32, 1).toString("base64"), + USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY: Buffer.alloc(32, 2).toString( + "base64", + ), + USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY: Buffer.alloc(32, 3).toString( + "base64", + ), + USER_RECOVERY_CODES_ENCRYPTION_KEY: Buffer.alloc(32, 4).toString("base64"), + ...overrides, + }; +} + +describe("sendEmailChangeVerificationEmail", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("no-ops when SEND_EMAILS is false (no Resend, no fetch)", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch"); + await sendEmailChangeVerificationEmail({ + env: minimalEnv({ SEND_EMAILS: "false" }), + toEmail: "u@x.co", + emailVerificationCode: "123456", + }); + expect(fetchSpy).not.toHaveBeenCalled(); + }); + + it("throws 503 when outbound email is enabled but RESEND_API_KEY is missing", async () => { + await expect( + sendEmailChangeVerificationEmail({ + env: minimalEnv({ SEND_EMAILS: "true", RESEND_API_KEY: undefined }), + toEmail: "u@x.co", + emailVerificationCode: "000000", + }), + ).rejects.toMatchObject({ + name: "SessionError", + status: 503, + code: "SERVICE_UNAVAILABLE", + }); + }); + + it("throws 503 when RESEND_API_KEY is whitespace only", async () => { + await expect( + sendEmailChangeVerificationEmail({ + env: minimalEnv({ + SEND_EMAILS: "true", + RESEND_API_KEY: " \t", + }), + toEmail: "u@x.co", + emailVerificationCode: "000000", + }), + ).rejects.toMatchObject({ status: 503 }); + }); + + it("throws 502 when Resend returns non-OK", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response("bad", { status: 422 }), + ); + await expect( + sendEmailChangeVerificationEmail({ + env: minimalEnv({ + SEND_EMAILS: "true", + RESEND_API_KEY: "re_test", + }), + toEmail: "u@x.co", + emailVerificationCode: "654321", + }), + ).rejects.toMatchObject({ + status: 502, + code: "EMAIL_SEND_FAILED", + }); + }); + + it("resolves when Resend returns OK", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue(new Response("", { status: 200 })); + await expect( + sendEmailChangeVerificationEmail({ + env: minimalEnv({ + SEND_EMAILS: "true", + RESEND_API_KEY: "re_ok", + }), + toEmail: "user@example.com", + emailVerificationCode: "111222", + }), + ).resolves.toBeUndefined(); + expect(fetch).toHaveBeenCalledWith( + "https://api.resend.com/emails", + expect.objectContaining({ + method: "POST", + headers: expect.objectContaining({ + Authorization: "Bearer re_ok", + }), + }), + ); + }); +}); From d58303ee3aaa0ed2ce574e0f38b3427435b40ff7 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:05:34 -0300 Subject: [PATCH 035/243] test(new-deepnotes): email change integration tests and template DB --- new-deepnotes/PLAN_PROGRESS.md | 5 +- new-deepnotes/packages/db/package.json | 8 + new-deepnotes/packages/session/package.json | 2 + .../src/email-change.integration.test.ts | 267 ++++++++++++++++++ new-deepnotes/pnpm-lock.yaml | 6 + 5 files changed, 286 insertions(+), 2 deletions(-) create mode 100644 new-deepnotes/packages/session/src/email-change.integration.test.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index d05656fd..e3b06bb4 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change byte fields (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [ ] **DB integration:** register → email-change request/confirm (or password change) against **template DB** with real `perform*` + minimal fixtures. +- [x] **DB integration:** `email-change.integration.test.ts` — `performUserRegister` → `performUserEmailChangeRequest` → `performUserEmailChangeConfirm` on a **clone** of migrated Postgres (template `dn_test_tpl_session_email`, distinct from `@deepnotes/db`’s `dn_test_tpl_deepnotes` for parallel Turbo); asserts decrypted email, `email_hash`, cleared pending fields; wrong-password request → **400**. Skips when `DATABASE_URL` / admin URL unavailable (`describe.skipIf`). ### Sessions + account (current) @@ -116,7 +116,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts`: clone template DB, smoke SQL | More assertions on FKs / critical columns after schema grows | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | `login-rate-limit.test.ts` (Redis port in memory); **`encrypt-user-email.test.ts`** (round-trip, case exceptions, tamper); **`email-hash.test.ts`** (stability, secret sensitivity, exceptions); **`send-email-change-code.test.ts`** (SEND_EMAILS=false no fetch, missing key **503**, Resend **502**/OK) | **Integration:** `performUserRegister`, login, password/email change against **template DB** + mocked Redis; JWT cookie helpers | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `email-change.integration.test.ts` (template `dn_test_tpl_session_email`, `performUserRegister` → email-change request/confirm, JWT). Uses **`@deepnotes/db/testing/template-db`** + **`db-url`**. | Login + password-change template tests; Redis-backed rate limit with real `performSessionLogin` | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + route registry); **`schemas/users.test.ts`** (email/password change bodies, 6-digit code) | Schemas for sessions + remaining routes; optional **snapshot** of OpenAPI fragment for drift | | **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: health, OpenAPI JSON, **503** when secrets/DB not bound (incl. email-change paths) | **200-path tests** with test `SessionEnv` + Hyperdrive stub + template DB (heavier CI job) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | @@ -159,6 +159,7 @@ Use this when resuming: **(done)** account HTTP surface through email change inc | Date | Change | |------|--------| +| 2026-04-26 | **Integration tests:** `@deepnotes/db` exports `@deepnotes/db/testing/template-db` + `db-url`; `@deepnotes/session` — `email-change.integration.test.ts` (Postgres template clone, register + email change + wrong password). PLAN_PROGRESS matrix + Phase 3 checklist updated. | | 2026-04-26 | **Tests:** `@deepnotes/session` — `encrypt-user-email.test.ts`, `email-hash.test.ts`, `send-email-change-code.test.ts`; `@deepnotes/api` — `schemas/users.test.ts`; api-worker — email-change routes in `503` matrix; PLAN_PROGRESS — package test matrix + Phase 3 test checklist. | | 2026-04-26 | Phase 3: **email change** — `POST /api/users/me/email-change` + `…/confirm` (`change-user-email.ts`, `decryptUserEmail`, `send-email-change-code`); `userEmailChange*Request` schemas, OpenAPI, Hono; TRPC_REST_MAP; PLAN_PROGRESS detail + suggested Phase 3 order. | | 2026-04-26 | Phase 3: **`POST /api/users/me/password`** — `performUserPasswordChange` (`change-user-password.ts`): old password verify, demo **403**, new keyrings + PHC, invalidate all `sessions`, clear cookies **204**; `userPasswordChangeRequestSchema`, OpenAPI + worker; export **`byteB64`** from `@deepnotes/api`; TRPC_REST_MAP rows for change-password; PLAN_PROGRESS Phase 3 account section expanded. | diff --git a/new-deepnotes/packages/db/package.json b/new-deepnotes/packages/db/package.json index a2d64826..6526eccd 100644 --- a/new-deepnotes/packages/db/package.json +++ b/new-deepnotes/packages/db/package.json @@ -11,6 +11,14 @@ "./client": { "types": "./src/client.ts", "default": "./src/client.ts" + }, + "./testing/template-db": { + "types": "./src/test/template-db.ts", + "default": "./src/test/template-db.ts" + }, + "./testing/db-url": { + "types": "./src/test/db-url.ts", + "default": "./src/test/db-url.ts" } }, "scripts": { diff --git a/new-deepnotes/packages/session/package.json b/new-deepnotes/packages/session/package.json index d59d8efb..21a802c6 100644 --- a/new-deepnotes/packages/session/package.json +++ b/new-deepnotes/packages/session/package.json @@ -27,6 +27,8 @@ "devDependencies": { "@types/crypto-js": "^4.2.2", "@types/node": "^22.14.1", + "dotenv": "^16.5.0", + "postgres": "^3.4.5", "typescript": "^5.8.3", "vitest": "^3.2.4" } diff --git a/new-deepnotes/packages/session/src/email-change.integration.test.ts b/new-deepnotes/packages/session/src/email-change.integration.test.ts new file mode 100644 index 00000000..e0d0998c --- /dev/null +++ b/new-deepnotes/packages/session/src/email-change.integration.test.ts @@ -0,0 +1,267 @@ +import { randomBytes } from "node:crypto"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { config as loadEnv } from "dotenv"; +import { eq } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/postgres-js"; +import sodium from "libsodium-wrappers-sumo"; +import { nanoid } from "nanoid"; +import postgres from "postgres"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import { withDatabaseName } from "@deepnotes/db/testing/db-url"; +import { + createDatabaseFromTemplate, + dropDatabaseIfExists, + ensureTemplateDatabase, + resolveTemplateContext, + type TemplateDbContext, +} from "@deepnotes/db/testing/template-db"; +import * as schema from "@deepnotes/db/schema"; +import { users } from "@deepnotes/db/schema"; + +import { + performUserEmailChangeConfirm, + performUserEmailChangeRequest, +} from "./change-user-email.js"; +import type { UserRegisterInput } from "./register-user.js"; +import { performUserRegister } from "./register-user.js"; +import { ensureSodiumReady } from "./crypto/session-crypto.js"; +import { decryptUserEmail } from "./encrypt-user-email.js"; +import { hashUserEmail } from "./email-hash.js"; +import type { SessionEnv } from "./env.js"; +import { signAccessToken } from "./jwt.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +loadEnv({ path: join(__dirname, "../../../.env") }); + +/** Dedicated template name so `turbo test` can run `@deepnotes/db` and `@deepnotes/session` in parallel. */ +const SESSION_TEMPLATE_NAME = "dn_test_tpl_session_email"; + +function testSessionEnv(): SessionEnv { + const b32 = (n: number) => Buffer.alloc(32, n).toString("base64"); + return { + ACCESS_SECRET: "test-access-secret-min-32-chars-long!!", + REFRESH_SECRET: "test-refresh-secret-min-32-chars-long!!", + USER_EMAIL_SECRET: "test-user-email-secret-hmac-key!!", + USER_EMAIL_ENCRYPTION_KEY: b32(1), + USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY: b32(2), + USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY: b32(3), + USER_RECOVERY_CODES_ENCRYPTION_KEY: b32(4), + SEND_EMAILS: "false", + DEV: "true", + }; +} + +function rand32(): Uint8Array { + return sodium.randombytes_buf(32); +} + +async function buildRegisterBody( + email: string, + loginHash: Uint8Array, +): Promise { + await ensureSodiumReady(); + const userId = nanoid(); + const groupId = nanoid(); + const pageId = nanoid(); + return { + userId, + groupId, + pageId, + email, + loginHash, + userPublicKeyring: rand32(), + userEncryptedPrivateKeyring: rand32(), + userEncryptedSymmetricKeyring: rand32(), + userEncryptedName: rand32(), + userEncryptedDefaultNote: rand32(), + userEncryptedDefaultArrow: rand32(), + groupCreation: { + groupEncryptedName: rand32(), + groupIsPublic: true, + groupAccessKeyring: rand32(), + groupEncryptedInternalKeyring: rand32(), + groupEncryptedContentKeyring: rand32(), + groupPublicKeyring: rand32(), + groupEncryptedPrivateKeyring: rand32(), + groupOwnerEncryptedName: rand32(), + }, + pageCreation: { + pageEncryptedSymmetricKeyring: rand32(), + pageEncryptedRelativeTitle: rand32(), + pageEncryptedAbsoluteTitle: rand32(), + }, + }; +} + +describe.skipIf(resolveTemplateContext() == null)( + "email change (Postgres template DB)", + () => { + const baseCtx = resolveTemplateContext()!; + const ctx: TemplateDbContext = { + ...baseCtx, + templateName: SESSION_TEMPLATE_NAME, + }; + + beforeAll(async () => { + await ensureSodiumReady(); + await ensureTemplateDatabase(ctx); + }); + + afterAll(async () => { + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + }); + + it("register → request → confirm updates email and clears pending state", async () => { + const env = testSessionEnv(); + const exceptions = ""; + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const initialEmail = `u-${nanoid()}@example.com`; + const newEmail = `v-${nanoid()}@example.com`; + const loginHash = rand32(); + const newLoginHash = rand32(); + + const reg = await buildRegisterBody(initialEmail, loginHash); + const { userId } = await performUserRegister({ + db, + env, + body: reg, + }); + expect(userId).toBe(reg.userId); + + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId, + sessionId: nanoid(), + }); + + const reqOut = await performUserEmailChangeRequest({ + db, + env, + accessCookie: access, + oldLoginHash: loginHash, + newEmail, + }); + expect(reqOut.devEmailVerificationCode).toMatch(/^\d{6}$/); + + const pending = await db + .select({ + code: users.emailVerificationCode, + encNew: users.encryptedNewEmail, + }) + .from(users) + .where(eq(users.id, userId)); + expect(pending[0]?.code).toBe(reqOut.devEmailVerificationCode); + expect(pending[0]?.encNew).not.toBeNull(); + + const confirm = await performUserEmailChangeConfirm({ + db, + env, + accessCookie: access, + oldLoginHash: loginHash, + emailVerificationCode: reqOut.devEmailVerificationCode!, + newLoginHash, + newEncryptedPrivateKeyring: rand32(), + newEncryptedSymmetricKeyring: rand32(), + }); + expect(confirm.cookieLines.length).toBeGreaterThan(0); + + const row = await db + .select({ + encryptedEmail: users.encryptedEmail, + emailHash: users.emailHash, + encNew: users.encryptedNewEmail, + code: users.emailVerificationCode, + }) + .from(users) + .where(eq(users.id, userId)); + const u = row[0]!; + expect(u.encNew).toBeNull(); + expect(u.code).toBeNull(); + + const storedPlain = decryptUserEmail( + new Uint8Array(u.encryptedEmail), + env.USER_EMAIL_ENCRYPTION_KEY, + exceptions, + ); + expect(storedPlain).toBe(newEmail.trim().toLowerCase()); + + const expectedHash = Buffer.from( + await hashUserEmail(storedPlain, env.USER_EMAIL_SECRET, exceptions), + ); + expect(Buffer.from(u.emailHash).equals(expectedHash)).toBe(true); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("email change request rejects wrong password", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `w-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + + const wrongHash = rand32(); + await expect( + performUserEmailChangeRequest({ + db, + env, + accessCookie: access, + oldLoginHash: wrongHash, + newEmail: `x-${nanoid()}@example.com`, + }), + ).rejects.toMatchObject({ status: 400, code: "BAD_REQUEST" }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + }, +); diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml index ca9ca458..a183f741 100644 --- a/new-deepnotes/pnpm-lock.yaml +++ b/new-deepnotes/pnpm-lock.yaml @@ -163,6 +163,12 @@ importers: '@types/node': specifier: ^22.14.1 version: 22.19.17 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + postgres: + specifier: ^3.4.5 + version: 3.4.9 typescript: specifier: ^5.8.3 version: 5.9.3 From 90b8b144d68a24c77558106cc6fcb74a7df8e665 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:09:33 -0300 Subject: [PATCH 036/243] test(new-deepnotes): consolidate account flow integration tests --- new-deepnotes/PLAN_PROGRESS.md | 28 +- .../src/account-flows.integration.test.ts | 534 ++++++++++++++++++ .../src/email-change.integration.test.ts | 267 --------- 3 files changed, 556 insertions(+), 273 deletions(-) create mode 100644 new-deepnotes/packages/session/src/account-flows.integration.test.ts delete mode 100644 new-deepnotes/packages/session/src/email-change.integration.test.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index e3b06bb4..9a736e85 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -2,7 +2,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../docs/RESTART_PLAN.md). Update this file when phases advance or decisions change. -**Last reviewed:** 2026-04-26 +**Last reviewed:** 2026-04-27 --- @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Auth + account: sessions, register, public email verify, account delete, password + **email change** (see Phase 3 checklist). **Backlog (priority):** 2FA HTTP surface → pages/groups CRUD → realtime/collab → Stripe webhook. | +| **3** — REST + Drizzle features | **In progress** | Auth + account: sessions, register, public email verify, account delete, password + **email change** + **Postgres integration tests** for both (see [Phase 3 test coverage](#phase-3-test-coverage-detail)). **Next (priority):** 2FA HTTP surface → pages/groups CRUD → realtime/collab → Stripe webhook. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -38,7 +38,22 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change byte fields (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration:** `email-change.integration.test.ts` — `performUserRegister` → `performUserEmailChangeRequest` → `performUserEmailChangeConfirm` on a **clone** of migrated Postgres (template `dn_test_tpl_session_email`, distinct from `@deepnotes/db`’s `dn_test_tpl_deepnotes` for parallel Turbo); asserts decrypted email, `email_hash`, cleared pending fields; wrong-password request → **400**. Skips when `DATABASE_URL` / admin URL unavailable (`describe.skipIf`). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` (renamed from `email-change.integration.test.ts`). See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) for the per-case list. + +### Phase 3 test coverage (detail) + +Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for `CREATE DATABASE`) are unset; they clone template `dn_test_tpl_session_email` (isolated from `@deepnotes/db`’s `dn_test_tpl_deepnotes` so **Turbo** can run both packages in parallel). + +| Test case | Exercises | Assertions | +|-----------|-----------|------------| +| Register → email-change request → confirm | `performUserRegister`, `performUserEmailChange*`, `signAccessToken` | New email in `decryptUserEmail` + `email_hash` match; `encrypted_new_email` / `email_verification_code` cleared; clear-session cookie lines on confirm. | +| Email change request, wrong password | `performUserEmailChangeRequest` | **400** `BAD_REQUEST` when `oldLoginHash` does not match. | +| Email change confirm, wrong 6-digit code | `performUserEmailChangeConfirm` | **400** `BAD_REQUEST` (code mismatch). | +| Password change, happy path | `performUserPasswordChange` | After change: PHC decrypts to a hash matching **new** password; private + symmetric keyrings **unwrap** with the same `derivePasswordValues` key as login (salt from stored PHC), round-trip to the new keyring bytes passed in. | +| Password change invalidates sessions | `performUserPasswordChange` + explicit `devices` / `sessions` insert | `sessions.invalidated === true` for the user. | +| Password change, wrong old password | `performUserPasswordChange` | **400** `BAD_REQUEST`. | + +**Not yet in integration:** `performSessionLogin` + refresh (cookie/session rows), Redis failed-login with real `ioredis`/Upstash, demo account **403** on password change (covered in unit path via `performUserPasswordChange` branches if added later). ### Sessions + account (current) @@ -116,7 +131,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts`: clone template DB, smoke SQL | More assertions on FKs / critical columns after schema grows | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `email-change.integration.test.ts` (template `dn_test_tpl_session_email`, `performUserRegister` → email-change request/confirm, JWT). Uses **`@deepnotes/db/testing/template-db`** + **`db-url`**. | Login + password-change template tests; Redis-backed rate limit with real `performSessionLogin` | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` — email change + **password change** (PHC + unwrap), wrong passwords/codes, session invalidation; template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **`performSessionLogin` / refresh** with template DB + device/session rows; **Redis** + `performSessionLogin` failed-login; optional **demo 403** integration | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + route registry); **`schemas/users.test.ts`** (email/password change bodies, 6-digit code) | Schemas for sessions + remaining routes; optional **snapshot** of OpenAPI fragment for drift | | **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: health, OpenAPI JSON, **503** when secrets/DB not bound (incl. email-change paths) | **200-path tests** with test `SessionEnv` + Hyperdrive stub + template DB (heavier CI job) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | @@ -139,8 +154,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Drizzle migrations from empty DB documented for production upgrades. - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). -- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` template test. -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto email path (`encrypt-user-email`, `email-hash`), Resend branch behavior, and **user** Zod bodies are covered in Vitest; session **perform\*** flows still need DB integration tests. +- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` template test; `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation). +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** template tests for account **register / email change / password change** (see [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail)). **Next:** login + refresh + optional Redis in integration; Stripe when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -159,6 +174,7 @@ Use this when resuming: **(done)** account HTTP surface through email change inc | Date | Change | |------|--------| +| 2026-04-27 | **Integration tests:** expanded `account-flows.integration.test.ts` (email wrong code; password change PHC + keyring unwrap with salt from PHC; `sessions` invalidation; wrong old password). Renamed from `email-change.integration.test.ts`. PLAN_PROGRESS: detailed Phase 3 test table + matrix gaps. | | 2026-04-26 | **Integration tests:** `@deepnotes/db` exports `@deepnotes/db/testing/template-db` + `db-url`; `@deepnotes/session` — `email-change.integration.test.ts` (Postgres template clone, register + email change + wrong password). PLAN_PROGRESS matrix + Phase 3 checklist updated. | | 2026-04-26 | **Tests:** `@deepnotes/session` — `encrypt-user-email.test.ts`, `email-hash.test.ts`, `send-email-change-code.test.ts`; `@deepnotes/api` — `schemas/users.test.ts`; api-worker — email-change routes in `503` matrix; PLAN_PROGRESS — package test matrix + Phase 3 test checklist. | | 2026-04-26 | Phase 3: **email change** — `POST /api/users/me/email-change` + `…/confirm` (`change-user-email.ts`, `decryptUserEmail`, `send-email-change-code`); `userEmailChange*Request` schemas, OpenAPI, Hono; TRPC_REST_MAP; PLAN_PROGRESS detail + suggested Phase 3 order. | diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts new file mode 100644 index 00000000..01d0a8ba --- /dev/null +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -0,0 +1,534 @@ +import { randomBytes } from "node:crypto"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { config as loadEnv } from "dotenv"; +import { eq } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/postgres-js"; +import sodium from "libsodium-wrappers-sumo"; +import { nanoid } from "nanoid"; +import postgres from "postgres"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +import { withDatabaseName } from "@deepnotes/db/testing/db-url"; +import { + createDatabaseFromTemplate, + dropDatabaseIfExists, + ensureTemplateDatabase, + resolveTemplateContext, + type TemplateDbContext, +} from "@deepnotes/db/testing/template-db"; +import * as schema from "@deepnotes/db/schema"; +import { devices, sessions, users } from "@deepnotes/db/schema"; + +import { performUserPasswordChange } from "./change-user-password.js"; +import { + performUserEmailChangeConfirm, + performUserEmailChangeRequest, +} from "./change-user-email.js"; +import { + createPrivateKeyring, + createSymmetricKeyring, + getPasswordHashValues, +} from "./crypto/index.js"; +import { + derivePasswordValues, + decryptUserRehashedLoginHash, + ensureSodiumReady, +} from "./crypto/session-crypto.js"; +import type { UserRegisterInput } from "./register-user.js"; +import { performUserRegister } from "./register-user.js"; +import { decryptUserEmail } from "./encrypt-user-email.js"; +import { hashUserEmail } from "./email-hash.js"; +import type { SessionEnv } from "./env.js"; +import { signAccessToken } from "./jwt.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +loadEnv({ path: join(__dirname, "../../../.env") }); + +/** Dedicated template name so `turbo test` can run `@deepnotes/db` and `@deepnotes/session` in parallel. */ +const SESSION_TEMPLATE_NAME = "dn_test_tpl_session_email"; + +function testSessionEnv(): SessionEnv { + const b32 = (n: number) => Buffer.alloc(32, n).toString("base64"); + return { + ACCESS_SECRET: "test-access-secret-min-32-chars-long!!", + REFRESH_SECRET: "test-refresh-secret-min-32-chars-long!!", + USER_EMAIL_SECRET: "test-user-email-secret-hmac-key!!", + USER_EMAIL_ENCRYPTION_KEY: b32(1), + USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY: b32(2), + USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY: b32(3), + USER_RECOVERY_CODES_ENCRYPTION_KEY: b32(4), + SEND_EMAILS: "false", + DEV: "true", + }; +} + +function rand32(): Uint8Array { + return sodium.randombytes_buf(32); +} + +async function buildRegisterBody( + email: string, + loginHash: Uint8Array, +): Promise { + await ensureSodiumReady(); + const userId = nanoid(); + const groupId = nanoid(); + const pageId = nanoid(); + return { + userId, + groupId, + pageId, + email, + loginHash, + userPublicKeyring: rand32(), + userEncryptedPrivateKeyring: rand32(), + userEncryptedSymmetricKeyring: rand32(), + userEncryptedName: rand32(), + userEncryptedDefaultNote: rand32(), + userEncryptedDefaultArrow: rand32(), + groupCreation: { + groupEncryptedName: rand32(), + groupIsPublic: true, + groupAccessKeyring: rand32(), + groupEncryptedInternalKeyring: rand32(), + groupEncryptedContentKeyring: rand32(), + groupPublicKeyring: rand32(), + groupEncryptedPrivateKeyring: rand32(), + groupOwnerEncryptedName: rand32(), + }, + pageCreation: { + pageEncryptedSymmetricKeyring: rand32(), + pageEncryptedRelativeTitle: rand32(), + pageEncryptedAbsoluteTitle: rand32(), + }, + }; +} + +describe.skipIf(resolveTemplateContext() == null)( + "account flows: email + password (Postgres template DB)", + () => { + const baseCtx = resolveTemplateContext()!; + const ctx: TemplateDbContext = { + ...baseCtx, + templateName: SESSION_TEMPLATE_NAME, + }; + + beforeAll(async () => { + await ensureSodiumReady(); + await ensureTemplateDatabase(ctx); + }); + + afterAll(async () => { + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + }); + + it("register → request → confirm updates email and clears pending state", async () => { + const env = testSessionEnv(); + const exceptions = ""; + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const initialEmail = `u-${nanoid()}@example.com`; + const newEmail = `v-${nanoid()}@example.com`; + const loginHash = rand32(); + const newLoginHash = rand32(); + + const reg = await buildRegisterBody(initialEmail, loginHash); + const { userId } = await performUserRegister({ + db, + env, + body: reg, + }); + expect(userId).toBe(reg.userId); + + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId, + sessionId: nanoid(), + }); + + const reqOut = await performUserEmailChangeRequest({ + db, + env, + accessCookie: access, + oldLoginHash: loginHash, + newEmail, + }); + expect(reqOut.devEmailVerificationCode).toMatch(/^\d{6}$/); + + const pending = await db + .select({ + code: users.emailVerificationCode, + encNew: users.encryptedNewEmail, + }) + .from(users) + .where(eq(users.id, userId)); + expect(pending[0]?.code).toBe(reqOut.devEmailVerificationCode); + expect(pending[0]?.encNew).not.toBeNull(); + + const confirm = await performUserEmailChangeConfirm({ + db, + env, + accessCookie: access, + oldLoginHash: loginHash, + emailVerificationCode: reqOut.devEmailVerificationCode!, + newLoginHash, + newEncryptedPrivateKeyring: rand32(), + newEncryptedSymmetricKeyring: rand32(), + }); + expect(confirm.cookieLines.length).toBeGreaterThan(0); + + const row = await db + .select({ + encryptedEmail: users.encryptedEmail, + emailHash: users.emailHash, + encNew: users.encryptedNewEmail, + code: users.emailVerificationCode, + }) + .from(users) + .where(eq(users.id, userId)); + const u = row[0]!; + expect(u.encNew).toBeNull(); + expect(u.code).toBeNull(); + + const storedPlain = decryptUserEmail( + new Uint8Array(u.encryptedEmail), + env.USER_EMAIL_ENCRYPTION_KEY, + exceptions, + ); + expect(storedPlain).toBe(newEmail.trim().toLowerCase()); + + const expectedHash = Buffer.from( + await hashUserEmail(storedPlain, env.USER_EMAIL_SECRET, exceptions), + ); + expect(Buffer.from(u.emailHash).equals(expectedHash)).toBe(true); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("email change request rejects wrong password", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `w-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + + const wrongHash = rand32(); + await expect( + performUserEmailChangeRequest({ + db, + env, + accessCookie: access, + oldLoginHash: wrongHash, + newEmail: `x-${nanoid()}@example.com`, + }), + ).rejects.toMatchObject({ status: 400, code: "BAD_REQUEST" }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("email change confirm rejects wrong verification code", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const initialEmail = `e-${nanoid()}@example.com`; + const newEmail = `f-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(initialEmail, loginHash); + const { userId } = await performUserRegister({ db, env, body: reg }); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId, + sessionId: nanoid(), + }); + const reqOut = await performUserEmailChangeRequest({ + db, + env, + accessCookie: access, + oldLoginHash: loginHash, + newEmail, + }); + const wrongCode = + reqOut.devEmailVerificationCode === "000000" ? "000001" : "000000"; + await expect( + performUserEmailChangeConfirm({ + db, + env, + accessCookie: access, + oldLoginHash: loginHash, + emailVerificationCode: wrongCode, + newLoginHash: rand32(), + newEncryptedPrivateKeyring: rand32(), + newEncryptedSymmetricKeyring: rand32(), + }), + ).rejects.toMatchObject({ status: 400, code: "BAD_REQUEST" }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("password change updates PHC and keyrings; new password unwraps storage", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `p-${nanoid()}@example.com`; + const oldLogin = rand32(); + const newLogin = rand32(); + const newPriv = rand32(); + const newSym = rand32(); + const reg = await buildRegisterBody(email, oldLogin); + const { userId } = await performUserRegister({ db, env, body: reg }); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId, + sessionId: nanoid(), + }); + const out = await performUserPasswordChange({ + db, + env, + accessCookie: access, + oldLoginHash: oldLogin, + newLoginHash: newLogin, + newEncryptedPrivateKeyring: newPriv, + newEncryptedSymmetricKeyring: newSym, + }); + expect(out.cookieLines.length).toBeGreaterThan(0); + + const [u] = await db + .select({ + encPhc: users.encryptedRehashedLoginHash, + encPriv: users.encryptedPrivateKeyring, + encSym: users.encryptedSymmetricKeyring, + }) + .from(users) + .where(eq(users.id, userId)); + const phcPlain = decryptUserRehashedLoginHash( + new Uint8Array(u!.encPhc), + env.USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + ); + const phcVals = getPasswordHashValues(phcPlain); + const derived = derivePasswordValues({ + password: newLogin, + salt: phcVals.saltBytes, + }); + expect(sodium.memcmp(derived.hash, phcVals.hashBytes)).toBe(true); + + const unwrapKey = derived.key; + const privUnwrapped = createPrivateKeyring(new Uint8Array(u!.encPriv)).unwrapSymmetric( + unwrapKey, + { + associatedData: { + context: "UserEncryptedPrivateKeyring", + userId, + }, + }, + ); + const symUnwrapped = createSymmetricKeyring( + new Uint8Array(u!.encSym), + ).unwrapSymmetric(unwrapKey, { + associatedData: { + context: "UserEncryptedSymmetricKeyring", + userId, + }, + }); + expect(new Uint8Array(privUnwrapped.value)).toEqual(newPriv); + expect(new Uint8Array(symUnwrapped.value)).toEqual(newSym); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("password change sets invalidated on all sessions", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `q-${nanoid()}@example.com`; + const oldLogin = rand32(); + const newLogin = rand32(); + const reg = await buildRegisterBody(email, oldLogin); + await performUserRegister({ db, env, body: reg }); + const { userId } = reg; + const deviceId = nanoid(); + const sessionId = nanoid(); + const exp = new Date(Date.now() + 86_400_000).toISOString(); + await db.insert(devices).values({ + id: deviceId, + userId, + hash: randomBytes(32), + trusted: false, + }); + await db.insert(sessions).values({ + id: sessionId, + userId, + deviceId, + invalidated: false, + encryptionKey: randomBytes(32), + refreshCode: nanoid(), + expirationDate: exp, + }); + + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId, + sessionId: nanoid(), + }); + await performUserPasswordChange({ + db, + env, + accessCookie: access, + oldLoginHash: oldLogin, + newLoginHash: newLogin, + newEncryptedPrivateKeyring: rand32(), + newEncryptedSymmetricKeyring: rand32(), + }); + const sessRows = await db + .select({ invalidated: sessions.invalidated }) + .from(sessions) + .where(eq(sessions.userId, userId)); + expect(sessRows).toEqual([{ invalidated: true }]); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("password change rejects wrong old password", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `r-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + await expect( + performUserPasswordChange({ + db, + env, + accessCookie: access, + oldLoginHash: rand32(), + newLoginHash: rand32(), + newEncryptedPrivateKeyring: rand32(), + newEncryptedSymmetricKeyring: rand32(), + }), + ).rejects.toMatchObject({ status: 400, code: "BAD_REQUEST" }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + }, +); diff --git a/new-deepnotes/packages/session/src/email-change.integration.test.ts b/new-deepnotes/packages/session/src/email-change.integration.test.ts deleted file mode 100644 index e0d0998c..00000000 --- a/new-deepnotes/packages/session/src/email-change.integration.test.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { randomBytes } from "node:crypto"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { config as loadEnv } from "dotenv"; -import { eq } from "drizzle-orm"; -import { drizzle } from "drizzle-orm/postgres-js"; -import sodium from "libsodium-wrappers-sumo"; -import { nanoid } from "nanoid"; -import postgres from "postgres"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; - -import { withDatabaseName } from "@deepnotes/db/testing/db-url"; -import { - createDatabaseFromTemplate, - dropDatabaseIfExists, - ensureTemplateDatabase, - resolveTemplateContext, - type TemplateDbContext, -} from "@deepnotes/db/testing/template-db"; -import * as schema from "@deepnotes/db/schema"; -import { users } from "@deepnotes/db/schema"; - -import { - performUserEmailChangeConfirm, - performUserEmailChangeRequest, -} from "./change-user-email.js"; -import type { UserRegisterInput } from "./register-user.js"; -import { performUserRegister } from "./register-user.js"; -import { ensureSodiumReady } from "./crypto/session-crypto.js"; -import { decryptUserEmail } from "./encrypt-user-email.js"; -import { hashUserEmail } from "./email-hash.js"; -import type { SessionEnv } from "./env.js"; -import { signAccessToken } from "./jwt.js"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -loadEnv({ path: join(__dirname, "../../../.env") }); - -/** Dedicated template name so `turbo test` can run `@deepnotes/db` and `@deepnotes/session` in parallel. */ -const SESSION_TEMPLATE_NAME = "dn_test_tpl_session_email"; - -function testSessionEnv(): SessionEnv { - const b32 = (n: number) => Buffer.alloc(32, n).toString("base64"); - return { - ACCESS_SECRET: "test-access-secret-min-32-chars-long!!", - REFRESH_SECRET: "test-refresh-secret-min-32-chars-long!!", - USER_EMAIL_SECRET: "test-user-email-secret-hmac-key!!", - USER_EMAIL_ENCRYPTION_KEY: b32(1), - USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY: b32(2), - USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY: b32(3), - USER_RECOVERY_CODES_ENCRYPTION_KEY: b32(4), - SEND_EMAILS: "false", - DEV: "true", - }; -} - -function rand32(): Uint8Array { - return sodium.randombytes_buf(32); -} - -async function buildRegisterBody( - email: string, - loginHash: Uint8Array, -): Promise { - await ensureSodiumReady(); - const userId = nanoid(); - const groupId = nanoid(); - const pageId = nanoid(); - return { - userId, - groupId, - pageId, - email, - loginHash, - userPublicKeyring: rand32(), - userEncryptedPrivateKeyring: rand32(), - userEncryptedSymmetricKeyring: rand32(), - userEncryptedName: rand32(), - userEncryptedDefaultNote: rand32(), - userEncryptedDefaultArrow: rand32(), - groupCreation: { - groupEncryptedName: rand32(), - groupIsPublic: true, - groupAccessKeyring: rand32(), - groupEncryptedInternalKeyring: rand32(), - groupEncryptedContentKeyring: rand32(), - groupPublicKeyring: rand32(), - groupEncryptedPrivateKeyring: rand32(), - groupOwnerEncryptedName: rand32(), - }, - pageCreation: { - pageEncryptedSymmetricKeyring: rand32(), - pageEncryptedRelativeTitle: rand32(), - pageEncryptedAbsoluteTitle: rand32(), - }, - }; -} - -describe.skipIf(resolveTemplateContext() == null)( - "email change (Postgres template DB)", - () => { - const baseCtx = resolveTemplateContext()!; - const ctx: TemplateDbContext = { - ...baseCtx, - templateName: SESSION_TEMPLATE_NAME, - }; - - beforeAll(async () => { - await ensureSodiumReady(); - await ensureTemplateDatabase(ctx); - }); - - afterAll(async () => { - const admin = postgres(ctx.adminUrl, { max: 1 }); - try { - await dropDatabaseIfExists(admin, ctx.templateName); - } finally { - await admin.end({ timeout: 5 }); - } - }); - - it("register → request → confirm updates email and clears pending state", async () => { - const env = testSessionEnv(); - const exceptions = ""; - const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; - const admin = postgres(ctx.adminUrl, { max: 1 }); - try { - await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); - } finally { - await admin.end({ timeout: 5 }); - } - - const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); - const client = postgres(cloneUrl, { max: 1 }); - const db = drizzle(client, { schema }); - try { - const initialEmail = `u-${nanoid()}@example.com`; - const newEmail = `v-${nanoid()}@example.com`; - const loginHash = rand32(); - const newLoginHash = rand32(); - - const reg = await buildRegisterBody(initialEmail, loginHash); - const { userId } = await performUserRegister({ - db, - env, - body: reg, - }); - expect(userId).toBe(reg.userId); - - const access = await signAccessToken({ - secret: env.ACCESS_SECRET, - userId, - sessionId: nanoid(), - }); - - const reqOut = await performUserEmailChangeRequest({ - db, - env, - accessCookie: access, - oldLoginHash: loginHash, - newEmail, - }); - expect(reqOut.devEmailVerificationCode).toMatch(/^\d{6}$/); - - const pending = await db - .select({ - code: users.emailVerificationCode, - encNew: users.encryptedNewEmail, - }) - .from(users) - .where(eq(users.id, userId)); - expect(pending[0]?.code).toBe(reqOut.devEmailVerificationCode); - expect(pending[0]?.encNew).not.toBeNull(); - - const confirm = await performUserEmailChangeConfirm({ - db, - env, - accessCookie: access, - oldLoginHash: loginHash, - emailVerificationCode: reqOut.devEmailVerificationCode!, - newLoginHash, - newEncryptedPrivateKeyring: rand32(), - newEncryptedSymmetricKeyring: rand32(), - }); - expect(confirm.cookieLines.length).toBeGreaterThan(0); - - const row = await db - .select({ - encryptedEmail: users.encryptedEmail, - emailHash: users.emailHash, - encNew: users.encryptedNewEmail, - code: users.emailVerificationCode, - }) - .from(users) - .where(eq(users.id, userId)); - const u = row[0]!; - expect(u.encNew).toBeNull(); - expect(u.code).toBeNull(); - - const storedPlain = decryptUserEmail( - new Uint8Array(u.encryptedEmail), - env.USER_EMAIL_ENCRYPTION_KEY, - exceptions, - ); - expect(storedPlain).toBe(newEmail.trim().toLowerCase()); - - const expectedHash = Buffer.from( - await hashUserEmail(storedPlain, env.USER_EMAIL_SECRET, exceptions), - ); - expect(Buffer.from(u.emailHash).equals(expectedHash)).toBe(true); - } finally { - await client.end({ timeout: 5 }); - const admin2 = postgres(ctx.adminUrl, { max: 1 }); - try { - await dropDatabaseIfExists(admin2, cloneName); - } finally { - await admin2.end({ timeout: 5 }); - } - } - }); - - it("email change request rejects wrong password", async () => { - const env = testSessionEnv(); - const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; - const admin = postgres(ctx.adminUrl, { max: 1 }); - try { - await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); - } finally { - await admin.end({ timeout: 5 }); - } - - const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); - const client = postgres(cloneUrl, { max: 1 }); - const db = drizzle(client, { schema }); - try { - const email = `w-${nanoid()}@example.com`; - const loginHash = rand32(); - const reg = await buildRegisterBody(email, loginHash); - await performUserRegister({ db, env, body: reg }); - - const access = await signAccessToken({ - secret: env.ACCESS_SECRET, - userId: reg.userId, - sessionId: nanoid(), - }); - - const wrongHash = rand32(); - await expect( - performUserEmailChangeRequest({ - db, - env, - accessCookie: access, - oldLoginHash: wrongHash, - newEmail: `x-${nanoid()}@example.com`, - }), - ).rejects.toMatchObject({ status: 400, code: "BAD_REQUEST" }); - } finally { - await client.end({ timeout: 5 }); - const admin2 = postgres(ctx.adminUrl, { max: 1 }); - try { - await dropDatabaseIfExists(admin2, cloneName); - } finally { - await admin2.end({ timeout: 5 }); - } - } - }); - }, -); From a47a87b70c940e3e9064abb4f8a463bb6cf0a3e4 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:13:07 -0300 Subject: [PATCH 037/243] fix: ci --- .github/workflows/new-deepnotes-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/new-deepnotes-ci.yml b/.github/workflows/new-deepnotes-ci.yml index 43872fc9..67995db5 100644 --- a/.github/workflows/new-deepnotes-ci.yml +++ b/.github/workflows/new-deepnotes-ci.yml @@ -40,7 +40,7 @@ jobs: - uses: pnpm/action-setup@v4 with: - version: 9 + package_json_file: new-deepnotes/package.json - uses: actions/setup-node@v4 with: From 74a87055581721aeedc401f651e74d6e59448a7c Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:13:47 -0300 Subject: [PATCH 038/243] feat(new-deepnotes): two-factor user settings and session crypto --- new-deepnotes/PLAN_PROGRESS.md | 26 +- .../apps/api-worker/src/index.test.ts | 6 + new-deepnotes/apps/api-worker/src/index.ts | 293 +++++++++++++++ new-deepnotes/docs/TRPC_REST_MAP.md | 2 +- new-deepnotes/packages/api/src/index.ts | 4 + .../packages/api/src/openapi.test.ts | 14 + new-deepnotes/packages/api/src/openapi.ts | 224 +++++++++++ .../packages/api/src/schemas/users.test.ts | 15 + .../packages/api/src/schemas/users.ts | 35 ++ .../session/src/crypto/session-crypto.ts | 10 + new-deepnotes/packages/session/src/index.ts | 8 + .../session/src/user-two-factor-settings.ts | 355 ++++++++++++++++++ 12 files changed, 985 insertions(+), 7 deletions(-) create mode 100644 new-deepnotes/packages/session/src/user-two-factor-settings.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 9a736e85..7acb51ac 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Auth + account: sessions, register, public email verify, account delete, password + **email change** + **Postgres integration tests** for both (see [Phase 3 test coverage](#phase-3-test-coverage-detail)). **Next (priority):** 2FA HTTP surface → pages/groups CRUD → realtime/collab → Stripe webhook. | +| **3** — REST + Drizzle features | **In progress** | Account surface includes **2FA** (`/api/users/me/2fa/...`); see [2FA HTTP routes](#2fa-http-routes-phase-3). **Next (priority):** pages + groups CRUD (per [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md)) → realtime/collab → Stripe webhook. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -36,7 +36,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Rate limit:** failed login counters (`login-rate-limit.test.ts`). - [x] **Email crypto:** `encryptUserEmail` / `decryptUserEmail` + `hashUserEmail` (legacy parity cases). - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). -- [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change byte fields (`schemas/users.test.ts`). +- [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. - [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` (renamed from `email-change.integration.test.ts`). See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) for the per-case list. @@ -76,7 +76,20 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` - [x] `POST /api/users/me/email-change` — `performUserEmailChangeRequest`: `oldLoginHash` + `newEmail`; **403** demo, **400** bad password or “email already in use” (global `email_hash` match, same as legacy); sets `encrypted_new_email` + 6-digit `email_verification_code`; Resend (subject/body like legacy) or **200** `{ "emailVerificationCode" }` when `SEND_EMAILS=false`; **204** when emailed. - [x] `POST /api/users/me/email-change/confirm` — `performUserEmailChangeConfirm`: one call (WS two-step collapsed); `oldLoginHash`, `emailVerificationCode` (6 digits), `newLoginHash`, `userEncrypted*Keyring` (b64, same as register/password); verifies code + password; applies new `encrypted_email` / `email_hash`, clears pending fields, PHC + rewrapped keyrings, invalidates **all** `sessions`, **204** + `buildClearSessionCookies`; optional `updateStripeCustomerEmail` in worker (matches legacy `customers.update` after commit, errors non-fatal). - [x] **`decryptUserEmail`** in `@deepnotes/session` for confirm; **`sendEmailChangeVerificationEmail`** (Resend); OpenAPI + [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md) updated. -- [ ] **2FA (HTTP surface)** — `POST /api/users/me/2fa/enable/request|finish`, `GET /api/users/me/2fa`, `POST /api/users/me/2fa/recovery-codes`, `POST /api/users/me/2fa/devices/forget`, `POST /api/users/me/2fa/disable` ([docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md)). **Note:** `@deepnotes/session` already implements TOTP/recovery verification for **`POST /api/sessions/login`**; these routes expose enable/disable/load for the SPA. +- [x] **2FA (HTTP surface)** — Hono + OpenAPI: `user-two-factor-settings.ts` (`encryptUserAuthenticatorSecret` in `session-crypto`). Routes: [2FA HTTP routes](#2fa-http-routes-phase-3). `load` is **`POST /api/users/me/2fa/load`** (password in JSON, not a `GET` — [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) footnote). **Not yet in integration template DB:** 2FA enable → login with TOTP (optional follow-up; login path already uses `assertTwoFactorOk` in [two-factor.ts](./packages/session/src/two-factor.ts)). + +### 2FA HTTP routes (Phase 3) + +| Path | Replaces (legacy) | Request body | Success | +|------|-------------------|-------------|---------| +| `POST /api/users/me/2fa/enable/request` | `twoFactorAuth.enable.request` | `{ "loginHash" }` b64 | **200** `{ "secret", "keyUri" }` (pending TOTP, not yet enabled) | +| `POST /api/users/me/2fa/enable/finish` | `twoFactorAuth.enable.finish` | `{ "loginHash", "authenticatorToken" }` (6 digits) | **200** `{ "recoveryCodes" }` (6 × 32-char hex) | +| `POST /api/users/me/2fa/load` | `twoFactorAuth.load` | `{ "loginHash" }` | **200** `{ "secret", "keyUri" }` (2FA must already be on) | +| `POST /api/users/me/2fa/recovery-codes` | `generateRecoveryCodes` | `{ "loginHash" }` | **200** new recovery codes | +| `POST /api/users/me/2fa/devices/forget` | `forgetTrustedDevices` | `{ "loginHash" }` | **204** | +| `POST /api/users/me/2fa/disable` | `disable` | `{ "loginHash" }` | **204** | + +- **Parity:** Demo accounts **403**; wrong password **400** “Password is incorrect.”; TOTP fail on finish **400** “Authenticator token is incorrect.”; `otplib` `keyuri` issuer **“DeepNotes”** (same as legacy tRPC). Recovery codes: `libsodium` hex + [hashRecoveryCode / encryptRecoveryCodes](packages/session/src/crypto/session-crypto.ts) (legacy-equivalent). Forget devices: `UPDATE devices SET trusted = false` for `user_id`. ### Not started (Phase 3 — pages, groups, infra) @@ -132,8 +145,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts`: clone template DB, smoke SQL | More assertions on FKs / critical columns after schema grows | | **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` — email change + **password change** (PHC + unwrap), wrong passwords/codes, session invalidation; template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **`performSessionLogin` / refresh** with template DB + device/session rows; **Redis** + `performSessionLogin` failed-login; optional **demo 403** integration | -| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + route registry); **`schemas/users.test.ts`** (email/password change bodies, 6-digit code) | Schemas for sessions + remaining routes; optional **snapshot** of OpenAPI fragment for drift | -| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: health, OpenAPI JSON, **503** when secrets/DB not bound (incl. email-change paths) | **200-path tests** with test `SessionEnv` + Hyperdrive stub + template DB (heavier CI job) | +| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA paths); **`schemas/users.test.ts`** (email/password change, 2fa finish) | Schemas for pages/groups when they land; optional OpenAPI **snapshot** | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: 503 when env missing — includes **2FA** routes in matrix | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -166,7 +179,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Phase 3 working order (suggested) -Use this when resuming: **(done)** account HTTP surface through email change including password change. **(next)** 2FA CRUD on `/api/users/me/2fa*`, reusing session crypto already used at login. **(then)** pages + groups from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) (user prefs, CRUD, group privacy/password). **(then)** long pole: **realtime + collab** (protocol, Worker/DO, no key rotation) and **Stripe** webhook + billing routes + wire `updateStripeCustomerEmail` / `deleteStripeCustomer` from account flows where applicable. +Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POST, see map). **(next)** `users.pages` + `groups` + `pages` REST from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md). **(then)** **realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes. --- @@ -174,6 +187,7 @@ Use this when resuming: **(done)** account HTTP surface through email change inc | Date | Change | |------|--------| +| 2026-04-27 | **2FA account HTTP:** `user-two-factor-settings.ts`, `encryptUserAuthenticatorSecret` in `session-crypto`, Zod + OpenAPI + Hono for `/api/users/me/2fa/*` (6 routes); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — `load` is POST not GET; see [2FA HTTP routes](#2fa-http-routes-phase-3) below. | | 2026-04-27 | **Integration tests:** expanded `account-flows.integration.test.ts` (email wrong code; password change PHC + keyring unwrap with salt from PHC; `sessions` invalidation; wrong old password). Renamed from `email-change.integration.test.ts`. PLAN_PROGRESS: detailed Phase 3 test table + matrix gaps. | | 2026-04-26 | **Integration tests:** `@deepnotes/db` exports `@deepnotes/db/testing/template-db` + `db-url`; `@deepnotes/session` — `email-change.integration.test.ts` (Postgres template clone, register + email change + wrong password). PLAN_PROGRESS matrix + Phase 3 checklist updated. | | 2026-04-26 | **Tests:** `@deepnotes/session` — `encrypt-user-email.test.ts`, `email-hash.test.ts`, `send-email-change-code.test.ts`; `@deepnotes/api` — `schemas/users.test.ts`; api-worker — email-change routes in `503` matrix; PLAN_PROGRESS — package test matrix + Phase 3 test checklist. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index b0ed1108..0bbf06f7 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -34,6 +34,12 @@ describe("api-worker", () => { ["POST", "/api/users/email-verification/resend"], ["POST", "/api/users/me/email-change"], ["POST", "/api/users/me/email-change/confirm"], + ["POST", "/api/users/me/2fa/enable/request"], + ["POST", "/api/users/me/2fa/enable/finish"], + ["POST", "/api/users/me/2fa/load"], + ["POST", "/api/users/me/2fa/recovery-codes"], + ["POST", "/api/users/me/2fa/devices/forget"], + ["POST", "/api/users/me/2fa/disable"], ] as const)("returns 503 for %s %s when auth env is not configured", async (method, path) => { const res = await app.request(`http://test${path}`, { method }); expect(res.status).toBe(503); diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 7ab13c06..0b5c1ec4 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -7,6 +7,8 @@ import { sessionLoginRequestSchema, userAccountDeleteRequestSchema, userEmailChangeConfirmRequestSchema, + user2faEnableFinishRequestSchema, + user2faPasswordBodySchema, userEmailChangeRequestSchema, userPasswordChangeRequestSchema, userRegisterRequestSchema, @@ -742,4 +744,295 @@ app.post("/api/users/email-verification/confirm", async (c) => { } }); +app.post("/api/users/me/2fa/enable/request", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = user2faPasswordBodySchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { code: "VALIDATION_ERROR", message: parsed.error.flatten().formErrors.join("; ") }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performUserTwoFactorEnableRequest } = await import("@deepnotes/session"); + const out = await performUserTwoFactorEnableRequest({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + loginHash: parsed.data.loginHash, + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json({ code: e.code, message: e.message }, e.status as ContentfulStatusCode); + } + throw e; + } +}); + +app.post("/api/users/me/2fa/enable/finish", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = user2faEnableFinishRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { code: "VALIDATION_ERROR", message: parsed.error.flatten().formErrors.join("; ") }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performUserTwoFactorEnableFinish } = await import("@deepnotes/session"); + const out = await performUserTwoFactorEnableFinish({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + loginHash: parsed.data.loginHash, + authenticatorToken: parsed.data.authenticatorToken, + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json({ code: e.code, message: e.message }, e.status as ContentfulStatusCode); + } + throw e; + } +}); + +app.post("/api/users/me/2fa/load", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = user2faPasswordBodySchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { code: "VALIDATION_ERROR", message: parsed.error.flatten().formErrors.join("; ") }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performUserTwoFactorLoad } = await import("@deepnotes/session"); + const out = await performUserTwoFactorLoad({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + loginHash: parsed.data.loginHash, + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json({ code: e.code, message: e.message }, e.status as ContentfulStatusCode); + } + throw e; + } +}); + +app.post("/api/users/me/2fa/recovery-codes", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = user2faPasswordBodySchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { code: "VALIDATION_ERROR", message: parsed.error.flatten().formErrors.join("; ") }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performUserTwoFactorGenerateRecoveryCodes } = await import( + "@deepnotes/session" + ); + const out = await performUserTwoFactorGenerateRecoveryCodes({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + loginHash: parsed.data.loginHash, + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json({ code: e.code, message: e.message }, e.status as ContentfulStatusCode); + } + throw e; + } +}); + +app.post("/api/users/me/2fa/devices/forget", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = user2faPasswordBodySchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { code: "VALIDATION_ERROR", message: parsed.error.flatten().formErrors.join("; ") }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performUserTwoFactorForgetDevices } = await import("@deepnotes/session"); + await performUserTwoFactorForgetDevices({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + loginHash: parsed.data.loginHash, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json({ code: e.code, message: e.message }, e.status as ContentfulStatusCode); + } + throw e; + } +}); + +app.post("/api/users/me/2fa/disable", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = user2faPasswordBodySchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { code: "VALIDATION_ERROR", message: parsed.error.flatten().formErrors.join("; ") }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performUserTwoFactorDisable } = await import("@deepnotes/session"); + await performUserTwoFactorDisable({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + loginHash: parsed.data.loginHash, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json({ code: e.code, message: e.message }, e.status as ContentfulStatusCode); + } + throw e; + } +}); + export default app; diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index de7bf9df..bc7f70f9 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -21,7 +21,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `users.account.emailChange.request` | `POST /api/users/me/email-change` (body: `oldLoginHash` b64, `newEmail`; **204** or **200** with `{ "emailVerificationCode" }` when `SEND_EMAILS=false`) | | `users.account.twoFactorAuth.enable.request` | `POST /api/users/me/2fa/enable/request` | | `users.account.twoFactorAuth.enable.finish` | `POST /api/users/me/2fa/enable/finish` | -| `users.account.twoFactorAuth.load` | `GET /api/users/me/2fa` | +| `users.account.twoFactorAuth.load` | `POST /api/users/me/2fa/load` (body `{ "loginHash" }` — **not** `GET` with a password, to avoid query/logging leakage) | | `users.account.twoFactorAuth.generateRecoveryCodes` | `POST /api/users/me/2fa/recovery-codes` | | `users.account.twoFactorAuth.forgetTrustedDevices` | `POST /api/users/me/2fa/devices/forget` | | `users.account.twoFactorAuth.disable` | `POST /api/users/me/2fa/disable` | diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index f4b1c50b..a48d116c 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -39,4 +39,8 @@ export { type UserMeResponse, type UserPasswordChangeRequest, type UserRegisterResponse, + user2faEnableFinishRequestSchema, + user2faEnableRequestResponseSchema, + user2faPasswordBodySchema, + user2faRecoveryCodesResponseSchema, } from "./schemas/users.js"; diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 6706e0e2..5a76b4f0 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -30,5 +30,19 @@ describe("getOpenApiDocument", () => { doc.paths?.["/api/users/email-verification/confirm"]?.post, ).toBeDefined(); expect(doc.paths?.["/api/users/me"]?.delete).toBeDefined(); + expect( + doc.paths?.["/api/users/me/2fa/enable/request"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/users/me/2fa/enable/finish"]?.post, + ).toBeDefined(); + expect(doc.paths?.["/api/users/me/2fa/load"]?.post).toBeDefined(); + expect( + doc.paths?.["/api/users/me/2fa/recovery-codes"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/users/me/2fa/devices/forget"]?.post, + ).toBeDefined(); + expect(doc.paths?.["/api/users/me/2fa/disable"]?.post).toBeDefined(); }); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index 78fe0380..c485b469 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -19,6 +19,10 @@ import { import { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, + user2faEnableFinishRequestSchema, + user2faEnableRequestResponseSchema, + user2faPasswordBodySchema, + user2faRecoveryCodesResponseSchema, userAccountDeleteRequestSchema, userEmailChangeConfirmRequestSchema, userEmailChangeRequestResponseSchema, @@ -352,6 +356,226 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/users/me/2fa/enable/request", + summary: "Start 2FA setup (TOTP secret + otpauth URI)", + description: + "Replaces `users.account.twoFactorAuth.enable.request`. Stores a pending encrypted authenticator secret; client shows QR from `keyUri` or `secret`.", + request: { + body: { + content: { + "application/json": { + schema: user2faPasswordBodySchema, + }, + }, + }, + }, + responses: { + 200: { + description: "Secret generated; not yet enabled until `…/enable/finish`.", + content: { + "application/json": { + schema: user2faEnableRequestResponseSchema, + }, + }, + }, + 400: { + description: "Validation error, or 2FA already fully enabled.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/2fa/enable/finish", + summary: "Complete 2FA setup (TOTP + recovery codes)", + description: "Replaces `users.account.twoFactorAuth.enable.finish`.", + request: { + body: { + content: { + "application/json": { + schema: user2faEnableFinishRequestSchema, + }, + }, + }, + }, + responses: { + 200: { + description: "2FA enabled; one-time recovery codes returned.", + content: { + "application/json": { + schema: user2faRecoveryCodesResponseSchema, + }, + }, + }, + 400: { + description: "Wrong password, wrong TOTP, or already enabled.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/2fa/load", + summary: "Reveal TOTP secret and otpauth URI (after password check)", + description: + "Replaces `users.account.twoFactorAuth.load` (legacy tRPC had `loginHash` in the query; this API uses a JSON body on POST to avoid putting secrets in query strings or logs).", + request: { + body: { + content: { + "application/json": { + schema: user2faPasswordBodySchema, + }, + }, + }, + }, + responses: { + 200: { + description: "Secret and `keyUri` for re-provisioning an authenticator.", + content: { + "application/json": { + schema: user2faEnableRequestResponseSchema, + }, + }, + }, + 400: { + description: "Wrong password or 2FA not enabled.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/2fa/recovery-codes", + summary: "Regenerate recovery codes", + description: "Replaces `users.account.twoFactorAuth.generateRecoveryCodes`.", + request: { + body: { + content: { + "application/json": { + schema: user2faPasswordBodySchema, + }, + }, + }, + }, + responses: { + 200: { + description: "New recovery codes (previous codes invalidated).", + content: { + "application/json": { + schema: user2faRecoveryCodesResponseSchema, + }, + }, + }, + 400: { + description: "Wrong password or 2FA not enabled.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/2fa/devices/forget", + summary: "Mark all user devices as not trusted", + description: "Replaces `users.account.twoFactorAuth.forgetTrustedDevices`.", + request: { + body: { + content: { + "application/json": { + schema: user2faPasswordBodySchema, + }, + }, + }, + }, + responses: { + 204: { + description: "`devices.trusted` cleared for this user.", + }, + 400: { + description: "Wrong password or 2FA not enabled.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/2fa/disable", + summary: "Disable 2FA", + description: "Replaces `users.account.twoFactorAuth.disable`.", + request: { + body: { + content: { + "application/json": { + schema: user2faPasswordBodySchema, + }, + }, + }, + }, + responses: { + 204: { + description: "2FA disabled; authenticator and recovery material cleared.", + }, + 400: { + description: "Wrong password or 2FA not enabled.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "post", path: "/api/users/email-verification/resend", diff --git a/new-deepnotes/packages/api/src/schemas/users.test.ts b/new-deepnotes/packages/api/src/schemas/users.test.ts index 1ce28c9d..cbde7ccb 100644 --- a/new-deepnotes/packages/api/src/schemas/users.test.ts +++ b/new-deepnotes/packages/api/src/schemas/users.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { + user2faEnableFinishRequestSchema, userEmailChangeConfirmRequestSchema, userEmailChangeRequestSchema, userPasswordChangeRequestSchema, @@ -50,4 +51,18 @@ describe("user request schemas (REST body validation)", () => { expect(new TextDecoder().decode(p.oldLoginHash)).toBe("aa"); expect(new TextDecoder().decode(p.newLoginHash)).toBe("bb"); }); + + it("user2faEnableFinishRequestSchema requires six-digit authenticatorToken", () => { + const ok = user2faEnableFinishRequestSchema.parse({ + loginHash: oneByteB64, + authenticatorToken: "000000", + }); + expect(ok.authenticatorToken).toBe("000000"); + expect(() => + user2faEnableFinishRequestSchema.parse({ + ...ok, + authenticatorToken: "00", + }), + ).toThrow(); + }); }); diff --git a/new-deepnotes/packages/api/src/schemas/users.ts b/new-deepnotes/packages/api/src/schemas/users.ts index 3bf9eaf9..4bd228de 100644 --- a/new-deepnotes/packages/api/src/schemas/users.ts +++ b/new-deepnotes/packages/api/src/schemas/users.ts @@ -119,3 +119,38 @@ export const userEmailChangeConfirmRequestSchema = z export type UserEmailChangeConfirmRequest = z.infer< typeof userEmailChangeConfirmRequestSchema >; + +const totp6 = z + .string() + .regex(/^\d{6}$/, "expected 6-digit TOTP code"); + +/** + * Bodies for `/api/users/me/2fa/*` (password re-check on each call; same `loginHash` as login). + */ +export const user2faPasswordBodySchema = z + .object({ + loginHash: byteB64, + }) + .openapi("User2faPasswordBody"); + +export const user2faEnableFinishRequestSchema = z + .object({ + loginHash: byteB64, + authenticatorToken: totp6, + }) + .openapi("User2faEnableFinishRequest"); + +export const user2faEnableRequestResponseSchema = z + .object({ + secret: z.string(), + keyUri: z.string().openapi({ description: "otpauth:// URI for authenticator apps." }), + }) + .openapi("User2faEnableRequestResponse"); + +export const user2faRecoveryCodesResponseSchema = z + .object({ + recoveryCodes: z.array( + z.string().regex(/^[a-f0-9]{32}$/), + ), + }) + .openapi("User2faRecoveryCodesResponse"); diff --git a/new-deepnotes/packages/session/src/crypto/session-crypto.ts b/new-deepnotes/packages/session/src/crypto/session-crypto.ts index 3293423c..1ae175b6 100644 --- a/new-deepnotes/packages/session/src/crypto/session-crypto.ts +++ b/new-deepnotes/packages/session/src/crypto/session-crypto.ts @@ -78,6 +78,16 @@ export function decryptUserAuthenticatorSecret( ); } +export function encryptUserAuthenticatorSecret( + userAuthenticatorSecret: string, + encryptionKeyB64: string, +): Uint8Array { + const key = wrapSymmetricKey(base64ToBytes(encryptionKeyB64)); + return key.encrypt(textToBytes(userAuthenticatorSecret), { + associatedData: { context: "UserAuthenticatorSecret" }, + }); +} + export function decryptRecoveryCodes( userEncryptedRecoveryCodes: Uint8Array, encryptionKeyB64: string, diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 65ca8880..0c17a865 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -26,3 +26,11 @@ export { } from "./email-verification.js"; export { getAuthenticatedUserSummary } from "./user-me.js"; export type { AuthenticatedUserSummary } from "./user-me.js"; +export { + performUserTwoFactorDisable, + performUserTwoFactorEnableFinish, + performUserTwoFactorEnableRequest, + performUserTwoFactorForgetDevices, + performUserTwoFactorGenerateRecoveryCodes, + performUserTwoFactorLoad, +} from "./user-two-factor-settings.js"; diff --git a/new-deepnotes/packages/session/src/user-two-factor-settings.ts b/new-deepnotes/packages/session/src/user-two-factor-settings.ts new file mode 100644 index 00000000..57485dbb --- /dev/null +++ b/new-deepnotes/packages/session/src/user-two-factor-settings.ts @@ -0,0 +1,355 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { eq } from "drizzle-orm"; +import sodium from "libsodium-wrappers-sumo"; +import { authenticator } from "otplib"; + +import { devices, users } from "@deepnotes/db/schema"; + +import { getPasswordHashValues } from "./crypto/index.js"; +import { decryptUserEmail } from "./encrypt-user-email.js"; +import { + decryptUserAuthenticatorSecret, + decryptUserRehashedLoginHash, + derivePasswordValues, + encryptRecoveryCodes, + encryptUserAuthenticatorSecret, + ensureSodiumReady, + hashRecoveryCode, +} from "./crypto/session-crypto.js"; +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { verifyAccessToken } from "./jwt.js"; + +function toBuf(u: Uint8Array): Buffer { + return Buffer.from(u); +} + +async function requireUserId( + accessCookie: string | undefined, + accessSecret: string, +): Promise { + if (accessCookie == null || accessCookie === "") { + throw new SessionError(401, "UNAUTHORIZED", "No access token."); + } + const payload = await verifyAccessToken(accessCookie, accessSecret); + if (payload == null) { + throw new SessionError(401, "UNAUTHORIZED", "Invalid access token."); + } + return payload.uid; +} + +async function assertPasswordAndLoadUser(input: { + db: DeepnotesDb; + env: SessionEnv; + userId: string; + loginHash: Uint8Array; +}) { + await ensureSodiumReady(); + const userRows = await input.db + .select({ + id: users.id, + demo: users.demo, + twoFactorAuthEnabled: users.twoFactorAuthEnabled, + encryptedAuthenticatorSecret: users.encryptedAuthenticatorSecret, + encryptedRecoveryCodes: users.encryptedRecoveryCodes, + encryptedRehashedLoginHash: users.encryptedRehashedLoginHash, + encryptedEmail: users.encryptedEmail, + }) + .from(users) + .where(eq(users.id, input.userId)) + .limit(1); + + const userRow = userRows[0]; + if (userRow == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + + if (userRow.demo === true) { + throw new SessionError( + 403, + "FORBIDDEN", + "This action is unavailable for demo accounts.", + ); + } + + const passwordHashValues = getPasswordHashValues( + decryptUserRehashedLoginHash( + new Uint8Array(userRow.encryptedRehashedLoginHash), + input.env.USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, + ), + ); + const passwordValues = derivePasswordValues({ + password: input.loginHash, + salt: passwordHashValues.saltBytes, + }); + if (!sodium.memcmp(passwordValues.hash, passwordHashValues.hashBytes)) { + throw new SessionError(400, "BAD_REQUEST", "Password is incorrect."); + } + return userRow; +} + +const emailEx = (env: SessionEnv) => env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS ?? ""; + +/** + * `POST /api/users/me/2fa/enable/request` — store pending TOTP secret, return + * raw secret + otpauth URI (legacy `users.account.twoFactorAuth.enable.request`). + */ +export async function performUserTwoFactorEnableRequest(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + loginHash: Uint8Array; +}): Promise<{ secret: string; keyUri: string }> { + const userId = await requireUserId( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + const userRow = await assertPasswordAndLoadUser({ ...input, userId }); + + if (userRow.twoFactorAuthEnabled) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Two factor authentication is already enabled.", + ); + } + + const authenticatorSecret = authenticator.generateSecret(); + const enc = encryptUserAuthenticatorSecret( + authenticatorSecret, + input.env.USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, + ); + + const updated = await input.db + .update(users) + .set({ + encryptedAuthenticatorSecret: toBuf(enc), + }) + .where(eq(users.id, userId)) + .returning({ id: users.id }); + if (updated.length !== 1) { + throw new SessionError(500, "SERVER_MISCONFIG", "2FA request did not apply."); + } + + const email = decryptUserEmail( + new Uint8Array(userRow.encryptedEmail), + input.env.USER_EMAIL_ENCRYPTION_KEY, + emailEx(input.env), + ); + return { + secret: authenticatorSecret, + keyUri: authenticator.keyuri(email, "DeepNotes", authenticatorSecret), + }; +} + +/** + * `POST /api/users/me/2fa/enable/finish` — verify 6-digit code, enable 2FA, store recovery codes. + */ +export async function performUserTwoFactorEnableFinish(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + loginHash: Uint8Array; + authenticatorToken: string; +}): Promise<{ recoveryCodes: string[] }> { + const userId = await requireUserId( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + const userRow = await assertPasswordAndLoadUser({ ...input, userId }); + + if (userRow.twoFactorAuthEnabled) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Two factor authentication is already enabled.", + ); + } + if (userRow.encryptedAuthenticatorSecret == null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Two-factor authentication is not enabled.", + ); + } + + const authSecret = decryptUserAuthenticatorSecret( + new Uint8Array(userRow.encryptedAuthenticatorSecret), + input.env.USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, + ); + if (!authenticator.check(input.authenticatorToken, authSecret)) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Authenticator token is incorrect.", + ); + } + + await ensureSodiumReady(); + const recoveryCodes = Array.from({ length: 6 }, () => + sodium.to_hex(sodium.randombytes_buf(16)), + ); + const hashed = recoveryCodes.map((c) => hashRecoveryCode(c)); + const encRecovery = encryptRecoveryCodes( + hashed, + input.env.USER_RECOVERY_CODES_ENCRYPTION_KEY, + ); + + const updated = await input.db + .update(users) + .set({ + twoFactorAuthEnabled: true, + encryptedRecoveryCodes: toBuf(encRecovery), + }) + .where(eq(users.id, userId)) + .returning({ id: users.id }); + if (updated.length !== 1) { + throw new SessionError(500, "SERVER_MISCONFIG", "2FA finish did not apply."); + } + + return { recoveryCodes }; +} + +/** + * `POST /api/users/me/2fa/load` — returns TOTP secret + key URI (password in JSON; legacy `load` tRPC with loginHash). + */ +export async function performUserTwoFactorLoad(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + loginHash: Uint8Array; +}): Promise<{ secret: string; keyUri: string }> { + const userId = await requireUserId( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + const userRow = await assertPasswordAndLoadUser({ ...input, userId }); + + if (!userRow.twoFactorAuthEnabled || userRow.encryptedAuthenticatorSecret == null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Two factor authentication is not enabled.", + ); + } + + const email = decryptUserEmail( + new Uint8Array(userRow.encryptedEmail), + input.env.USER_EMAIL_ENCRYPTION_KEY, + emailEx(input.env), + ); + const secret = decryptUserAuthenticatorSecret( + new Uint8Array(userRow.encryptedAuthenticatorSecret), + input.env.USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, + ); + return { + secret, + keyUri: authenticator.keyuri(email, "DeepNotes", secret), + }; +} + +/** + * `POST /api/users/me/2fa/recovery-codes` — replace recovery codes. + */ +export async function performUserTwoFactorGenerateRecoveryCodes(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + loginHash: Uint8Array; +}): Promise<{ recoveryCodes: string[] }> { + const userId = await requireUserId( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + const userRow = await assertPasswordAndLoadUser({ ...input, userId }); + + if (!userRow.twoFactorAuthEnabled) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Two factor authentication is not enabled.", + ); + } + + await ensureSodiumReady(); + const recoveryCodes = Array.from({ length: 6 }, () => + sodium.to_hex(sodium.randombytes_buf(16)), + ); + const encRecovery = encryptRecoveryCodes( + recoveryCodes.map((c) => hashRecoveryCode(c)), + input.env.USER_RECOVERY_CODES_ENCRYPTION_KEY, + ); + const updated = await input.db + .update(users) + .set({ + encryptedRecoveryCodes: toBuf(encRecovery), + }) + .where(eq(users.id, userId)) + .returning({ id: users.id }); + if (updated.length !== 1) { + throw new SessionError( + 500, + "SERVER_MISCONFIG", + "Recovery code rotation did not apply.", + ); + } + return { recoveryCodes }; +} + +/** + * `POST /api/users/me/2fa/devices/forget` — set `devices.trusted = false` for this user. + */ +export async function performUserTwoFactorForgetDevices(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + loginHash: Uint8Array; +}): Promise { + const userId = await requireUserId( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + const userRow = await assertPasswordAndLoadUser({ ...input, userId }); + if (!userRow.twoFactorAuthEnabled) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Two factor authentication is not enabled.", + ); + } + await input.db + .update(devices) + .set({ trusted: false }) + .where(eq(devices.userId, userId)); +} + +/** + * `POST /api/users/me/2fa/disable` — turn off 2FA and clear secrets. + */ +export async function performUserTwoFactorDisable(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + loginHash: Uint8Array; +}): Promise { + const userId = await requireUserId( + input.accessCookie, + input.env.ACCESS_SECRET, + ); + const userRow = await assertPasswordAndLoadUser({ ...input, userId }); + if (!userRow.twoFactorAuthEnabled) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Two factor authentication is not enabled.", + ); + } + await input.db + .update(users) + .set({ + twoFactorAuthEnabled: false, + encryptedAuthenticatorSecret: null, + encryptedRecoveryCodes: null, + }) + .where(eq(users.id, userId)); +} From a2948544764f4f51d5a56801d975163e96f333df Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:18:00 -0300 Subject: [PATCH 039/243] fix: ci --- .../packages/db/src/template-db.test.ts | 120 ++++++++++++------ 1 file changed, 81 insertions(+), 39 deletions(-) diff --git a/new-deepnotes/packages/db/src/template-db.test.ts b/new-deepnotes/packages/db/src/template-db.test.ts index ac45b0b1..7536c8bf 100644 --- a/new-deepnotes/packages/db/src/template-db.test.ts +++ b/new-deepnotes/packages/db/src/template-db.test.ts @@ -8,7 +8,7 @@ import postgres from "postgres"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import * as schema from "./schema.js"; -import { users } from "./schema.js"; +import { sessions, users } from "./schema.js"; import { withDatabaseName } from "./test/db-url.js"; import { createDatabaseFromTemplate, @@ -22,25 +22,76 @@ loadEnv({ path: join(__dirname, "../../../.env") }); const ctx = resolveTemplateContext(); -describe.skipIf(ctx == null)("postgres template database (§5.7)", () => { - const templateName = ctx!.templateName; - const adminUrl = ctx!.adminUrl; - const appBaseUrl = ctx!.appBaseUrl; - - beforeAll(async () => { - await ensureTemplateDatabase(ctx!); +// Vitest still executes the describe callback to collect tests when using +// describe.skipIf, so we must not dereference ctx at suite top level when null. +if (ctx == null) { + describe.skip("postgres template database (§5.7) — skipped without DATABASE_URL", () => { + it("requires DATABASE_URL (and optional DATABASE_ADMIN_URL / TEST_DB_TEMPLATE_NAME)", () => {}); }); +} else { + const { templateName, adminUrl, appBaseUrl } = ctx; - afterAll(async () => { - const admin = postgres(adminUrl, { max: 1 }); - try { - await dropDatabaseIfExists(admin, templateName); - } finally { - await admin.end({ timeout: 5 }); - } - }); + describe("postgres template database (§5.7)", () => { + beforeAll(async () => { + await ensureTemplateDatabase(ctx); + }); + + afterAll(async () => { + const admin = postgres(adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin, templateName); + } finally { + await admin.end({ timeout: 5 }); + } + }); - it("clones template and sees isolated empty users", async () => { + it("clones template and sees isolated empty users", async () => { + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const countRows = await db + .select({ n: sql`count(*)::int` }) + .from(users); + expect(countRows[0]?.n).toBe(0); + + const uid = "012345678901234567890"; + await db.insert(users).values({ + id: uid, + startingPageId: uid, + personalGroupId: uid, + publicKeyring: Buffer.alloc(1), + encryptedPrivateKeyring: Buffer.alloc(1), + encryptedSymmetricKeyring: Buffer.alloc(1), + encryptedDefaultArrow: Buffer.alloc(1), + encryptedDefaultNote: Buffer.alloc(1), + encryptedEmail: Buffer.alloc(1), + emailHash: Buffer.alloc(1), + encryptedRehashedLoginHash: Buffer.alloc(1), + }); + + const row = await db.select().from(users).where(eq(users.id, uid)); + expect(row).toHaveLength(1); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("rejects orphan session row (FK to users and devices)", async () => { const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; const admin = postgres(adminUrl, { max: 1 }); try { @@ -53,28 +104,18 @@ describe.skipIf(ctx == null)("postgres template database (§5.7)", () => { const client = postgres(cloneUrl, { max: 1 }); const db = drizzle(client, { schema }); try { - const countRows = await db - .select({ n: sql`count(*)::int` }) - .from(users); - expect(countRows[0]?.n).toBe(0); - - const uid = "012345678901234567890"; - await db.insert(users).values({ - id: uid, - startingPageId: uid, - personalGroupId: uid, - publicKeyring: Buffer.alloc(1), - encryptedPrivateKeyring: Buffer.alloc(1), - encryptedSymmetricKeyring: Buffer.alloc(1), - encryptedDefaultArrow: Buffer.alloc(1), - encryptedDefaultNote: Buffer.alloc(1), - encryptedEmail: Buffer.alloc(1), - emailHash: Buffer.alloc(1), - encryptedRehashedLoginHash: Buffer.alloc(1), - }); - - const row = await db.select().from(users).where(eq(users.id, uid)); - expect(row).toHaveLength(1); + const exp = new Date(Date.now() + 86_400_000).toISOString(); + await expect( + db.insert(sessions).values({ + id: "sess01234567890123456", + userId: "user01234567890123456", + deviceId: "devc01234567890123456", + encryptionKey: randomBytes(32), + refreshCode: "refr01234567890123456", + expirationDate: exp, + invalidated: false, + }), + ).rejects.toThrow(); } finally { await client.end({ timeout: 5 }); const admin2 = postgres(adminUrl, { max: 1 }); @@ -86,3 +127,4 @@ describe.skipIf(ctx == null)("postgres template database (§5.7)", () => { } }); }); +} From da44dc05ab761473af372b47c887bd4a2393d871 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:18:34 -0300 Subject: [PATCH 040/243] test(new-deepnotes): expand account flow integration tests --- new-deepnotes/PLAN_PROGRESS.md | 25 +- .../src/account-flows.integration.test.ts | 229 +++++++++++++++++- 2 files changed, 247 insertions(+), 7 deletions(-) diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 7acb51ac..bc170abe 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -38,12 +38,14 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` (renamed from `email-change.integration.test.ts`). See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) for the per-case list. +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** / demo **403**. `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions FK**. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for `CREATE DATABASE`) are unset; they clone template `dn_test_tpl_session_email` (isolated from `@deepnotes/db`’s `dn_test_tpl_deepnotes` so **Turbo** can run both packages in parallel). +**Session package file:** `packages/session/src/account-flows.integration.test.ts` (describe: **account flows + sessions: Postgres template DB**). + | Test case | Exercises | Assertions | |-----------|-----------|------------| | Register → email-change request → confirm | `performUserRegister`, `performUserEmailChange*`, `signAccessToken` | New email in `decryptUserEmail` + `email_hash` match; `encrypted_new_email` / `email_verification_code` cleared; clear-session cookie lines on confirm. | @@ -52,8 +54,18 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` | Password change, happy path | `performUserPasswordChange` | After change: PHC decrypts to a hash matching **new** password; private + symmetric keyrings **unwrap** with the same `derivePasswordValues` key as login (salt from stored PHC), round-trip to the new keyring bytes passed in. | | Password change invalidates sessions | `performUserPasswordChange` + explicit `devices` / `sessions` insert | `sessions.invalidated === true` for the user. | | Password change, wrong old password | `performUserPasswordChange` | **400** `BAD_REQUEST`. | +| **Login → refresh → refresh** | `performUserRegister`, `performSessionLogin`, `performSessionRefresh` | Login sets `Set-Cookie` (`refreshToken`, `loggedIn=true`); DB `sessions.refresh_code` + `encryption_key` change on refresh; JSON `oldSessionKey` / `newSessionKey` match pre/post row `encryption_key`; **second** refresh with rotated cookies succeeds. | +| **Login, wrong password** | `performSessionLogin` | **401** `UNAUTHORIZED` (wrong `loginHash`). | +| **Password change, demo user** | `performUserRegister` + `UPDATE users SET demo`, `performUserPasswordChange` | **403** `FORBIDDEN` (“demo accounts”). | + +**`@deepnotes/db` real Postgres (`template-db.test.ts`):** + +| Test case | Exercises | Assertions | +|-----------|-----------|------------| +| Clone + insert user | Template clone, `users` insert | Isolated DB starts with **0** users; insert + select by `id`. | +| **Sessions FK** | `INSERT sessions` without parent `users` / `devices` | Insert **rejects** (FK violation) for orphan `user_id` / `device_id`. | -**Not yet in integration:** `performSessionLogin` + refresh (cookie/session rows), Redis failed-login with real `ioredis`/Upstash, demo account **403** on password change (covered in unit path via `performUserPasswordChange` branches if added later). +**Not yet in integration:** Redis failed-login with real `ioredis`/Upstash against `performSessionLogin`; **2FA** enable → `performSessionLogin` with TOTP (HTTP + `user-two-factor-settings` exist; DB path not covered in template suite yet); `performSessionRefresh` edge cases (expired JWT, `loggedIn` false, replayed refresh token invalidation). ### Sessions + account (current) @@ -76,7 +88,7 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` - [x] `POST /api/users/me/email-change` — `performUserEmailChangeRequest`: `oldLoginHash` + `newEmail`; **403** demo, **400** bad password or “email already in use” (global `email_hash` match, same as legacy); sets `encrypted_new_email` + 6-digit `email_verification_code`; Resend (subject/body like legacy) or **200** `{ "emailVerificationCode" }` when `SEND_EMAILS=false`; **204** when emailed. - [x] `POST /api/users/me/email-change/confirm` — `performUserEmailChangeConfirm`: one call (WS two-step collapsed); `oldLoginHash`, `emailVerificationCode` (6 digits), `newLoginHash`, `userEncrypted*Keyring` (b64, same as register/password); verifies code + password; applies new `encrypted_email` / `email_hash`, clears pending fields, PHC + rewrapped keyrings, invalidates **all** `sessions`, **204** + `buildClearSessionCookies`; optional `updateStripeCustomerEmail` in worker (matches legacy `customers.update` after commit, errors non-fatal). - [x] **`decryptUserEmail`** in `@deepnotes/session` for confirm; **`sendEmailChangeVerificationEmail`** (Resend); OpenAPI + [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md) updated. -- [x] **2FA (HTTP surface)** — Hono + OpenAPI: `user-two-factor-settings.ts` (`encryptUserAuthenticatorSecret` in `session-crypto`). Routes: [2FA HTTP routes](#2fa-http-routes-phase-3). `load` is **`POST /api/users/me/2fa/load`** (password in JSON, not a `GET` — [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) footnote). **Not yet in integration template DB:** 2FA enable → login with TOTP (optional follow-up; login path already uses `assertTwoFactorOk` in [two-factor.ts](./packages/session/src/two-factor.ts)). +- [x] **2FA (HTTP surface)** — Hono + OpenAPI: `user-two-factor-settings.ts` (`encryptUserAuthenticatorSecret` in `session-crypto`). Routes: [2FA HTTP routes](#2fa-http-routes-phase-3). `load` is **`POST /api/users/me/2fa/load`** (password in JSON, not a `GET` — [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) footnote). **Postgres integration:** not yet — follow-up: enable/finish 2FA then `performSessionLogin` with TOTP (`assertTwoFactorOk` in [two-factor.ts](./packages/session/src/two-factor.ts)). ### 2FA HTTP routes (Phase 3) @@ -143,8 +155,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| -| **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts`: clone template DB, smoke SQL | More assertions on FKs / critical columns after schema grows | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` — email change + **password change** (PHC + unwrap), wrong passwords/codes, session invalidation; template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **`performSessionLogin` / refresh** with template DB + device/session rows; **Redis** + `performSessionLogin` failed-login; optional **demo 403** integration | +| **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts`: clone template, empty `users`, **FK rejection** on orphan `sessions` row | More composite FK paths (e.g. `group_members`) when groups work lands | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` — email change, password change (PHC + unwrap, session invalidation, wrong password, **demo 403**), **`performSessionLogin`** + **double `performSessionRefresh`** (cookie parse + DB `refresh_code` / `encryption_key`); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; **2FA** finish → login with TOTP in template DB | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA paths); **`schemas/users.test.ts`** (email/password change, 2fa finish) | Schemas for pages/groups when they land; optional OpenAPI **snapshot** | | **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: 503 when env missing — includes **2FA** routes in matrix | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | @@ -168,7 +180,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). - [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` template test; `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** template tests for account **register / email change / password change** (see [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail)). **Next:** login + refresh + optional Redis in integration; Stripe when billing exists. +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** template tests: register, email change, password change, **login + refresh (two rotations)**, wrong login password, demo password **403**; `@deepnotes/db` **sessions FK** (see [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail)). **Next:** Redis failed-login integration; 2FA + login in template DB; Stripe when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -187,6 +199,7 @@ Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POS | Date | Change | |------|--------| +| 2026-04-27 | **More real Postgres tests:** `account-flows.integration.test.ts` — `performSessionLogin` + chained `performSessionRefresh` (DB `refresh_code` / `encryption_key`, cookie parse), wrong-password login **401**, demo-flag password change **403**; `template-db.test.ts` — orphan `sessions` insert FK failure. PLAN_PROGRESS: expanded Phase 3 tables + matrix. | | 2026-04-27 | **2FA account HTTP:** `user-two-factor-settings.ts`, `encryptUserAuthenticatorSecret` in `session-crypto`, Zod + OpenAPI + Hono for `/api/users/me/2fa/*` (6 routes); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — `load` is POST not GET; see [2FA HTTP routes](#2fa-http-routes-phase-3) below. | | 2026-04-27 | **Integration tests:** expanded `account-flows.integration.test.ts` (email wrong code; password change PHC + keyring unwrap with salt from PHC; `sessions` invalidation; wrong old password). Renamed from `email-change.integration.test.ts`. PLAN_PROGRESS: detailed Phase 3 test table + matrix gaps. | | 2026-04-26 | **Integration tests:** `@deepnotes/db` exports `@deepnotes/db/testing/template-db` + `db-url`; `@deepnotes/session` — `email-change.integration.test.ts` (Postgres template clone, register + email change + wrong password). PLAN_PROGRESS matrix + Phase 3 checklist updated. | diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index 01d0a8ba..86b4bc70 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -25,6 +25,8 @@ import { performUserEmailChangeConfirm, performUserEmailChangeRequest, } from "./change-user-email.js"; +import { performSessionLogin } from "./login.js"; +import { performSessionRefresh } from "./refresh.js"; import { createPrivateKeyring, createSymmetricKeyring, @@ -67,6 +69,22 @@ function rand32(): Uint8Array { return sodium.randombytes_buf(32); } +/** First `name=value` segment from `Set-Cookie` lines (values are URI-encoded). */ +function cookieValueFromSetCookieLines( + lines: string[], + name: string, +): string | undefined { + const prefix = `${name}=`; + for (const line of lines) { + if (!line.startsWith(prefix)) continue; + const rest = line.slice(prefix.length); + const semi = rest.indexOf(";"); + const raw = (semi === -1 ? rest : rest.slice(0, semi)).trim(); + return decodeURIComponent(raw); + } + return undefined; +} + async function buildRegisterBody( email: string, loginHash: Uint8Array, @@ -106,7 +124,7 @@ async function buildRegisterBody( } describe.skipIf(resolveTemplateContext() == null)( - "account flows: email + password (Postgres template DB)", + "account flows + sessions: Postgres template DB", () => { const baseCtx = resolveTemplateContext()!; const ctx: TemplateDbContext = { @@ -530,5 +548,214 @@ describe.skipIf(resolveTemplateContext() == null)( } } }); + + it("login creates session row; refresh rotates encryption key and refresh code", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + const clientIp = "203.0.113.50"; + const userAgent = "integration-test/1"; + try { + const email = `l-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + + const loginOut = await performSessionLogin({ + db, + env, + body: { + email, + loginHash, + rememberSession: false, + }, + clientIp, + userAgent, + }); + const sessionId = loginOut.json.sessionId; + expect(typeof sessionId).toBe("string"); + + const [before] = await db + .select({ + refreshCode: sessions.refreshCode, + encryptionKey: sessions.encryptionKey, + }) + .from(sessions) + .where(eq(sessions.id, sessionId as string)); + expect(before).toBeDefined(); + + const refresh1 = cookieValueFromSetCookieLines( + loginOut.cookieLines, + "refreshToken", + ); + const loggedIn1 = cookieValueFromSetCookieLines( + loginOut.cookieLines, + "loggedIn", + ); + expect(refresh1).toBeDefined(); + expect(loggedIn1).toBe("true"); + + const refreshOut = await performSessionRefresh({ + db, + env, + refreshCookie: refresh1, + loggedInCookie: loggedIn1, + }); + + const oldKeyB64 = refreshOut.json.oldSessionKey; + const newKeyB64 = refreshOut.json.newSessionKey; + expect(typeof oldKeyB64).toBe("string"); + expect(typeof newKeyB64).toBe("string"); + expect( + Buffer.from(oldKeyB64 as string, "base64").equals( + new Uint8Array(before!.encryptionKey), + ), + ).toBe(true); + + const [after] = await db + .select({ + refreshCode: sessions.refreshCode, + encryptionKey: sessions.encryptionKey, + }) + .from(sessions) + .where(eq(sessions.id, sessionId as string)); + expect(after!.refreshCode).not.toBe(before!.refreshCode); + expect( + Buffer.from(newKeyB64 as string, "base64").equals( + new Uint8Array(after!.encryptionKey), + ), + ).toBe(true); + + const refresh2 = cookieValueFromSetCookieLines( + refreshOut.cookieLines, + "refreshToken", + ); + const loggedIn2 = cookieValueFromSetCookieLines( + refreshOut.cookieLines, + "loggedIn", + ); + const refreshOut2 = await performSessionRefresh({ + db, + env, + refreshCookie: refresh2, + loggedInCookie: loggedIn2, + }); + expect(typeof refreshOut2.json.newSessionKey).toBe("string"); + const [after2] = await db + .select({ encryptionKey: sessions.encryptionKey }) + .from(sessions) + .where(eq(sessions.id, sessionId as string)); + expect( + Buffer.from(refreshOut2.json.newSessionKey as string, "base64").equals( + new Uint8Array(after2!.encryptionKey), + ), + ).toBe(true); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("login rejects wrong password", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `m-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + await expect( + performSessionLogin({ + db, + env, + body: { + email, + loginHash: rand32(), + rememberSession: false, + }, + clientIp: "198.51.100.1", + userAgent: "integration-test/2", + }), + ).rejects.toMatchObject({ status: 401, code: "UNAUTHORIZED" }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("password change rejects demo-flagged user", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `d-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + await db.update(users).set({ demo: true }).where(eq(users.id, reg.userId)); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + await expect( + performUserPasswordChange({ + db, + env, + accessCookie: access, + oldLoginHash: loginHash, + newLoginHash: rand32(), + newEncryptedPrivateKeyring: rand32(), + newEncryptedSymmetricKeyring: rand32(), + }), + ).rejects.toMatchObject({ status: 403, code: "FORBIDDEN" }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); }, ); From dc14a37cc176d345874d8fe3924c70edd3658230 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:21:41 -0300 Subject: [PATCH 041/243] fix: ci --- new-deepnotes/packages/api/package.json | 1 + new-deepnotes/pnpm-lock.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/new-deepnotes/packages/api/package.json b/new-deepnotes/packages/api/package.json index 3519286d..53be712c 100644 --- a/new-deepnotes/packages/api/package.json +++ b/new-deepnotes/packages/api/package.json @@ -25,6 +25,7 @@ "zod": "^3.24.3" }, "devDependencies": { + "@types/node": "^22.14.1", "typescript": "^5.8.3", "vitest": "^3.1.1" } diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml index a183f741..106cd1f4 100644 --- a/new-deepnotes/pnpm-lock.yaml +++ b/new-deepnotes/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: specifier: ^3.24.3 version: 3.25.76 devDependencies: + '@types/node': + specifier: ^22.14.1 + version: 22.19.17 typescript: specifier: ^5.8.3 version: 5.9.3 From 713babeeab9501cf188491f53373f362cf6490c6 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:22:05 -0300 Subject: [PATCH 042/243] test(new-deepnotes): template DB and more account flow coverage --- new-deepnotes/PLAN_PROGRESS.md | 22 +- .../packages/db/src/template-db.test.ts | 82 +++++- .../src/account-flows.integration.test.ts | 265 ++++++++++++++++++ 3 files changed, 353 insertions(+), 16 deletions(-) diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index bc170abe..457271f9 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** / demo **403**. `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions FK**. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** / demo **403** / **2FA** (enable → finish → login with TOTP; wrong finish token; missing MFA; bad TOTP). `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) @@ -57,6 +57,10 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` | **Login → refresh → refresh** | `performUserRegister`, `performSessionLogin`, `performSessionRefresh` | Login sets `Set-Cookie` (`refreshToken`, `loggedIn=true`); DB `sessions.refresh_code` + `encryption_key` change on refresh; JSON `oldSessionKey` / `newSessionKey` match pre/post row `encryption_key`; **second** refresh with rotated cookies succeeds. | | **Login, wrong password** | `performSessionLogin` | **401** `UNAUTHORIZED` (wrong `loginHash`). | | **Password change, demo user** | `performUserRegister` + `UPDATE users SET demo`, `performUserPasswordChange` | **403** `FORBIDDEN` (“demo accounts”). | +| **2FA → login (TOTP)** | `performUserTwoFactorEnableRequest` / `Finish`, `performSessionLogin` | After finish: `two_factor_auth_enabled`, `encrypted_authenticator_secret`, `encrypted_recovery_codes` set; **6×32-char hex** recovery codes; login with fresh `authenticator.generate(secret)` returns **200**-equivalent payload (`sessionId`). | +| **2FA finish, wrong code** | `EnableRequest` then `EnableFinish` with `"000000"` | **400** `BAD_REQUEST` (“Authenticator token is incorrect.”). | +| **2FA login without MFA** | After finish, `performSessionLogin` without `authenticatorToken` | **401** “Requires two-factor authentication.” (untrusted device). | +| **2FA login, bad TOTP** | `authenticatorToken: "111111"` | **401** “Invalid authenticator token.” | **`@deepnotes/db` real Postgres (`template-db.test.ts`):** @@ -64,8 +68,10 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` |-----------|-----------|------------| | Clone + insert user | Template clone, `users` insert | Isolated DB starts with **0** users; insert + select by `id`. | | **Sessions FK** | `INSERT sessions` without parent `users` / `devices` | Insert **rejects** (FK violation) for orphan `user_id` / `device_id`. | +| **Devices FK** | `INSERT devices` with non-existent `user_id` | Insert **rejects** (FK to `users`). | +| **Pages → groups FK** | `INSERT pages` with unknown `group_id` | Insert **rejects** (FK to `groups`). | -**Not yet in integration:** Redis failed-login with real `ioredis`/Upstash against `performSessionLogin`; **2FA** enable → `performSessionLogin` with TOTP (HTTP + `user-two-factor-settings` exist; DB path not covered in template suite yet); `performSessionRefresh` edge cases (expired JWT, `loggedIn` false, replayed refresh token invalidation). +**Not yet in integration:** Redis failed-login with real `ioredis`/Upstash against `performSessionLogin` (unit tests cover rate-limit helpers); `performSessionRefresh` edge cases (expired JWT, `loggedIn` false, replayed refresh token invalidation); **2FA recovery-code login** path against real Postgres (logic exists in `two-factor.ts` + `login.ts`). ### Sessions + account (current) @@ -88,7 +94,7 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` - [x] `POST /api/users/me/email-change` — `performUserEmailChangeRequest`: `oldLoginHash` + `newEmail`; **403** demo, **400** bad password or “email already in use” (global `email_hash` match, same as legacy); sets `encrypted_new_email` + 6-digit `email_verification_code`; Resend (subject/body like legacy) or **200** `{ "emailVerificationCode" }` when `SEND_EMAILS=false`; **204** when emailed. - [x] `POST /api/users/me/email-change/confirm` — `performUserEmailChangeConfirm`: one call (WS two-step collapsed); `oldLoginHash`, `emailVerificationCode` (6 digits), `newLoginHash`, `userEncrypted*Keyring` (b64, same as register/password); verifies code + password; applies new `encrypted_email` / `email_hash`, clears pending fields, PHC + rewrapped keyrings, invalidates **all** `sessions`, **204** + `buildClearSessionCookies`; optional `updateStripeCustomerEmail` in worker (matches legacy `customers.update` after commit, errors non-fatal). - [x] **`decryptUserEmail`** in `@deepnotes/session` for confirm; **`sendEmailChangeVerificationEmail`** (Resend); OpenAPI + [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md) updated. -- [x] **2FA (HTTP surface)** — Hono + OpenAPI: `user-two-factor-settings.ts` (`encryptUserAuthenticatorSecret` in `session-crypto`). Routes: [2FA HTTP routes](#2fa-http-routes-phase-3). `load` is **`POST /api/users/me/2fa/load`** (password in JSON, not a `GET` — [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) footnote). **Postgres integration:** not yet — follow-up: enable/finish 2FA then `performSessionLogin` with TOTP (`assertTwoFactorOk` in [two-factor.ts](./packages/session/src/two-factor.ts)). +- [x] **2FA (HTTP surface)** — Hono + OpenAPI: `user-two-factor-settings.ts` (`encryptUserAuthenticatorSecret` in `session-crypto`). Routes: [2FA HTTP routes](#2fa-http-routes-phase-3). `load` is **`POST /api/users/me/2fa/load`** (password in JSON, not a `GET` — [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) footnote). **Postgres integration:** [account-flows.integration.test.ts](./packages/session/src/account-flows.integration.test.ts) — enable/finish + `performSessionLogin` with TOTP; wrong finish token; missing MFA; invalid TOTP (`assertTwoFactorOk` in [two-factor.ts](./packages/session/src/two-factor.ts)). ### 2FA HTTP routes (Phase 3) @@ -155,8 +161,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| -| **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts`: clone template, empty `users`, **FK rejection** on orphan `sessions` row | More composite FK paths (e.g. `group_members`) when groups work lands | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` — email change, password change (PHC + unwrap, session invalidation, wrong password, **demo 403**), **`performSessionLogin`** + **double `performSessionRefresh`** (cookie parse + DB `refresh_code` / `encryption_key`); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; **2FA** finish → login with TOTP in template DB | +| **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts`: clone template, empty `users`, **FK rejection** on orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`** | More paths when groups CRUD lands (`group_members`, join tables, cascades) | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` — email change, password change (PHC + unwrap, session invalidation, wrong password, **demo 403**), **`performSessionLogin`** + **double `performSessionRefresh`**, **2FA** (request/finish/DB columns, login + TOTP, wrong finish code, MFA required, bad TOTP); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; **2FA recovery-code** consumption against template DB | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA paths); **`schemas/users.test.ts`** (email/password change, 2fa finish) | Schemas for pages/groups when they land; optional OpenAPI **snapshot** | | **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: 503 when env missing — includes **2FA** routes in matrix | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | @@ -180,7 +186,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). - [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` template test; `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** template tests: register, email change, password change, **login + refresh (two rotations)**, wrong login password, demo password **403**; `@deepnotes/db` **sessions FK** (see [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail)). **Next:** Redis failed-login integration; 2FA + login in template DB; Stripe when billing exists. +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** template tests: register, email change, password change, **login + refresh (two rotations)**, wrong login password, demo password **403**, **2FA** (finish + login with TOTP, error paths); `@deepnotes/db` **sessions / devices / pages FK** (see [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail)). **Next:** Redis failed-login integration; 2FA **recovery code** login in template DB; refresh-token replay / JWT expiry; Stripe when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -191,7 +197,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Phase 3 working order (suggested) -Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POST, see map). **(next)** `users.pages` + `groups` + `pages` REST from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md). **(then)** **realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes. +Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POST, see map); **Postgres** coverage for 2FA service layer + extra **`@deepnotes/db` FK** tests. **(next)** `users.pages` + `groups` + `pages` REST from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — each slice should add **template DB** tests for new FKs and happy paths where SQL risk is high. **(then)** **realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes. --- @@ -199,7 +205,7 @@ Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POS | Date | Change | |------|--------| -| 2026-04-27 | **More real Postgres tests:** `account-flows.integration.test.ts` — `performSessionLogin` + chained `performSessionRefresh` (DB `refresh_code` / `encryption_key`, cookie parse), wrong-password login **401**, demo-flag password change **403**; `template-db.test.ts` — orphan `sessions` insert FK failure. PLAN_PROGRESS: expanded Phase 3 tables + matrix. | +| 2026-04-27 | **Real Postgres tests (session + db):** `account-flows.integration.test.ts` — login + double refresh, wrong password, demo **403**, **2FA** (enable/finish + TOTP login, wrong finish code, MFA required, bad TOTP). `template-db.test.ts` — orphan **`sessions`**, **`devices`**, **`pages`→`groups`** FK rejects. PLAN_PROGRESS: Phase 3 detail table, package matrix, success criteria, working order. | | 2026-04-27 | **2FA account HTTP:** `user-two-factor-settings.ts`, `encryptUserAuthenticatorSecret` in `session-crypto`, Zod + OpenAPI + Hono for `/api/users/me/2fa/*` (6 routes); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — `load` is POST not GET; see [2FA HTTP routes](#2fa-http-routes-phase-3) below. | | 2026-04-27 | **Integration tests:** expanded `account-flows.integration.test.ts` (email wrong code; password change PHC + keyring unwrap with salt from PHC; `sessions` invalidation; wrong old password). Renamed from `email-change.integration.test.ts`. PLAN_PROGRESS: detailed Phase 3 test table + matrix gaps. | | 2026-04-26 | **Integration tests:** `@deepnotes/db` exports `@deepnotes/db/testing/template-db` + `db-url`; `@deepnotes/session` — `email-change.integration.test.ts` (Postgres template clone, register + email change + wrong password). PLAN_PROGRESS matrix + Phase 3 checklist updated. | diff --git a/new-deepnotes/packages/db/src/template-db.test.ts b/new-deepnotes/packages/db/src/template-db.test.ts index 7536c8bf..26fd296d 100644 --- a/new-deepnotes/packages/db/src/template-db.test.ts +++ b/new-deepnotes/packages/db/src/template-db.test.ts @@ -8,7 +8,7 @@ import postgres from "postgres"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import * as schema from "./schema.js"; -import { sessions, users } from "./schema.js"; +import { devices, pages, sessions, users } from "./schema.js"; import { withDatabaseName } from "./test/db-url.js"; import { createDatabaseFromTemplate, @@ -116,15 +116,81 @@ if (ctx == null) { invalidated: false, }), ).rejects.toThrow(); - } finally { - await client.end({ timeout: 5 }); - const admin2 = postgres(adminUrl, { max: 1 }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("rejects orphan device row (FK to users)", async () => { + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(adminUrl, { max: 1 }); try { - await dropDatabaseIfExists(admin2, cloneName); + await createDatabaseFromTemplate(admin, cloneName, templateName); } finally { - await admin2.end({ timeout: 5 }); + await admin.end({ timeout: 5 }); } - } + + const cloneUrl = withDatabaseName(appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + await expect( + db.insert(devices).values({ + id: "dvc012345678901234567", + userId: "usr012345678901234567", + hash: randomBytes(32), + trusted: false, + }), + ).rejects.toThrow(); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("rejects page row with unknown group_id (FK to groups)", async () => { + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + const bogusGroupId = "grp000000000000000000"; + try { + await expect( + db.insert(pages).values({ + id: "pg0000000000000000000", + groupId: bogusGroupId, + encryptedRelativeTitle: Buffer.alloc(1), + encryptedSymmetricKeyring: Buffer.alloc(1), + encryptedAbsoluteTitle: Buffer.alloc(1), + }), + ).rejects.toThrow(); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); }); -}); } diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index 86b4bc70..c82ed985 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -6,6 +6,7 @@ import { eq } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import sodium from "libsodium-wrappers-sumo"; import { nanoid } from "nanoid"; +import { authenticator } from "otplib"; import postgres from "postgres"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; @@ -27,6 +28,10 @@ import { } from "./change-user-email.js"; import { performSessionLogin } from "./login.js"; import { performSessionRefresh } from "./refresh.js"; +import { + performUserTwoFactorEnableFinish, + performUserTwoFactorEnableRequest, +} from "./user-two-factor-settings.js"; import { createPrivateKeyring, createSymmetricKeyring, @@ -757,5 +762,265 @@ describe.skipIf(resolveTemplateContext() == null)( } } }); + + it("2FA enable/finish persists flags; login succeeds with TOTP", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + const clientIp = "203.0.113.51"; + const userAgent = "integration-test/2fa"; + try { + const email = `2fa-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + + const { secret } = await performUserTwoFactorEnableRequest({ + db, + env, + accessCookie: access, + loginHash, + }); + const finishToken = authenticator.generate(secret); + const { recoveryCodes } = await performUserTwoFactorEnableFinish({ + db, + env, + accessCookie: access, + loginHash, + authenticatorToken: finishToken, + }); + expect(recoveryCodes).toHaveLength(6); + expect(recoveryCodes.every((c) => /^[0-9a-f]{32}$/.test(c))).toBe(true); + + const [u2fa] = await db + .select({ + enabled: users.twoFactorAuthEnabled, + encAuth: users.encryptedAuthenticatorSecret, + encRec: users.encryptedRecoveryCodes, + }) + .from(users) + .where(eq(users.id, reg.userId)); + expect(u2fa?.enabled).toBe(true); + expect(u2fa?.encAuth).not.toBeNull(); + expect(u2fa?.encRec).not.toBeNull(); + + const totp = authenticator.generate(secret); + const loginOut = await performSessionLogin({ + db, + env, + body: { + email, + loginHash, + rememberSession: false, + authenticatorToken: totp, + }, + clientIp, + userAgent, + }); + expect(typeof loginOut.json.sessionId).toBe("string"); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("2FA enable/finish rejects wrong authenticator token", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `2fb-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + await performUserTwoFactorEnableRequest({ + db, + env, + accessCookie: access, + loginHash, + }); + await expect( + performUserTwoFactorEnableFinish({ + db, + env, + accessCookie: access, + loginHash, + authenticatorToken: "000000", + }), + ).rejects.toMatchObject({ + status: 400, + code: "BAD_REQUEST", + message: "Authenticator token is incorrect.", + }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("login with 2FA enabled requires TOTP when device is not trusted", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `2fc-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + const { secret } = await performUserTwoFactorEnableRequest({ + db, + env, + accessCookie: access, + loginHash, + }); + await performUserTwoFactorEnableFinish({ + db, + env, + accessCookie: access, + loginHash, + authenticatorToken: authenticator.generate(secret), + }); + + await expect( + performSessionLogin({ + db, + env, + body: { email, loginHash, rememberSession: false }, + clientIp: "198.51.100.20", + userAgent: "integration-test/2fa-missing", + }), + ).rejects.toMatchObject({ + status: 401, + code: "UNAUTHORIZED", + message: "Requires two-factor authentication.", + }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("login with 2FA rejects invalid TOTP", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `2fd-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + const { secret } = await performUserTwoFactorEnableRequest({ + db, + env, + accessCookie: access, + loginHash, + }); + await performUserTwoFactorEnableFinish({ + db, + env, + accessCookie: access, + loginHash, + authenticatorToken: authenticator.generate(secret), + }); + + await expect( + performSessionLogin({ + db, + env, + body: { + email, + loginHash, + rememberSession: false, + authenticatorToken: "111111", + }, + clientIp: "198.51.100.21", + userAgent: "integration-test/2fa-bad", + }), + ).rejects.toMatchObject({ + status: 401, + code: "UNAUTHORIZED", + message: "Invalid authenticator token.", + }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); }, ); From 36e3f04e741ecebc75f721928f5c4d2da02db754 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:26:43 -0300 Subject: [PATCH 043/243] test(new-deepnotes): more account flow and template DB integration tests --- new-deepnotes/PLAN_PROGRESS.md | 29 ++- .../packages/db/src/template-db.test.ts | 83 +++++++- .../src/account-flows.integration.test.ts | 194 ++++++++++++++++++ 3 files changed, 298 insertions(+), 8 deletions(-) diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 457271f9..cbe8c99e 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -38,12 +38,19 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** / demo **403** / **2FA** (enable → finish → login with TOTP; wrong finish token; missing MFA; bad TOTP). `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP). `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for `CREATE DATABASE`) are unset; they clone template `dn_test_tpl_session_email` (isolated from `@deepnotes/db`’s `dn_test_tpl_deepnotes` so **Turbo** can run both packages in parallel). +**How to run locally:** ensure `.env` at `new-deepnotes/.env` has `DATABASE_URL` and (for template create/drop) `DATABASE_ADMIN_URL` with a role that can `CREATE DATABASE`. Then: + +- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` +- `pnpm --filter @deepnotes/db exec vitest run src/template-db.test.ts` + +CI should set the same vars against the workflow Postgres service (role with `CREATEDB`). + **Session package file:** `packages/session/src/account-flows.integration.test.ts` (describe: **account flows + sessions: Postgres template DB**). | Test case | Exercises | Assertions | @@ -55,12 +62,17 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` | Password change invalidates sessions | `performUserPasswordChange` + explicit `devices` / `sessions` insert | `sessions.invalidated === true` for the user. | | Password change, wrong old password | `performUserPasswordChange` | **400** `BAD_REQUEST`. | | **Login → refresh → refresh** | `performUserRegister`, `performSessionLogin`, `performSessionRefresh` | Login sets `Set-Cookie` (`refreshToken`, `loggedIn=true`); DB `sessions.refresh_code` + `encryption_key` change on refresh; JSON `oldSessionKey` / `newSessionKey` match pre/post row `encryption_key`; **second** refresh with rotated cookies succeeds. | +| **Replay pre-rotation refresh JWT** | Same as above, then third call with **first** login’s `refreshToken` + original `loggedIn` | **401** `UNAUTHORIZED` “Session was invalidated.” — JWT still verifies but `payload.rfc` no longer matches `sessions.refresh_code` after rotation. | +| **Refresh, `loggedIn` ≠ true** | `performSessionLogin`, `performSessionRefresh` with `loggedInCookie: "false"` | **401** “User not logged in.” | +| **Refresh, no refresh cookie** | `performSessionRefresh` with `refreshCookie: undefined`, `loggedIn: "true"` | **401** “No refresh token received.” | | **Login, wrong password** | `performSessionLogin` | **401** `UNAUTHORIZED` (wrong `loginHash`). | | **Password change, demo user** | `performUserRegister` + `UPDATE users SET demo`, `performUserPasswordChange` | **403** `FORBIDDEN` (“demo accounts”). | | **2FA → login (TOTP)** | `performUserTwoFactorEnableRequest` / `Finish`, `performSessionLogin` | After finish: `two_factor_auth_enabled`, `encrypted_authenticator_secret`, `encrypted_recovery_codes` set; **6×32-char hex** recovery codes; login with fresh `authenticator.generate(secret)` returns **200**-equivalent payload (`sessionId`). | | **2FA finish, wrong code** | `EnableRequest` then `EnableFinish` with `"000000"` | **400** `BAD_REQUEST` (“Authenticator token is incorrect.”). | | **2FA login without MFA** | After finish, `performSessionLogin` without `authenticatorToken` | **401** “Requires two-factor authentication.” (untrusted device). | | **2FA login, bad TOTP** | `authenticatorToken: "111111"` | **401** “Invalid authenticator token.” | +| **2FA login with recovery code** | `performUserTwoFactorEnableFinish` → `performSessionLogin` with `recoveryCode` (no TOTP) | **200**-equivalent (`sessionId`); `decryptRecoveryCodes` on row shows **5** hashes left (one consumed). | +| **2FA recovery code reuse** | Second `performSessionLogin` with same plaintext recovery code, new IP/UA | **401** “Invalid recovery code.” | **`@deepnotes/db` real Postgres (`template-db.test.ts`):** @@ -70,8 +82,10 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` | **Sessions FK** | `INSERT sessions` without parent `users` / `devices` | Insert **rejects** (FK violation) for orphan `user_id` / `device_id`. | | **Devices FK** | `INSERT devices` with non-existent `user_id` | Insert **rejects** (FK to `users`). | | **Pages → groups FK** | `INSERT pages` with unknown `group_id` | Insert **rejects** (FK to `groups`). | +| **`group_members` → `users` FK** | `INSERT group_members` with bogus `user_id` and `group_id` | Insert **rejects** (no parent user). | +| **`group_members` → `groups` FK** | Insert minimal `users` row, then `group_members` with unknown `group_id` | Insert **rejects** (no parent group). | -**Not yet in integration:** Redis failed-login with real `ioredis`/Upstash against `performSessionLogin` (unit tests cover rate-limit helpers); `performSessionRefresh` edge cases (expired JWT, `loggedIn` false, replayed refresh token invalidation); **2FA recovery-code login** path against real Postgres (logic exists in `two-factor.ts` + `login.ts`). +**Not yet in integration:** Redis failed-login with real `ioredis`/Upstash against `performSessionLogin` (unit tests cover rate-limit helpers); `performSessionRefresh` with **expired** refresh JWT (would need clock-skew or short-lived token minting in test); **invalid/tampered** refresh JWT where `verifyRefreshToken` fails but `decodeRefreshTokenUnsafe` returns `sid` (invalidates row — behaviour worth an explicit test when touching refresh again). ### Sessions + account (current) @@ -161,8 +175,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| -| **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts`: clone template, empty `users`, **FK rejection** on orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`** | More paths when groups CRUD lands (`group_members`, join tables, cascades) | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` — email change, password change (PHC + unwrap, session invalidation, wrong password, **demo 403**), **`performSessionLogin`** + **double `performSessionRefresh`**, **2FA** (request/finish/DB columns, login + TOTP, wrong finish code, MFA required, bad TOTP); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; **2FA recovery-code** consumption against template DB | +| **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (16 cases) — email change, password change (PHC + unwrap, session invalidation, wrong password, **demo 403**), **`performSessionLogin`** + **double refresh** + **stale refresh JWT replay**, **refresh** `loggedIn`/missing-token guards, **2FA** (TOTP paths + **recovery code** login + one-time use + `decryptRecoveryCodes` count); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; optional explicit test for decode-without-verify invalidation path | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA paths); **`schemas/users.test.ts`** (email/password change, 2fa finish) | Schemas for pages/groups when they land; optional OpenAPI **snapshot** | | **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: 503 when env missing — includes **2FA** routes in matrix | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | @@ -185,8 +199,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Drizzle migrations from empty DB documented for production upgrades. - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). -- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` template test; `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** template tests: register, email change, password change, **login + refresh (two rotations)**, wrong login password, demo password **403**, **2FA** (finish + login with TOTP, error paths); `@deepnotes/db` **sessions / devices / pages FK** (see [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail)). **Next:** Redis failed-login integration; 2FA **recovery code** login in template DB; refresh-token replay / JWT expiry; Stripe when billing exists. +- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation, login + refresh + **stale JWT replay**, refresh **cookie guards**, **2FA** TOTP + **recovery codes**). +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** as in [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) (16 session + 6 db integration cases when DB env is set). **Next:** Redis failed-login integration; refresh **expired** JWT; Stripe when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -197,7 +211,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Phase 3 working order (suggested) -Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POST, see map); **Postgres** coverage for 2FA service layer + extra **`@deepnotes/db` FK** tests. **(next)** `users.pages` + `groups` + `pages` REST from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — each slice should add **template DB** tests for new FKs and happy paths where SQL risk is high. **(then)** **realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes. +Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POST, see map); **Postgres** for 2FA (TOTP + recovery codes + refresh replay/cookie guards) + **`@deepnotes/db`** FKs through **`group_members`**. **(next)** `users.pages` + `groups` + `pages` REST from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — each slice should add **template DB** tests for new FKs and happy paths where SQL risk is high. **(then)** **realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes. --- @@ -205,6 +219,7 @@ Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POS | Date | Change | |------|--------| +| 2026-04-27 | **More real Postgres tests:** `account-flows.integration.test.ts` — **2FA recovery-code** login + one-time use + `decryptRecoveryCodes` count; **replay** of first refresh JWT after two rotations (**401**); **`loggedIn`** / missing refresh guards. `template-db.test.ts` — **`group_members`** FK to `users` and to `groups`. PLAN_PROGRESS: run commands, expanded tables, matrix + success criteria + working order. | | 2026-04-27 | **Real Postgres tests (session + db):** `account-flows.integration.test.ts` — login + double refresh, wrong password, demo **403**, **2FA** (enable/finish + TOTP login, wrong finish code, MFA required, bad TOTP). `template-db.test.ts` — orphan **`sessions`**, **`devices`**, **`pages`→`groups`** FK rejects. PLAN_PROGRESS: Phase 3 detail table, package matrix, success criteria, working order. | | 2026-04-27 | **2FA account HTTP:** `user-two-factor-settings.ts`, `encryptUserAuthenticatorSecret` in `session-crypto`, Zod + OpenAPI + Hono for `/api/users/me/2fa/*` (6 routes); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — `load` is POST not GET; see [2FA HTTP routes](#2fa-http-routes-phase-3) below. | | 2026-04-27 | **Integration tests:** expanded `account-flows.integration.test.ts` (email wrong code; password change PHC + keyring unwrap with salt from PHC; `sessions` invalidation; wrong old password). Renamed from `email-change.integration.test.ts`. PLAN_PROGRESS: detailed Phase 3 test table + matrix gaps. | diff --git a/new-deepnotes/packages/db/src/template-db.test.ts b/new-deepnotes/packages/db/src/template-db.test.ts index 26fd296d..f466c90e 100644 --- a/new-deepnotes/packages/db/src/template-db.test.ts +++ b/new-deepnotes/packages/db/src/template-db.test.ts @@ -8,7 +8,7 @@ import postgres from "postgres"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import * as schema from "./schema.js"; -import { devices, pages, sessions, users } from "./schema.js"; +import { devices, groupMembers, pages, sessions, users } from "./schema.js"; import { withDatabaseName } from "./test/db-url.js"; import { createDatabaseFromTemplate, @@ -159,6 +159,87 @@ if (ctx == null) { } }); + it("rejects group_members row with unknown user_id (FK to users)", async () => { + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + const bogusUser = "usr000000000000000000"; + const bogusGroup = "grp000000000000000000"; + try { + await expect( + db.insert(groupMembers).values({ + userId: bogusUser, + groupId: bogusGroup, + role: "member", + encryptedInternalKeyring: Buffer.alloc(1), + }), + ).rejects.toThrow(); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("rejects group_members row with unknown group_id (FK to groups)", async () => { + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + const uid = "012345678901234567890"; + try { + await db.insert(users).values({ + id: uid, + startingPageId: uid, + personalGroupId: uid, + publicKeyring: Buffer.alloc(1), + encryptedPrivateKeyring: Buffer.alloc(1), + encryptedSymmetricKeyring: Buffer.alloc(1), + encryptedDefaultArrow: Buffer.alloc(1), + encryptedDefaultNote: Buffer.alloc(1), + encryptedEmail: Buffer.alloc(1), + emailHash: Buffer.alloc(1), + encryptedRehashedLoginHash: Buffer.alloc(1), + }); + const unknownGroup = "grp999999999999999999"; + await expect( + db.insert(groupMembers).values({ + userId: uid, + groupId: unknownGroup, + role: "member", + encryptedInternalKeyring: Buffer.alloc(1), + }), + ).rejects.toThrow(); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + it("rejects page row with unknown group_id (FK to groups)", async () => { const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; const admin = postgres(adminUrl, { max: 1 }); diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index c82ed985..5cdb2281 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -38,6 +38,7 @@ import { getPasswordHashValues, } from "./crypto/index.js"; import { + decryptRecoveryCodes, derivePasswordValues, decryptUserRehashedLoginHash, ensureSodiumReady, @@ -664,6 +665,106 @@ describe.skipIf(resolveTemplateContext() == null)( new Uint8Array(after2!.encryptionKey), ), ).toBe(true); + + await expect( + performSessionRefresh({ + db, + env, + refreshCookie: refresh1, + loggedInCookie: loggedIn1, + }), + ).rejects.toMatchObject({ + status: 401, + code: "UNAUTHORIZED", + message: "Session was invalidated.", + }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("refresh rejects when loggedIn cookie is not true", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `rf-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + const loginOut = await performSessionLogin({ + db, + env, + body: { email, loginHash, rememberSession: false }, + clientIp: "203.0.113.60", + userAgent: "integration-test/refresh-cookie", + }); + const refresh = cookieValueFromSetCookieLines( + loginOut.cookieLines, + "refreshToken", + ); + await expect( + performSessionRefresh({ + db, + env, + refreshCookie: refresh, + loggedInCookie: "false", + }), + ).rejects.toMatchObject({ + status: 401, + message: "User not logged in.", + }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + + it("refresh rejects missing refresh token", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + await expect( + performSessionRefresh({ + db, + env, + refreshCookie: undefined, + loggedInCookie: "true", + }), + ).rejects.toMatchObject({ + status: 401, + message: "No refresh token received.", + }); } finally { await client.end({ timeout: 5 }); const admin2 = postgres(ctx.adminUrl, { max: 1 }); @@ -1022,5 +1123,98 @@ describe.skipIf(resolveTemplateContext() == null)( } } }); + + it("2FA login succeeds with recovery code; same code cannot be reused", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + const clientIp = "203.0.113.70"; + const userAgent = "integration-test/2fa-recovery"; + try { + const email = `2fr-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + const { secret } = await performUserTwoFactorEnableRequest({ + db, + env, + accessCookie: access, + loginHash, + }); + const { recoveryCodes } = await performUserTwoFactorEnableFinish({ + db, + env, + accessCookie: access, + loginHash, + authenticatorToken: authenticator.generate(secret), + }); + const firstCode = recoveryCodes[0]!; + expect(firstCode).toMatch(/^[0-9a-f]{32}$/); + + const loginOut = await performSessionLogin({ + db, + env, + body: { + email, + loginHash, + rememberSession: false, + recoveryCode: firstCode, + }, + clientIp, + userAgent, + }); + expect(typeof loginOut.json.sessionId).toBe("string"); + + const [uAfter] = await db + .select({ encRec: users.encryptedRecoveryCodes }) + .from(users) + .where(eq(users.id, reg.userId)); + const remaining = decryptRecoveryCodes( + new Uint8Array(uAfter!.encRec!), + env.USER_RECOVERY_CODES_ENCRYPTION_KEY, + ); + expect(remaining).toHaveLength(5); + + await expect( + performSessionLogin({ + db, + env, + body: { + email, + loginHash, + rememberSession: false, + recoveryCode: firstCode, + }, + clientIp: "198.51.100.71", + userAgent: `${userAgent}-replay`, + }), + ).rejects.toMatchObject({ + status: 401, + message: "Invalid recovery code.", + }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); }, ); From 317c48210d448e7877b520ddb6a8c76f9a7ff6c3 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:37:18 -0300 Subject: [PATCH 044/243] feat(new-deepnotes): group pages, permissions, and list-user-groups API --- new-deepnotes/PLAN_PROGRESS.md | 34 ++- .../apps/api-worker/src/index.test.ts | 9 + new-deepnotes/apps/api-worker/src/index.ts | 156 +++++++++++++ new-deepnotes/docs/TRPC_REST_MAP.md | 6 +- new-deepnotes/packages/api/src/index.ts | 8 + .../packages/api/src/openapi.test.ts | 3 + new-deepnotes/packages/api/src/openapi.ts | 103 ++++++++ .../packages/api/src/schemas/pages-groups.ts | 66 ++++++ .../src/account-flows.integration.test.ts | 97 +++++++- .../packages/session/src/group-pages.ts | 221 ++++++++++++++++++ .../packages/session/src/group-permissions.ts | 56 +++++ new-deepnotes/packages/session/src/index.ts | 6 + .../packages/session/src/user-group-ids.ts | 26 +++ 13 files changed, 778 insertions(+), 13 deletions(-) create mode 100644 new-deepnotes/packages/api/src/schemas/pages-groups.ts create mode 100644 new-deepnotes/packages/session/src/group-pages.ts create mode 100644 new-deepnotes/packages/session/src/group-permissions.ts create mode 100644 new-deepnotes/packages/session/src/user-group-ids.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index cbe8c99e..34aeccb2 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Account surface includes **2FA** (`/api/users/me/2fa/...`); see [2FA HTTP routes](#2fa-http-routes-phase-3). **Next (priority):** pages + groups CRUD (per [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md)) → realtime/collab → Stripe webhook. | +| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped (2026-04-27):** first **pages/groups** vertical — `GET /api/users/me/groups`, `GET/POST /api/groups/:groupId/pages` (list + create; see [Pages/groups REST — slice 1](#pagesgroups-rest--slice-1)). **Still ahead:** user page prefs, remaining `groupsRouter` / `pagesRouter` rows, WS→REST parity, realtime/collab, Stripe. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP). `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP) / **groups + pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage` + unknown group **404**). `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) @@ -73,6 +73,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **2FA login, bad TOTP** | `authenticatorToken: "111111"` | **401** “Invalid authenticator token.” | | **2FA login with recovery code** | `performUserTwoFactorEnableFinish` → `performSessionLogin` with `recoveryCode` (no TOTP) | **200**-equivalent (`sessionId`); `decryptRecoveryCodes` on row shows **5** hashes left (one consumed). | | **2FA recovery code reuse** | Second `performSessionLogin` with same plaintext recovery code, new IP/UA | **401** “Invalid recovery code.” | +| **Groups + pages (personal)** | `performUserRegister` then `performGetUserGroupIds` / `performListGroupPages` / `performCreatePage` (second page, `parentPageId` = initial page) | `groupIds` = `[personalGroupId]`; list returns initial `pageId`; create returns `numFreePages` **1** for default `plan`; `users.num_free_pages` = 1; two rows in `pages` for group; unknown `groupId` list → **404** `NOT_FOUND`. | **`@deepnotes/db` real Postgres (`template-db.test.ts`):** @@ -123,9 +124,23 @@ CI should set the same vars against the workflow Postgres service (role with `CR - **Parity:** Demo accounts **403**; wrong password **400** “Password is incorrect.”; TOTP fail on finish **400** “Authenticator token is incorrect.”; `otplib` `keyuri` issuer **“DeepNotes”** (same as legacy tRPC). Recovery codes: `libsodium` hex + [hashRecoveryCode / encryptRecoveryCodes](packages/session/src/crypto/session-crypto.ts) (legacy-equivalent). Forget devices: `UPDATE devices SET trusted = false` for `user_id`. +### Pages/groups REST — slice 1 + +**Goal:** unblock client “list my groups → list pages → create page” without tRPC. + +| Layer | What shipped | +|-------|----------------| +| **`@deepnotes/session`** | `group-permissions.ts` — `userHasGroupPermission` (roles `owner`/`admin`/`moderator`/`member`/`viewer` vs legacy `@deeplib/misc`; **public** group = `groups.access_keyring` not null grants `viewGroupPages` without membership). `user-group-ids.ts` — `performGetUserGroupIds`. `group-pages.ts` — `performListGroupPages` (cursor `lastPageId`, excludes `permanent_deletion_date` set), `performCreatePage` (parent page must belong to group; **Pro** required when `groupId !== personalGroupId`; **50 free pages** for `plan !== 'pro'`; bumps `group_members.last_activity_date`). | +| **`@deepnotes/api`** | `schemas/pages-groups.ts` — path/query/body/response Zod + OpenAPI; wired in `openapi.ts`. | +| **`@deepnotes/api-worker`** | Hono: `GET /api/users/me/groups`, `GET /api/groups/:groupId/pages`, `POST /api/groups/:groupId/pages` (**201** on create). | + +**Intentional gaps (next slices):** `pages.create` optional **`groupCreation`** (new non-personal group + first page) not exposed yet; no Redis locks (legacy redlock); **`GET /api/groups/:groupId/pages`** is **auth-only** (legacy allowed optional auth for public read — can add later). + ### Not started (Phase 3 — pages, groups, infra) -- [ ] **Pages** (user prefs + CRUD) and **groups** CRUD / privacy / passwords per map. +- [ ] **`users.pages`** prefs: notifications, starting page, path, recent/favorites, encrypted defaults (`GET/PATCH` per [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md)). +- [ ] **Remaining `groupsRouter`:** `main-page`, `members`, password, privacy, soft delete / restore / purge. +- [ ] **Remaining `pagesRouter`:** bump, backlinks, snapshots, deletion, move (plus **`groupCreation`** on create if still required for parity). - [ ] **Realtime / collab** (new or adapted protocols; no key rotation). - [ ] **Stripe:** `POST /api/webhooks/stripe`, checkout/portal (no RevenueCat); wire **`deleteStripeCustomer`** from account delete when keys exist. @@ -176,9 +191,9 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (16 cases) — email change, password change (PHC + unwrap, session invalidation, wrong password, **demo 403**), **`performSessionLogin`** + **double refresh** + **stale refresh JWT replay**, **refresh** `loggedIn`/missing-token guards, **2FA** (TOTP paths + **recovery code** login + one-time use + `decryptRecoveryCodes` count); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; optional explicit test for decode-without-verify invalidation path | -| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA paths); **`schemas/users.test.ts`** (email/password change, 2fa finish) | Schemas for pages/groups when they land; optional OpenAPI **snapshot** | -| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: 503 when env missing — includes **2FA** routes in matrix | **200** tests with stub `SessionEnv` + template DB (heavier) | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**17** cases when DB env set) — … + **groups/pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage`, unknown group **404**); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; **Pro-only** create in non-personal group; **viewer** cannot create; optional `groupCreation` on create | +| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA + **me/groups** + **groups/{id}/pages**); **`schemas/users.test.ts`** (email/password change, 2fa finish); **`schemas/pages-groups.ts`** | Optional OpenAPI **snapshot**; Zod tests for `pages-groups` query/body edge cases | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: 503 when env missing — includes **2FA** + **groups/pages** routes in matrix | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -199,8 +214,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Drizzle migrations from empty DB documented for production upgrades. - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). -- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation, login + refresh + **stale JWT replay**, refresh **cookie guards**, **2FA** TOTP + **recovery codes**). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** as in [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) (16 session + 6 db integration cases when DB env is set). **Next:** Redis failed-login integration; refresh **expired** JWT; Stripe when billing exists. +- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation, login + refresh + **stale JWT replay**, refresh **cookie guards**, **2FA** TOTP + **recovery codes**, **groups list + page list + page create**). +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** as in [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) (**17** session + 6 db integration cases when DB env is set). **Next:** Redis failed-login integration; refresh **expired** JWT; more **pages/groups** routes; Stripe when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -211,7 +226,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Phase 3 working order (suggested) -Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POST, see map); **Postgres** for 2FA (TOTP + recovery codes + refresh replay/cookie guards) + **`@deepnotes/db`** FKs through **`group_members`**. **(next)** `users.pages` + `groups` + `pages` REST from [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — each slice should add **template DB** tests for new FKs and happy paths where SQL risk is high. **(then)** **realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes. +Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2FA + refresh guards; **`@deepnotes/db`** FKs through **`group_members`**. **(done 2026-04-27)** First **pages/groups** slice: `GET /api/users/me/groups`, `GET/POST /api/groups/:groupId/pages` + session services + integration test. **(next)** Remaining **`users.pages`** prefs; **`groupsRouter`** (members, main page, password, privacy, delete/restore/purge); **`pagesRouter`** (bump, backlinks, snapshots, delete, move; optional **`groupCreation`** on create); each slice + **template DB** tests where SQL risk is high. **(then)** **realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes. --- @@ -219,6 +234,7 @@ Use this when resuming: **(done)** account HTTP through 2FA (incl. `load` as POS | Date | Change | |------|--------| +| 2026-04-27 | **Phase 3 — pages/groups slice 1:** `performGetUserGroupIds`, `performListGroupPages`, `performCreatePage` + `group-permissions.ts`; OpenAPI + Zod `pages-groups.ts`; api-worker `GET /api/users/me/groups`, `GET/POST /api/groups/:groupId/pages` (**201** create); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) marked implemented for `getGroupIds`, `getPages`, `pages.create`; integration test **groups + pages**; PLAN_PROGRESS [Pages/groups REST — slice 1](#pagesgroups-rest--slice-1) + matrix bumps. | | 2026-04-27 | **More real Postgres tests:** `account-flows.integration.test.ts` — **2FA recovery-code** login + one-time use + `decryptRecoveryCodes` count; **replay** of first refresh JWT after two rotations (**401**); **`loggedIn`** / missing refresh guards. `template-db.test.ts` — **`group_members`** FK to `users` and to `groups`. PLAN_PROGRESS: run commands, expanded tables, matrix + success criteria + working order. | | 2026-04-27 | **Real Postgres tests (session + db):** `account-flows.integration.test.ts` — login + double refresh, wrong password, demo **403**, **2FA** (enable/finish + TOTP login, wrong finish code, MFA required, bad TOTP). `template-db.test.ts` — orphan **`sessions`**, **`devices`**, **`pages`→`groups`** FK rejects. PLAN_PROGRESS: Phase 3 detail table, package matrix, success criteria, working order. | | 2026-04-27 | **2FA account HTTP:** `user-two-factor-settings.ts`, `encryptUserAuthenticatorSecret` in `session-crypto`, Zod + OpenAPI + Hono for `/api/users/me/2fa/*` (6 routes); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) — `load` is POST not GET; see [2FA HTTP routes](#2fa-http-routes-phase-3) below. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 0bbf06f7..3f166419 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -27,6 +27,15 @@ describe("api-worker", () => { ["POST", "/api/sessions/refresh"], ["POST", "/api/sessions/logout"], ["POST", "/api/sessions/demo"], + ["GET", "/api/users/me/groups"], + [ + "GET", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/pages", + ], + [ + "POST", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/pages", + ], ["GET", "/api/users/me"], ["POST", "/api/users/me/password"], ["DELETE", "/api/users/me"], diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 0b5c1ec4..ada704f9 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -2,6 +2,8 @@ import { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, getOpenApiDocument, + groupPageCreateRequestSchema, + groupPagesListQuerySchema, healthResponseSchema, sessionDemoRequestSchema, sessionLoginRequestSchema, @@ -599,6 +601,160 @@ app.delete("/api/users/me", async (c) => { } }); +app.get("/api/users/me/groups", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performGetUserGroupIds } = await import("@deepnotes/session"); + const out = await performGetUserGroupIds({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.get("/api/groups/:groupId/pages", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const groupId = c.req.param("groupId"); + const qParsed = groupPagesListQuerySchema.safeParse({ + lastPageId: c.req.query("lastPageId") ?? undefined, + }); + if (!qParsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: qParsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performListGroupPages } = await import("@deepnotes/session"); + const out = await performListGroupPages({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + lastPageId: qParsed.data.lastPageId, + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/groups/:groupId/pages", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = groupPageCreateRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performCreatePage } = await import("@deepnotes/session"); + const out = await performCreatePage({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + body: parsed.data, + }); + return c.json(out, 201); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.get("/api/users/me", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index bc7f70f9..904cc743 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -45,7 +45,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `users.pages.clearFavoritePages` | `POST /api/users/me/pages/favorites/clear` | | `users.pages.setEncryptedDefaultNote` | `PATCH /api/users/me/defaults/note` | | `users.pages.setEncryptedDefaultArrow` | `PATCH /api/users/me/defaults/arrow` | -| `users.pages.getGroupIds` | `GET /api/users/me/groups` | +| `users.pages.getGroupIds` | `GET /api/users/me/groups` (**implemented** — `performGetUserGroupIds` in `@deepnotes/session`) | ## Groups (`groupsRouter`) @@ -53,7 +53,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. |------------------|----------------------| | `groups.getMainPageId` | `GET /api/groups/:groupId/main-page` | | `groups.getUserIds` | `GET /api/groups/:groupId/members` (ids / minimal DTO) | -| `groups.getPages` | `GET /api/groups/:groupId/pages` | +| `groups.getPages` | `GET /api/groups/:groupId/pages` (**implemented** — `performListGroupPages`; query `lastPageId`; soft-deleted pages excluded) | | `groups.password.enable` | `POST /api/groups/:groupId/password` | | `groups.password.change` | `PATCH /api/groups/:groupId/password` | | `groups.password.disable` | `DELETE /api/groups/:groupId/password` | @@ -67,7 +67,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | Legacy procedure | Proposed REST / notes | |------------------|----------------------| -| `pages.create` | `POST /api/groups/:groupId/pages` | +| `pages.create` | `POST /api/groups/:groupId/pages` (**implemented** — `performCreatePage`; optional `groupCreation` not yet exposed; Pro + free-page rules per legacy) | | `pages.bump` | `POST /api/pages/:pageId/bump` | | `pages.backlinks.create` | `POST /api/pages/:pageId/backlinks` | | `pages.backlinks.delete` | `DELETE /api/pages/:pageId/backlinks/:targetPageId` | diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index a48d116c..593f181f 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -23,6 +23,14 @@ export { type SessionLoginRequest, type UserRegisterRequest, } from "./schemas/sessions.js"; +export { + groupIdPathSchema, + groupPageCreateRequestSchema, + groupPageCreateResponseSchema, + groupPagesListQuerySchema, + groupPagesListResponseSchema, + userGroupIdsResponseSchema, +} from "./schemas/pages-groups.js"; export { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 5a76b4f0..0b29983c 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -44,5 +44,8 @@ describe("getOpenApiDocument", () => { doc.paths?.["/api/users/me/2fa/devices/forget"]?.post, ).toBeDefined(); expect(doc.paths?.["/api/users/me/2fa/disable"]?.post).toBeDefined(); + expect(doc.paths?.["/api/users/me/groups"]?.get).toBeDefined(); + expect(doc.paths?.["/api/groups/{groupId}/pages"]?.get).toBeDefined(); + expect(doc.paths?.["/api/groups/{groupId}/pages"]?.post).toBeDefined(); }); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index c485b469..288f6a52 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -16,6 +16,14 @@ import { sessionLoginRequestSchema, userRegisterRequestSchema, } from "./schemas/sessions.js"; +import { + groupIdPathSchema, + groupPageCreateRequestSchema, + groupPageCreateResponseSchema, + groupPagesListQuerySchema, + groupPagesListResponseSchema, + userGroupIdsResponseSchema, +} from "./schemas/pages-groups.js"; import { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, @@ -202,6 +210,101 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "get", + path: "/api/users/me/groups", + summary: "List group IDs for the current user", + description: + "Replaces legacy `users.pages.getGroupIds`. Returns `group_id` values from `group_members` ordered by recent activity (desc).", + responses: { + 200: { + description: "Ordered group ids.", + content: { + "application/json": { + schema: userGroupIdsResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "get", + path: "/api/groups/{groupId}/pages", + summary: "List page IDs in a group", + description: + "Replaces legacy `groups.getPages` (authenticated). Optional `lastPageId` cursor for pagination (newest `last_activity_date` first). Omits soft-deleted pages (`permanent_deletion_date` set). Public groups allow `viewGroupPages` without membership.", + request: { + params: groupIdPathSchema, + query: groupPagesListQuerySchema, + }, + responses: { + 200: { + description: "Page id window (max 20) and `hasMore`.", + content: { + "application/json": { + schema: groupPagesListResponseSchema, + }, + }, + }, + 400: { + description: "Invalid `lastPageId` (not in group).", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/pages", + summary: "Create a page in a group", + description: + "Replaces legacy `pages.create` for an existing group (optional `groupCreation` path not yet exposed). Enforces `editGroupPages`, Pro subscription when `groupId` is not the user’s personal group, and the 50 free-page cap for non‑Pro users.", + request: { + params: groupIdPathSchema, + body: { + content: { + "application/json": { + schema: groupPageCreateRequestSchema, + }, + }, + }, + }, + responses: { + 201: { + description: "Page and `users_pages` row created.", + content: { + "application/json": { + schema: groupPageCreateResponseSchema, + }, + }, + }, + 400: { + description: "Invalid parent page or body.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "post", path: "/api/users/me/password", diff --git a/new-deepnotes/packages/api/src/schemas/pages-groups.ts b/new-deepnotes/packages/api/src/schemas/pages-groups.ts new file mode 100644 index 00000000..dc799f89 --- /dev/null +++ b/new-deepnotes/packages/api/src/schemas/pages-groups.ts @@ -0,0 +1,66 @@ +import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; +import { z } from "zod"; + +import { byteB64 } from "./sessions.js"; + +extendZodWithOpenApi(z); + +export const nanoidIdOpenapi = { + description: "21-character nanoid (URL-safe alphabet).", + example: "V1StGXR8_Z5jdHi6B-myT", +} as const; + +export const groupIdPathSchema = z.object({ + groupId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi({ ...nanoidIdOpenapi, param: { name: "groupId", in: "path" } }), +}); + +export const userGroupIdsResponseSchema = z + .object({ + groupIds: z.array(z.string()), + }) + .openapi("UserGroupIdsResponse"); + +export const groupPagesListResponseSchema = z + .object({ + pageIds: z.array(z.string()), + hasMore: z.boolean(), + }) + .openapi("GroupPagesListResponse"); + +export const groupPagesListQuerySchema = z.object({ + lastPageId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .optional() + .openapi({ + description: + "Pagination cursor: return pages older than this page's activity (legacy `lastPageId`).", + param: { name: "lastPageId", in: "query" }, + }), +}); + +export const groupPageCreateRequestSchema = z + .object({ + parentPageId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi(nanoidIdOpenapi), + pageId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi(nanoidIdOpenapi), + pageEncryptedSymmetricKeyring: byteB64, + pageEncryptedRelativeTitle: byteB64, + pageEncryptedAbsoluteTitle: byteB64, + }) + .openapi("GroupPageCreateRequest"); + +export const groupPageCreateResponseSchema = z + .object({ + pageId: z.string(), + numFreePages: z.number().int().optional(), + }) + .openapi("GroupPageCreateResponse"); diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index 5cdb2281..4e89ec66 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -19,7 +19,7 @@ import { type TemplateDbContext, } from "@deepnotes/db/testing/template-db"; import * as schema from "@deepnotes/db/schema"; -import { devices, sessions, users } from "@deepnotes/db/schema"; +import { devices, pages, sessions, users } from "@deepnotes/db/schema"; import { performUserPasswordChange } from "./change-user-password.js"; import { @@ -44,6 +44,11 @@ import { ensureSodiumReady, } from "./crypto/session-crypto.js"; import type { UserRegisterInput } from "./register-user.js"; +import { + performCreatePage, + performListGroupPages, +} from "./group-pages.js"; +import { performGetUserGroupIds } from "./user-group-ids.js"; import { performUserRegister } from "./register-user.js"; import { decryptUserEmail } from "./encrypt-user-email.js"; import { hashUserEmail } from "./email-hash.js"; @@ -1216,5 +1221,95 @@ describe.skipIf(resolveTemplateContext() == null)( } } }); + + it("groups: get ids, list pages, create second page in personal group", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `grp-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + + const { groupIds } = await performGetUserGroupIds({ + db, + env, + accessCookie: access, + }); + expect(groupIds).toEqual([reg.groupId]); + + const listed = await performListGroupPages({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + }); + expect(listed.hasMore).toBe(false); + expect(listed.pageIds).toEqual([reg.pageId]); + + const newPageId = nanoid(); + const out = await performCreatePage({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + body: { + parentPageId: reg.pageId, + pageId: newPageId, + pageEncryptedSymmetricKeyring: rand32(), + pageEncryptedRelativeTitle: rand32(), + pageEncryptedAbsoluteTitle: rand32(), + }, + }); + expect(out.pageId).toBe(newPageId); + expect(out.numFreePages).toBe(1); + + const [urow] = await db + .select({ n: users.numFreePages }) + .from(users) + .where(eq(users.id, reg.userId)); + expect(urow?.n).toBe(1); + + const ids = await db + .select({ id: pages.id }) + .from(pages) + .where(eq(pages.groupId, reg.groupId)); + expect(ids.map((r) => r.id).sort()).toEqual( + [reg.pageId, newPageId].sort(), + ); + + await expect( + performListGroupPages({ + db, + env, + accessCookie: access, + groupId: "nononononononononono1", + }), + ).rejects.toMatchObject({ status: 404, code: "NOT_FOUND" }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); }, ); diff --git a/new-deepnotes/packages/session/src/group-pages.ts b/new-deepnotes/packages/session/src/group-pages.ts new file mode 100644 index 00000000..91409fb7 --- /dev/null +++ b/new-deepnotes/packages/session/src/group-pages.ts @@ -0,0 +1,221 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { groupMembers, groups, pages, users, usersPages } from "@deepnotes/db/schema"; +import { and, desc, eq, isNull, lt } from "drizzle-orm"; + +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { userHasGroupPermission } from "./group-permissions.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; + +function toBuf(u: Uint8Array): Buffer { + return Buffer.from(u); +} + +/** + * Replaces legacy `groups.getPages` (authenticated): page IDs in the group, + * newest activity first, optional cursor `lastPageId`. + */ +export async function performListGroupPages(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + lastPageId?: string | undefined; +}): Promise<{ pageIds: string[]; hasMore: boolean }> { + const { userId } = await getAuthenticatedUserSummary(input); + + const [groupRow] = await input.db + .select({ id: groups.id }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + + if (groupRow == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + + const allowed = await userHasGroupPermission({ + db: input.db, + userId, + groupId: input.groupId, + permission: "viewGroupPages", + }); + if (!allowed) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + let cursorActivity: string | undefined; + if (input.lastPageId != null && input.lastPageId.length > 0) { + const [p] = await input.db + .select({ lastActivityDate: pages.lastActivityDate }) + .from(pages) + .where( + and(eq(pages.id, input.lastPageId), eq(pages.groupId, input.groupId)), + ) + .limit(1); + if (p == null) { + throw new SessionError(400, "BAD_REQUEST", "lastPageId not in group."); + } + cursorActivity = p.lastActivityDate; + } + + const whereClause = + cursorActivity != null + ? and( + eq(pages.groupId, input.groupId), + isNull(pages.permanentDeletionDate), + lt(pages.lastActivityDate, cursorActivity), + ) + : and( + eq(pages.groupId, input.groupId), + isNull(pages.permanentDeletionDate), + ); + + const rows = await input.db + .select({ id: pages.id }) + .from(pages) + .where(whereClause) + .orderBy(desc(pages.lastActivityDate)) + .limit(21); + + const hasMore = rows.length > 20; + const pageIds = hasMore ? rows.slice(0, 20).map((r) => r.id) : rows.map((r) => r.id); + + return { pageIds, hasMore }; +} + +export type CreatePageBody = { + parentPageId: string; + pageId: string; + pageEncryptedSymmetricKeyring: Uint8Array; + pageEncryptedRelativeTitle: Uint8Array; + pageEncryptedAbsoluteTitle: Uint8Array; +}; + +/** + * Replaces legacy `pages.create` without optional `groupCreation` (new shared + * group + first page). Pro subscription and free-page limits match legacy. + */ +export async function performCreatePage(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + body: CreatePageBody; +}): Promise<{ pageId: string; numFreePages?: number }> { + const { userId, personalGroupId } = await getAuthenticatedUserSummary(input); + + const [parent] = await input.db + .select({ id: pages.id, groupId: pages.groupId }) + .from(pages) + .where(eq(pages.id, input.body.parentPageId)) + .limit(1); + + if (parent == null || parent.groupId !== input.groupId) { + throw new SessionError( + 400, + "BAD_REQUEST", + "parentPageId must refer to a page in this group.", + ); + } + + const [groupRow] = await input.db + .select({ id: groups.id }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + + if (groupRow == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + + const canEdit = await userHasGroupPermission({ + db: input.db, + userId, + groupId: input.groupId, + permission: "editGroupPages", + }); + if (!canEdit) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const mustSubscribe = + input.groupId !== personalGroupId; + if (mustSubscribe) { + const [u] = await input.db + .select({ plan: users.plan }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + if (u?.plan !== "pro") { + throw new SessionError( + 403, + "FORBIDDEN", + "This action requires a Pro plan subscription.", + ); + } + } + + return await input.db.transaction(async (tx) => { + const [urow] = await tx + .select({ + plan: users.plan, + numFreePages: users.numFreePages, + }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (urow == null) { + throw new SessionError(401, "UNAUTHORIZED", "User not found."); + } + + let numFreePagesOut: number | undefined; + + if (urow.plan !== "pro") { + const next = urow.numFreePages + 1; + if (next > 50) { + throw new SessionError( + 403, + "FORBIDDEN", + "You have reached your limit of 50 free pages.", + ); + } + await tx + .update(users) + .set({ numFreePages: next }) + .where(eq(users.id, userId)); + numFreePagesOut = next; + } + + await tx.insert(pages).values({ + id: input.body.pageId, + groupId: input.groupId, + encryptedSymmetricKeyring: toBuf(input.body.pageEncryptedSymmetricKeyring), + encryptedRelativeTitle: toBuf(input.body.pageEncryptedRelativeTitle), + encryptedAbsoluteTitle: toBuf(input.body.pageEncryptedAbsoluteTitle), + free: urow.plan !== "pro", + }); + + await tx.insert(usersPages).values({ + userId, + pageId: input.body.pageId, + lastParentId: input.body.parentPageId, + }); + + await tx + .update(groupMembers) + .set({ lastActivityDate: new Date().toISOString() }) + .where( + and( + eq(groupMembers.groupId, input.groupId), + eq(groupMembers.userId, userId), + ), + ); + + return { + pageId: input.body.pageId, + ...(numFreePagesOut != null ? { numFreePages: numFreePagesOut } : {}), + }; + }); +} diff --git a/new-deepnotes/packages/session/src/group-permissions.ts b/new-deepnotes/packages/session/src/group-permissions.ts new file mode 100644 index 00000000..eabb04de --- /dev/null +++ b/new-deepnotes/packages/session/src/group-permissions.ts @@ -0,0 +1,56 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { groupMembers, groups } from "@deepnotes/db/schema"; +import { and, eq } from "drizzle-orm"; + +/** Mirrors legacy `@deeplib/misc` roles for `group_members.role` text. */ +const ROLE_PERMISSIONS: Record< + string, + { viewGroupPages: boolean; editGroupPages: boolean } +> = { + owner: { viewGroupPages: true, editGroupPages: true }, + admin: { viewGroupPages: true, editGroupPages: true }, + moderator: { viewGroupPages: true, editGroupPages: true }, + member: { viewGroupPages: true, editGroupPages: true }, + viewer: { viewGroupPages: true, editGroupPages: false }, +}; + +export type GroupPagePermission = "viewGroupPages" | "editGroupPages"; + +export async function userHasGroupPermission(input: { + db: DeepnotesDb; + userId: string; + groupId: string; + permission: GroupPagePermission; +}): Promise { + const [group] = await input.db + .select({ accessKeyring: groups.accessKeyring }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + + if (group == null) { + return false; + } + + const [member] = await input.db + .select({ role: groupMembers.role }) + .from(groupMembers) + .where( + and( + eq(groupMembers.groupId, input.groupId), + eq(groupMembers.userId, input.userId), + ), + ) + .limit(1); + + if (member != null) { + const perms = ROLE_PERMISSIONS[member.role]; + if (perms?.[input.permission]) { + return true; + } + } + + return ( + group.accessKeyring != null && input.permission === "viewGroupPages" + ); +} diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 0c17a865..c269ba94 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -26,6 +26,12 @@ export { } from "./email-verification.js"; export { getAuthenticatedUserSummary } from "./user-me.js"; export type { AuthenticatedUserSummary } from "./user-me.js"; +export { + performCreatePage, + performListGroupPages, +} from "./group-pages.js"; +export type { CreatePageBody } from "./group-pages.js"; +export { performGetUserGroupIds } from "./user-group-ids.js"; export { performUserTwoFactorDisable, performUserTwoFactorEnableFinish, diff --git a/new-deepnotes/packages/session/src/user-group-ids.ts b/new-deepnotes/packages/session/src/user-group-ids.ts new file mode 100644 index 00000000..e2ee7207 --- /dev/null +++ b/new-deepnotes/packages/session/src/user-group-ids.ts @@ -0,0 +1,26 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { groupMembers } from "@deepnotes/db/schema"; +import { desc, eq } from "drizzle-orm"; + +import type { SessionEnv } from "./env.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; + +/** + * Replaces legacy `users.pages.getGroupIds`: group IDs the user belongs to, + * ordered by `group_members.last_activity_date` descending. + */ +export async function performGetUserGroupIds(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; +}): Promise<{ groupIds: string[] }> { + const { userId } = await getAuthenticatedUserSummary(input); + + const rows = await input.db + .select({ groupId: groupMembers.groupId }) + .from(groupMembers) + .where(eq(groupMembers.userId, userId)) + .orderBy(desc(groupMembers.lastActivityDate)); + + return { groupIds: rows.map((r) => r.groupId) }; +} From c2a76115af4aaecb995910e8e66d10359f474682 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:48:36 -0300 Subject: [PATCH 045/243] feat(new-deepnotes): user page preferences and favorite page IDs --- new-deepnotes/PLAN_PROGRESS.md | 38 +- .../apps/api-worker/src/index.test.ts | 14 + new-deepnotes/apps/api-worker/src/index.ts | 523 ++++++ new-deepnotes/docs/TRPC_REST_MAP.md | 22 +- new-deepnotes/packages/api/src/index.ts | 11 + .../packages/api/src/openapi.test.ts | 11 + new-deepnotes/packages/api/src/openapi.ts | 279 ++++ .../packages/api/src/schemas/user-pages.ts | 80 + .../db/migrations/0001_favorite_page_ids.sql | 1 + .../db/migrations/meta/0001_snapshot.json | 1406 +++++++++++++++++ .../packages/db/migrations/meta/_journal.json | 9 +- new-deepnotes/packages/db/src/schema.ts | 5 + .../src/account-flows.integration.test.ts | 215 ++- new-deepnotes/packages/session/src/index.ts | 14 + .../packages/session/src/user-page-prefs.ts | 420 +++++ 15 files changed, 3024 insertions(+), 24 deletions(-) create mode 100644 new-deepnotes/packages/api/src/schemas/user-pages.ts create mode 100644 new-deepnotes/packages/db/migrations/0001_favorite_page_ids.sql create mode 100644 new-deepnotes/packages/db/migrations/meta/0001_snapshot.json create mode 100644 new-deepnotes/packages/session/src/user-page-prefs.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 34aeccb2..ffc23210 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped (2026-04-27):** first **pages/groups** vertical — `GET /api/users/me/groups`, `GET/POST /api/groups/:groupId/pages` (list + create; see [Pages/groups REST — slice 1](#pagesgroups-rest--slice-1)). **Still ahead:** user page prefs, remaining `groupsRouter` / `pagesRouter` rows, WS→REST parity, realtime/collab, Stripe. | +| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** [pages/groups slice 1](#pagesgroups-rest--slice-1); **[users.pages prefs slice 2](#userspages-rest--slice-2)** (starting/path, recent/favorites, encrypted defaults, notifications + migration `favorite_page_ids`). **Still ahead:** remaining `groupsRouter` / `pagesRouter`, WS→REST parity, realtime/collab, Stripe. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP) / **groups + pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage` + unknown group **404**). `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP) / **groups + pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage` + unknown group **404**) / **user page prefs** ([slice 2](#userspages-rest--slice-2): starting + path + favorites + recent remove/clear + default note PATCH + notifications load + mark read). `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) @@ -46,7 +46,7 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` **How to run locally:** ensure `.env` at `new-deepnotes/.env` has `DATABASE_URL` and (for template create/drop) `DATABASE_ADMIN_URL` with a role that can `CREATE DATABASE`. Then: -- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` +- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**18** cases when DB env set, including prefs slice) - `pnpm --filter @deepnotes/db exec vitest run src/template-db.test.ts` CI should set the same vars against the workflow Postgres service (role with `CREATEDB`). @@ -74,6 +74,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **2FA login with recovery code** | `performUserTwoFactorEnableFinish` → `performSessionLogin` with `recoveryCode` (no TOTP) | **200**-equivalent (`sessionId`); `decryptRecoveryCodes` on row shows **5** hashes left (one consumed). | | **2FA recovery code reuse** | Second `performSessionLogin` with same plaintext recovery code, new IP/UA | **401** “Invalid recovery code.” | | **Groups + pages (personal)** | `performUserRegister` then `performGetUserGroupIds` / `performListGroupPages` / `performCreatePage` (second page, `parentPageId` = initial page) | `groupIds` = `[personalGroupId]`; list returns initial `pageId`; create returns `numFreePages` **1** for default `plan`; `users.num_free_pages` = 1; two rows in `pages` for group; unknown `groupId` list → **404** `NOT_FOUND`. | +| **User page prefs** | Register + access JWT; `performGetStartingPageId`; `performGetCurrentPath` (root page + child); unknown page **404**; `performAddFavoritePages` / `performRemoveFavoritePages` (order on `users.favorite_page_ids`); `performRemoveRecentPages` bogus id **404** / missing child in recent **404** / remove root then `recent` empty + `performClearRecentPages`; `performPatchDefaultNote`; insert `notifications` + `users_notifications` → `performLoadNotifications` (base64 ciphertext) + `performMarkNotificationsRead` → `users.last_notification_read` | Matches legacy semantics where tested; favorites column from migration `0001_favorite_page_ids`. | **`@deepnotes/db` real Postgres (`template-db.test.ts`):** @@ -103,7 +104,9 @@ CI should set the same vars against the workflow Postgres service (role with `CR - [x] **`DELETE /api/users/me`** — replaces legacy `users.account.delete`: JSON `{ "loginHash" }` (base64); verifies access JWT + password (`encrypted_rehashed_login_hash`); blocks when any membership has `member_count > 1` and `owner_count <= 1`; deletes join invites/requests, solo-member groups (cascade pages), remaining `group_members`, then user row; clears session cookies; optional `deleteStripeCustomer(customerId)` hook (worker can wire Stripe later; failures swallowed like legacy). - [x] **`POST /api/users/me/password`** — replaces legacy WS `change-password` (two RPC steps → one REST call after client re-wraps keyrings). **`performUserPasswordChange`** (`packages/session/src/change-user-password.ts`): body `oldLoginHash`, `newLoginHash`, `userEncryptedPrivateKeyring`, `userEncryptedSymmetricKeyring` (base64, same semantics as `POST /api/users`); verifies current password; **403** if `users.demo === true`; updates `encrypted_rehashed_login_hash`, `encrypted_private_keyring`, `encrypted_symmetric_keyring`; sets **`sessions.invalidated`** for all user sessions; **204** + **`buildClearSessionCookies`**. Contract: `userPasswordChangeRequestSchema` in `@deepnotes/api`; map in [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). -### Account routes still to ship (Phase 3) +### Account routes — detail (Phase 3) + +- [x] **`users.pages` prefs (HTTP)** — [Users/pages REST — slice 2](#userspages-rest--slice-2); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) `users.pages` rows. - [x] **Email change** - [x] `POST /api/users/me/email-change` — `performUserEmailChangeRequest`: `oldLoginHash` + `newEmail`; **403** demo, **400** bad password or “email already in use” (global `email_hash` match, same as legacy); sets `encrypted_new_email` + 6-digit `email_verification_code`; Resend (subject/body like legacy) or **200** `{ "emailVerificationCode" }` when `SEND_EMAILS=false`; **204** when emailed. @@ -136,9 +139,21 @@ CI should set the same vars against the workflow Postgres service (role with `CR **Intentional gaps (next slices):** `pages.create` optional **`groupCreation`** (new non-personal group + first page) not exposed yet; no Redis locks (legacy redlock); **`GET /api/groups/:groupId/pages`** is **auth-only** (legacy allowed optional auth for public read — can add later). +### Users/pages REST — slice 2 + +**Goal:** ship legacy `users.pages` *prefs* surface on REST + Drizzle so the SPA can manage recents, favorites, breadcrumbs, defaults, and notifications without tRPC/KeyDB. + +| Layer | What shipped | +|-------|----------------| +| **`@deepnotes/db`** | Migration **`0001_favorite_page_ids`**: `users.favorite_page_ids char(21)[] NOT NULL DEFAULT '{}'`. Legacy stored favorites only in KeyDB; greenfield persists them in Postgres (RESTART_PLAN: portable Redis, no `DataAbstraction` mirror for this field). | +| **`@deepnotes/session`** | `user-page-prefs.ts`: `performGetStartingPageId`, `performGetCurrentPath` (`users_pages.last_parent_id` walk + one-shot repair like legacy KeyDB), `performRemoveRecentPages` / `performClearRecentPages`, `performAddFavoritePages` / `performRemoveFavoritePages` / `performClearFavoritePages`, `performPatchDefaultNote` / `performPatchDefaultArrow`, `performLoadNotifications`, `performMarkNotificationsRead`. | +| **`@deepnotes/api`** | `schemas/user-pages.ts` — Zod + OpenAPI for query/body/response shapes; wired in `openapi.ts`. | +| **`@deepnotes/api-worker`** | Hono routes: `GET …/me/pages/starting`, `GET …/me/pages/path`, `POST …/pages/recent/remove|clear`, `POST …/pages/favorites|…/remove|…/clear`, `PATCH …/me/defaults/note|arrow`, `GET …/me/notifications`, `POST …/me/notifications/read`. | + +**Cutover note:** Existing production users who had favorites only in KeyDB will see an **empty** `favorite_page_ids` after migration until a one-off backfill is run (if ever needed); new installs and new favorites use Postgres only. + ### Not started (Phase 3 — pages, groups, infra) -- [ ] **`users.pages`** prefs: notifications, starting page, path, recent/favorites, encrypted defaults (`GET/PATCH` per [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md)). - [ ] **Remaining `groupsRouter`:** `main-page`, `members`, password, privacy, soft delete / restore / purge. - [ ] **Remaining `pagesRouter`:** bump, backlinks, snapshots, deletion, move (plus **`groupCreation`** on create if still required for parity). - [ ] **Realtime / collab** (new or adapted protocols; no key rotation). @@ -191,9 +206,9 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**17** cases when DB env set) — … + **groups/pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage`, unknown group **404**); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; **Pro-only** create in non-personal group; **viewer** cannot create; optional `groupCreation` on create | -| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA + **me/groups** + **groups/{id}/pages**); **`schemas/users.test.ts`** (email/password change, 2fa finish); **`schemas/pages-groups.ts`** | Optional OpenAPI **snapshot**; Zod tests for `pages-groups` query/body edge cases | -| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: 503 when env missing — includes **2FA** + **groups/pages** routes in matrix | **200** tests with stub `SessionEnv` + template DB (heavier) | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**18** cases when DB env set) — … + **groups/pages** + **user page prefs** ([slice 2](#userspages-rest--slice-2)); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; **Pro-only** create in non-personal group; **viewer** cannot create; optional `groupCreation` on create; pagination edge cases on `performLoadNotifications` | +| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA + **me/groups** + **users/pages\*** prefs paths + **groups/{id}/pages**); **`schemas/users.test.ts`** (email/password change, 2fa finish); **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; Zod tests for `pages-groups` / `user-pages` query edge cases | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **34** tests (503 matrix when env/Hyperdrive missing) — **2FA**, **groups/pages**, **users/pages prefs** | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -214,8 +229,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Drizzle migrations from empty DB documented for production upgrades. - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). -- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation, login + refresh + **stale JWT replay**, refresh **cookie guards**, **2FA** TOTP + **recovery codes**, **groups list + page list + page create**). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** as in [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) (**17** session + 6 db integration cases when DB env is set). **Next:** Redis failed-login integration; refresh **expired** JWT; more **pages/groups** routes; Stripe when billing exists. +- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation, login + refresh + **stale JWT replay**, refresh **cookie guards**, **2FA** TOTP + **recovery codes**, **groups list + page list + page create**, **users.pages prefs** incl. notifications + `favorite_page_ids`). +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** as in [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) (**18** session + 6 db integration cases when DB env is set). **Next:** Redis failed-login integration; refresh **expired** JWT; more **pages/groups** routes; Stripe when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -226,7 +241,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Phase 3 working order (suggested) -Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2FA + refresh guards; **`@deepnotes/db`** FKs through **`group_members`**. **(done 2026-04-27)** First **pages/groups** slice: `GET /api/users/me/groups`, `GET/POST /api/groups/:groupId/pages` + session services + integration test. **(next)** Remaining **`users.pages`** prefs; **`groupsRouter`** (members, main page, password, privacy, delete/restore/purge); **`pagesRouter`** (bump, backlinks, snapshots, delete, move; optional **`groupCreation`** on create); each slice + **template DB** tests where SQL risk is high. **(then)** **realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes. +Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2FA + refresh guards; **`@deepnotes/db`** FKs through **`group_members`**. **(done)** First **pages/groups** slice + **`users.pages` prefs** ([slice 1](#pagesgroups-rest--slice-1), [slice 2](#userspages-rest--slice-2)). **(next)** **`groupsRouter`** (members, main page, password, privacy, delete/restore/purge); **`pagesRouter`** (bump, backlinks, snapshots, delete, move; optional **`groupCreation`** on create); each slice + **template DB** tests where SQL risk is high. **(then)** **realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes. --- @@ -234,6 +249,7 @@ Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2F | Date | Change | |------|--------| +| 2026-04-27 | **Phase 3 — `users.pages` prefs (slice 2):** migration `0001_favorite_page_ids`; `user-page-prefs.ts` + Hono/OpenAPI routes (starting, path, recent, favorites, defaults PATCH, notifications); `schemas/user-pages.ts`; integration test **user page prefs**; TRPC_REST_MAP marked implemented; PLAN_PROGRESS sections + matrix counts (**18** session integration, **34** worker 503 rows). | | 2026-04-27 | **Phase 3 — pages/groups slice 1:** `performGetUserGroupIds`, `performListGroupPages`, `performCreatePage` + `group-permissions.ts`; OpenAPI + Zod `pages-groups.ts`; api-worker `GET /api/users/me/groups`, `GET/POST /api/groups/:groupId/pages` (**201** create); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) marked implemented for `getGroupIds`, `getPages`, `pages.create`; integration test **groups + pages**; PLAN_PROGRESS [Pages/groups REST — slice 1](#pagesgroups-rest--slice-1) + matrix bumps. | | 2026-04-27 | **More real Postgres tests:** `account-flows.integration.test.ts` — **2FA recovery-code** login + one-time use + `decryptRecoveryCodes` count; **replay** of first refresh JWT after two rotations (**401**); **`loggedIn`** / missing refresh guards. `template-db.test.ts` — **`group_members`** FK to `users` and to `groups`. PLAN_PROGRESS: run commands, expanded tables, matrix + success criteria + working order. | | 2026-04-27 | **Real Postgres tests (session + db):** `account-flows.integration.test.ts` — login + double refresh, wrong password, demo **403**, **2FA** (enable/finish + TOTP login, wrong finish code, MFA required, bad TOTP). `template-db.test.ts` — orphan **`sessions`**, **`devices`**, **`pages`→`groups`** FK rejects. PLAN_PROGRESS: Phase 3 detail table, package matrix, success criteria, working order. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 3f166419..1309b87a 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -28,6 +28,20 @@ describe("api-worker", () => { ["POST", "/api/sessions/logout"], ["POST", "/api/sessions/demo"], ["GET", "/api/users/me/groups"], + ["GET", "/api/users/me/pages/starting"], + [ + "GET", + "/api/users/me/pages/path?initialPageId=aaaaaaaaaaaaaaaaaaaaa", + ], + ["POST", "/api/users/me/pages/recent/remove"], + ["POST", "/api/users/me/pages/recent/clear"], + ["POST", "/api/users/me/pages/favorites"], + ["POST", "/api/users/me/pages/favorites/remove"], + ["POST", "/api/users/me/pages/favorites/clear"], + ["PATCH", "/api/users/me/defaults/note"], + ["PATCH", "/api/users/me/defaults/arrow"], + ["GET", "/api/users/me/notifications"], + ["POST", "/api/users/me/notifications/read"], [ "GET", "/api/groups/aaaaaaaaaaaaaaaaaaaaa/pages", diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index ada704f9..48f22b00 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -5,6 +5,11 @@ import { groupPageCreateRequestSchema, groupPagesListQuerySchema, healthResponseSchema, + userDefaultArrowPatchSchema, + userDefaultNotePatchSchema, + userNotificationsQuerySchema, + userPageIdsBodySchema, + userPagesPathQuerySchema, sessionDemoRequestSchema, sessionLoginRequestSchema, userAccountDeleteRequestSchema, @@ -640,6 +645,524 @@ app.get("/api/users/me/groups", async (c) => { } }); +app.get("/api/users/me/pages/starting", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performGetStartingPageId } = await import("@deepnotes/session"); + const out = await performGetStartingPageId({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.get("/api/users/me/pages/path", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + const qParsed = userPagesPathQuerySchema.safeParse({ + initialPageId: c.req.query("initialPageId") ?? undefined, + }); + if (!qParsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: qParsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performGetCurrentPath } = await import("@deepnotes/session"); + const out = await performGetCurrentPath({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + initialPageId: qParsed.data.initialPageId, + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/users/me/pages/recent/remove", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = userPageIdsBodySchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performRemoveRecentPages } = await import("@deepnotes/session"); + await performRemoveRecentPages({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageIds: parsed.data.pageIds, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/users/me/pages/recent/clear", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performClearRecentPages } = await import("@deepnotes/session"); + await performClearRecentPages({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/users/me/pages/favorites", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = userPageIdsBodySchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performAddFavoritePages } = await import("@deepnotes/session"); + await performAddFavoritePages({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageIds: parsed.data.pageIds, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/users/me/pages/favorites/remove", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = userPageIdsBodySchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performRemoveFavoritePages } = await import("@deepnotes/session"); + await performRemoveFavoritePages({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageIds: parsed.data.pageIds, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/users/me/pages/favorites/clear", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performClearFavoritePages } = await import("@deepnotes/session"); + await performClearFavoritePages({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.patch("/api/users/me/defaults/note", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = userDefaultNotePatchSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performPatchDefaultNote } = await import("@deepnotes/session"); + await performPatchDefaultNote({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + userEncryptedDefaultNote: parsed.data.userEncryptedDefaultNote, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.patch("/api/users/me/defaults/arrow", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = userDefaultArrowPatchSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performPatchDefaultArrow } = await import("@deepnotes/session"); + await performPatchDefaultArrow({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + userEncryptedDefaultArrow: parsed.data.userEncryptedDefaultArrow, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.get("/api/users/me/notifications", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + const qParsed = userNotificationsQuerySchema.safeParse({ + lastNotificationId: c.req.query("lastNotificationId") ?? undefined, + }); + if (!qParsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: qParsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performLoadNotifications } = await import("@deepnotes/session"); + const out = await performLoadNotifications({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + lastNotificationId: qParsed.data.lastNotificationId, + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/users/me/notifications/read", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performMarkNotificationsRead } = await import("@deepnotes/session"); + await performMarkNotificationsRead({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.get("/api/groups/:groupId/pages", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index 904cc743..6ffcdab8 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -34,17 +34,17 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | Legacy procedure | Proposed REST / notes | |------------------|----------------------| -| `users.pages.notifications.load` | `GET /api/users/me/notifications` | -| `users.pages.notifications.markAsRead` | `POST /api/users/me/notifications/read` | -| `users.pages.getStartingPageId` | `GET /api/users/me/pages/starting` | -| `users.pages.getCurrentPath` | `GET /api/users/me/pages/path` | -| `users.pages.removeRecentPages` | `POST /api/users/me/pages/recent/remove` | -| `users.pages.clearRecentPages` | `POST /api/users/me/pages/recent/clear` | -| `users.pages.addFavoritePages` | `POST /api/users/me/pages/favorites` | -| `users.pages.removeFavoritePages` | `POST /api/users/me/pages/favorites/remove` | -| `users.pages.clearFavoritePages` | `POST /api/users/me/pages/favorites/clear` | -| `users.pages.setEncryptedDefaultNote` | `PATCH /api/users/me/defaults/note` | -| `users.pages.setEncryptedDefaultArrow` | `PATCH /api/users/me/defaults/arrow` | +| `users.pages.notifications.load` | `GET /api/users/me/notifications` (**implemented** — `performLoadNotifications`; ciphertext fields base64 in JSON; optional `lastNotificationId` query) | +| `users.pages.notifications.markAsRead` | `POST /api/users/me/notifications/read` (**implemented** — `performMarkNotificationsRead`) | +| `users.pages.getStartingPageId` | `GET /api/users/me/pages/starting` (**implemented** — `performGetStartingPageId`) | +| `users.pages.getCurrentPath` | `GET /api/users/me/pages/path?initialPageId=` (**implemented** — `performGetCurrentPath`; `users_pages` repair like legacy) | +| `users.pages.removeRecentPages` | `POST /api/users/me/pages/recent/remove` (**implemented** — JSON `{ "pageIds": [...] }`; `performRemoveRecentPages`) | +| `users.pages.clearRecentPages` | `POST /api/users/me/pages/recent/clear` (**implemented** — `performClearRecentPages`) | +| `users.pages.addFavoritePages` | `POST /api/users/me/pages/favorites` (**implemented** — `performAddFavoritePages`; favorites in Postgres `users.favorite_page_ids`, migration `0001_favorite_page_ids`) | +| `users.pages.removeFavoritePages` | `POST /api/users/me/pages/favorites/remove` (**implemented** — `performRemoveFavoritePages`) | +| `users.pages.clearFavoritePages` | `POST /api/users/me/pages/favorites/clear` (**implemented** — `performClearFavoritePages`) | +| `users.pages.setEncryptedDefaultNote` | `PATCH /api/users/me/defaults/note` (**implemented** — JSON `userEncryptedDefaultNote` base64; `performPatchDefaultNote`) | +| `users.pages.setEncryptedDefaultArrow` | `PATCH /api/users/me/defaults/arrow` (**implemented** — `performPatchDefaultArrow`) | | `users.pages.getGroupIds` | `GET /api/users/me/groups` (**implemented** — `performGetUserGroupIds` in `@deepnotes/session`) | ## Groups (`groupsRouter`) diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index 593f181f..000e012a 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -31,6 +31,17 @@ export { groupPagesListResponseSchema, userGroupIdsResponseSchema, } from "./schemas/pages-groups.js"; +export { + userCurrentPathResponseSchema, + userDefaultArrowPatchSchema, + userDefaultNotePatchSchema, + userNotificationItemSchema, + userNotificationsLoadResponseSchema, + userNotificationsQuerySchema, + userPageIdsBodySchema, + userPagesPathQuerySchema, + userStartingPageResponseSchema, +} from "./schemas/user-pages.js"; export { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 0b29983c..09c64bba 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -45,6 +45,17 @@ describe("getOpenApiDocument", () => { ).toBeDefined(); expect(doc.paths?.["/api/users/me/2fa/disable"]?.post).toBeDefined(); expect(doc.paths?.["/api/users/me/groups"]?.get).toBeDefined(); + expect(doc.paths?.["/api/users/me/pages/starting"]?.get).toBeDefined(); + expect(doc.paths?.["/api/users/me/pages/path"]?.get).toBeDefined(); + expect(doc.paths?.["/api/users/me/pages/recent/remove"]?.post).toBeDefined(); + expect(doc.paths?.["/api/users/me/pages/recent/clear"]?.post).toBeDefined(); + expect(doc.paths?.["/api/users/me/pages/favorites"]?.post).toBeDefined(); + expect(doc.paths?.["/api/users/me/pages/favorites/remove"]?.post).toBeDefined(); + expect(doc.paths?.["/api/users/me/pages/favorites/clear"]?.post).toBeDefined(); + expect(doc.paths?.["/api/users/me/defaults/note"]?.patch).toBeDefined(); + expect(doc.paths?.["/api/users/me/defaults/arrow"]?.patch).toBeDefined(); + expect(doc.paths?.["/api/users/me/notifications"]?.get).toBeDefined(); + expect(doc.paths?.["/api/users/me/notifications/read"]?.post).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/pages"]?.get).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/pages"]?.post).toBeDefined(); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index 288f6a52..7974f735 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -24,6 +24,16 @@ import { groupPagesListResponseSchema, userGroupIdsResponseSchema, } from "./schemas/pages-groups.js"; +import { + userCurrentPathResponseSchema, + userDefaultArrowPatchSchema, + userDefaultNotePatchSchema, + userNotificationsLoadResponseSchema, + userNotificationsQuerySchema, + userPageIdsBodySchema, + userPagesPathQuerySchema, + userStartingPageResponseSchema, +} from "./schemas/user-pages.js"; import { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, @@ -231,6 +241,275 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "get", + path: "/api/users/me/pages/starting", + summary: "Starting page id for the current user", + description: "Replaces legacy `users.pages.getStartingPageId` (reads `users.starting_page_id`).", + responses: { + 200: { + description: "Nanoid of the user’s starting page.", + content: { + "application/json": { + schema: userStartingPageResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "get", + path: "/api/users/me/pages/path", + summary: "Breadcrumb path from a page to the personal main page", + description: + "Replaces legacy `users.pages.getCurrentPath`. Uses `users_pages.last_parent_id` and may repair a missing parent link once (legacy KeyDB behavior).", + request: { + query: userPagesPathQuerySchema, + }, + responses: { + 200: { + description: "Ordered page ids from root (personal main) to `initialPageId`.", + content: { + "application/json": { + schema: userCurrentPathResponseSchema, + }, + }, + }, + 400: { + description: "Missing or invalid `initialPageId` query parameter.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/pages/recent/remove", + summary: "Remove page ids from recent list", + description: "Replaces legacy `users.pages.removeRecentPages`.", + request: { + body: { + content: { + "application/json": { + schema: userPageIdsBodySchema, + }, + }, + }, + }, + responses: { + 204: { description: "Updated `users.recent_page_ids`." }, + 400: { + description: "Validation error.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/pages/recent/clear", + summary: "Clear recent pages", + description: "Replaces legacy `users.pages.clearRecentPages`.", + responses: { + 204: { description: "Recent list emptied." }, + 401: sessionUnauthorized401, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/pages/favorites", + summary: "Add favorite pages", + description: + "Replaces legacy `users.pages.addFavoritePages`. Favorites are stored in Postgres (`users.favorite_page_ids`); legacy used KeyDB only.", + request: { + body: { + content: { + "application/json": { + schema: userPageIdsBodySchema, + }, + }, + }, + }, + responses: { + 204: { description: "Favorites merged (order: new ids first, then existing)." }, + 400: { + description: "Validation error.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/pages/favorites/remove", + summary: "Remove favorite pages", + description: "Replaces legacy `users.pages.removeFavoritePages`.", + request: { + body: { + content: { + "application/json": { + schema: userPageIdsBodySchema, + }, + }, + }, + }, + responses: { + 204: { description: "Favorites updated." }, + 400: { + description: "Validation error.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/pages/favorites/clear", + summary: "Clear favorite pages", + description: "Replaces legacy `users.pages.clearFavoritePages`.", + responses: { + 204: { description: "Favorites emptied." }, + 401: sessionUnauthorized401, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "patch", + path: "/api/users/me/defaults/note", + summary: "Update encrypted default note template", + description: "Replaces legacy `users.pages.setEncryptedDefaultNote`.", + request: { + body: { + content: { + "application/json": { + schema: userDefaultNotePatchSchema, + }, + }, + }, + }, + responses: { + 204: { description: "`users.encrypted_default_note` updated." }, + 400: { + description: "Validation error.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "patch", + path: "/api/users/me/defaults/arrow", + summary: "Update encrypted default arrow template", + description: "Replaces legacy `users.pages.setEncryptedDefaultArrow`.", + request: { + body: { + content: { + "application/json": { + schema: userDefaultArrowPatchSchema, + }, + }, + }, + }, + responses: { + 204: { description: "`users.encrypted_default_arrow` updated." }, + 400: { + description: "Validation error.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "get", + path: "/api/users/me/notifications", + summary: "Load notifications for the current user", + description: + "Replaces legacy `users.pages.notifications.load`. Ciphertext fields are base64 in JSON.", + request: { + query: userNotificationsQuerySchema, + }, + responses: { + 200: { + description: "Window of notifications and optional `lastNotificationRead`.", + content: { + "application/json": { + schema: userNotificationsLoadResponseSchema, + }, + }, + }, + 400: { + description: "Invalid query parameters.", + content: { + "application/json": { + schema: sessionErrorResponseSchema, + }, + }, + }, + 401: sessionUnauthorized401, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/users/me/notifications/read", + summary: "Mark all notifications as read", + description: + "Replaces legacy `users.pages.notifications.markAsRead`. Sets `users.last_notification_read` to the latest linked notification id.", + responses: { + 204: { description: "Read cursor updated (no-op if user has no notifications)." }, + 401: sessionUnauthorized401, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "get", path: "/api/groups/{groupId}/pages", diff --git a/new-deepnotes/packages/api/src/schemas/user-pages.ts b/new-deepnotes/packages/api/src/schemas/user-pages.ts new file mode 100644 index 00000000..141ae529 --- /dev/null +++ b/new-deepnotes/packages/api/src/schemas/user-pages.ts @@ -0,0 +1,80 @@ +import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; +import { z } from "zod"; + +import { byteB64 } from "./sessions.js"; +import { nanoidIdOpenapi } from "./pages-groups.js"; + +extendZodWithOpenApi(z); + +const nanoid21 = z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi(nanoidIdOpenapi); + +export const userPagesPathQuerySchema = z.object({ + initialPageId: nanoid21.openapi({ + description: "Page to resolve toward the personal group main page.", + param: { name: "initialPageId", in: "query" }, + }), +}); + +export const userStartingPageResponseSchema = z + .object({ + startingPageId: z.string(), + }) + .openapi("UserStartingPageResponse"); + +export const userCurrentPathResponseSchema = z + .object({ + pathPageIds: z.array(z.string()), + }) + .openapi("UserCurrentPathResponse"); + +export const userPageIdsBodySchema = z + .object({ + pageIds: z.array(nanoid21).min(1), + }) + .openapi("UserPageIdsBody"); + +export const userDefaultNotePatchSchema = z + .object({ + userEncryptedDefaultNote: byteB64, + }) + .openapi("UserDefaultNotePatch"); + +export const userDefaultArrowPatchSchema = z + .object({ + userEncryptedDefaultArrow: byteB64, + }) + .openapi("UserDefaultArrowPatch"); + +export const userNotificationsQuerySchema = z.object({ + lastNotificationId: z.coerce + .number() + .int() + .positive() + .optional() + .openapi({ + description: + "Return notifications strictly older than this id (legacy pagination).", + param: { name: "lastNotificationId", in: "query" }, + }), +}); + +export const userNotificationItemSchema = z + .object({ + id: z.number().int(), + type: z.string(), + encryptedSymmetricKey: byteB64, + encryptedContent: byteB64, + dateTime: z.string(), + }) + .openapi("UserNotificationItem"); + +export const userNotificationsLoadResponseSchema = z + .object({ + items: z.array(userNotificationItemSchema), + hasMore: z.boolean(), + lastNotificationRead: z.number().int().nullable().optional(), + }) + .openapi("UserNotificationsLoadResponse"); diff --git a/new-deepnotes/packages/db/migrations/0001_favorite_page_ids.sql b/new-deepnotes/packages/db/migrations/0001_favorite_page_ids.sql new file mode 100644 index 00000000..6a009d7f --- /dev/null +++ b/new-deepnotes/packages/db/migrations/0001_favorite_page_ids.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "favorite_page_ids" char(21)[] DEFAULT '{}'::character(21)[] NOT NULL; \ No newline at end of file diff --git a/new-deepnotes/packages/db/migrations/meta/0001_snapshot.json b/new-deepnotes/packages/db/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..5cbba99a --- /dev/null +++ b/new-deepnotes/packages/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,1406 @@ +{ + "id": "e8b17110-84c5-40ea-a97e-73944d6aac6f", + "prevId": "e2275e57-a99e-45e4-8228-df7ecbd085bf", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.devices": { + "name": "devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(21)", + "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + }, + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "trusted": { + "name": "trusted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hash": { + "name": "hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "devices_user_id_users_id_fk": { + "name": "devices_user_id_users_id_fk", + "tableFrom": "devices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_join_invitations": { + "name": "group_join_invitations", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_access_keyring": { + "name": "encrypted_access_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "encrypted_internal_keyring": { + "name": "encrypted_internal_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "encrypted_name_for_user": { + "name": "encrypted_name_for_user", + "type": "bytea", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "group_join_invitations_user_id_idx": { + "name": "group_join_invitations_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"creation_date\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_join_invitations_group_id_groups_id_fk": { + "name": "group_join_invitations_group_id_groups_id_fk", + "tableFrom": "group_join_invitations", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_join_invitations_user_id_users_id_fk": { + "name": "group_join_invitations_user_id_users_id_fk", + "tableFrom": "group_join_invitations", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_join_invitations_pkey": { + "name": "group_join_invitations_pkey", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_join_requests": { + "name": "group_join_requests", + "schema": "", + "columns": { + "group_id": { + "name": "group_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "rejected": { + "name": "rejected", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "encrypted_name_for_user": { + "name": "encrypted_name_for_user", + "type": "bytea", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "group_join_requests_user_id_idx": { + "name": "group_join_requests_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"creation_date\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_join_requests_group_id_groups_id_fk": { + "name": "group_join_requests_group_id_groups_id_fk", + "tableFrom": "group_join_requests", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_join_requests_user_id_users_id_fk": { + "name": "group_join_requests_user_id_users_id_fk", + "tableFrom": "group_join_requests", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "group_join_requests_pkey": { + "name": "group_join_requests_pkey", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group_members": { + "name": "group_members", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "encrypted_access_keyring": { + "name": "encrypted_access_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_internal_keyring": { + "name": "encrypted_internal_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "last_activity_date": { + "name": "last_activity_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "encrypted_name_for_user": { + "name": "encrypted_name_for_user", + "type": "bytea", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "group_members_user_id_idx": { + "name": "group_members_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"last_activity_date\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "group_members_user_id_users_id_fk": { + "name": "group_members_user_id_users_id_fk", + "tableFrom": "group_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "group_members_group_id_groups_id_fk": { + "name": "group_members_group_id_groups_id_fk", + "tableFrom": "group_members", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "groups_users_pkey": { + "name": "groups_users_pkey", + "columns": [ + "group_id", + "user_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.groups": { + "name": "groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(21)", + "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + }, + "main_page_id": { + "name": "main_page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": false + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "public_keyring": { + "name": "public_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_private_keyring": { + "name": "encrypted_private_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "access_keyring": { + "name": "access_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "encrypted_content_keyring": { + "name": "encrypted_content_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "permanent_deletion_date": { + "name": "permanent_deletion_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "encrypted_rehashed_password_hash": { + "name": "encrypted_rehashed_password_hash", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "are_join_requests_allowed": { + "name": "are_join_requests_allowed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": { + "groups_user_id_users_id_fk": { + "name": "groups_user_id_users_id_fk", + "tableFrom": "groups", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notifications": { + "name": "notifications", + "schema": "", + "columns": { + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "datetime": { + "name": "datetime", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "encrypted_content": { + "name": "encrypted_content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "notifications_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_links": { + "name": "page_links", + "schema": "", + "columns": { + "target_page_id": { + "name": "target_page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "source_page_id": { + "name": "source_page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "last_activity_date": { + "name": "last_activity_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "page_links_target_page_id_idx": { + "name": "page_links_target_page_id_idx", + "columns": [ + { + "expression": "target_page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "\"last_activity_date\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "page_links_target_page_id_pages_id_fk": { + "name": "page_links_target_page_id_pages_id_fk", + "tableFrom": "page_links", + "tableTo": "pages", + "columnsFrom": [ + "target_page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "page_links_source_page_id_pages_id_fk": { + "name": "page_links_source_page_id_pages_id_fk", + "tableFrom": "page_links", + "tableTo": "pages", + "columnsFrom": [ + "source_page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "page_links_pkey": { + "name": "page_links_pkey", + "columns": [ + "source_page_id", + "target_page_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_snapshots": { + "name": "page_snapshots", + "schema": "", + "columns": { + "page_id": { + "name": "page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "encrypted_data": { + "name": "encrypted_data", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "char(21)", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_symmetric_key": { + "name": "encrypted_symmetric_key", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "char(21)", + "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + } + }, + "indexes": {}, + "foreignKeys": { + "page_snapshots_page_id_pages_id_fk": { + "name": "page_snapshots_page_id_pages_id_fk", + "tableFrom": "page_snapshots", + "tableTo": "pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.page_updates": { + "name": "page_updates", + "schema": "", + "columns": { + "page_id": { + "name": "page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "index": { + "name": "index", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "encrypted_data": { + "name": "encrypted_data", + "type": "bytea", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "page_updates_page_id_pages_id_fk": { + "name": "page_updates_page_id_pages_id_fk", + "tableFrom": "page_updates", + "tableTo": "pages", + "columnsFrom": [ + "page_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "pages_updates_pkey": { + "name": "pages_updates_pkey", + "columns": [ + "page_id", + "index" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pages": { + "name": "pages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(21)", + "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_activity_date": { + "name": "last_activity_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "group_id": { + "name": "group_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "encrypted_relative_title": { + "name": "encrypted_relative_title", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_symmetric_keyring": { + "name": "encrypted_symmetric_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "free": { + "name": "free", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "next_snapshot_update_index": { + "name": "next_snapshot_update_index", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "next_snapshot_date": { + "name": "next_snapshot_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "(now() + '00:15:00'::interval)" + }, + "next_key_rotation_date": { + "name": "next_key_rotation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "(now() + '7 days'::interval)" + }, + "permanent_deletion_date": { + "name": "permanent_deletion_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "encrypted_absolute_title": { + "name": "encrypted_absolute_title", + "type": "bytea", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "pages_group_id_groups_id_fk": { + "name": "pages_group_id_groups_id_fk", + "tableFrom": "pages", + "tableTo": "groups", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(21)", + "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + }, + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "invalidated": { + "name": "invalidated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "device_id": { + "name": "device_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "last_refresh_date": { + "name": "last_refresh_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expiration_date": { + "name": "expiration_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "encryption_key": { + "name": "encryption_key", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "refresh_code": { + "name": "refresh_code", + "type": "char(21)", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sessions_refresh_code_idx": { + "name": "sessions_refresh_code_idx", + "columns": [ + { + "expression": "refresh_code", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sessions_device_id_devices_id_fk": { + "name": "sessions_device_id_devices_id_fk", + "tableFrom": "sessions", + "tableTo": "devices", + "columnsFrom": [ + "device_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "char(21)", + "primaryKey": true, + "notNull": true, + "default": "public.nanoid()" + }, + "creation_date": { + "name": "creation_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "starting_page_id": { + "name": "starting_page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "recent_page_ids": { + "name": "recent_page_ids", + "type": "char(21)[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::character(21)[]" + }, + "favorite_page_ids": { + "name": "favorite_page_ids", + "type": "char(21)[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::character(21)[]" + }, + "personal_group_id": { + "name": "personal_group_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "public_keyring": { + "name": "public_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_private_keyring": { + "name": "encrypted_private_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_symmetric_keyring": { + "name": "encrypted_symmetric_keyring", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_default_arrow": { + "name": "encrypted_default_arrow", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_default_note": { + "name": "encrypted_default_note", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "two_factor_auth_enabled": { + "name": "two_factor_auth_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "email_verification_expiration_date": { + "name": "email_verification_expiration_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_verification_code": { + "name": "email_verification_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recent_group_ids": { + "name": "recent_group_ids", + "type": "char(21)[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::character(21)[]" + }, + "last_notification_read": { + "name": "last_notification_read", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'basic'" + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_name": { + "name": "encrypted_name", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "num_free_pages": { + "name": "num_free_pages", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "encrypted_authenticator_secret": { + "name": "encrypted_authenticator_secret", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "encrypted_email": { + "name": "encrypted_email", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_new_email": { + "name": "encrypted_new_email", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "encrypted_recovery_codes": { + "name": "encrypted_recovery_codes", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "demo": { + "name": "demo", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "email_hash": { + "name": "email_hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "encrypted_rehashed_login_hash": { + "name": "encrypted_rehashed_login_hash", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "new": { + "name": "new", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": { + "users_encrypted_email_key": { + "name": "users_encrypted_email_key", + "columns": [ + { + "expression": "encrypted_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_email_hash_idx": { + "name": "users_email_hash_idx", + "columns": [ + { + "expression": "email_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_creation_date_idx": { + "name": "users_creation_date_idx", + "columns": [ + { + "expression": "\"creation_date\" DESC", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users_customer_id_idx": { + "name": "users_customer_id_idx", + "columns": [ + { + "expression": "customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_notifications": { + "name": "users_notifications", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "encrypted_symmetric_key": { + "name": "encrypted_symmetric_key", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "notification_id": { + "name": "notification_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "users_notifications_user_id_users_id_fk": { + "name": "users_notifications_user_id_users_id_fk", + "tableFrom": "users_notifications", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users_notifications_notification_id_notifications_id_fk": { + "name": "users_notifications_notification_id_notifications_id_fk", + "tableFrom": "users_notifications", + "tableTo": "notifications", + "columnsFrom": [ + "notification_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_notifications_pkey": { + "name": "users_notifications_pkey", + "columns": [ + "user_id", + "notification_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users_pages": { + "name": "users_pages", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "page_id": { + "name": "page_id", + "type": "char(21)", + "primaryKey": false, + "notNull": true + }, + "last_parent_id": { + "name": "last_parent_id", + "type": "char(21)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "users_pages_page_id_idx": { + "name": "users_pages_page_id_idx", + "columns": [ + { + "expression": "page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users_pages_user_id_users_id_fk": { + "name": "users_pages_user_id_users_id_fk", + "tableFrom": "users_pages", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "users_pages_pkey": { + "name": "users_pages_pkey", + "columns": [ + "user_id", + "page_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/new-deepnotes/packages/db/migrations/meta/_journal.json b/new-deepnotes/packages/db/migrations/meta/_journal.json index 6b813454..17747f0b 100644 --- a/new-deepnotes/packages/db/migrations/meta/_journal.json +++ b/new-deepnotes/packages/db/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1777253675226, "tag": "0000_legacy_baseline", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1777261172475, + "tag": "0001_favorite_page_ids", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/new-deepnotes/packages/db/src/schema.ts b/new-deepnotes/packages/db/src/schema.ts index f1ec3b18..fbc47baa 100644 --- a/new-deepnotes/packages/db/src/schema.ts +++ b/new-deepnotes/packages/db/src/schema.ts @@ -35,6 +35,11 @@ export const users = pgTable( .array() .notNull() .default(sql`'{}'::character(21)[]`), + /** Favorites lived only in KeyDB in legacy; persisted in Postgres for the new stack. */ + favoritePageIds: char("favorite_page_ids", { length: 21 }) + .array() + .notNull() + .default(sql`'{}'::character(21)[]`), personalGroupId: char("personal_group_id", { length: 21 }).notNull(), emailVerified: boolean("email_verified").notNull().default(false), publicKeyring: bytea("public_keyring").notNull(), diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index 4e89ec66..5129c4b2 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -19,7 +19,14 @@ import { type TemplateDbContext, } from "@deepnotes/db/testing/template-db"; import * as schema from "@deepnotes/db/schema"; -import { devices, pages, sessions, users } from "@deepnotes/db/schema"; +import { + devices, + notifications, + pages, + sessions, + users, + usersNotifications, +} from "@deepnotes/db/schema"; import { performUserPasswordChange } from "./change-user-password.js"; import { @@ -49,6 +56,17 @@ import { performListGroupPages, } from "./group-pages.js"; import { performGetUserGroupIds } from "./user-group-ids.js"; +import { + performAddFavoritePages, + performClearRecentPages, + performGetCurrentPath, + performGetStartingPageId, + performLoadNotifications, + performMarkNotificationsRead, + performPatchDefaultNote, + performRemoveFavoritePages, + performRemoveRecentPages, +} from "./user-page-prefs.js"; import { performUserRegister } from "./register-user.js"; import { decryptUserEmail } from "./encrypt-user-email.js"; import { hashUserEmail } from "./email-hash.js"; @@ -1311,5 +1329,200 @@ describe.skipIf(resolveTemplateContext() == null)( } } }); + + it("user page prefs: starting, path, favorites, recent, defaults, notifications", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `prefs-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + + const start = await performGetStartingPageId({ + db, + env, + accessCookie: access, + }); + expect(start.startingPageId).toBe(reg.pageId); + + const path1 = await performGetCurrentPath({ + db, + env, + accessCookie: access, + initialPageId: reg.pageId, + }); + expect(path1.pathPageIds).toEqual([reg.pageId]); + + const newPageId = nanoid(); + await performCreatePage({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + body: { + parentPageId: reg.pageId, + pageId: newPageId, + pageEncryptedSymmetricKeyring: rand32(), + pageEncryptedRelativeTitle: rand32(), + pageEncryptedAbsoluteTitle: rand32(), + }, + }); + + const path2 = await performGetCurrentPath({ + db, + env, + accessCookie: access, + initialPageId: newPageId, + }); + expect(path2.pathPageIds).toEqual([reg.pageId, newPageId]); + + await expect( + performGetCurrentPath({ + db, + env, + accessCookie: access, + initialPageId: "nononononononononono1", + }), + ).rejects.toMatchObject({ status: 404, code: "NOT_FOUND" }); + + await performAddFavoritePages({ + db, + env, + accessCookie: access, + pageIds: [reg.pageId, newPageId], + }); + const [favRow] = await db + .select({ favoritePageIds: users.favoritePageIds }) + .from(users) + .where(eq(users.id, reg.userId)); + expect(favRow?.favoritePageIds).toEqual([reg.pageId, newPageId]); + + await performRemoveFavoritePages({ + db, + env, + accessCookie: access, + pageIds: [reg.pageId], + }); + const [favAfter] = await db + .select({ favoritePageIds: users.favoritePageIds }) + .from(users) + .where(eq(users.id, reg.userId)); + expect(favAfter?.favoritePageIds).toEqual([newPageId]); + + await expect( + performRemoveRecentPages({ + db, + env, + accessCookie: access, + pageIds: ["nononononononononono1"], + }), + ).rejects.toMatchObject({ status: 404, code: "NOT_FOUND" }); + + await expect( + performRemoveRecentPages({ + db, + env, + accessCookie: access, + pageIds: [newPageId], + }), + ).rejects.toMatchObject({ status: 404, code: "NOT_FOUND" }); + + await performRemoveRecentPages({ + db, + env, + accessCookie: access, + pageIds: [reg.pageId], + }); + const [recentMid] = await db + .select({ recentPageIds: users.recentPageIds }) + .from(users) + .where(eq(users.id, reg.userId)); + expect(recentMid?.recentPageIds).toEqual([]); + + await performClearRecentPages({ + db, + env, + accessCookie: access, + }); + const [recentClear] = await db + .select({ recentPageIds: users.recentPageIds }) + .from(users) + .where(eq(users.id, reg.userId)); + expect(recentClear?.recentPageIds).toEqual([]); + + const newNote = rand32(); + await performPatchDefaultNote({ + db, + env, + accessCookie: access, + userEncryptedDefaultNote: newNote, + }); + const [noteRow] = await db + .select({ enc: users.encryptedDefaultNote }) + .from(users) + .where(eq(users.id, reg.userId)); + expect(Buffer.from(newNote).equals(noteRow!.enc)).toBe(true); + + const [n] = await db + .insert(notifications) + .values({ + type: "unit-test", + encryptedContent: Buffer.from("hello-notif"), + }) + .returning({ id: notifications.id }); + + await db.insert(usersNotifications).values({ + userId: reg.userId, + notificationId: n!.id, + encryptedSymmetricKey: Buffer.from("sym-key"), + }); + + const loaded = await performLoadNotifications({ + db, + env, + accessCookie: access, + }); + expect(loaded.hasMore).toBe(false); + expect(loaded.items).toHaveLength(1); + expect(loaded.items[0]!.id).toBe(n!.id); + expect(loaded.items[0]!.type).toBe("unit-test"); + expect(loaded.lastNotificationRead).toBeNull(); + + await performMarkNotificationsRead({ + db, + env, + accessCookie: access, + }); + const [readRow] = await db + .select({ lastNotificationRead: users.lastNotificationRead }) + .from(users) + .where(eq(users.id, reg.userId)); + expect(readRow?.lastNotificationRead).toBe(n!.id); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); }, ); diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index c269ba94..7a090a11 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -32,6 +32,20 @@ export { } from "./group-pages.js"; export type { CreatePageBody } from "./group-pages.js"; export { performGetUserGroupIds } from "./user-group-ids.js"; +export { + performAddFavoritePages, + performClearFavoritePages, + performClearRecentPages, + performGetCurrentPath, + performGetStartingPageId, + performLoadNotifications, + performMarkNotificationsRead, + performPatchDefaultArrow, + performPatchDefaultNote, + performRemoveFavoritePages, + performRemoveRecentPages, +} from "./user-page-prefs.js"; +export type { UserNotificationItemDto } from "./user-page-prefs.js"; export { performUserTwoFactorDisable, performUserTwoFactorEnableFinish, diff --git a/new-deepnotes/packages/session/src/user-page-prefs.ts b/new-deepnotes/packages/session/src/user-page-prefs.ts new file mode 100644 index 00000000..f3314c57 --- /dev/null +++ b/new-deepnotes/packages/session/src/user-page-prefs.ts @@ -0,0 +1,420 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { + groups, + notifications, + pages, + users, + usersNotifications, + usersPages, +} from "@deepnotes/db/schema"; +import { and, desc, eq, isNull, lt } from "drizzle-orm"; + +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; + +function toBuf(u: Uint8Array): Buffer { + return Buffer.from(u); +} + +function b64(buf: Buffer): string { + return buf.toString("base64"); +} + +function unionPageIds(preferred: string[], existing: string[]): string[] { + const out: string[] = []; + const seen = new Set(); + for (const id of preferred) { + if (!seen.has(id)) { + seen.add(id); + out.push(id); + } + } + for (const id of existing) { + if (!seen.has(id)) { + seen.add(id); + out.push(id); + } + } + return out; +} + +async function getPathPageIds(input: { + db: DeepnotesDb; + userId: string; + initialPageId: string; + mainPageId: string; +}): Promise { + const pathPageIds: string[] = []; + const visited = new Set(); + let pathPageId: string | null = input.initialPageId; + + while (pathPageId != null) { + if (visited.has(pathPageId)) { + return undefined; + } + visited.add(pathPageId); + pathPageIds.unshift(pathPageId); + if (pathPageId === input.mainPageId) { + return pathPageIds; + } + + const [up] = await input.db + .select({ lastParentId: usersPages.lastParentId }) + .from(usersPages) + .where( + and( + eq(usersPages.userId, input.userId), + eq(usersPages.pageId, pathPageId), + ), + ) + .limit(1); + + pathPageId = up?.lastParentId ?? null; + } + return undefined; +} + +export async function performGetStartingPageId(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; +}): Promise<{ startingPageId: string }> { + const { userId } = await getAuthenticatedUserSummary(input); + + const [row] = await input.db + .select({ startingPageId: users.startingPageId }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (row == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + + return { startingPageId: row.startingPageId }; +} + +/** + * Replaces legacy `users.pages.getCurrentPath` (KeyDB `user-page` chain + repair). + */ +export async function performGetCurrentPath(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + initialPageId: string; +}): Promise<{ pathPageIds: string[] }> { + const { userId } = await getAuthenticatedUserSummary(input); + + const [pageRow] = await input.db + .select({ id: pages.id }) + .from(pages) + .where( + and(eq(pages.id, input.initialPageId), isNull(pages.permanentDeletionDate)), + ) + .limit(1); + + if (pageRow == null) { + throw new SessionError(404, "NOT_FOUND", "This page does not exist."); + } + + const [userRow] = await input.db + .select({ + personalGroupId: users.personalGroupId, + startingPageId: users.startingPageId, + }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (userRow == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + + const [groupRow] = await input.db + .select({ mainPageId: groups.mainPageId }) + .from(groups) + .where(eq(groups.id, userRow.personalGroupId)) + .limit(1); + + if (groupRow == null) { + throw new SessionError(404, "NOT_FOUND", "Personal group not found."); + } + + let path = await getPathPageIds({ + db: input.db, + userId, + initialPageId: input.initialPageId, + mainPageId: groupRow.mainPageId, + }); + + if (path != null) { + return { pathPageIds: path }; + } + + const newParent = + input.initialPageId === userRow.startingPageId + ? groupRow.mainPageId + : userRow.startingPageId; + + await input.db + .insert(usersPages) + .values({ + userId, + pageId: input.initialPageId, + lastParentId: newParent, + }) + .onConflictDoUpdate({ + target: [usersPages.userId, usersPages.pageId], + set: { lastParentId: newParent }, + }); + + path = await getPathPageIds({ + db: input.db, + userId, + initialPageId: input.initialPageId, + mainPageId: groupRow.mainPageId, + }); + + if (path == null) { + throw new SessionError( + 500, + "SERVER_MISCONFIG", + "Could not resolve page path after repair.", + ); + } + + return { pathPageIds: path }; +} + +export async function performRemoveRecentPages(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageIds: string[]; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + + const [row] = await input.db + .select({ recentPageIds: users.recentPageIds }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (row == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + + const remove = new Set(input.pageIds); + const next = row.recentPageIds.filter((id) => !remove.has(id)); + if (next.length === row.recentPageIds.length) { + throw new SessionError(404, "NOT_FOUND", "Recent page not found."); + } + + await input.db + .update(users) + .set({ recentPageIds: next }) + .where(eq(users.id, userId)); +} + +export async function performClearRecentPages(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + await input.db + .update(users) + .set({ recentPageIds: [] }) + .where(eq(users.id, userId)); +} + +export async function performAddFavoritePages(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageIds: string[]; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + + const [row] = await input.db + .select({ favoritePageIds: users.favoritePageIds }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (row == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + + const next = unionPageIds(input.pageIds, row.favoritePageIds); + await input.db + .update(users) + .set({ favoritePageIds: next }) + .where(eq(users.id, userId)); +} + +export async function performRemoveFavoritePages(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageIds: string[]; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + + const [row] = await input.db + .select({ favoritePageIds: users.favoritePageIds }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + + if (row == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + + const remove = new Set(input.pageIds); + const next = row.favoritePageIds.filter((id) => !remove.has(id)); + if (next.length === row.favoritePageIds.length) { + throw new SessionError(404, "NOT_FOUND", "Favorite page not found."); + } + + await input.db + .update(users) + .set({ favoritePageIds: next }) + .where(eq(users.id, userId)); +} + +export async function performClearFavoritePages(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + await input.db + .update(users) + .set({ favoritePageIds: [] }) + .where(eq(users.id, userId)); +} + +export async function performPatchDefaultNote(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + userEncryptedDefaultNote: Uint8Array; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + await input.db + .update(users) + .set({ encryptedDefaultNote: toBuf(input.userEncryptedDefaultNote) }) + .where(eq(users.id, userId)); +} + +export async function performPatchDefaultArrow(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + userEncryptedDefaultArrow: Uint8Array; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + await input.db + .update(users) + .set({ encryptedDefaultArrow: toBuf(input.userEncryptedDefaultArrow) }) + .where(eq(users.id, userId)); +} + +export type UserNotificationItemDto = { + id: number; + type: string; + encryptedSymmetricKey: string; + encryptedContent: string; + dateTime: string; +}; + +export async function performLoadNotifications(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + lastNotificationId?: number | undefined; +}): Promise<{ + items: UserNotificationItemDto[]; + hasMore: boolean; + lastNotificationRead?: number | null; +}> { + const { userId } = await getAuthenticatedUserSummary(input); + + const whereClause = + input.lastNotificationId != null + ? and( + eq(usersNotifications.userId, userId), + lt(usersNotifications.notificationId, input.lastNotificationId), + ) + : eq(usersNotifications.userId, userId); + + const rows = await input.db + .select({ + id: notifications.id, + type: notifications.type, + encSym: usersNotifications.encryptedSymmetricKey, + encContent: notifications.encryptedContent, + datetime: notifications.datetime, + }) + .from(usersNotifications) + .innerJoin( + notifications, + eq(usersNotifications.notificationId, notifications.id), + ) + .where(whereClause) + .orderBy(desc(usersNotifications.notificationId)) + .limit(21); + + const hasMore = rows.length > 20; + const slice = hasMore ? rows.slice(0, 20) : rows; + + let lastNotificationRead: number | null | undefined; + if (input.lastNotificationId == null) { + const [u] = await input.db + .select({ lastNotificationRead: users.lastNotificationRead }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + lastNotificationRead = u?.lastNotificationRead ?? null; + } + + return { + items: slice.map((r) => ({ + id: r.id, + type: r.type, + encryptedSymmetricKey: b64(r.encSym), + encryptedContent: b64(r.encContent), + dateTime: r.datetime, + })), + hasMore, + ...(input.lastNotificationId == null + ? { lastNotificationRead } + : {}), + }; +} + +export async function performMarkNotificationsRead(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + + const [last] = await input.db + .select({ notificationId: usersNotifications.notificationId }) + .from(usersNotifications) + .where(eq(usersNotifications.userId, userId)) + .orderBy(desc(usersNotifications.notificationId)) + .limit(1); + + if (last == null) { + return; + } + + await input.db + .update(users) + .set({ lastNotificationRead: last.notificationId }) + .where(eq(users.id, userId)); +} From 374905ae0b1318c7c7471a0c052069d7f6cf6cd1 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 00:53:33 -0300 Subject: [PATCH 046/243] feat(new-deepnotes): group main page, members, and permissions --- new-deepnotes/PLAN_PROGRESS.md | 30 +++-- .../apps/api-worker/src/index.test.ts | 8 ++ new-deepnotes/apps/api-worker/src/index.ts | 82 ++++++++++++++ new-deepnotes/docs/TRPC_REST_MAP.md | 4 +- new-deepnotes/packages/api/src/index.ts | 2 + .../packages/api/src/openapi.test.ts | 2 + new-deepnotes/packages/api/src/openapi.ts | 44 ++++++++ .../packages/api/src/schemas/pages-groups.ts | 12 ++ .../src/account-flows.integration.test.ts | 20 ++++ .../session/src/group-main-and-members.ts | 104 ++++++++++++++++++ .../packages/session/src/group-permissions.ts | 33 ++++-- new-deepnotes/packages/session/src/index.ts | 4 + 12 files changed, 327 insertions(+), 18 deletions(-) create mode 100644 new-deepnotes/packages/session/src/group-main-and-members.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index ffc23210..7ac7bb98 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** [pages/groups slice 1](#pagesgroups-rest--slice-1); **[users.pages prefs slice 2](#userspages-rest--slice-2)** (starting/path, recent/favorites, encrypted defaults, notifications + migration `favorite_page_ids`). **Still ahead:** remaining `groupsRouter` / `pagesRouter`, WS→REST parity, realtime/collab, Stripe. | +| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** [pages/groups slice 1](#pagesgroups-rest--slice-1); [slice 3 — main page + members](#pagesgroups-rest--slice-3); **[users.pages prefs slice 2](#userspages-rest--slice-2)** (starting/path, recent/favorites, encrypted defaults, notifications + migration `favorite_page_ids`). **Still ahead:** group password/privacy/deletion, `pagesRouter` (bump, backlinks, snapshots, move, …), WS→REST parity, realtime/collab, Stripe. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP) / **groups + pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage` + unknown group **404**) / **user page prefs** ([slice 2](#userspages-rest--slice-2): starting + path + favorites + recent remove/clear + default note PATCH + notifications load + mark read). `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP) / **groups + pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage`, [`performGetGroupMainPageId`](#pagesgroups-rest--slice-3), [`performGetGroupMemberUserIds`](#pagesgroups-rest--slice-3) + unknown group **404**) / **user page prefs** ([slice 2](#userspages-rest--slice-2): starting + path + favorites + recent remove/clear + default note PATCH + notifications load + mark read). `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) @@ -46,7 +46,7 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` **How to run locally:** ensure `.env` at `new-deepnotes/.env` has `DATABASE_URL` and (for template create/drop) `DATABASE_ADMIN_URL` with a role that can `CREATE DATABASE`. Then: -- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**18** cases when DB env set, including prefs slice) +- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**18** cases when DB env set — includes groups main-page/members + prefs slice) - `pnpm --filter @deepnotes/db exec vitest run src/template-db.test.ts` CI should set the same vars against the workflow Postgres service (role with `CREATEDB`). @@ -73,7 +73,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **2FA login, bad TOTP** | `authenticatorToken: "111111"` | **401** “Invalid authenticator token.” | | **2FA login with recovery code** | `performUserTwoFactorEnableFinish` → `performSessionLogin` with `recoveryCode` (no TOTP) | **200**-equivalent (`sessionId`); `decryptRecoveryCodes` on row shows **5** hashes left (one consumed). | | **2FA recovery code reuse** | Second `performSessionLogin` with same plaintext recovery code, new IP/UA | **401** “Invalid recovery code.” | -| **Groups + pages (personal)** | `performUserRegister` then `performGetUserGroupIds` / `performListGroupPages` / `performCreatePage` (second page, `parentPageId` = initial page) | `groupIds` = `[personalGroupId]`; list returns initial `pageId`; create returns `numFreePages` **1** for default `plan`; `users.num_free_pages` = 1; two rows in `pages` for group; unknown `groupId` list → **404** `NOT_FOUND`. | +| **Groups + pages (personal)** | `performUserRegister` then `performGetUserGroupIds` / `performListGroupPages` / `performGetGroupMainPageId` / `performGetGroupMemberUserIds` / `performCreatePage` (second page, `parentPageId` = initial page) | `groupIds` = `[personalGroupId]`; list returns initial `pageId`; **main page** = `reg.pageId` (matches `groups.main_page_id`); **members** = `[reg.userId]` (sole `group_members` row); create returns `numFreePages` **1** for default `plan`; `users.num_free_pages` = 1; two rows in `pages` for group; unknown `groupId` list → **404** `NOT_FOUND`. | | **User page prefs** | Register + access JWT; `performGetStartingPageId`; `performGetCurrentPath` (root page + child); unknown page **404**; `performAddFavoritePages` / `performRemoveFavoritePages` (order on `users.favorite_page_ids`); `performRemoveRecentPages` bogus id **404** / missing child in recent **404** / remove root then `recent` empty + `performClearRecentPages`; `performPatchDefaultNote`; insert `notifications` + `users_notifications` → `performLoadNotifications` (base64 ciphertext) + `performMarkNotificationsRead` → `users.last_notification_read` | Matches legacy semantics where tested; favorites column from migration `0001_favorite_page_ids`. | **`@deepnotes/db` real Postgres (`template-db.test.ts`):** @@ -152,9 +152,22 @@ CI should set the same vars against the workflow Postgres service (role with `CR **Cutover note:** Existing production users who had favorites only in KeyDB will see an **empty** `favorite_page_ids` after migration until a one-off backfill is run (if ever needed); new installs and new favorites use Postgres only. +### Pages/groups REST — slice 3 + +**Goal:** legacy `groups.getMainPageId` and `groups.getUserIds` without KeyDB — sourced from Postgres, with permission rules aligned to `@deeplib/data` / `@deeplib/misc` (public **read pages** does **not** imply **view members**). + +| Layer | What shipped | +|-------|----------------| +| **`@deepnotes/session`** | `group-permissions.ts` — `viewGroupMembers` on role rows (all five roles **true**); public-only path still grants only `viewGroupPages`. `group-main-and-members.ts` — `performGetGroupMainPageId` (`viewGroupPages`), `performGetGroupMemberUserIds` (union `group_members`, `group_join_requests`, `group_join_invitations`; deduped set). | +| **`@deepnotes/api`** | `groupMainPageResponseSchema`, `groupMemberUserIdsResponseSchema` in `schemas/pages-groups.ts`; OpenAPI `GET /api/groups/{groupId}/main-page` and `…/members`. | +| **`@deepnotes/api-worker`** | Hono: same paths, **200** JSON. | +| **Tests** | Integration extends **groups + pages** case; worker **503** matrix + `openapi.test.ts` paths. | + +**Intentional vs legacy tRPC:** old `getMainPageId` was auth-only (no explicit permission); REST requires **`viewGroupPages`** like `getPages`. **`getUserIds`** matches legacy union + `viewGroupMembers` (stricter than anonymous public read). + ### Not started (Phase 3 — pages, groups, infra) -- [ ] **Remaining `groupsRouter`:** `main-page`, `members`, password, privacy, soft delete / restore / purge. +- [ ] **Remaining `groupsRouter`:** password enable/change/disable, privacy (public / join-requests / private), soft delete / restore / purge. - [ ] **Remaining `pagesRouter`:** bump, backlinks, snapshots, deletion, move (plus **`groupCreation`** on create if still required for parity). - [ ] **Realtime / collab** (new or adapted protocols; no key rotation). - [ ] **Stripe:** `POST /api/webhooks/stripe`, checkout/portal (no RevenueCat); wire **`deleteStripeCustomer`** from account delete when keys exist. @@ -207,8 +220,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | | **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**18** cases when DB env set) — … + **groups/pages** + **user page prefs** ([slice 2](#userspages-rest--slice-2)); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; **Pro-only** create in non-personal group; **viewer** cannot create; optional `groupCreation` on create; pagination edge cases on `performLoadNotifications` | -| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA + **me/groups** + **users/pages\*** prefs paths + **groups/{id}/pages**); **`schemas/users.test.ts`** (email/password change, 2fa finish); **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; Zod tests for `pages-groups` / `user-pages` query edge cases | -| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **34** tests (503 matrix when env/Hyperdrive missing) — **2FA**, **groups/pages**, **users/pages prefs** | **200** tests with stub `SessionEnv` + template DB (heavier) | +| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA + **me/groups** + **users/pages\*** prefs + **groups/{id}/main-page** + **members** + **pages**); **`schemas/users.test.ts`** (email/password change, 2fa finish); **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; Zod tests for `pages-groups` / `user-pages` query edge cases | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **36** tests (503 matrix when env/Hyperdrive missing) — **2FA**, **groups** (main-page, members, pages), **users/pages prefs** | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -241,7 +254,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Phase 3 working order (suggested) -Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2FA + refresh guards; **`@deepnotes/db`** FKs through **`group_members`**. **(done)** First **pages/groups** slice + **`users.pages` prefs** ([slice 1](#pagesgroups-rest--slice-1), [slice 2](#userspages-rest--slice-2)). **(next)** **`groupsRouter`** (members, main page, password, privacy, delete/restore/purge); **`pagesRouter`** (bump, backlinks, snapshots, delete, move; optional **`groupCreation`** on create); each slice + **template DB** tests where SQL risk is high. **(then)** **realtime + collab** (no key rotation) and **Stripe** + wire billing hooks on account routes. +Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2FA + refresh guards; **`@deepnotes/db`** FKs through **`group_members`**. **(done)** **pages/groups** [slice 1](#pagesgroups-rest--slice-1) (list/create pages, group ids), [slice 3](#pagesgroups-rest--slice-3) (main-page, member user ids); **`users.pages` prefs** [slice 2](#userspages-rest--slice-2). **(next)** **`groupsRouter`** remainder: password, privacy, soft delete / restore / purge; **`pagesRouter`** (bump, backlinks, snapshots, delete, move; optional **`groupCreation`** on create); WS flows → REST per [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md). **(then)** **realtime + collab** (no key rotation) and **Stripe** + billing hooks on account routes. --- @@ -249,6 +262,7 @@ Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2F | Date | Change | |------|--------| +| 2026-04-27 | **Phase 3 — groups main-page + members (slice 3):** `performGetGroupMainPageId` / `performGetGroupMemberUserIds` (`group-main-and-members.ts`); `viewGroupMembers` in `group-permissions.ts` (public-only still **not** enough for members list); `GET /api/groups/:groupId/main-page` + `…/members`; OpenAPI + schemas; integration extends **groups + pages**; TRPC_REST_MAP **implemented** for `getMainPageId` / `getUserIds`; worker **503** matrix **36** rows; PLAN_PROGRESS slice 3 + working-order refresh. | | 2026-04-27 | **Phase 3 — `users.pages` prefs (slice 2):** migration `0001_favorite_page_ids`; `user-page-prefs.ts` + Hono/OpenAPI routes (starting, path, recent, favorites, defaults PATCH, notifications); `schemas/user-pages.ts`; integration test **user page prefs**; TRPC_REST_MAP marked implemented; PLAN_PROGRESS sections + matrix counts (**18** session integration, **34** worker 503 rows). | | 2026-04-27 | **Phase 3 — pages/groups slice 1:** `performGetUserGroupIds`, `performListGroupPages`, `performCreatePage` + `group-permissions.ts`; OpenAPI + Zod `pages-groups.ts`; api-worker `GET /api/users/me/groups`, `GET/POST /api/groups/:groupId/pages` (**201** create); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) marked implemented for `getGroupIds`, `getPages`, `pages.create`; integration test **groups + pages**; PLAN_PROGRESS [Pages/groups REST — slice 1](#pagesgroups-rest--slice-1) + matrix bumps. | | 2026-04-27 | **More real Postgres tests:** `account-flows.integration.test.ts` — **2FA recovery-code** login + one-time use + `decryptRecoveryCodes` count; **replay** of first refresh JWT after two rotations (**401**); **`loggedIn`** / missing refresh guards. `template-db.test.ts` — **`group_members`** FK to `users` and to `groups`. PLAN_PROGRESS: run commands, expanded tables, matrix + success criteria + working order. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 1309b87a..66b1be7e 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -42,6 +42,14 @@ describe("api-worker", () => { ["PATCH", "/api/users/me/defaults/arrow"], ["GET", "/api/users/me/notifications"], ["POST", "/api/users/me/notifications/read"], + [ + "GET", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/main-page", + ], + [ + "GET", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/members", + ], [ "GET", "/api/groups/aaaaaaaaaaaaaaaaaaaaa/pages", diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 48f22b00..423ebcae 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -1163,6 +1163,88 @@ app.post("/api/users/me/notifications/read", async (c) => { } }); +app.get("/api/groups/:groupId/main-page", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGetGroupMainPageId } = await import("@deepnotes/session"); + const out = await performGetGroupMainPageId({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.get("/api/groups/:groupId/members", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGetGroupMemberUserIds } = await import("@deepnotes/session"); + const out = await performGetGroupMemberUserIds({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.get("/api/groups/:groupId/pages", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index 6ffcdab8..172eb662 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -51,8 +51,8 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | Legacy procedure | Proposed REST / notes | |------------------|----------------------| -| `groups.getMainPageId` | `GET /api/groups/:groupId/main-page` | -| `groups.getUserIds` | `GET /api/groups/:groupId/members` (ids / minimal DTO) | +| `groups.getMainPageId` | `GET /api/groups/:groupId/main-page` (**implemented** — `performGetGroupMainPageId`; `groups.main_page_id`; requires `viewGroupPages`) | +| `groups.getUserIds` | `GET /api/groups/:groupId/members` (**implemented** — `performGetGroupMemberUserIds`; members ∪ join requests ∪ invitations; requires `viewGroupMembers`, not public-only read) | | `groups.getPages` | `GET /api/groups/:groupId/pages` (**implemented** — `performListGroupPages`; query `lastPageId`; soft-deleted pages excluded) | | `groups.password.enable` | `POST /api/groups/:groupId/password` | | `groups.password.change` | `PATCH /api/groups/:groupId/password` | diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index 000e012a..ce8e9d0f 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -25,6 +25,8 @@ export { } from "./schemas/sessions.js"; export { groupIdPathSchema, + groupMainPageResponseSchema, + groupMemberUserIdsResponseSchema, groupPageCreateRequestSchema, groupPageCreateResponseSchema, groupPagesListQuerySchema, diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 09c64bba..c1f3e2a4 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -56,6 +56,8 @@ describe("getOpenApiDocument", () => { expect(doc.paths?.["/api/users/me/defaults/arrow"]?.patch).toBeDefined(); expect(doc.paths?.["/api/users/me/notifications"]?.get).toBeDefined(); expect(doc.paths?.["/api/users/me/notifications/read"]?.post).toBeDefined(); + expect(doc.paths?.["/api/groups/{groupId}/main-page"]?.get).toBeDefined(); + expect(doc.paths?.["/api/groups/{groupId}/members"]?.get).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/pages"]?.get).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/pages"]?.post).toBeDefined(); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index 7974f735..058512a6 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -18,6 +18,8 @@ import { } from "./schemas/sessions.js"; import { groupIdPathSchema, + groupMainPageResponseSchema, + groupMemberUserIdsResponseSchema, groupPageCreateRequestSchema, groupPageCreateResponseSchema, groupPagesListQuerySchema, @@ -510,6 +512,48 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "get", + path: "/api/groups/{groupId}/main-page", + summary: "Get the group main page id", + description: + "Replaces legacy `groups.getMainPageId` (KeyDB `main-page-id`). Source: `groups.main_page_id`. Requires `viewGroupPages` (same as listing pages).", + request: { params: groupIdPathSchema }, + responses: { + 200: { + description: "Main page id for the group.", + content: { + "application/json": { schema: groupMainPageResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "get", + path: "/api/groups/{groupId}/members", + summary: "List user ids (members, requests, invitations)", + description: + "Replaces legacy `groups.getUserIds`: union of `group_members`, `group_join_requests`, and `group_join_invitations` for the group. Requires `viewGroupMembers` (not granted for public read without membership).", + request: { params: groupIdPathSchema }, + responses: { + 200: { + description: "Distinct user ids (unordered).", + content: { + "application/json": { schema: groupMemberUserIdsResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "get", path: "/api/groups/{groupId}/pages", diff --git a/new-deepnotes/packages/api/src/schemas/pages-groups.ts b/new-deepnotes/packages/api/src/schemas/pages-groups.ts index dc799f89..cda5acb2 100644 --- a/new-deepnotes/packages/api/src/schemas/pages-groups.ts +++ b/new-deepnotes/packages/api/src/schemas/pages-groups.ts @@ -64,3 +64,15 @@ export const groupPageCreateResponseSchema = z numFreePages: z.number().int().optional(), }) .openapi("GroupPageCreateResponse"); + +export const groupMainPageResponseSchema = z + .object({ + mainPageId: z.string(), + }) + .openapi("GroupMainPageResponse"); + +export const groupMemberUserIdsResponseSchema = z + .object({ + userIds: z.array(z.string()), + }) + .openapi("GroupMemberUserIdsResponse"); diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index 5129c4b2..c6de2ce8 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -51,6 +51,10 @@ import { ensureSodiumReady, } from "./crypto/session-crypto.js"; import type { UserRegisterInput } from "./register-user.js"; +import { + performGetGroupMainPageId, + performGetGroupMemberUserIds, +} from "./group-main-and-members.js"; import { performCreatePage, performListGroupPages, @@ -1280,6 +1284,22 @@ describe.skipIf(resolveTemplateContext() == null)( expect(listed.hasMore).toBe(false); expect(listed.pageIds).toEqual([reg.pageId]); + const main = await performGetGroupMainPageId({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + }); + expect(main.mainPageId).toBe(reg.pageId); + + const members = await performGetGroupMemberUserIds({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + }); + expect(members.userIds.sort()).toEqual([reg.userId]); + const newPageId = nanoid(); const out = await performCreatePage({ db, diff --git a/new-deepnotes/packages/session/src/group-main-and-members.ts b/new-deepnotes/packages/session/src/group-main-and-members.ts new file mode 100644 index 00000000..07a8eb38 --- /dev/null +++ b/new-deepnotes/packages/session/src/group-main-and-members.ts @@ -0,0 +1,104 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { + groupJoinInvitations, + groupJoinRequests, + groupMembers, + groups, +} from "@deepnotes/db/schema"; +import { eq } from "drizzle-orm"; + +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { userHasGroupPermission } from "./group-permissions.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; + +/** + * Replaces legacy `groups.getMainPageId` with an explicit `viewGroupPages` + * check (legacy tRPC had auth only; REST aligns with `groups.getPages`). + */ +export async function performGetGroupMainPageId(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; +}): Promise<{ mainPageId: string }> { + const { userId } = await getAuthenticatedUserSummary(input); + + const [row] = await input.db + .select({ mainPageId: groups.mainPageId }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + + if (row == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + + const allowed = await userHasGroupPermission({ + db: input.db, + userId, + groupId: input.groupId, + permission: "viewGroupPages", + }); + if (!allowed) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + return { mainPageId: row.mainPageId }; +} + +/** + * Replaces legacy `groups.getUserIds`: members, pending join requests, and + * invitations (SQL UNION). Requires `viewGroupMembers` — not granted for + * public read without membership (legacy `@deeplib/data` `userHasPermission`). + */ +export async function performGetGroupMemberUserIds(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; +}): Promise<{ userIds: string[] }> { + const { userId } = await getAuthenticatedUserSummary(input); + + const [groupRow] = await input.db + .select({ id: groups.id }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + + if (groupRow == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + + const allowed = await userHasGroupPermission({ + db: input.db, + userId, + groupId: input.groupId, + permission: "viewGroupMembers", + }); + if (!allowed) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const [memberRows, requestRows, invitationRows] = await Promise.all([ + input.db + .select({ userId: groupMembers.userId }) + .from(groupMembers) + .where(eq(groupMembers.groupId, input.groupId)), + input.db + .select({ userId: groupJoinRequests.userId }) + .from(groupJoinRequests) + .where(eq(groupJoinRequests.groupId, input.groupId)), + input.db + .select({ userId: groupJoinInvitations.userId }) + .from(groupJoinInvitations) + .where(eq(groupJoinInvitations.groupId, input.groupId)), + ]); + + const ids = new Set(); + for (const r of memberRows) ids.add(r.userId); + for (const r of requestRows) ids.add(r.userId); + for (const r of invitationRows) ids.add(r.userId); + + return { userIds: [...ids] }; +} diff --git a/new-deepnotes/packages/session/src/group-permissions.ts b/new-deepnotes/packages/session/src/group-permissions.ts index eabb04de..d1941da8 100644 --- a/new-deepnotes/packages/session/src/group-permissions.ts +++ b/new-deepnotes/packages/session/src/group-permissions.ts @@ -5,22 +5,37 @@ import { and, eq } from "drizzle-orm"; /** Mirrors legacy `@deeplib/misc` roles for `group_members.role` text. */ const ROLE_PERMISSIONS: Record< string, - { viewGroupPages: boolean; editGroupPages: boolean } + { + viewGroupPages: boolean; + editGroupPages: boolean; + viewGroupMembers: boolean; + } > = { - owner: { viewGroupPages: true, editGroupPages: true }, - admin: { viewGroupPages: true, editGroupPages: true }, - moderator: { viewGroupPages: true, editGroupPages: true }, - member: { viewGroupPages: true, editGroupPages: true }, - viewer: { viewGroupPages: true, editGroupPages: false }, + owner: { viewGroupPages: true, editGroupPages: true, viewGroupMembers: true }, + admin: { viewGroupPages: true, editGroupPages: true, viewGroupMembers: true }, + moderator: { + viewGroupPages: true, + editGroupPages: true, + viewGroupMembers: true, + }, + member: { viewGroupPages: true, editGroupPages: true, viewGroupMembers: true }, + viewer: { + viewGroupPages: true, + editGroupPages: false, + viewGroupMembers: true, + }, }; -export type GroupPagePermission = "viewGroupPages" | "editGroupPages"; +export type GroupPermission = + | "viewGroupPages" + | "editGroupPages" + | "viewGroupMembers"; export async function userHasGroupPermission(input: { db: DeepnotesDb; userId: string; groupId: string; - permission: GroupPagePermission; + permission: GroupPermission; }): Promise { const [group] = await input.db .select({ accessKeyring: groups.accessKeyring }) @@ -50,6 +65,8 @@ export async function userHasGroupPermission(input: { } } + // Legacy `userHasPermission`: public groups grant `viewGroupPages` only, not + // `viewGroupMembers` (see `@deeplib/data` roles.ts). return ( group.accessKeyring != null && input.permission === "viewGroupPages" ); diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 7a090a11..ebb3df2f 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -31,6 +31,10 @@ export { performListGroupPages, } from "./group-pages.js"; export type { CreatePageBody } from "./group-pages.js"; +export { + performGetGroupMainPageId, + performGetGroupMemberUserIds, +} from "./group-main-and-members.js"; export { performGetUserGroupIds } from "./user-group-ids.js"; export { performAddFavoritePages, From 73aeaa8a3586c156855e095c6bf630ab7ac26a34 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 01:02:22 -0300 Subject: [PATCH 047/243] feat(new-deepnotes): group delete, password, privacy, and user plan --- new-deepnotes/PLAN_PROGRESS.md | 37 +- .../apps/api-worker/src/index.test.ts | 26 ++ new-deepnotes/apps/api-worker/src/index.ts | 438 +++++++++++++++++- .../apps/api-worker/src/session-env.ts | 7 +- new-deepnotes/docs/TRPC_REST_MAP.md | 16 +- new-deepnotes/packages/api/src/index.ts | 5 + .../packages/api/src/openapi.test.ts | 12 + new-deepnotes/packages/api/src/openapi.ts | 218 +++++++++ .../packages/api/src/schemas/pages-groups.ts | 35 ++ .../src/account-flows.integration.test.ts | 172 +++++++ .../session/src/crypto/session-crypto.ts | 31 ++ new-deepnotes/packages/session/src/env.ts | 2 + .../packages/session/src/group-deletion.ts | 147 ++++++ .../packages/session/src/group-password.ts | 217 +++++++++ .../packages/session/src/group-permissions.ts | 27 +- .../packages/session/src/group-privacy.ts | 103 ++++ new-deepnotes/packages/session/src/index.ts | 14 + .../src/send-email-change-code.test.ts | 3 + .../packages/session/src/user-plan.ts | 26 ++ new-deepnotes/template.env | 1 + 20 files changed, 1513 insertions(+), 24 deletions(-) create mode 100644 new-deepnotes/packages/session/src/group-deletion.ts create mode 100644 new-deepnotes/packages/session/src/group-password.ts create mode 100644 new-deepnotes/packages/session/src/group-privacy.ts create mode 100644 new-deepnotes/packages/session/src/user-plan.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 7ac7bb98..d123295b 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** [pages/groups slice 1](#pagesgroups-rest--slice-1); [slice 3 — main page + members](#pagesgroups-rest--slice-3); **[users.pages prefs slice 2](#userspages-rest--slice-2)** (starting/path, recent/favorites, encrypted defaults, notifications + migration `favorite_page_ids`). **Still ahead:** group password/privacy/deletion, `pagesRouter` (bump, backlinks, snapshots, move, …), WS→REST parity, realtime/collab, Stripe. | +| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** [pages/groups slice 1](#pagesgroups-rest--slice-1); [slice 3 — main page + members](#pagesgroups-rest--slice-3); **[users.pages prefs slice 2](#userspages-rest--slice-2)**; **[groups — password, privacy, deletion slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion)**. **Still ahead:** `pagesRouter` (bump, backlinks, snapshots, move, optional `groupCreation`), WS join/invite/role (or REST in TRPC map), `POST …/privacy/private` (re-key payload; no legacy rotation), realtime/collab, Stripe. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP) / **groups + pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage`, [`performGetGroupMainPageId`](#pagesgroups-rest--slice-3), [`performGetGroupMemberUserIds`](#pagesgroups-rest--slice-3) + unknown group **404**) / **user page prefs** ([slice 2](#userspages-rest--slice-2): starting + path + favorites + recent remove/clear + default note PATCH + notifications load + mark read). `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP) / **groups + pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage`, [`performGetGroupMainPageId`](#pagesgroups-rest--slice-3), [`performGetGroupMemberUserIds`](#pagesgroups-rest--slice-3) + unknown group **404**) / **user page prefs** ([slice 2](#userspages-rest--slice-2): starting + path + favorites + recent remove/clear + default note PATCH + notifications load + mark read) / **[group password, privacy, soft delete, purge, restore after purge (slice 4)](#pagesgroups-rest--slice-4-group-password-privacy-deletion)**. `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) @@ -46,7 +46,7 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` **How to run locally:** ensure `.env` at `new-deepnotes/.env` has `DATABASE_URL` and (for template create/drop) `DATABASE_ADMIN_URL` with a role that can `CREATE DATABASE`. Then: -- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**18** cases when DB env set — includes groups main-page/members + prefs slice) +- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**19** cases when DB env set — includes groups + prefs + [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) group admin flows) - `pnpm --filter @deepnotes/db exec vitest run src/template-db.test.ts` CI should set the same vars against the workflow Postgres service (role with `CREATEDB`). @@ -75,6 +75,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **2FA recovery code reuse** | Second `performSessionLogin` with same plaintext recovery code, new IP/UA | **401** “Invalid recovery code.” | | **Groups + pages (personal)** | `performUserRegister` then `performGetUserGroupIds` / `performListGroupPages` / `performGetGroupMainPageId` / `performGetGroupMemberUserIds` / `performCreatePage` (second page, `parentPageId` = initial page) | `groupIds` = `[personalGroupId]`; list returns initial `pageId`; **main page** = `reg.pageId` (matches `groups.main_page_id`); **members** = `[reg.userId]` (sole `group_members` row); create returns `numFreePages` **1** for default `plan`; `users.num_free_pages` = 1; two rows in `pages` for group; unknown `groupId` list → **404** `NOT_FOUND`. | | **User page prefs** | Register + access JWT; `performGetStartingPageId`; `performGetCurrentPath` (root page + child); unknown page **404**; `performAddFavoritePages` / `performRemoveFavoritePages` (order on `users.favorite_page_ids`); `performRemoveRecentPages` bogus id **404** / missing child in recent **404** / remove root then `recent` empty + `performClearRecentPages`; `performPatchDefaultNote`; insert `notifications` + `users_notifications` → `performLoadNotifications` (base64 ciphertext) + `performMarkNotificationsRead` → `users.last_notification_read` | Matches legacy semantics where tested; favorites column from migration `0001_favorite_page_ids`. | +| **Group password, privacy, deletion (slice 4)** | `UPDATE users` → `plan = pro` on registered user; `performGroupPasswordEnable` / `Change` / `Disable` on **personal** group; `performGroupPrivacyMakePublic` after `accessKeyring` cleared; `performGroupPrivacySetJoinRequestsAllowed` **false**; `performGroupSoftDelete` (future `permanent_deletion_date`) → `Restore` (null) → `SoftDelete` + `Purge` (past date); `Restore` after purge **400** | PHC + server-side encrypt via `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`; `group-permissions` includes **`editGroupSettings`** (owner/admin only). | **`@deepnotes/db` real Postgres (`template-db.test.ts`):** @@ -165,10 +166,25 @@ CI should set the same vars against the workflow Postgres service (role with `CR **Intentional vs legacy tRPC:** old `getMainPageId` was auth-only (no explicit permission); REST requires **`viewGroupPages`** like `getPages`. **`getUserIds`** matches legacy union + `viewGroupMembers` (stricter than anonymous public read). +### Pages/groups REST — slice 4 (group password, privacy, deletion) + +**Goal:** legacy `groups.password.*`, `groups.privacy.*` (public + join-requests), `groups.deletion.*` on REST + Drizzle — no KeyDB/redlock; Pro gating where legacy used `assertUserSubscribed`. + +| Layer | What shipped | +|-------|----------------| +| **`@deepnotes/session`** | `group-permissions.ts` — new permission **`editGroupSettings`** (owner/admin only, per `@deeplib/misc`); `user-plan.ts` — `assertUserProPlan` (Pro-only flows). `crypto/session-crypto.ts` — `computeGroupPasswordPhc` (Argon2id), `encryptGroupRehashedPasswordHash` / `decrypt...` (context `GroupRehashedPasswordHash` + **`GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`**). `group-password.ts` — enable / change / disable. `group-privacy.ts` — make public (clears `group_members` + `group_join_invitations` `encrypted_access_keyring`); set join-requests. `group-deletion.ts` — soft delete (~+1 month), restore (grace only), purge (past date). `env.ts` — required **`GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`**. | +| **`@deepnotes/api`** | `schemas/pages-groups.ts` — password + privacy Zod; OpenAPI: `POST|PATCH|DELETE …/password`, `POST …/privacy/public`, `PATCH …/privacy/join-requests`, `DELETE /api/groups/{groupId}`, `POST …/restore`, `POST …/purge`. | +| **`@deepnotes/api-worker`** | Hono: same paths; `byteB64` pass-through to session (no double-decode). Worker **`getSessionEnv`** requires the new key. `503` matrix **+8** routes (**44** total 503 cases). | +| **Tests** | [Integration](#phase-3-test-coverage-detail): personal group set to Pro in DB, full password + privacy + delete cycle + restore after purge **400**; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) **implemented** rows. | + +**Not in this slice:** `POST /api/groups/:groupId/privacy/private` (make-private re-key) — see **Not started (Phase 3 — pages, groups, infra)** below; legacy used two-step WS with rotation; greenfield = one REST with full re-key body when built. + ### Not started (Phase 3 — pages, groups, infra) -- [ ] **Remaining `groupsRouter`:** password enable/change/disable, privacy (public / join-requests / private), soft delete / restore / purge. +- [x] **Groups (REST, slice 4):** [password, privacy, soft delete, restore, purge](#pagesgroups-rest--slice-4-group-password-privacy-deletion) — `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY` on `SessionEnv` / worker bindings. +- [ ] **Groups (still):** `POST /api/groups/:groupId/privacy/private` — re-key to private without legacy rotation (single REST body like legacy `rotateGroupKeys` + **`websocket/groups/privacy/make-private`**); document vs OpenAPI. - [ ] **Remaining `pagesRouter`:** bump, backlinks, snapshots, deletion, move (plus **`groupCreation`** on create if still required for parity). +- [ ] **WS / REST:** group join/invite, member role, etc. (see [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) WebSocket table). - [ ] **Realtime / collab** (new or adapted protocols; no key rotation). - [ ] **Stripe:** `POST /api/webhooks/stripe`, checkout/portal (no RevenueCat); wire **`deleteStripeCustomer`** from account delete when keys exist. @@ -219,9 +235,9 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**18** cases when DB env set) — … + **groups/pages** + **user page prefs** ([slice 2](#userspages-rest--slice-2)); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; **Pro-only** create in non-personal group; **viewer** cannot create; optional `groupCreation` on create; pagination edge cases on `performLoadNotifications` | -| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA + **me/groups** + **users/pages\*** prefs + **groups/{id}/main-page** + **members** + **pages**); **`schemas/users.test.ts`** (email/password change, 2fa finish); **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; Zod tests for `pages-groups` / `user-pages` query edge cases | -| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **36** tests (503 matrix when env/Hyperdrive missing) — **2FA**, **groups** (main-page, members, pages), **users/pages prefs** | **200** tests with stub `SessionEnv` + template DB (heavier) | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**19** cases when DB env set) — … + **groups/pages** + **user page prefs** ([slice 2](#userspages-rest--slice-2)) + [slice 4 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion) (`GROUP_*` + Pro); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; **Pro-only** create in non-personal group; **viewer** cannot create; optional `groupCreation` on create; pagination edge cases on `performLoadNotifications` | +| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA + **me/groups** + **users/pages\*** prefs + **groups** list/create + **main-page** + **members** + [slice 4 **password/privacy/deletion** paths](#pagesgroups-rest--slice-4-group-password-privacy-deletion)); **`schemas/users.test.ts`** (email/password change, 2fa finish); **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; Zod tests for `pages-groups` / `user-pages` query edge cases | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **44** tests (503 matrix when env/Hyperdrive missing) — [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) **+8** (password ×3, privacy ×2, `DELETE` group, restore, purge) | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -242,8 +258,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Drizzle migrations from empty DB documented for production upgrades. - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). -- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation, login + refresh + **stale JWT replay**, refresh **cookie guards**, **2FA** TOTP + **recovery codes**, **groups list + page list + page create**, **users.pages prefs** incl. notifications + `favorite_page_ids`). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** as in [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) (**18** session + 6 db integration cases when DB env is set). **Next:** Redis failed-login integration; refresh **expired** JWT; more **pages/groups** routes; Stripe when billing exists. +- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation, login + refresh + **stale JWT replay**, refresh **cookie guards**, **2FA** TOTP + **recovery codes**, **groups list + page list + page create**, **users.pages prefs** incl. notifications + `favorite_page_ids`, [group password / privacy / deletion (slice 4)](#pagesgroups-rest--slice-4-group-password-privacy-deletion)). +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** as in [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) (**19** session + 6 db integration cases when DB env is set; includes [slice 4 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion)). **Next:** Redis failed-login integration; refresh **expired** JWT; **pages** router; **`POST …/privacy/private`**; Stripe when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -254,7 +270,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Phase 3 working order (suggested) -Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2FA + refresh guards; **`@deepnotes/db`** FKs through **`group_members`**. **(done)** **pages/groups** [slice 1](#pagesgroups-rest--slice-1) (list/create pages, group ids), [slice 3](#pagesgroups-rest--slice-3) (main-page, member user ids); **`users.pages` prefs** [slice 2](#userspages-rest--slice-2). **(next)** **`groupsRouter`** remainder: password, privacy, soft delete / restore / purge; **`pagesRouter`** (bump, backlinks, snapshots, delete, move; optional **`groupCreation`** on create); WS flows → REST per [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md). **(then)** **realtime + collab** (no key rotation) and **Stripe** + billing hooks on account routes. +Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2FA + refresh guards; **`@deepnotes/db`** FKs through **`group_members`**. **(done)** **pages/groups** [slice 1](#pagesgroups-rest--slice-1) (list/create pages, group ids), [slice 3](#pagesgroups-rest--slice-3) (main-page, member user ids), [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) (group password, privacy public + join-requests, soft delete, restore, purge). **`users.pages` prefs** [slice 2](#userspages-rest--slice-2). **(next)** **`POST …/privacy/private`** (re-key) or defer until client needs it; **`pagesRouter`** (bump, backlinks, snapshots, delete, move; optional **`groupCreation`** on create); WS table in [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) (join, invite, member role, …). **(then)** **realtime + collab** (no key rotation) and **Stripe** + billing hooks on account routes. --- @@ -262,6 +278,7 @@ Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2F | Date | Change | |------|--------| +| 2026-04-27 | **Phase 3 — groups slice 4 (password, privacy, deletion):** `editGroupSettings` in `group-permissions.ts`; `SESSION` `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`; `user-plan.ts` (`assertUserProPlan`); `group-password.ts` / `group-privacy.ts` / `group-deletion.ts`; Hono + OpenAPI + Zod; worker **503** **44** tests; `account-flows` **19** integration cases; **TRPC_REST_MAP** updated; [slice 4 section](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + not-started `privacy/private`. | | 2026-04-27 | **Phase 3 — groups main-page + members (slice 3):** `performGetGroupMainPageId` / `performGetGroupMemberUserIds` (`group-main-and-members.ts`); `viewGroupMembers` in `group-permissions.ts` (public-only still **not** enough for members list); `GET /api/groups/:groupId/main-page` + `…/members`; OpenAPI + schemas; integration extends **groups + pages**; TRPC_REST_MAP **implemented** for `getMainPageId` / `getUserIds`; worker **503** matrix **36** rows; PLAN_PROGRESS slice 3 + working-order refresh. | | 2026-04-27 | **Phase 3 — `users.pages` prefs (slice 2):** migration `0001_favorite_page_ids`; `user-page-prefs.ts` + Hono/OpenAPI routes (starting, path, recent, favorites, defaults PATCH, notifications); `schemas/user-pages.ts`; integration test **user page prefs**; TRPC_REST_MAP marked implemented; PLAN_PROGRESS sections + matrix counts (**18** session integration, **34** worker 503 rows). | | 2026-04-27 | **Phase 3 — pages/groups slice 1:** `performGetUserGroupIds`, `performListGroupPages`, `performCreatePage` + `group-permissions.ts`; OpenAPI + Zod `pages-groups.ts`; api-worker `GET /api/users/me/groups`, `GET/POST /api/groups/:groupId/pages` (**201** create); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) marked implemented for `getGroupIds`, `getPages`, `pages.create`; integration test **groups + pages**; PLAN_PROGRESS [Pages/groups REST — slice 1](#pagesgroups-rest--slice-1) + matrix bumps. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 66b1be7e..8baa032e 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -58,6 +58,32 @@ describe("api-worker", () => { "POST", "/api/groups/aaaaaaaaaaaaaaaaaaaaa/pages", ], + [ + "POST", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/password", + ], + [ + "PATCH", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/password", + ], + [ + "DELETE", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/password", + ], + [ + "POST", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/privacy/public", + ], + [ + "PATCH", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/privacy/join-requests", + ], + ["DELETE", "/api/groups/aaaaaaaaaaaaaaaaaaaaa"], + [ + "POST", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/restore", + ], + ["POST", "/api/groups/aaaaaaaaaaaaaaaaaaaaa/purge"], ["GET", "/api/users/me"], ["POST", "/api/users/me/password"], ["DELETE", "/api/users/me"], diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 423ebcae..d241e725 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -4,6 +4,11 @@ import { getOpenApiDocument, groupPageCreateRequestSchema, groupPagesListQuerySchema, + groupPasswordChangeRequestSchema, + groupPasswordDisableRequestSchema, + groupPasswordEnableRequestSchema, + groupPrivacyJoinRequestsPatchSchema, + groupPrivacyPublicRequestSchema, healthResponseSchema, userDefaultArrowPatchSchema, userDefaultNotePatchSchema, @@ -38,7 +43,7 @@ const app = new Hono<{ Bindings: Bindings }>(); const serviceUnavailableBody = { code: "SERVICE_UNAVAILABLE" as const, message: - "Session routes require ACCESS_SECRET, REFRESH_SECRET, USER_EMAIL_SECRET, USER_EMAIL_ENCRYPTION_KEY, USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, and USER_RECOVERY_CODES_ENCRYPTION_KEY (e.g. Wrangler secrets / .dev.vars).", + "Session routes require ACCESS_SECRET, REFRESH_SECRET, USER_EMAIL_SECRET, USER_EMAIL_ENCRYPTION_KEY, USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, USER_RECOVERY_CODES_ENCRYPTION_KEY, and GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY (e.g. Wrangler secrets / .dev.vars).", }; function appendSetCookies(res: Response, lines: string[]): void { @@ -1360,6 +1365,437 @@ app.post("/api/groups/:groupId/pages", async (c) => { } }); +app.post("/api/groups/:groupId/password", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = groupPasswordEnableRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupPasswordEnable } = await import("@deepnotes/session"); + await performGroupPasswordEnable({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + groupPasswordHash: parsed.data.groupPasswordHash, + groupEncryptedContentKeyring: parsed.data.groupEncryptedContentKeyring, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.patch("/api/groups/:groupId/password", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = groupPasswordChangeRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupPasswordChange } = await import("@deepnotes/session"); + await performGroupPasswordChange({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + groupCurrentPasswordHash: parsed.data.groupCurrentPasswordHash, + groupNewPasswordHash: parsed.data.groupNewPasswordHash, + groupEncryptedContentKeyring: parsed.data.groupEncryptedContentKeyring, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.delete("/api/groups/:groupId/password", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = groupPasswordDisableRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupPasswordDisable } = await import("@deepnotes/session"); + await performGroupPasswordDisable({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + groupPasswordHash: parsed.data.groupPasswordHash, + groupEncryptedContentKeyring: parsed.data.groupEncryptedContentKeyring, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/groups/:groupId/privacy/public", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = groupPrivacyPublicRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupPrivacyMakePublic } = await import( + "@deepnotes/session" + ); + await performGroupPrivacyMakePublic({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + accessKeyring: parsed.data.accessKeyring, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.patch("/api/groups/:groupId/privacy/join-requests", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = groupPrivacyJoinRequestsPatchSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupPrivacySetJoinRequestsAllowed } = await import( + "@deepnotes/session" + ); + await performGroupPrivacySetJoinRequestsAllowed({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + areJoinRequestsAllowed: parsed.data.areJoinRequestsAllowed, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.delete("/api/groups/:groupId", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupSoftDelete } = await import("@deepnotes/session"); + await performGroupSoftDelete({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/groups/:groupId/restore", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupRestore } = await import("@deepnotes/session"); + await performGroupRestore({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/groups/:groupId/purge", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupPurge } = await import("@deepnotes/session"); + await performGroupPurge({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.get("/api/users/me", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/apps/api-worker/src/session-env.ts b/new-deepnotes/apps/api-worker/src/session-env.ts index d58bd78e..5a275f27 100644 --- a/new-deepnotes/apps/api-worker/src/session-env.ts +++ b/new-deepnotes/apps/api-worker/src/session-env.ts @@ -8,6 +8,8 @@ export type WorkerSessionBindings = { USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY?: string; USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY?: string; USER_RECOVERY_CODES_ENCRYPTION_KEY?: string; + /** Base64 symmetric key for `groups.encrypted_rehashed_password_hash` (legacy `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`). */ + GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY?: string; DEV?: string; COOKIE_DOMAIN?: string; EMAIL_CASE_SENSITIVITY_EXCEPTIONS?: string; @@ -36,6 +38,7 @@ export function getSessionEnv( USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, USER_RECOVERY_CODES_ENCRYPTION_KEY, + GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY, } = env; if ( !ACCESS_SECRET || @@ -44,7 +47,8 @@ export function getSessionEnv( !USER_EMAIL_ENCRYPTION_KEY || !USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY || !USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY || - !USER_RECOVERY_CODES_ENCRYPTION_KEY + !USER_RECOVERY_CODES_ENCRYPTION_KEY || + !GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY ) { return null; } @@ -56,6 +60,7 @@ export function getSessionEnv( USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY, USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY, USER_RECOVERY_CODES_ENCRYPTION_KEY, + GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY, DEV: env.DEV, COOKIE_DOMAIN: env.COOKIE_DOMAIN, EMAIL_CASE_SENSITIVITY_EXCEPTIONS: env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS, diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index 172eb662..2343b9c0 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -54,14 +54,14 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `groups.getMainPageId` | `GET /api/groups/:groupId/main-page` (**implemented** — `performGetGroupMainPageId`; `groups.main_page_id`; requires `viewGroupPages`) | | `groups.getUserIds` | `GET /api/groups/:groupId/members` (**implemented** — `performGetGroupMemberUserIds`; members ∪ join requests ∪ invitations; requires `viewGroupMembers`, not public-only read) | | `groups.getPages` | `GET /api/groups/:groupId/pages` (**implemented** — `performListGroupPages`; query `lastPageId`; soft-deleted pages excluded) | -| `groups.password.enable` | `POST /api/groups/:groupId/password` | -| `groups.password.change` | `PATCH /api/groups/:groupId/password` | -| `groups.password.disable` | `DELETE /api/groups/:groupId/password` | -| `groups.privacy.makePublic` | `POST /api/groups/:groupId/privacy/public` | -| `groups.privacy.setJoinRequestsAllowed` | `PATCH /api/groups/:groupId/privacy/join-requests` | -| `groups.deletion.delete` | `DELETE /api/groups/:groupId` (soft) | -| `groups.deletion.restore` | `POST /api/groups/:groupId/restore` | -| `groups.deletion.deletePermanently` | `POST /api/groups/:groupId/purge` | +| `groups.password.enable` | `POST /api/groups/:groupId/password` (**implemented** — `performGroupPasswordEnable`; Pro + `editGroupSettings`; `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`) | +| `groups.password.change` | `PATCH /api/groups/:groupId/password` (**implemented** — `performGroupPasswordChange`) | +| `groups.password.disable` | `DELETE /api/groups/:groupId/password` (JSON body; **implemented** — `performGroupPasswordDisable`; not Pro-gated, legacy match) | +| `groups.privacy.makePublic` | `POST /api/groups/:groupId/privacy/public` (**implemented** — `performGroupPrivacyMakePublic`; clears `group_members` / `group_join_invitations` `encrypted_access_keyring`) | +| `groups.privacy.setJoinRequestsAllowed` | `PATCH /api/groups/:groupId/privacy/join-requests` (**implemented** — `performGroupPrivacySetJoinRequestsAllowed`) | +| `groups.deletion.delete` | `DELETE /api/groups/:groupId` (soft) (**implemented** — `performGroupSoftDelete`) | +| `groups.deletion.restore` | `POST /api/groups/:groupId/restore` (**implemented** — `performGroupRestore`; grace only; not after purge) | +| `groups.deletion.deletePermanently` | `POST /api/groups/:groupId/purge` (**implemented** — `performGroupPurge`) | ## Pages (`pagesRouter`) diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index ce8e9d0f..35e53e87 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -31,6 +31,11 @@ export { groupPageCreateResponseSchema, groupPagesListQuerySchema, groupPagesListResponseSchema, + groupPasswordChangeRequestSchema, + groupPasswordDisableRequestSchema, + groupPasswordEnableRequestSchema, + groupPrivacyJoinRequestsPatchSchema, + groupPrivacyPublicRequestSchema, userGroupIdsResponseSchema, } from "./schemas/pages-groups.js"; export { diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index c1f3e2a4..5e60ad1b 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -60,5 +60,17 @@ describe("getOpenApiDocument", () => { expect(doc.paths?.["/api/groups/{groupId}/members"]?.get).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/pages"]?.get).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/pages"]?.post).toBeDefined(); + expect(doc.paths?.["/api/groups/{groupId}/password"]?.post).toBeDefined(); + expect(doc.paths?.["/api/groups/{groupId}/password"]?.patch).toBeDefined(); + expect(doc.paths?.["/api/groups/{groupId}/password"]?.delete).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/privacy/public"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/privacy/join-requests"]?.patch, + ).toBeDefined(); + expect(doc.paths?.["/api/groups/{groupId}"]?.delete).toBeDefined(); + expect(doc.paths?.["/api/groups/{groupId}/restore"]?.post).toBeDefined(); + expect(doc.paths?.["/api/groups/{groupId}/purge"]?.post).toBeDefined(); }); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index 058512a6..74126836 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -24,6 +24,11 @@ import { groupPageCreateResponseSchema, groupPagesListQuerySchema, groupPagesListResponseSchema, + groupPasswordChangeRequestSchema, + groupPasswordDisableRequestSchema, + groupPasswordEnableRequestSchema, + groupPrivacyJoinRequestsPatchSchema, + groupPrivacyPublicRequestSchema, userGroupIdsResponseSchema, } from "./schemas/pages-groups.js"; import { @@ -628,6 +633,219 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/password", + summary: "Enable group password (Pro)", + description: + "Replaces `groups.password.enable`. Argon2id is applied on the server to the provided `groupPasswordHash` material (base64) and stored encrypted. Requires `editGroupSettings` and a Pro plan.", + request: { + params: groupIdPathSchema, + body: { + content: { + "application/json": { + schema: groupPasswordEnableRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Password protection enabled." }, + 400: { + description: "Already protected or bad password material.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "patch", + path: "/api/groups/{groupId}/password", + summary: "Change group password (Pro)", + description: "Replaces `groups.password.change`. Verifies the current group password, then re-wraps the content keyring.", + request: { + params: groupIdPathSchema, + body: { + content: { + "application/json": { + schema: groupPasswordChangeRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Password updated." }, + 400: { + description: "Wrong password, or group not protected.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "delete", + path: "/api/groups/{groupId}/password", + summary: "Disable group password (not Pro check in legacy for disable-only)", + description: + "Replaces `groups.password.disable`. Verifies the current group password, removes server-side group password, updates `groupEncryptedContentKeyring`.", + request: { + params: groupIdPathSchema, + body: { + content: { + "application/json": { + schema: groupPasswordDisableRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Password protection disabled." }, + 400: { + description: "Wrong password, or not protected.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/privacy/public", + summary: "Make group public (Pro)", + description: + "Replaces `groups.privacy.makePublic`. Sets `access_keyring` and clears member/invite `encrypted_access_keyring`.", + request: { + params: groupIdPathSchema, + body: { + content: { + "application/json": { + schema: groupPrivacyPublicRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Group is public." }, + 400: { + description: "Already public.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "patch", + path: "/api/groups/{groupId}/privacy/join-requests", + summary: "Allow or reject join requests (Pro)", + description: "Replaces `groups.privacy.setJoinRequestsAllowed`.", + request: { + params: groupIdPathSchema, + body: { + content: { + "application/json": { + schema: groupPrivacyJoinRequestsPatchSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Setting updated." }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "delete", + path: "/api/groups/{groupId}", + summary: "Soft-delete group (grace period)", + description: + "Replaces `groups.deletion.delete`. Sets `permanent_deletion_date` ~1 month ahead.", + request: { params: groupIdPathSchema }, + responses: { + 204: { description: "Deletion scheduled." }, + 400: { + description: "Already soft-deleted.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/restore", + summary: "Restore a soft-deleted group", + description: + "Replaces `groups.deletion.restore` during the grace period (`permanent_deletion_date` in the future).", + request: { params: groupIdPathSchema }, + responses: { + 204: { description: "Group removed from scheduled deletion." }, + 400: { + description: "Not soft-deleted, or no longer in grace period.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/purge", + summary: "Permanently mark group deleted (purge active or grace state)", + description: + "Replaces `groups.deletion.deletePermanently` — `permanent_deletion_date` set in the past (legacy).", + request: { params: groupIdPathSchema }, + responses: { + 204: { description: "Purge recorded." }, + 400: { + description: "Already purged.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "post", path: "/api/users/me/password", diff --git a/new-deepnotes/packages/api/src/schemas/pages-groups.ts b/new-deepnotes/packages/api/src/schemas/pages-groups.ts index cda5acb2..62525379 100644 --- a/new-deepnotes/packages/api/src/schemas/pages-groups.ts +++ b/new-deepnotes/packages/api/src/schemas/pages-groups.ts @@ -76,3 +76,38 @@ export const groupMemberUserIdsResponseSchema = z userIds: z.array(z.string()), }) .openapi("GroupMemberUserIdsResponse"); + +/** Same material as legacy `groupPasswordHash` (Argon2id pre-hash input on the client), base64. */ +export const groupPasswordEnableRequestSchema = z + .object({ + groupPasswordHash: byteB64, + groupEncryptedContentKeyring: byteB64, + }) + .openapi("GroupPasswordEnableRequest"); + +export const groupPasswordChangeRequestSchema = z + .object({ + groupCurrentPasswordHash: byteB64, + groupNewPasswordHash: byteB64, + groupEncryptedContentKeyring: byteB64, + }) + .openapi("GroupPasswordChangeRequest"); + +export const groupPasswordDisableRequestSchema = z + .object({ + groupPasswordHash: byteB64, + groupEncryptedContentKeyring: byteB64, + }) + .openapi("GroupPasswordDisableRequest"); + +export const groupPrivacyPublicRequestSchema = z + .object({ + accessKeyring: byteB64, + }) + .openapi("GroupPrivacyPublicRequest"); + +export const groupPrivacyJoinRequestsPatchSchema = z + .object({ + areJoinRequestsAllowed: z.boolean(), + }) + .openapi("GroupPrivacyJoinRequestsPatch"); diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index c6de2ce8..f5f3f22c 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -21,6 +21,7 @@ import { import * as schema from "@deepnotes/db/schema"; import { devices, + groups, notifications, pages, sessions, @@ -55,6 +56,16 @@ import { performGetGroupMainPageId, performGetGroupMemberUserIds, } from "./group-main-and-members.js"; +import { + performGroupPasswordChange, + performGroupPasswordDisable, + performGroupPasswordEnable, + performGroupPrivacyMakePublic, + performGroupPrivacySetJoinRequestsAllowed, + performGroupPurge, + performGroupRestore, + performGroupSoftDelete, +} from "./index.js"; import { performCreatePage, performListGroupPages, @@ -93,6 +104,7 @@ function testSessionEnv(): SessionEnv { USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY: b32(2), USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY: b32(3), USER_RECOVERY_CODES_ENCRYPTION_KEY: b32(4), + GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY: b32(5), SEND_EMAILS: "false", DEV: "true", }; @@ -1544,5 +1556,165 @@ describe.skipIf(resolveTemplateContext() == null)( } } }); + + it("group password, privacy, soft delete, purge, restore failure after purge", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `gadm-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + await db + .update(users) + .set({ plan: "pro" }) + .where(eq(users.id, reg.userId)); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + const gpass = new TextEncoder().encode("gpass-1"); + const gpass2 = new TextEncoder().encode("gpass-2"); + const kr = rand32(); + + await performGroupPasswordEnable({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + groupPasswordHash: gpass, + groupEncryptedContentKeyring: kr, + }); + const [h1] = await db + .select({ h: groups.encryptedRehashedPasswordHash }) + .from(groups) + .where(eq(groups.id, reg.groupId)); + expect(h1?.h).toBeDefined(); + + await performGroupPasswordChange({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + groupCurrentPasswordHash: gpass, + groupNewPasswordHash: gpass2, + groupEncryptedContentKeyring: rand32(), + }); + + await performGroupPasswordDisable({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + groupPasswordHash: gpass2, + groupEncryptedContentKeyring: rand32(), + }); + const [h2] = await db + .select({ h: groups.encryptedRehashedPasswordHash }) + .from(groups) + .where(eq(groups.id, reg.groupId)); + expect(h2?.h).toBeNull(); + + await db + .update(groups) + .set({ accessKeyring: null }) + .where(eq(groups.id, reg.groupId)); + await performGroupPrivacyMakePublic({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + accessKeyring: rand32(), + }); + const [pbl] = await db + .select({ a: groups.accessKeyring, j: groups.areJoinRequestsAllowed }) + .from(groups) + .where(eq(groups.id, reg.groupId)); + expect(pbl?.a).not.toBeNull(); + + await performGroupPrivacySetJoinRequestsAllowed({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + areJoinRequestsAllowed: false, + }); + const [jr] = await db + .select({ j: groups.areJoinRequestsAllowed }) + .from(groups) + .where(eq(groups.id, reg.groupId)); + expect(jr?.j).toBe(false); + + await performGroupSoftDelete({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + }); + const [sd] = await db + .select({ d: groups.permanentDeletionDate }) + .from(groups) + .where(eq(groups.id, reg.groupId)); + expect(sd?.d).toBeDefined(); + expect(new Date(sd!.d!).getTime()).toBeGreaterThan(Date.now()); + + await performGroupRestore({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + }); + const [rs] = await db + .select({ d: groups.permanentDeletionDate }) + .from(groups) + .where(eq(groups.id, reg.groupId)); + expect(rs?.d).toBeNull(); + + await performGroupSoftDelete({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + }); + await performGroupPurge({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + }); + const [pg] = await db + .select({ d: groups.permanentDeletionDate }) + .from(groups) + .where(eq(groups.id, reg.groupId)); + expect(new Date(pg!.d!).getTime()).toBeLessThan(Date.now()); + + await expect( + performGroupRestore({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + }), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); }, ); diff --git a/new-deepnotes/packages/session/src/crypto/session-crypto.ts b/new-deepnotes/packages/session/src/crypto/session-crypto.ts index 1ae175b6..6f166ac2 100644 --- a/new-deepnotes/packages/session/src/crypto/session-crypto.ts +++ b/new-deepnotes/packages/session/src/crypto/session-crypto.ts @@ -135,3 +135,34 @@ export function verifyRecoveryCode( hashRecoveryCode(recoveryCode, salt).slice(16), ); } + +/** PHC string for a group password (Argon2id, libsodium). Call after `ensureSodiumReady()`. */ +export function computeGroupPasswordPhc(groupPasswordPrehash: Uint8Array): string { + return sodium.crypto_pwhash_str( + groupPasswordPrehash, + 2, + 32 * 1024 * 1024, + ) as string; +} + +export function encryptGroupRehashedPasswordHash( + groupRehashedPasswordHashPhc: string, + encryptionKeyB64: string, +): Uint8Array { + const key = wrapSymmetricKey(base64ToBytes(encryptionKeyB64)); + return key.encrypt(textToBytes(groupRehashedPasswordHashPhc), { + associatedData: { context: "GroupRehashedPasswordHash" }, + }); +} + +export function decryptGroupRehashedPasswordHash( + groupEncryptedRehashedPasswordHash: Uint8Array, + encryptionKeyB64: string, +): string { + const key = wrapSymmetricKey(base64ToBytes(encryptionKeyB64)); + return bytesToText( + key.decrypt(groupEncryptedRehashedPasswordHash, { + associatedData: { context: "GroupRehashedPasswordHash" }, + }), + ); +} diff --git a/new-deepnotes/packages/session/src/env.ts b/new-deepnotes/packages/session/src/env.ts index fcacc125..45bc5b48 100644 --- a/new-deepnotes/packages/session/src/env.ts +++ b/new-deepnotes/packages/session/src/env.ts @@ -10,6 +10,8 @@ export type SessionEnv = { USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY: string; USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY: string; USER_RECOVERY_CODES_ENCRYPTION_KEY: string; + /** Base64 symmetric key for `groups.encrypted_rehashed_password_hash` PHC plaintext. */ + GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY: string; /** When `"true"`, cookies omit `Secure` (local HTTP). */ DEV?: string; /** Optional `Domain=` attribute (legacy `HOST`). */ diff --git a/new-deepnotes/packages/session/src/group-deletion.ts b/new-deepnotes/packages/session/src/group-deletion.ts new file mode 100644 index 00000000..3544ad27 --- /dev/null +++ b/new-deepnotes/packages/session/src/group-deletion.ts @@ -0,0 +1,147 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { groups } from "@deepnotes/db/schema"; +import { eq } from "drizzle-orm"; + +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { userHasGroupPermission } from "./group-permissions.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; + +function addMonths(d: Date, m: number): Date { + const x = new Date(d.getTime()); + x.setMonth(x.getMonth() + m); + return x; +} + +function addDays(d: Date, n: number): Date { + const x = new Date(d.getTime()); + x.setDate(x.getDate() + n); + return x; +} + +function tsString(d: Date): string { + return d.toISOString(); +} + +async function requireEditGroupSettings(input: { + db: DeepnotesDb; + userId: string; + groupId: string; +}): Promise { + const allowed = await userHasGroupPermission({ + db: input.db, + userId: input.userId, + groupId: input.groupId, + permission: "editGroupSettings", + }); + if (!allowed) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } +} + +/** `groups.deletion.delete` — soft: schedule in ~1 month. */ +export async function performGroupSoftDelete(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await requireEditGroupSettings({ db: input.db, userId, groupId: input.groupId }); + + const [g] = await input.db + .select({ d: groups.permanentDeletionDate }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (g.d != null) { + throw new SessionError(400, "BAD_REQUEST", "Group is already deleted."); + } + + await input.db + .update(groups) + .set({ permanentDeletionDate: tsString(addMonths(new Date(), 1)) }) + .where(eq(groups.id, input.groupId)); +} + +/** `groups.deletion.restore` — only during soft-delete grace (future `permanent_deletion_date`). */ +export async function performGroupRestore(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await requireEditGroupSettings({ db: input.db, userId, groupId: input.groupId }); + + const [g] = await input.db + .select({ d: groups.permanentDeletionDate }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (g.d == null) { + throw new SessionError(400, "BAD_REQUEST", "Group is not deleted."); + } + const end = new Date(g.d); + if (end.getTime() <= Date.now()) { + throw new SessionError( + 400, + "BAD_REQUEST", + "This group can no longer be restored.", + ); + } + + await input.db + .update(groups) + .set({ permanentDeletionDate: null }) + .where(eq(groups.id, input.groupId)); +} + +/** + * `groups.deletion.deletePermanently` — mark purged; allowed from active or grace state. + * Legacy sets `permanent_deletion_date` in the past. + */ +export async function performGroupPurge(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await requireEditGroupSettings({ db: input.db, userId, groupId: input.groupId }); + + const [g] = await input.db + .select({ d: groups.permanentDeletionDate }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (g.d != null && new Date(g.d).getTime() < Date.now()) { + throw new SessionError(400, "BAD_REQUEST", "Group is already permanently deleted."); + } + + await input.db + .update(groups) + .set({ permanentDeletionDate: tsString(addDays(new Date(), -1)) }) + .where(eq(groups.id, input.groupId)); +} diff --git a/new-deepnotes/packages/session/src/group-password.ts b/new-deepnotes/packages/session/src/group-password.ts new file mode 100644 index 00000000..bd09c88e --- /dev/null +++ b/new-deepnotes/packages/session/src/group-password.ts @@ -0,0 +1,217 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { groups } from "@deepnotes/db/schema"; +import { eq } from "drizzle-orm"; +import { Buffer } from "node:buffer"; +import sodium from "libsodium-wrappers-sumo"; + +import { + computeGroupPasswordPhc, + decryptGroupRehashedPasswordHash, + encryptGroupRehashedPasswordHash, + ensureSodiumReady, +} from "./crypto/session-crypto.js"; +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { userHasGroupPermission } from "./group-permissions.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; +import { assertUserProPlan } from "./user-plan.js"; + +async function assertGroupPasswordCorrect(input: { + db: DeepnotesDb; + env: SessionEnv; + groupId: string; + groupPasswordHash: Uint8Array; +}): Promise { + const [g] = await input.db + .select({ hash: groups.encryptedRehashedPasswordHash }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (g.hash == null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "This group is not password protected.", + ); + } + const phc = decryptGroupRehashedPasswordHash( + new Uint8Array(g.hash), + input.env.GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY, + ); + const ok = sodium.crypto_pwhash_str_verify( + phc, + input.groupPasswordHash, + ); + if (!ok) { + throw new SessionError(400, "BAD_REQUEST", "Group password is incorrect."); + } +} + +async function requireEditGroupSettings(input: { + db: DeepnotesDb; + userId: string; + groupId: string; +}): Promise { + const allowed = await userHasGroupPermission({ + db: input.db, + userId: input.userId, + groupId: input.groupId, + permission: "editGroupSettings", + }); + if (!allowed) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } +} + +export async function performGroupPasswordEnable(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + groupPasswordHash: Uint8Array; + groupEncryptedContentKeyring: Uint8Array; +}): Promise { + await ensureSodiumReady(); + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await assertUserProPlan({ db: input.db, userId }); + await requireEditGroupSettings({ db: input.db, userId, groupId: input.groupId }); + + const [g] = await input.db + .select({ hash: groups.encryptedRehashedPasswordHash }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (g.hash != null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "This group is already password protected.", + ); + } + + const phc = computeGroupPasswordPhc(input.groupPasswordHash); + const enc = encryptGroupRehashedPasswordHash( + phc, + input.env.GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY, + ); + + await input.db + .update(groups) + .set({ + encryptedRehashedPasswordHash: Buffer.from(enc), + encryptedContentKeyring: Buffer.from(input.groupEncryptedContentKeyring), + }) + .where(eq(groups.id, input.groupId)); +} + +export async function performGroupPasswordChange(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + groupCurrentPasswordHash: Uint8Array; + groupNewPasswordHash: Uint8Array; + groupEncryptedContentKeyring: Uint8Array; +}): Promise { + await ensureSodiumReady(); + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await assertGroupPasswordCorrect({ + db: input.db, + env: input.env, + groupId: input.groupId, + groupPasswordHash: input.groupCurrentPasswordHash, + }); + await assertUserProPlan({ db: input.db, userId }); + await requireEditGroupSettings({ db: input.db, userId, groupId: input.groupId }); + + const [g] = await input.db + .select({ hash: groups.encryptedRehashedPasswordHash }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (g.hash == null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "This group is not password protected.", + ); + } + + const newPhc = computeGroupPasswordPhc(input.groupNewPasswordHash); + const enc = encryptGroupRehashedPasswordHash( + newPhc, + input.env.GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY, + ); + + await input.db + .update(groups) + .set({ + encryptedRehashedPasswordHash: Buffer.from(enc), + encryptedContentKeyring: Buffer.from(input.groupEncryptedContentKeyring), + }) + .where(eq(groups.id, input.groupId)); +} + +export async function performGroupPasswordDisable(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + groupPasswordHash: Uint8Array; + groupEncryptedContentKeyring: Uint8Array; +}): Promise { + await ensureSodiumReady(); + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await assertGroupPasswordCorrect({ + db: input.db, + env: input.env, + groupId: input.groupId, + groupPasswordHash: input.groupPasswordHash, + }); + await requireEditGroupSettings({ db: input.db, userId, groupId: input.groupId }); + + const [g] = await input.db + .select({ hash: groups.encryptedRehashedPasswordHash }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (g.hash == null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "This group is not password protected.", + ); + } + + await input.db + .update(groups) + .set({ + encryptedRehashedPasswordHash: null, + encryptedContentKeyring: Buffer.from(input.groupEncryptedContentKeyring), + }) + .where(eq(groups.id, input.groupId)); +} diff --git a/new-deepnotes/packages/session/src/group-permissions.ts b/new-deepnotes/packages/session/src/group-permissions.ts index d1941da8..c77e9e4c 100644 --- a/new-deepnotes/packages/session/src/group-permissions.ts +++ b/new-deepnotes/packages/session/src/group-permissions.ts @@ -9,27 +9,46 @@ const ROLE_PERMISSIONS: Record< viewGroupPages: boolean; editGroupPages: boolean; viewGroupMembers: boolean; + editGroupSettings: boolean; } > = { - owner: { viewGroupPages: true, editGroupPages: true, viewGroupMembers: true }, - admin: { viewGroupPages: true, editGroupPages: true, viewGroupMembers: true }, + owner: { + viewGroupPages: true, + editGroupPages: true, + viewGroupMembers: true, + editGroupSettings: true, + }, + admin: { + viewGroupPages: true, + editGroupPages: true, + viewGroupMembers: true, + editGroupSettings: true, + }, moderator: { viewGroupPages: true, editGroupPages: true, viewGroupMembers: true, + editGroupSettings: false, + }, + member: { + viewGroupPages: true, + editGroupPages: true, + viewGroupMembers: true, + editGroupSettings: false, }, - member: { viewGroupPages: true, editGroupPages: true, viewGroupMembers: true }, viewer: { viewGroupPages: true, editGroupPages: false, viewGroupMembers: true, + editGroupSettings: false, }, }; export type GroupPermission = | "viewGroupPages" | "editGroupPages" - | "viewGroupMembers"; + | "viewGroupMembers" + | "editGroupSettings"; export async function userHasGroupPermission(input: { db: DeepnotesDb; diff --git a/new-deepnotes/packages/session/src/group-privacy.ts b/new-deepnotes/packages/session/src/group-privacy.ts new file mode 100644 index 00000000..cc1fbe77 --- /dev/null +++ b/new-deepnotes/packages/session/src/group-privacy.ts @@ -0,0 +1,103 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { groupJoinInvitations, groupMembers, groups } from "@deepnotes/db/schema"; +import { eq } from "drizzle-orm"; +import { Buffer } from "node:buffer"; + +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { userHasGroupPermission } from "./group-permissions.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; +import { assertUserProPlan } from "./user-plan.js"; + +async function requireEditGroupSettings(input: { + db: DeepnotesDb; + userId: string; + groupId: string; +}): Promise { + const allowed = await userHasGroupPermission({ + db: input.db, + userId: input.userId, + groupId: input.groupId, + permission: "editGroupSettings", + }); + if (!allowed) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } +} + +/** + * `groups.privacy.makePublic` — shared read keyring; clears member/invite + * `encrypted_access_keyring` (legacy KeyDB parity). + */ +export async function performGroupPrivacyMakePublic(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + accessKeyring: Uint8Array; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await assertUserProPlan({ db: input.db, userId }); + await requireEditGroupSettings({ db: input.db, userId, groupId: input.groupId }); + + const [g] = await input.db + .select({ accessKeyring: groups.accessKeyring }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (g.accessKeyring != null) { + throw new SessionError(400, "BAD_REQUEST", "Group is already public."); + } + + await input.db.transaction(async (tx) => { + await tx + .update(groups) + .set({ accessKeyring: Buffer.from(input.accessKeyring) }) + .where(eq(groups.id, input.groupId)); + await tx + .update(groupMembers) + .set({ encryptedAccessKeyring: null }) + .where(eq(groupMembers.groupId, input.groupId)); + await tx + .update(groupJoinInvitations) + .set({ encryptedAccessKeyring: null }) + .where(eq(groupJoinInvitations.groupId, input.groupId)); + }); +} + +export async function performGroupPrivacySetJoinRequestsAllowed(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + areJoinRequestsAllowed: boolean; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await assertUserProPlan({ db: input.db, userId }); + await requireEditGroupSettings({ db: input.db, userId, groupId: input.groupId }); + + const [g] = await input.db + .select({ id: groups.id }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + + await input.db + .update(groups) + .set({ areJoinRequestsAllowed: input.areJoinRequestsAllowed }) + .where(eq(groups.id, input.groupId)); +} diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index ebb3df2f..241fbc3f 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -58,3 +58,17 @@ export { performUserTwoFactorGenerateRecoveryCodes, performUserTwoFactorLoad, } from "./user-two-factor-settings.js"; +export { + performGroupPasswordChange, + performGroupPasswordDisable, + performGroupPasswordEnable, +} from "./group-password.js"; +export { + performGroupPurge, + performGroupRestore, + performGroupSoftDelete, +} from "./group-deletion.js"; +export { + performGroupPrivacyMakePublic, + performGroupPrivacySetJoinRequestsAllowed, +} from "./group-privacy.js"; diff --git a/new-deepnotes/packages/session/src/send-email-change-code.test.ts b/new-deepnotes/packages/session/src/send-email-change-code.test.ts index 63072015..f5e584b9 100644 --- a/new-deepnotes/packages/session/src/send-email-change-code.test.ts +++ b/new-deepnotes/packages/session/src/send-email-change-code.test.ts @@ -18,6 +18,9 @@ function minimalEnv( "base64", ), USER_RECOVERY_CODES_ENCRYPTION_KEY: Buffer.alloc(32, 4).toString("base64"), + GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY: Buffer.alloc(32, 5).toString( + "base64", + ), ...overrides, }; } diff --git a/new-deepnotes/packages/session/src/user-plan.ts b/new-deepnotes/packages/session/src/user-plan.ts new file mode 100644 index 00000000..ececd9ac --- /dev/null +++ b/new-deepnotes/packages/session/src/user-plan.ts @@ -0,0 +1,26 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { users } from "@deepnotes/db/schema"; +import { eq } from "drizzle-orm"; + +import { SessionError } from "./errors.js"; + +/** + * Replaces legacy `assertUserSubscribed` for Pro-only tRPC (group password, privacy, …). + */ +export async function assertUserProPlan(input: { + db: DeepnotesDb; + userId: string; +}): Promise { + const [row] = await input.db + .select({ plan: users.plan }) + .from(users) + .where(eq(users.id, input.userId)) + .limit(1); + if (row?.plan !== "pro") { + throw new SessionError( + 403, + "FORBIDDEN", + "This action requires a Pro plan subscription.", + ); + } +} diff --git a/new-deepnotes/template.env b/new-deepnotes/template.env index ebcf41b9..f9872bd9 100644 --- a/new-deepnotes/template.env +++ b/new-deepnotes/template.env @@ -20,6 +20,7 @@ REDIS_URL=redis://localhost:6380 # USER_REHASHED_LOGIN_HASH_ENCRYPTION_KEY= # base64, 32-byte symmetric key material # USER_AUTHENTICATOR_SECRET_ENCRYPTION_KEY= # base64 # USER_RECOVERY_CODES_ENCRYPTION_KEY= # base64 +# GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY= # base64; `groups.encrypted_rehashed_password_hash` # COOKIE_DOMAIN= # optional; legacy used HOST # DEV=true # local HTTP without Secure cookies # SEND_EMAILS=false # when unset, outbound mail is ON and registration/resend need RESEND_API_KEY From e261191f30158ff738d85b4bf4a93d8ae949d7b1 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 01:15:46 -0300 Subject: [PATCH 048/243] feat(new-deepnotes): group privacy rules and page APIs --- new-deepnotes/PLAN_PROGRESS.md | 33 ++- .../apps/api-worker/src/index.test.ts | 4 + new-deepnotes/apps/api-worker/src/index.ts | 73 ++++++ new-deepnotes/docs/TRPC_REST_MAP.md | 3 +- new-deepnotes/packages/api/src/index.ts | 2 + .../packages/api/src/openapi.test.ts | 3 + new-deepnotes/packages/api/src/openapi.ts | 32 +++ .../packages/api/src/schemas/pages-groups.ts | 60 +++++ .../src/account-flows.integration.test.ts | 158 ++++++++++++- .../packages/session/src/group-privacy.ts | 220 +++++++++++++++++- new-deepnotes/packages/session/src/index.ts | 2 + 11 files changed, 576 insertions(+), 14 deletions(-) diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index d123295b..19426a76 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** [pages/groups slice 1](#pagesgroups-rest--slice-1); [slice 3 — main page + members](#pagesgroups-rest--slice-3); **[users.pages prefs slice 2](#userspages-rest--slice-2)**; **[groups — password, privacy, deletion slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion)**. **Still ahead:** `pagesRouter` (bump, backlinks, snapshots, move, optional `groupCreation`), WS join/invite/role (or REST in TRPC map), `POST …/privacy/private` (re-key payload; no legacy rotation), realtime/collab, Stripe. | +| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** [pages/groups slice 1](#pagesgroups-rest--slice-1); [slice 3 — main page + members](#pagesgroups-rest--slice-3); **[users.pages prefs slice 2](#userspages-rest--slice-2)**; **[groups — password, privacy, deletion slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion)**; **[slice 5 — privacy private (re-key)](#pagesgroups-rest--slice-5-privacy-private-re-key)**. **Still ahead:** `pagesRouter` (bump, backlinks, snapshots, deletion, move, optional `groupCreation` on create), WS join/invite/role (or REST in [TRPC map](./docs/TRPC_REST_MAP.md)), realtime/collab, Stripe. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP) / **groups + pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage`, [`performGetGroupMainPageId`](#pagesgroups-rest--slice-3), [`performGetGroupMemberUserIds`](#pagesgroups-rest--slice-3) + unknown group **404**) / **user page prefs** ([slice 2](#userspages-rest--slice-2): starting + path + favorites + recent remove/clear + default note PATCH + notifications load + mark read) / **[group password, privacy, soft delete, purge, restore after purge (slice 4)](#pagesgroups-rest--slice-4-group-password-privacy-deletion)**. `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP) / **groups + pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage`, [`performGetGroupMainPageId`](#pagesgroups-rest--slice-3), [`performGetGroupMemberUserIds`](#pagesgroups-rest--slice-3) + unknown group **404**) / **user page prefs** ([slice 2](#userspages-rest--slice-2): starting + path + favorites + recent remove/clear + default note PATCH + notifications load + mark read) / **[group password, privacy, soft delete, purge, restore after purge (slice 4)](#pagesgroups-rest--slice-4-group-password-privacy-deletion)** / **[privacy make-private + keyset validation (slice 5)](#pagesgroups-rest--slice-5-privacy-private-re-key)**. `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) @@ -46,7 +46,7 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` **How to run locally:** ensure `.env` at `new-deepnotes/.env` has `DATABASE_URL` and (for template create/drop) `DATABASE_ADMIN_URL` with a role that can `CREATE DATABASE`. Then: -- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**19** cases when DB env set — includes groups + prefs + [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) group admin flows) +- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**20** cases when DB env set — includes groups + prefs + [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 5](#pagesgroups-rest--slice-5-privacy-private-re-key)) - `pnpm --filter @deepnotes/db exec vitest run src/template-db.test.ts` CI should set the same vars against the workflow Postgres service (role with `CREATEDB`). @@ -76,6 +76,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **Groups + pages (personal)** | `performUserRegister` then `performGetUserGroupIds` / `performListGroupPages` / `performGetGroupMainPageId` / `performGetGroupMemberUserIds` / `performCreatePage` (second page, `parentPageId` = initial page) | `groupIds` = `[personalGroupId]`; list returns initial `pageId`; **main page** = `reg.pageId` (matches `groups.main_page_id`); **members** = `[reg.userId]` (sole `group_members` row); create returns `numFreePages` **1** for default `plan`; `users.num_free_pages` = 1; two rows in `pages` for group; unknown `groupId` list → **404** `NOT_FOUND`. | | **User page prefs** | Register + access JWT; `performGetStartingPageId`; `performGetCurrentPath` (root page + child); unknown page **404**; `performAddFavoritePages` / `performRemoveFavoritePages` (order on `users.favorite_page_ids`); `performRemoveRecentPages` bogus id **404** / missing child in recent **404** / remove root then `recent` empty + `performClearRecentPages`; `performPatchDefaultNote`; insert `notifications` + `users_notifications` → `performLoadNotifications` (base64 ciphertext) + `performMarkNotificationsRead` → `users.last_notification_read` | Matches legacy semantics where tested; favorites column from migration `0001_favorite_page_ids`. | | **Group password, privacy, deletion (slice 4)** | `UPDATE users` → `plan = pro` on registered user; `performGroupPasswordEnable` / `Change` / `Disable` on **personal** group; `performGroupPrivacyMakePublic` after `accessKeyring` cleared; `performGroupPrivacySetJoinRequestsAllowed` **false**; `performGroupSoftDelete` (future `permanent_deletion_date`) → `Restore` (null) → `SoftDelete` + `Purge` (past date); `Restore` after purge **400** | PHC + server-side encrypt via `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`; `group-permissions` includes **`editGroupSettings`** (owner/admin only). | +| **Privacy make-private (slice 5)** | Register with **public** personal group (`access_keyring` set); Pro; `performGroupPrivacyMakePrivate` with full re-key payload (single member, empty invites/requests, one page); assert `access_keyring` **null** + page `encrypted_symmetric_keyring` updated; second call **400** “already private”; re-public in DB then payload with **extra** page id → **400** keyset mismatch | Mirrors legacy `groupKeyRotationSchema` key sets; **no** `next_key_rotation_date` writes (RESTART_PLAN). | **`@deepnotes/db` real Postgres (`template-db.test.ts`):** @@ -177,12 +178,23 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **`@deepnotes/api-worker`** | Hono: same paths; `byteB64` pass-through to session (no double-decode). Worker **`getSessionEnv`** requires the new key. `503` matrix **+8** routes (**44** total 503 cases). | | **Tests** | [Integration](#phase-3-test-coverage-detail): personal group set to Pro in DB, full password + privacy + delete cycle + restore after purge **400**; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) **implemented** rows. | -**Not in this slice:** `POST /api/groups/:groupId/privacy/private` (make-private re-key) — see **Not started (Phase 3 — pages, groups, infra)** below; legacy used two-step WS with rotation; greenfield = one REST with full re-key body when built. +### Pages/groups REST — slice 5 (privacy private, re-key) + +**Goal:** legacy WS `groups.privacy.makePrivate` (step 1 read + step 2 `rotateGroupKeys`) as **one** `POST` with the same ciphertext shape as legacy `groupKeyRotationSchema` / [group-key-rotation.ts](../apps/app-server/src/utils/group-key-rotation.ts) (records keyed by user id or page id). **No** per-page `next_key_rotation_date` updates (rotation machinery removed per RESTART_PLAN). + +| Layer | What shipped | +|-------|----------------| +| **`@deepnotes/session`** | `performGroupPrivacyMakePrivate` in `group-privacy.ts` — Pro + `editGroupSettings`; group must have **`access_keyring` non-null** (still “public”); validates `groupMembers` / `groupJoinInvitations` / `groupJoinRequests` / `groupPages` **key sets** match Postgres exactly; transaction updates `groups`, all member/invite/request rows, all pages; `groupAccessKeyring` optional (omit → `access_keyring` **null**, same as legacy). | +| **`@deepnotes/api`** | `groupPrivacyPrivateRequestSchema` (+ nested OpenAPI component schemas) in `schemas/pages-groups.ts`; path in `openapi.ts`. | +| **`@deepnotes/api-worker`** | `POST /api/groups/:groupId/privacy/private` — **204**; **503** matrix **+1** route (**45** total). | +| **Tests** | Integration: [slice 5 row](#phase-3-test-coverage-detail); [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) **Groups** + **WebSocket** rows. | + +**Client contract:** JSON uses **base64** for all byte fields (`byteB64`). Empty objects `{}` for invites/requests when none. ### Not started (Phase 3 — pages, groups, infra) - [x] **Groups (REST, slice 4):** [password, privacy, soft delete, restore, purge](#pagesgroups-rest--slice-4-group-password-privacy-deletion) — `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY` on `SessionEnv` / worker bindings. -- [ ] **Groups (still):** `POST /api/groups/:groupId/privacy/private` — re-key to private without legacy rotation (single REST body like legacy `rotateGroupKeys` + **`websocket/groups/privacy/make-private`**); document vs OpenAPI. +- [x] **Groups (REST, slice 5):** [make-private / re-key](#pagesgroups-rest--slice-5-privacy-private-re-key) — `POST /api/groups/:groupId/privacy/private`; OpenAPI + [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md). - [ ] **Remaining `pagesRouter`:** bump, backlinks, snapshots, deletion, move (plus **`groupCreation`** on create if still required for parity). - [ ] **WS / REST:** group join/invite, member role, etc. (see [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) WebSocket table). - [ ] **Realtime / collab** (new or adapted protocols; no key rotation). @@ -235,9 +247,9 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**19** cases when DB env set) — … + **groups/pages** + **user page prefs** ([slice 2](#userspages-rest--slice-2)) + [slice 4 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion) (`GROUP_*` + Pro); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; **Pro-only** create in non-personal group; **viewer** cannot create; optional `groupCreation` on create; pagination edge cases on `performLoadNotifications` | -| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA + **me/groups** + **users/pages\*** prefs + **groups** list/create + **main-page** + **members** + [slice 4 **password/privacy/deletion** paths](#pagesgroups-rest--slice-4-group-password-privacy-deletion)); **`schemas/users.test.ts`** (email/password change, 2fa finish); **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; Zod tests for `pages-groups` / `user-pages` query edge cases | -| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **44** tests (503 matrix when env/Hyperdrive missing) — [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) **+8** (password ×3, privacy ×2, `DELETE` group, restore, purge) | **200** tests with stub `SessionEnv` + template DB (heavier) | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**20** cases when DB env set) — … + **groups/pages** + **user page prefs** ([slice 2](#userspages-rest--slice-2)) + [slice 4 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 5 make-private](#pagesgroups-rest--slice-5-privacy-private-re-key); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; **Pro-only** create in non-personal group; **viewer** cannot create; optional `groupCreation` on create; pagination edge cases on `performLoadNotifications` | +| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA + **me/groups** + **users/pages\*** prefs + **groups** list/create + **main-page** + **members** + [slice 4 **password/privacy/deletion** paths](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + **`POST …/privacy/private`**); **`schemas/users.test.ts`** (email/password change, 2fa finish); **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; Zod tests for `groupPrivacyPrivateRequestSchema` edge cases | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **45** tests (503 matrix when env/Hyperdrive missing) — includes **`POST …/privacy/private`** | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -259,7 +271,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). - [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation, login + refresh + **stale JWT replay**, refresh **cookie guards**, **2FA** TOTP + **recovery codes**, **groups list + page list + page create**, **users.pages prefs** incl. notifications + `favorite_page_ids`, [group password / privacy / deletion (slice 4)](#pagesgroups-rest--slice-4-group-password-privacy-deletion)). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** as in [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) (**19** session + 6 db integration cases when DB env is set; includes [slice 4 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion)). **Next:** Redis failed-login integration; refresh **expired** JWT; **pages** router; **`POST …/privacy/private`**; Stripe when billing exists. +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** as in [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) (**20** session + 6 db integration cases when DB env is set; includes [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 5](#pagesgroups-rest--slice-5-privacy-private-re-key)). **Next:** Redis failed-login integration; refresh **expired** JWT; **`pagesRouter`** (bump, backlinks, snapshots, delete/move, optional `groupCreation`); Stripe when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -270,7 +282,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Phase 3 working order (suggested) -Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2FA + refresh guards; **`@deepnotes/db`** FKs through **`group_members`**. **(done)** **pages/groups** [slice 1](#pagesgroups-rest--slice-1) (list/create pages, group ids), [slice 3](#pagesgroups-rest--slice-3) (main-page, member user ids), [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) (group password, privacy public + join-requests, soft delete, restore, purge). **`users.pages` prefs** [slice 2](#userspages-rest--slice-2). **(next)** **`POST …/privacy/private`** (re-key) or defer until client needs it; **`pagesRouter`** (bump, backlinks, snapshots, delete, move; optional **`groupCreation`** on create); WS table in [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) (join, invite, member role, …). **(then)** **realtime + collab** (no key rotation) and **Stripe** + billing hooks on account routes. +Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2FA + refresh guards; **`@deepnotes/db`** FKs through **`group_members`**. **(done)** **pages/groups** [slice 1](#pagesgroups-rest--slice-1) (list/create pages, group ids), [slice 3](#pagesgroups-rest--slice-3) (main-page, member user ids), [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) (group password, privacy public + join-requests, soft delete, restore, purge), [slice 5](#pagesgroups-rest--slice-5-privacy-private-re-key) (**`POST …/privacy/private`**). **`users.pages` prefs** [slice 2](#userspages-rest--slice-2). **(next)** **`pagesRouter`** — `bump`, `backlinks` create/delete, `snapshots` save/load/delete, `deletion` soft/restore/purge, **`pages/move`**; optional **`groupCreation`** on `POST …/groups/:id/pages` if parity requires; then WS / REST for **join invitations**, **join requests**, **member role** / remove ([TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) WebSocket table). **(then)** **realtime + collab** (no key rotation) and **Stripe** (`POST /api/webhooks/stripe`, checkout/portal) + **`deleteStripeCustomer`** when keys exist. --- @@ -278,6 +290,7 @@ Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2F | Date | Change | |------|--------| +| 2026-04-27 | **Phase 3 — groups slice 5 (`POST …/privacy/private`):** `performGroupPrivacyMakePrivate` + `groupPrivacyPrivateRequestSchema` (legacy `groupKeyRotationSchema` shape, base64 JSON); Hono + OpenAPI; worker **503** **45** tests; `account-flows` **20** integration cases (make private, already-private **400**, bad page keyset **400**); **TRPC_REST_MAP** Groups + WS `make-private` rows; [slice 5 section](#pagesgroups-rest--slice-5-privacy-private-re-key); [working order](#phase-3-working-order-suggested) now leads with **`pagesRouter`**. | | 2026-04-27 | **Phase 3 — groups slice 4 (password, privacy, deletion):** `editGroupSettings` in `group-permissions.ts`; `SESSION` `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`; `user-plan.ts` (`assertUserProPlan`); `group-password.ts` / `group-privacy.ts` / `group-deletion.ts`; Hono + OpenAPI + Zod; worker **503** **44** tests; `account-flows` **19** integration cases; **TRPC_REST_MAP** updated; [slice 4 section](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + not-started `privacy/private`. | | 2026-04-27 | **Phase 3 — groups main-page + members (slice 3):** `performGetGroupMainPageId` / `performGetGroupMemberUserIds` (`group-main-and-members.ts`); `viewGroupMembers` in `group-permissions.ts` (public-only still **not** enough for members list); `GET /api/groups/:groupId/main-page` + `…/members`; OpenAPI + schemas; integration extends **groups + pages**; TRPC_REST_MAP **implemented** for `getMainPageId` / `getUserIds`; worker **503** matrix **36** rows; PLAN_PROGRESS slice 3 + working-order refresh. | | 2026-04-27 | **Phase 3 — `users.pages` prefs (slice 2):** migration `0001_favorite_page_ids`; `user-page-prefs.ts` + Hono/OpenAPI routes (starting, path, recent, favorites, defaults PATCH, notifications); `schemas/user-pages.ts`; integration test **user page prefs**; TRPC_REST_MAP marked implemented; PLAN_PROGRESS sections + matrix counts (**18** session integration, **34** worker 503 rows). | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 8baa032e..6471f41b 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -78,6 +78,10 @@ describe("api-worker", () => { "PATCH", "/api/groups/aaaaaaaaaaaaaaaaaaaaa/privacy/join-requests", ], + [ + "POST", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/privacy/private", + ], ["DELETE", "/api/groups/aaaaaaaaaaaaaaaaaaaaa"], [ "POST", diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index d241e725..f5382ae6 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -8,6 +8,7 @@ import { groupPasswordDisableRequestSchema, groupPasswordEnableRequestSchema, groupPrivacyJoinRequestsPatchSchema, + groupPrivacyPrivateRequestSchema, groupPrivacyPublicRequestSchema, healthResponseSchema, userDefaultArrowPatchSchema, @@ -1673,6 +1674,78 @@ app.patch("/api/groups/:groupId/privacy/join-requests", async (c) => { } }); +app.post("/api/groups/:groupId/privacy/private", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const parsed = groupPrivacyPrivateRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + const payload = { + groupAccessKeyring: parsed.data.groupAccessKeyring, + groupEncryptedName: parsed.data.groupEncryptedName, + groupEncryptedContentKeyring: parsed.data.groupEncryptedContentKeyring, + groupPublicKeyring: parsed.data.groupPublicKeyring, + groupEncryptedPrivateKeyring: parsed.data.groupEncryptedPrivateKeyring, + groupMembers: parsed.data.groupMembers, + groupJoinInvitations: parsed.data.groupJoinInvitations, + groupJoinRequests: parsed.data.groupJoinRequests, + groupPages: parsed.data.groupPages, + }; + + try { + const { performGroupPrivacyMakePrivate } = await import("@deepnotes/session"); + await performGroupPrivacyMakePrivate({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + payload, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.delete("/api/groups/:groupId", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index 2343b9c0..a5e5db82 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -59,6 +59,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `groups.password.disable` | `DELETE /api/groups/:groupId/password` (JSON body; **implemented** — `performGroupPasswordDisable`; not Pro-gated, legacy match) | | `groups.privacy.makePublic` | `POST /api/groups/:groupId/privacy/public` (**implemented** — `performGroupPrivacyMakePublic`; clears `group_members` / `group_join_invitations` `encrypted_access_keyring`) | | `groups.privacy.setJoinRequestsAllowed` | `PATCH /api/groups/:groupId/privacy/join-requests` (**implemented** — `performGroupPrivacySetJoinRequestsAllowed`) | +| (WS) `groups.privacy.makePrivate` step 1+2 | `POST /api/groups/:groupId/privacy/private` (**implemented** — `performGroupPrivacyMakePrivate`; single body = legacy `rotateGroupKeys` / `groupKeyRotationSchema`; omits `pages.next_key_rotation_date` bumps per RESTART_PLAN) | | `groups.deletion.delete` | `DELETE /api/groups/:groupId` (soft) (**implemented** — `performGroupSoftDelete`) | | `groups.deletion.restore` | `POST /api/groups/:groupId/restore` (**implemented** — `performGroupRestore`; grace only; not after purge) | | `groups.deletion.deletePermanently` | `POST /api/groups/:groupId/purge` (**implemented** — `performGroupPurge`) | @@ -86,7 +87,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `websocket/groups/join-requests/*` | same | send / accept / reject / cancel | | `websocket/groups/change-user-role` | `PATCH /api/groups/:groupId/members/:userId` | prefer REST if acceptable | | `websocket/groups/remove-user` | `DELETE /api/groups/:groupId/members/:userId` | | -| `websocket/groups/privacy/make-private` | `POST /api/groups/:groupId/privacy/private` | | +| `websocket/groups/privacy/make-private` | `POST /api/groups/:groupId/privacy/private` | **implemented** — see `groups.privacy.makePrivate` row above | | `websocket/groups/rotate-keys` | — | **removed** per RESTART_PLAN | | `websocket/pages/move` | `POST /api/pages/:pageId/move` | | | `websocket/users/account/change-password` | `POST /api/users/me/password` | **implemented** in `@deepnotes/session` (`performUserPasswordChange`) | diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index 35e53e87..efd1c241 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -35,9 +35,11 @@ export { groupPasswordDisableRequestSchema, groupPasswordEnableRequestSchema, groupPrivacyJoinRequestsPatchSchema, + groupPrivacyPrivateRequestSchema, groupPrivacyPublicRequestSchema, userGroupIdsResponseSchema, } from "./schemas/pages-groups.js"; +export type { GroupPrivacyPrivateRequest } from "./schemas/pages-groups.js"; export { userCurrentPathResponseSchema, userDefaultArrowPatchSchema, diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 5e60ad1b..f8c1632f 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -69,6 +69,9 @@ describe("getOpenApiDocument", () => { expect( doc.paths?.["/api/groups/{groupId}/privacy/join-requests"]?.patch, ).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/privacy/private"]?.post, + ).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}"]?.delete).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/restore"]?.post).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/purge"]?.post).toBeDefined(); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index 74126836..fe909e3a 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -28,6 +28,7 @@ import { groupPasswordDisableRequestSchema, groupPasswordEnableRequestSchema, groupPrivacyJoinRequestsPatchSchema, + groupPrivacyPrivateRequestSchema, groupPrivacyPublicRequestSchema, userGroupIdsResponseSchema, } from "./schemas/pages-groups.js"; @@ -780,6 +781,37 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/privacy/private", + summary: "Make group private (Pro) — full re-key payload", + description: + "Replaces legacy WS `groups.privacy.makePrivate` (step 2 `rotateGroupKeys`) in one request. Clears `access_keyring` when `groupAccessKeyring` is omitted. Member / invitation / request / page record keys must match the DB exactly.", + request: { + params: groupIdPathSchema, + body: { + content: { + "application/json": { + schema: groupPrivacyPrivateRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Group is private; ciphertext updated." }, + 400: { + description: "Already private or payload key sets do not match group.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "delete", path: "/api/groups/{groupId}", diff --git a/new-deepnotes/packages/api/src/schemas/pages-groups.ts b/new-deepnotes/packages/api/src/schemas/pages-groups.ts index 62525379..07c65067 100644 --- a/new-deepnotes/packages/api/src/schemas/pages-groups.ts +++ b/new-deepnotes/packages/api/src/schemas/pages-groups.ts @@ -111,3 +111,63 @@ export const groupPrivacyJoinRequestsPatchSchema = z areJoinRequestsAllowed: z.boolean(), }) .openapi("GroupPrivacyJoinRequestsPatch"); + +const nanoidRecordKeySchema = z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/, "expected nanoid id"); + +const groupPrivacyPrivateMemberSchema = z + .object({ + encryptedAccessKeyring: byteB64.optional(), + encryptedInternalKeyring: byteB64, + encryptedName: byteB64.nullable(), + }) + .openapi("GroupPrivacyPrivateMember"); + +const groupPrivacyPrivateInvitationSchema = z + .object({ + encryptedAccessKeyring: byteB64.optional(), + encryptedInternalKeyring: byteB64, + encryptedName: byteB64, + }) + .openapi("GroupPrivacyPrivateInvitation"); + +const groupPrivacyPrivateJoinRequestSchema = z + .object({ + encryptedName: byteB64, + }) + .openapi("GroupPrivacyPrivateJoinRequest"); + +const groupPrivacyPrivatePageSchema = z + .object({ + encryptedSymmetricKeyring: byteB64, + }) + .openapi("GroupPrivacyPrivatePage"); + +/** + * Re-key payload for `POST …/privacy/private` (legacy WS `groups.privacy.makePrivate` step 2 + `rotateGroupKeys` in one call). + * Record keys are user ids (members, invitations, requests) or page ids; must match current DB rows exactly. + */ +export const groupPrivacyPrivateRequestSchema = z + .object({ + groupAccessKeyring: byteB64.optional(), + groupEncryptedName: byteB64, + groupEncryptedContentKeyring: byteB64, + groupPublicKeyring: byteB64, + groupEncryptedPrivateKeyring: byteB64, + groupMembers: z.record(nanoidRecordKeySchema, groupPrivacyPrivateMemberSchema), + groupJoinInvitations: z.record( + nanoidRecordKeySchema, + groupPrivacyPrivateInvitationSchema, + ), + groupJoinRequests: z.record( + nanoidRecordKeySchema, + groupPrivacyPrivateJoinRequestSchema, + ), + groupPages: z.record(nanoidRecordKeySchema, groupPrivacyPrivatePageSchema), + }) + .openapi("GroupPrivacyPrivateRequest"); + +export type GroupPrivacyPrivateRequest = z.infer< + typeof groupPrivacyPrivateRequestSchema +>; diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index f5f3f22c..6260521f 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { config as loadEnv } from "dotenv"; -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import sodium from "libsodium-wrappers-sumo"; import { nanoid } from "nanoid"; @@ -21,6 +21,7 @@ import { import * as schema from "@deepnotes/db/schema"; import { devices, + groupMembers, groups, notifications, pages, @@ -60,6 +61,7 @@ import { performGroupPasswordChange, performGroupPasswordDisable, performGroupPasswordEnable, + performGroupPrivacyMakePrivate, performGroupPrivacyMakePublic, performGroupPrivacySetJoinRequestsAllowed, performGroupPurge, @@ -1557,6 +1559,160 @@ describe.skipIf(resolveTemplateContext() == null)( } }); + it("group privacy make private clears access_keyring and rejects repeat", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `priv-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + await db + .update(users) + .set({ plan: "pro" }) + .where(eq(users.id, reg.userId)); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + + const [mem] = await db + .select({ + encryptedName: groupMembers.encryptedName, + }) + .from(groupMembers) + .where( + and( + eq(groupMembers.groupId, reg.groupId), + eq(groupMembers.userId, reg.userId), + ), + ); + expect(mem).toBeDefined(); + const memberRow = mem!; + + const newPageSym = rand32(); + await performGroupPrivacyMakePrivate({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + payload: { + groupEncryptedName: rand32(), + groupEncryptedContentKeyring: rand32(), + groupPublicKeyring: rand32(), + groupEncryptedPrivateKeyring: rand32(), + groupMembers: { + [reg.userId]: { + encryptedAccessKeyring: rand32(), + encryptedInternalKeyring: rand32(), + encryptedName: + memberRow.encryptedName != null ? rand32() : null, + }, + }, + groupJoinInvitations: {}, + groupJoinRequests: {}, + groupPages: { + [reg.pageId]: { encryptedSymmetricKeyring: newPageSym }, + }, + }, + }); + + const [gAfter] = await db + .select({ accessKeyring: groups.accessKeyring }) + .from(groups) + .where(eq(groups.id, reg.groupId)); + expect(gAfter?.accessKeyring).toBeNull(); + + const [pAfter] = await db + .select({ sk: pages.encryptedSymmetricKeyring }) + .from(pages) + .where(eq(pages.id, reg.pageId)); + expect( + Buffer.from(pAfter!.sk!).equals(Buffer.from(newPageSym)), + ).toBe(true); + + await expect( + performGroupPrivacyMakePrivate({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + payload: { + groupEncryptedName: rand32(), + groupEncryptedContentKeyring: rand32(), + groupPublicKeyring: rand32(), + groupEncryptedPrivateKeyring: rand32(), + groupMembers: { + [reg.userId]: { + encryptedAccessKeyring: rand32(), + encryptedInternalKeyring: rand32(), + encryptedName: rand32(), + }, + }, + groupJoinInvitations: {}, + groupJoinRequests: {}, + groupPages: { + [reg.pageId]: { encryptedSymmetricKeyring: rand32() }, + }, + }, + }), + ).rejects.toMatchObject({ + code: "BAD_REQUEST", + message: "Group is already private.", + }); + + await db + .update(groups) + .set({ accessKeyring: Buffer.from(rand32()) }) + .where(eq(groups.id, reg.groupId)); + await expect( + performGroupPrivacyMakePrivate({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + payload: { + groupEncryptedName: rand32(), + groupEncryptedContentKeyring: rand32(), + groupPublicKeyring: rand32(), + groupEncryptedPrivateKeyring: rand32(), + groupMembers: { + [reg.userId]: { + encryptedAccessKeyring: rand32(), + encryptedInternalKeyring: rand32(), + encryptedName: rand32(), + }, + }, + groupJoinInvitations: {}, + groupJoinRequests: {}, + groupPages: { + [reg.pageId]: { encryptedSymmetricKeyring: rand32() }, + [nanoid()]: { encryptedSymmetricKeyring: rand32() }, + }, + }, + }), + ).rejects.toMatchObject({ code: "BAD_REQUEST" }); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + it("group password, privacy, soft delete, purge, restore failure after purge", async () => { const env = testSessionEnv(); const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; diff --git a/new-deepnotes/packages/session/src/group-privacy.ts b/new-deepnotes/packages/session/src/group-privacy.ts index cc1fbe77..61054205 100644 --- a/new-deepnotes/packages/session/src/group-privacy.ts +++ b/new-deepnotes/packages/session/src/group-privacy.ts @@ -1,6 +1,12 @@ import type { DeepnotesDb } from "@deepnotes/db/client"; -import { groupJoinInvitations, groupMembers, groups } from "@deepnotes/db/schema"; -import { eq } from "drizzle-orm"; +import { + groupJoinInvitations, + groupJoinRequests, + groupMembers, + groups, + pages, +} from "@deepnotes/db/schema"; +import { and, eq } from "drizzle-orm"; import { Buffer } from "node:buffer"; import type { SessionEnv } from "./env.js"; @@ -101,3 +107,213 @@ export async function performGroupPrivacySetJoinRequestsAllowed(input: { .set({ areJoinRequestsAllowed: input.areJoinRequestsAllowed }) .where(eq(groups.id, input.groupId)); } + +export type GroupPrivacyPrivateMemberPayload = { + encryptedAccessKeyring?: Uint8Array; + encryptedInternalKeyring: Uint8Array; + encryptedName: Uint8Array | null; +}; + +export type GroupPrivacyPrivateInvitationPayload = { + encryptedAccessKeyring?: Uint8Array; + encryptedInternalKeyring: Uint8Array; + encryptedName: Uint8Array; +}; + +export type GroupPrivacyPrivateJoinRequestPayload = { + encryptedName: Uint8Array; +}; + +export type GroupPrivacyPrivatePagePayload = { + encryptedSymmetricKeyring: Uint8Array; +}; + +export type GroupPrivacyPrivatePayload = { + groupAccessKeyring?: Uint8Array; + groupEncryptedName: Uint8Array; + groupEncryptedContentKeyring: Uint8Array; + groupPublicKeyring: Uint8Array; + groupEncryptedPrivateKeyring: Uint8Array; + groupMembers: Record; + groupJoinInvitations: Record; + groupJoinRequests: Record; + groupPages: Record; +}; + +function sortedIds(ids: string[]): string[] { + return [...ids].sort(); +} + +function assertSameKeyset( + label: string, + expected: string[], + payload: Record, +): void { + const exp = sortedIds(expected); + const got = sortedIds(Object.keys(payload)); + if (exp.length !== got.length || exp.some((id, i) => id !== got[i])) { + throw new SessionError( + 400, + "BAD_REQUEST", + `${label} keys do not match current group state.`, + ); + } +} + +/** + * `groups.privacy.makePrivate` — one-shot re-key while clearing shared `access_keyring` + * (legacy two-step WS collapsed). Payload mirrors legacy `rotateGroupKeys` / `groupKeyRotationSchema`. + * Does not bump `pages.next_key_rotation_date` (rotation machinery removed per RESTART_PLAN). + */ +export async function performGroupPrivacyMakePrivate(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + payload: GroupPrivacyPrivatePayload; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await assertUserProPlan({ db: input.db, userId }); + await requireEditGroupSettings({ db: input.db, userId, groupId: input.groupId }); + + const [g] = await input.db + .select({ + accessKeyring: groups.accessKeyring, + }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (g.accessKeyring == null) { + throw new SessionError(400, "BAD_REQUEST", "Group is already private."); + } + + const memberRows = await input.db + .select({ userId: groupMembers.userId }) + .from(groupMembers) + .where(eq(groupMembers.groupId, input.groupId)); + const memberIds = memberRows.map((r) => r.userId); + + const invitationRows = await input.db + .select({ userId: groupJoinInvitations.userId }) + .from(groupJoinInvitations) + .where(eq(groupJoinInvitations.groupId, input.groupId)); + const invitationIds = invitationRows.map((r) => r.userId); + + const requestRows = await input.db + .select({ userId: groupJoinRequests.userId }) + .from(groupJoinRequests) + .where(eq(groupJoinRequests.groupId, input.groupId)); + const requestIds = requestRows.map((r) => r.userId); + + const pageRows = await input.db + .select({ id: pages.id }) + .from(pages) + .where(eq(pages.groupId, input.groupId)); + const pageIds = pageRows.map((r) => r.id); + + assertSameKeyset("groupMembers", memberIds, input.payload.groupMembers); + assertSameKeyset( + "groupJoinInvitations", + invitationIds, + input.payload.groupJoinInvitations, + ); + assertSameKeyset( + "groupJoinRequests", + requestIds, + input.payload.groupJoinRequests, + ); + assertSameKeyset("groupPages", pageIds, input.payload.groupPages); + + const accessOut = + input.payload.groupAccessKeyring != null + ? Buffer.from(input.payload.groupAccessKeyring) + : null; + + await input.db.transaction(async (tx) => { + await tx + .update(groups) + .set({ + accessKeyring: accessOut, + encryptedName: Buffer.from(input.payload.groupEncryptedName), + encryptedContentKeyring: Buffer.from( + input.payload.groupEncryptedContentKeyring, + ), + publicKeyring: Buffer.from(input.payload.groupPublicKeyring), + encryptedPrivateKeyring: Buffer.from( + input.payload.groupEncryptedPrivateKeyring, + ), + }) + .where(eq(groups.id, input.groupId)); + + for (const userId_ of memberIds) { + const m = input.payload.groupMembers[userId_]!; + await tx + .update(groupMembers) + .set({ + encryptedAccessKeyring: + m.encryptedAccessKeyring != null + ? Buffer.from(m.encryptedAccessKeyring) + : null, + encryptedInternalKeyring: Buffer.from(m.encryptedInternalKeyring), + encryptedName: + m.encryptedName != null ? Buffer.from(m.encryptedName) : null, + }) + .where( + and( + eq(groupMembers.groupId, input.groupId), + eq(groupMembers.userId, userId_), + ), + ); + } + + for (const userId_ of invitationIds) { + const inv = input.payload.groupJoinInvitations[userId_]!; + await tx + .update(groupJoinInvitations) + .set({ + encryptedAccessKeyring: + inv.encryptedAccessKeyring != null + ? Buffer.from(inv.encryptedAccessKeyring) + : null, + encryptedInternalKeyring: Buffer.from(inv.encryptedInternalKeyring), + encryptedName: Buffer.from(inv.encryptedName), + }) + .where( + and( + eq(groupJoinInvitations.groupId, input.groupId), + eq(groupJoinInvitations.userId, userId_), + ), + ); + } + + for (const userId_ of requestIds) { + const jr = input.payload.groupJoinRequests[userId_]!; + await tx + .update(groupJoinRequests) + .set({ encryptedName: Buffer.from(jr.encryptedName) }) + .where( + and( + eq(groupJoinRequests.groupId, input.groupId), + eq(groupJoinRequests.userId, userId_), + ), + ); + } + + for (const pageId of pageIds) { + const p = input.payload.groupPages[pageId]!; + await tx + .update(pages) + .set({ + encryptedSymmetricKeyring: Buffer.from(p.encryptedSymmetricKeyring), + }) + .where(eq(pages.id, pageId)); + } + }); +} diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 241fbc3f..7a6ffcf0 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -69,6 +69,8 @@ export { performGroupSoftDelete, } from "./group-deletion.js"; export { + performGroupPrivacyMakePrivate, performGroupPrivacyMakePublic, performGroupPrivacySetJoinRequestsAllowed, } from "./group-privacy.js"; +export type { GroupPrivacyPrivatePayload } from "./group-privacy.js"; From 78da7d9287114ba2516136d0d872c7fdcc768691 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 01:38:17 -0300 Subject: [PATCH 049/243] feat(new-deepnotes): page CRUD in session and API --- new-deepnotes/PLAN_PROGRESS.md | 36 +- .../apps/api-worker/src/index.test.ts | 18 + new-deepnotes/apps/api-worker/src/index.ts | 523 +++++++++++++ new-deepnotes/docs/TRPC_REST_MAP.md | 18 +- new-deepnotes/packages/api/src/index.ts | 8 + .../packages/api/src/openapi.test.ts | 19 + new-deepnotes/packages/api/src/openapi.ts | 197 +++++ .../packages/api/src/schemas/pages-groups.ts | 71 ++ .../src/account-flows.integration.test.ts | 202 +++++ new-deepnotes/packages/session/src/index.ts | 11 + .../packages/session/src/page-operations.ts | 708 ++++++++++++++++++ 11 files changed, 1792 insertions(+), 19 deletions(-) create mode 100644 new-deepnotes/packages/session/src/page-operations.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 19426a76..fc9481e3 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** [pages/groups slice 1](#pagesgroups-rest--slice-1); [slice 3 — main page + members](#pagesgroups-rest--slice-3); **[users.pages prefs slice 2](#userspages-rest--slice-2)**; **[groups — password, privacy, deletion slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion)**; **[slice 5 — privacy private (re-key)](#pagesgroups-rest--slice-5-privacy-private-re-key)**. **Still ahead:** `pagesRouter` (bump, backlinks, snapshots, deletion, move, optional `groupCreation` on create), WS join/invite/role (or REST in [TRPC map](./docs/TRPC_REST_MAP.md)), realtime/collab, Stripe. | +| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); **[pages — bump / backlinks / snapshots / deletion (slice 6)](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)**. **Still ahead:** [`POST /api/pages/:pageId/move`](./docs/TRPC_REST_MAP.md) (legacy two-step WS + Yjs/updates; collab data), optional **`groupCreation`** on `POST /api/groups/:id/pages`, WS or REST for **join invitations / requests / member role** ([TRPC map](./docs/TRPC_REST_MAP.md)), **realtime / collab**, **Stripe**. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — register / email change / password change / **login + refresh** (including **replay of pre-rotation refresh JWT** → 401) / **refresh cookie guards** (`loggedIn` not `true`, missing refresh) / demo **403** / **2FA** (enable → finish → TOTP login; **recovery-code login** + DB-backed **one-time consumption** via `decryptRecoveryCodes` length 5; wrong finish token; missing MFA; bad TOTP) / **groups + pages** (`performGetUserGroupIds`, `performListGroupPages`, `performCreatePage`, [`performGetGroupMainPageId`](#pagesgroups-rest--slice-3), [`performGetGroupMemberUserIds`](#pagesgroups-rest--slice-3) + unknown group **404**) / **user page prefs** ([slice 2](#userspages-rest--slice-2): starting + path + favorites + recent remove/clear + default note PATCH + notifications load + mark read) / **[group password, privacy, soft delete, purge, restore after purge (slice 4)](#pagesgroups-rest--slice-4-group-password-privacy-deletion)** / **[privacy make-private + keyset validation (slice 5)](#pagesgroups-rest--slice-5-privacy-private-re-key)**. `@deepnotes/db` `template-db.test.ts` — clone smoke + **sessions / devices / pages→groups / group_members→users+groups FK** rejects. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — **21** cases when DB env set — register / email / password / **login + refresh** / **2FA** / **groups + pages** / **user page prefs** / **[slice 4–5 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion)** / **[slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)** (`bump` + `users` recents/starting, backlinks create/delete, Pro snapshots save/load/delete, page soft delete → restore → purge). `@deepnotes/db` `template-db.test.ts` — clone + FK matrix. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) @@ -46,7 +46,7 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` **How to run locally:** ensure `.env` at `new-deepnotes/.env` has `DATABASE_URL` and (for template create/drop) `DATABASE_ADMIN_URL` with a role that can `CREATE DATABASE`. Then: -- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**20** cases when DB env set — includes groups + prefs + [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 5](#pagesgroups-rest--slice-5-privacy-private-re-key)) +- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**21** cases when DB env set — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)) - `pnpm --filter @deepnotes/db exec vitest run src/template-db.test.ts` CI should set the same vars against the workflow Postgres service (role with `CREATEDB`). @@ -77,6 +77,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **User page prefs** | Register + access JWT; `performGetStartingPageId`; `performGetCurrentPath` (root page + child); unknown page **404**; `performAddFavoritePages` / `performRemoveFavoritePages` (order on `users.favorite_page_ids`); `performRemoveRecentPages` bogus id **404** / missing child in recent **404** / remove root then `recent` empty + `performClearRecentPages`; `performPatchDefaultNote`; insert `notifications` + `users_notifications` → `performLoadNotifications` (base64 ciphertext) + `performMarkNotificationsRead` → `users.last_notification_read` | Matches legacy semantics where tested; favorites column from migration `0001_favorite_page_ids`. | | **Group password, privacy, deletion (slice 4)** | `UPDATE users` → `plan = pro` on registered user; `performGroupPasswordEnable` / `Change` / `Disable` on **personal** group; `performGroupPrivacyMakePublic` after `accessKeyring` cleared; `performGroupPrivacySetJoinRequestsAllowed` **false**; `performGroupSoftDelete` (future `permanent_deletion_date`) → `Restore` (null) → `SoftDelete` + `Purge` (past date); `Restore` after purge **400** | PHC + server-side encrypt via `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`; `group-permissions` includes **`editGroupSettings`** (owner/admin only). | | **Privacy make-private (slice 5)** | Register with **public** personal group (`access_keyring` set); Pro; `performGroupPrivacyMakePrivate` with full re-key payload (single member, empty invites/requests, one page); assert `access_keyring` **null** + page `encrypted_symmetric_keyring` updated; second call **400** “already private”; re-public in DB then payload with **extra** page id → **400** keyset mismatch | Mirrors legacy `groupKeyRotationSchema` key sets; **no** `next_key_rotation_date` writes (RESTART_PLAN). | +| **Page bump / backlinks / snapshots / deletion (slice 6)** | Pro; `performCreatePage` (second + third child); `performPageBump` (child, parent = main’s child chain); `users.starting_page_id` + recent; `performPageBacklinkCreate`+`Delete` (scoped query — bump may add separate `page_links` row); `performPageSnapshotSave`+`Load`+`Delete`; `performPageSoftDelete`+`Restore`+`SoftDelete`+`Purge` | Postgres-only links; Pro for snapshot save/load; not main page for delete. | **`@deepnotes/db` real Postgres (`template-db.test.ts`):** @@ -191,11 +192,25 @@ CI should set the same vars against the workflow Postgres service (role with `CR **Client contract:** JSON uses **base64** for all byte fields (`byteB64`). Empty objects `{}` for invites/requests when none. +### Pages REST — slice 6 (bump, backlinks, snapshots, deletion) + +**Goal:** legacy `pagesRouter` **except** `pages.move` and optional **`groupCreation`** on create. No KeyDB mirrors for links/snapshots — only **`page_links`** / **`page_snapshots`**. + +| Layer | What shipped | +|-------|----------------| +| **`@deepnotes/session`** | `page-operations.ts` — `performPageBump` (personal-main breadcrumb validation, `users.*` recents, optional `page_links` on bump, activity timestamps), `performPageBacklinkCreate` / `performPageBacklinkDelete`, `performPageSnapshotSave` (Pro, legacy trim: >10 and oldest >14d), `performPageSnapshotLoad` (Pro), `performPageSnapshotDelete`, `performPageSoftDelete` / `performPageRestore` / `performPagePurge` (not group **main** page). | +| **`@deepnotes/api`** | `pageIdPathSchema`, `pageTargetPagePathSchema`, `pageSnapshotPathSchema`, `pageBumpRequestSchema`, `pageBacklinkCreateRequestSchema`, `pageSnapshot*`, OpenAPI paths under `/api/pages/...`. | +| **`@deepnotes/api-worker`** | Routes listed in [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) **Pages** table; `503` matrix **54** session routes. | +| **Tests** | [Phase 3 detail table](#phase-3-test-coverage-detail) **slice 6** row; integration case **pages: bump, backlinks, …** | + +**Path semantics:** `POST /api/pages/:pageId/backlinks` — `pageId` = **target**; `DELETE /api/pages/:sourcePageId/backlinks/:targetPageId` — **source** first (legacy `sourcePageId` / `targetPageId`). + ### Not started (Phase 3 — pages, groups, infra) - [x] **Groups (REST, slice 4):** [password, privacy, soft delete, restore, purge](#pagesgroups-rest--slice-4-group-password-privacy-deletion) — `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY` on `SessionEnv` / worker bindings. - [x] **Groups (REST, slice 5):** [make-private / re-key](#pagesgroups-rest--slice-5-privacy-private-re-key) — `POST /api/groups/:groupId/privacy/private`; OpenAPI + [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md). -- [ ] **Remaining `pagesRouter`:** bump, backlinks, snapshots, deletion, move (plus **`groupCreation`** on create if still required for parity). +- [x] **Pages (REST, slice 6):** [bump / backlinks / snapshots / page deletion](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) — **`pages.move` not** implemented. +- [ ] **`POST /api/pages/:pageId/move`** — legacy [websocket/pages/move](../apps/app-server/src/websocket/pages/move.ts) (two steps, Yjs `page_updates`, reencrypt snapshots, Redis cache bust); optional **`groupCreation`** in step 1. - [ ] **WS / REST:** group join/invite, member role, etc. (see [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) WebSocket table). - [ ] **Realtime / collab** (new or adapted protocols; no key rotation). - [ ] **Stripe:** `POST /api/webhooks/stripe`, checkout/portal (no RevenueCat); wire **`deleteStripeCustomer`** from account delete when keys exist. @@ -247,9 +262,9 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**20** cases when DB env set) — … + **groups/pages** + **user page prefs** ([slice 2](#userspages-rest--slice-2)) + [slice 4 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 5 make-private](#pagesgroups-rest--slice-5-privacy-private-re-key); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; **Pro-only** create in non-personal group; **viewer** cannot create; optional `groupCreation` on create; pagination edge cases on `performLoadNotifications` | -| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (health + session + 2FA + **me/groups** + **users/pages\*** prefs + **groups** list/create + **main-page** + **members** + [slice 4 **password/privacy/deletion** paths](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + **`POST …/privacy/private`**); **`schemas/users.test.ts`** (email/password change, 2fa finish); **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; Zod tests for `groupPrivacyPrivateRequestSchema` edge cases | -| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **45** tests (503 matrix when env/Hyperdrive missing) — includes **`POST …/privacy/private`** | **200** tests with stub `SessionEnv` + template DB (heavier) | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**21** cases when DB env set) — … + [slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; **`POST /api/pages/:pageId/move`**; optional `groupCreation` on create | +| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (session routes + [slice 6 `/api/pages/...` paths](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)); **`schemas/users.test.ts`**; **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; more Zod edge cases for new page schemas | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **54** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) page routes | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -270,8 +285,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Drizzle migrations from empty DB documented for production upgrades. - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). -- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (register, email change, password change, sessions invalidation, login + refresh + **stale JWT replay**, refresh **cookie guards**, **2FA** TOTP + **recovery codes**, **groups list + page list + page create**, **users.pages prefs** incl. notifications + `favorite_page_ids`, [group password / privacy / deletion (slice 4)](#pagesgroups-rest--slice-4-group-password-privacy-deletion)). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** crypto + Zod + Resend unit tests; **Postgres** as in [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) (**20** session + 6 db integration cases when DB env is set; includes [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 5](#pagesgroups-rest--slice-5-privacy-private-re-key)). **Next:** Redis failed-login integration; refresh **expired** JWT; **`pagesRouter`** (bump, backlinks, snapshots, delete/move, optional `groupCreation`); Stripe when billing exists. +- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (account + **2FA** + **groups/pages** + prefs + [slice 4–5](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 6 page router](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)). +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) — **21** `account-flows` + **6** `@deepnotes/db` when DB set; [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) page router. **Next:** **Redis** failed-login against real Redis; move route; **Stripe** when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -282,7 +297,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Phase 3 working order (suggested) -Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2FA + refresh guards; **`@deepnotes/db`** FKs through **`group_members`**. **(done)** **pages/groups** [slice 1](#pagesgroups-rest--slice-1) (list/create pages, group ids), [slice 3](#pagesgroups-rest--slice-3) (main-page, member user ids), [slice 4](#pagesgroups-rest--slice-4-group-password-privacy-deletion) (group password, privacy public + join-requests, soft delete, restore, purge), [slice 5](#pagesgroups-rest--slice-5-privacy-private-re-key) (**`POST …/privacy/private`**). **`users.pages` prefs** [slice 2](#userspages-rest--slice-2). **(next)** **`pagesRouter`** — `bump`, `backlinks` create/delete, `snapshots` save/load/delete, `deletion` soft/restore/purge, **`pages/move`**; optional **`groupCreation`** on `POST …/groups/:id/pages` if parity requires; then WS / REST for **join invitations**, **join requests**, **member role** / remove ([TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) WebSocket table). **(then)** **realtime + collab** (no key rotation) and **Stripe** (`POST /api/webhooks/stripe`, checkout/portal) + **`deleteStripeCustomer`** when keys exist. +Use this when resuming: **(done)** through [slice 6 — page bump / backlinks / snapshots / deletion](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). **(next)** **`POST /api/pages/:pageId/move`** (two-step collab/updates parity, see legacy `websocket/pages/move`); optional **`groupCreation`** on page create. **(then)** WS or REST for **join invitations**, **join requests**, **member role** / remove. **(then)** **realtime + collab** and **Stripe** + **`deleteStripeCustomer`**. --- @@ -290,6 +305,7 @@ Use this when resuming: **(done)** account HTTP through 2FA; **Postgres** for 2F | Date | Change | |------|--------| +| 2026-04-27 | **Phase 3 — pages router slice 6 (bump, backlinks, snapshots, page deletion):** `page-operations.ts` (`performPageBump` … `performPagePurge`); `page_links` / `page_snapshots` + Postgres-only (no `page-backlinks` KeyDB); OpenAPI + Hono; worker **503** matrix **54** tests; `account-flows` **21** integration cases; [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) `pages.*` rows; [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). **Next:** `POST /api/pages/:pageId/move` (WS parity + `page_updates` / collab cache). | | 2026-04-27 | **Phase 3 — groups slice 5 (`POST …/privacy/private`):** `performGroupPrivacyMakePrivate` + `groupPrivacyPrivateRequestSchema` (legacy `groupKeyRotationSchema` shape, base64 JSON); Hono + OpenAPI; worker **503** **45** tests; `account-flows` **20** integration cases (make private, already-private **400**, bad page keyset **400**); **TRPC_REST_MAP** Groups + WS `make-private` rows; [slice 5 section](#pagesgroups-rest--slice-5-privacy-private-re-key); [working order](#phase-3-working-order-suggested) now leads with **`pagesRouter`**. | | 2026-04-27 | **Phase 3 — groups slice 4 (password, privacy, deletion):** `editGroupSettings` in `group-permissions.ts`; `SESSION` `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`; `user-plan.ts` (`assertUserProPlan`); `group-password.ts` / `group-privacy.ts` / `group-deletion.ts`; Hono + OpenAPI + Zod; worker **503** **44** tests; `account-flows` **19** integration cases; **TRPC_REST_MAP** updated; [slice 4 section](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + not-started `privacy/private`. | | 2026-04-27 | **Phase 3 — groups main-page + members (slice 3):** `performGetGroupMainPageId` / `performGetGroupMemberUserIds` (`group-main-and-members.ts`); `viewGroupMembers` in `group-permissions.ts` (public-only still **not** enough for members list); `GET /api/groups/:groupId/main-page` + `…/members`; OpenAPI + schemas; integration extends **groups + pages**; TRPC_REST_MAP **implemented** for `getMainPageId` / `getUserIds`; worker **503** matrix **36** rows; PLAN_PROGRESS slice 3 + working-order refresh. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 6471f41b..e0d55ebe 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -88,6 +88,24 @@ describe("api-worker", () => { "/api/groups/aaaaaaaaaaaaaaaaaaaaa/restore", ], ["POST", "/api/groups/aaaaaaaaaaaaaaaaaaaaa/purge"], + ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/bump"], + ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/backlinks"], + [ + "DELETE", + "/api/pages/aaaaaaaaaaaaaaaaaaaaa/backlinks/bbbbbbbbbbbbbbbbbbbbb", + ], + ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/snapshots"], + [ + "GET", + "/api/pages/aaaaaaaaaaaaaaaaaaaaa/snapshots/bbbbbbbbbbbbbbbbbbbbb", + ], + [ + "DELETE", + "/api/pages/aaaaaaaaaaaaaaaaaaaaa/snapshots/bbbbbbbbbbbbbbbbbbbbb", + ], + ["DELETE", "/api/pages/aaaaaaaaaaaaaaaaaaaaa"], + ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/restore"], + ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/purge"], ["GET", "/api/users/me"], ["POST", "/api/users/me/password"], ["DELETE", "/api/users/me"], diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index f5382ae6..d154a0ab 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -4,6 +4,11 @@ import { getOpenApiDocument, groupPageCreateRequestSchema, groupPagesListQuerySchema, + pageBacklinkCreateRequestSchema, + pageBumpRequestSchema, + pageIdPathSchema, + pageSnapshotCreateResponseSchema, + pageSnapshotSaveRequestSchema, groupPasswordChangeRequestSchema, groupPasswordDisableRequestSchema, groupPasswordEnableRequestSchema, @@ -1366,6 +1371,524 @@ app.post("/api/groups/:groupId/pages", async (c) => { } }); +app.post("/api/pages/:pageId/bump", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown = {}; + try { + const txt = await c.req.text(); + if (txt.length > 0) { + bodyJson = JSON.parse(txt) as unknown; + } + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON object." }, 400); + } + + const pParams = pageIdPathSchema.safeParse({ pageId: c.req.param("pageId") }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + const parsed = pageBumpRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performPageBump } = await import("@deepnotes/session"); + await performPageBump({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageId: pParams.data.pageId, + parentPageId: parsed.data.parentPageId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/pages/:pageId/backlinks", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const pParams = pageIdPathSchema.safeParse({ pageId: c.req.param("pageId") }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + const parsed = pageBacklinkCreateRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performPageBacklinkCreate } = await import("@deepnotes/session"); + await performPageBacklinkCreate({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + targetPageId: pParams.data.pageId, + sourcePageId: parsed.data.sourcePageId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.delete("/api/pages/:pageId/backlinks/:targetPageId", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const pParams = pageIdPathSchema + .extend({ targetPageId: pageIdPathSchema.shape.pageId }) + .safeParse({ + pageId: c.req.param("pageId"), + targetPageId: c.req.param("targetPageId"), + }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performPageBacklinkDelete } = await import("@deepnotes/session"); + await performPageBacklinkDelete({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + sourcePageId: pParams.data.pageId, + targetPageId: pParams.data.targetPageId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/pages/:pageId/snapshots", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const pParams = pageIdPathSchema.safeParse({ pageId: c.req.param("pageId") }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + const parsed = pageSnapshotSaveRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performPageSnapshotSave } = await import("@deepnotes/session"); + const out = await performPageSnapshotSave({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageId: pParams.data.pageId, + encryptedSymmetricKey: parsed.data.encryptedSymmetricKey, + encryptedData: parsed.data.encryptedData, + preRestore: parsed.data.preRestore, + }); + const body = pageSnapshotCreateResponseSchema.parse(out); + return c.json(body, 201); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.get("/api/pages/:pageId/snapshots/:snapshotId", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const pParams = pageIdPathSchema + .extend({ snapshotId: pageIdPathSchema.shape.pageId }) + .safeParse({ + pageId: c.req.param("pageId"), + snapshotId: c.req.param("snapshotId"), + }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performPageSnapshotLoad } = await import("@deepnotes/session"); + const out = await performPageSnapshotLoad({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageId: pParams.data.pageId, + snapshotId: pParams.data.snapshotId, + }); + return c.json( + { + encryptedSymmetricKey: out.encryptedSymmetricKey + ? out.encryptedSymmetricKey.toString("base64") + : null, + encryptedData: out.encryptedData.toString("base64"), + }, + 200, + ); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.delete("/api/pages/:pageId/snapshots/:snapshotId", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const pParams = pageIdPathSchema + .extend({ snapshotId: pageIdPathSchema.shape.pageId }) + .safeParse({ + pageId: c.req.param("pageId"), + snapshotId: c.req.param("snapshotId"), + }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performPageSnapshotDelete } = await import("@deepnotes/session"); + await performPageSnapshotDelete({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageId: pParams.data.pageId, + snapshotId: pParams.data.snapshotId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.delete("/api/pages/:pageId", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const pParams = pageIdPathSchema.safeParse({ pageId: c.req.param("pageId") }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performPageSoftDelete } = await import("@deepnotes/session"); + await performPageSoftDelete({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageId: pParams.data.pageId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/pages/:pageId/restore", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const pParams = pageIdPathSchema.safeParse({ pageId: c.req.param("pageId") }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performPageRestore } = await import("@deepnotes/session"); + await performPageRestore({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageId: pParams.data.pageId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/pages/:pageId/purge", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const pParams = pageIdPathSchema.safeParse({ pageId: c.req.param("pageId") }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performPagePurge } = await import("@deepnotes/session"); + await performPagePurge({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageId: pParams.data.pageId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.post("/api/groups/:groupId/password", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index a5e5db82..0e2f9c8f 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -69,15 +69,15 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | Legacy procedure | Proposed REST / notes | |------------------|----------------------| | `pages.create` | `POST /api/groups/:groupId/pages` (**implemented** — `performCreatePage`; optional `groupCreation` not yet exposed; Pro + free-page rules per legacy) | -| `pages.bump` | `POST /api/pages/:pageId/bump` | -| `pages.backlinks.create` | `POST /api/pages/:pageId/backlinks` | -| `pages.backlinks.delete` | `DELETE /api/pages/:pageId/backlinks/:targetPageId` | -| `pages.snapshots.save` | `POST /api/pages/:pageId/snapshots` | -| `pages.snapshots.load` | `GET /api/pages/:pageId/snapshots/:snapshotId` | -| `pages.snapshots.delete` | `DELETE /api/pages/:pageId/snapshots/:snapshotId` | -| `pages.deletion.delete` | `DELETE /api/pages/:pageId` (soft) | -| `pages.deletion.restore` | `POST /api/pages/:pageId/restore` | -| `pages.deletion.deletePermanently` | `POST /api/pages/:pageId/purge` | +| `pages.bump` | `POST /api/pages/:pageId/bump` (**implemented** — `performPageBump`; path `pageId`, optional body `{ "parentPageId" }` must chain to personal main page) | +| `pages.backlinks.create` | `POST /api/pages/:pageId/backlinks` (**implemented** — `performPageBacklinkCreate`; path `pageId` = **target**; body `{ "sourcePageId" }`) | +| `pages.backlinks.delete` | `DELETE /api/pages/:pageId/backlinks/:targetPageId` (**implemented** — `performPageBacklinkDelete`; path `pageId` = **source**; `targetPageId` = link target) | +| `pages.snapshots.save` | `POST /api/pages/:pageId/snapshots` (**implemented** — `performPageSnapshotSave`; Pro; trim >10 + age rule like legacy `insertPageSnapshot`) | +| `pages.snapshots.load` | `GET /api/pages/:pageId/snapshots/:snapshotId` (**implemented** — `performPageSnapshotLoad`; Pro) | +| `pages.snapshots.delete` | `DELETE /api/pages/:pageId/snapshots/:snapshotId` (**implemented** — `performPageSnapshotDelete`) | +| `pages.deletion.delete` | `DELETE /api/pages/:pageId` (soft) (**implemented** — `performPageSoftDelete`) | +| `pages.deletion.restore` | `POST /api/pages/:pageId/restore` (**implemented** — `performPageRestore`) | +| `pages.deletion.deletePermanently` | `POST /api/pages/:pageId/purge` (**implemented** — `performPagePurge`; `num_free_pages` +1 when `pages.free` and user not Pro) | ## Legacy app-server WebSocket → target diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index efd1c241..b0cc003c 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -37,6 +37,14 @@ export { groupPrivacyJoinRequestsPatchSchema, groupPrivacyPrivateRequestSchema, groupPrivacyPublicRequestSchema, + pageBacklinkCreateRequestSchema, + pageBumpRequestSchema, + pageIdPathSchema, + pageSnapshotCreateResponseSchema, + pageSnapshotLoadResponseSchema, + pageSnapshotPathSchema, + pageSnapshotSaveRequestSchema, + pageTargetPagePathSchema, userGroupIdsResponseSchema, } from "./schemas/pages-groups.js"; export type { GroupPrivacyPrivateRequest } from "./schemas/pages-groups.js"; diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index f8c1632f..7b85a5fd 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -75,5 +75,24 @@ describe("getOpenApiDocument", () => { expect(doc.paths?.["/api/groups/{groupId}"]?.delete).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/restore"]?.post).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/purge"]?.post).toBeDefined(); + expect(doc.paths?.["/api/pages/{pageId}/bump"]?.post).toBeDefined(); + expect( + doc.paths?.["/api/pages/{pageId}/backlinks"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/pages/{pageId}/backlinks/{targetPageId}"]?.delete, + ).toBeDefined(); + expect( + doc.paths?.["/api/pages/{pageId}/snapshots"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/pages/{pageId}/snapshots/{snapshotId}"]?.get, + ).toBeDefined(); + expect( + doc.paths?.["/api/pages/{pageId}/snapshots/{snapshotId}"]?.delete, + ).toBeDefined(); + expect(doc.paths?.["/api/pages/{pageId}"]?.delete).toBeDefined(); + expect(doc.paths?.["/api/pages/{pageId}/restore"]?.post).toBeDefined(); + expect(doc.paths?.["/api/pages/{pageId}/purge"]?.post).toBeDefined(); }); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index fe909e3a..5b0448e4 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -30,6 +30,14 @@ import { groupPrivacyJoinRequestsPatchSchema, groupPrivacyPrivateRequestSchema, groupPrivacyPublicRequestSchema, + pageBacklinkCreateRequestSchema, + pageBumpRequestSchema, + pageIdPathSchema, + pageSnapshotCreateResponseSchema, + pageSnapshotLoadResponseSchema, + pageSnapshotPathSchema, + pageSnapshotSaveRequestSchema, + pageTargetPagePathSchema, userGroupIdsResponseSchema, } from "./schemas/pages-groups.js"; import { @@ -878,6 +886,195 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/pages/{pageId}/bump", + summary: "Bump page (recents, activity, optional breadcrumb parent)", + description: "Replaces `pages.bump` — `users` starting + recents, optional `users_pages.last_parent_id` when the parent chain ends at the personal main page (`lastParentId` walk).", + request: { + params: pageIdPathSchema, + body: { + content: { + "application/json": { + schema: pageBumpRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Bumped (best-effort; loop in chain exits without updating parent)." }, + 400: { + description: "Invalid parent (chain does not resolve to main page).", + content: { "application/json": { schema: sessionErrorResponseSchema } }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/pages/{pageId}/backlinks", + summary: "Create page backlink (source → this page as target)", + description: + "Replaces `pages.backlinks.create`. Path `pageId` is the **target**; body has `sourcePageId`.", + request: { + params: pageIdPathSchema, + body: { + content: { + "application/json": { + schema: pageBacklinkCreateRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Backlink created or activity updated (upsert)." }, + 400: { + description: "Source and target identical.", + content: { "application/json": { schema: sessionErrorResponseSchema } }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "delete", + path: "/api/pages/{pageId}/backlinks/{targetPageId}", + summary: "Delete backlink from source page to target page", + description: + "Replaces `pages.backlinks.delete`. Path `pageId` is **source**; `targetPageId` is the link target (legacy input names).", + request: { params: pageTargetPagePathSchema }, + responses: { + 204: { description: "Backlink removed." }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/pages/{pageId}/snapshots", + summary: "Save encrypted page snapshot (Pro)", + description: "Replaces `pages.snapshots.save` — asserts Pro plan (legacy `assertUserSubscribed`).", + request: { + params: pageIdPathSchema, + body: { + content: { + "application/json": { + schema: pageSnapshotSaveRequestSchema, + }, + }, + }, + }, + responses: { + 201: { + description: "Snapshot id", + content: { + "application/json": { schema: pageSnapshotCreateResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "get", + path: "/api/pages/{pageId}/snapshots/{snapshotId}", + summary: "Load page snapshot ciphertext (Pro)", + request: { params: pageSnapshotPathSchema }, + responses: { + 200: { + description: "Ciphertext (base64 fields).", + content: { + "application/json": { schema: pageSnapshotLoadResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "delete", + path: "/api/pages/{pageId}/snapshots/{snapshotId}", + summary: "Delete a page snapshot", + request: { params: pageSnapshotPathSchema }, + responses: { + 204: { description: "Snapshot removed." }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "delete", + path: "/api/pages/{pageId}", + summary: "Soft-delete page (grace period)", + request: { params: pageIdPathSchema }, + responses: { + 204: { description: "Deletion scheduled (not main page)." }, + 400: { + description: "Already deleted, or is group main page.", + content: { "application/json": { schema: sessionErrorResponseSchema } }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/pages/{pageId}/restore", + summary: "Restore a soft-deleted page", + request: { params: pageIdPathSchema }, + responses: { + 204: { description: "Page restored in grace." }, + 400: { + description: "Not deleted, or free page past purge date.", + content: { "application/json": { schema: sessionErrorResponseSchema } }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/pages/{pageId}/purge", + summary: "Permanently mark page deleted; refunds free page when applicable", + request: { params: pageIdPathSchema }, + responses: { + 204: { description: "Purge recorded." }, + 400: { + description: "Is main page, or already purged.", + content: { "application/json": { schema: sessionErrorResponseSchema } }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "post", path: "/api/users/me/password", diff --git a/new-deepnotes/packages/api/src/schemas/pages-groups.ts b/new-deepnotes/packages/api/src/schemas/pages-groups.ts index 07c65067..d600e9a4 100644 --- a/new-deepnotes/packages/api/src/schemas/pages-groups.ts +++ b/new-deepnotes/packages/api/src/schemas/pages-groups.ts @@ -171,3 +171,74 @@ export const groupPrivacyPrivateRequestSchema = z export type GroupPrivacyPrivateRequest = z.infer< typeof groupPrivacyPrivateRequestSchema >; + +export const pageIdPathSchema = z.object({ + pageId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi({ ...nanoidIdOpenapi, param: { name: "pageId", in: "path" } }), +}); + +export const pageTargetPagePathSchema = z.object({ + pageId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi({ ...nanoidIdOpenapi, param: { name: "pageId", in: "path" } }), + targetPageId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi({ ...nanoidIdOpenapi, param: { name: "targetPageId", in: "path" } }), +}); + +export const pageSnapshotPathSchema = z.object({ + pageId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi({ ...nanoidIdOpenapi, param: { name: "pageId", in: "path" } }), + snapshotId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi({ ...nanoidIdOpenapi, param: { name: "snapshotId", in: "path" } }), +}); + +/** Optional breadcrumb parent for `pages.bump` (must chain to personal main page). */ +export const pageBumpRequestSchema = z + .object({ + parentPageId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .optional(), + }) + .openapi("PageBumpRequest"); + +export const pageBacklinkCreateRequestSchema = z + .object({ + sourcePageId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi(nanoidIdOpenapi), + }) + .openapi("PageBacklinkCreateRequest"); + +export const pageSnapshotSaveRequestSchema = z + .object({ + encryptedSymmetricKey: byteB64, + encryptedData: byteB64, + preRestore: z.boolean().optional(), + }) + .openapi("PageSnapshotSaveRequest"); + +export const pageSnapshotCreateResponseSchema = z + .object({ + snapshotId: z.string(), + }) + .openapi("PageSnapshotCreateResponse"); + +export const pageSnapshotLoadResponseSchema = z + .object({ + encryptedSymmetricKey: z.string().nullable(), + encryptedData: z + .string() + .openapi({ format: "byte", description: "Base64 ciphertext." }), + }) + .openapi("PageSnapshotLoadResponse"); diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index 6260521f..fb838729 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -24,6 +24,7 @@ import { groupMembers, groups, notifications, + pageLinks, pages, sessions, users, @@ -67,6 +68,15 @@ import { performGroupPurge, performGroupRestore, performGroupSoftDelete, + performPageBacklinkCreate, + performPageBacklinkDelete, + performPageBump, + performPagePurge, + performPageRestore, + performPageSnapshotDelete, + performPageSnapshotLoad, + performPageSnapshotSave, + performPageSoftDelete, } from "./index.js"; import { performCreatePage, @@ -1872,5 +1882,197 @@ describe.skipIf(resolveTemplateContext() == null)( } } }); + + it("pages: bump, backlinks, snapshots, soft delete, restore, purge", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `pgops-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + await db + .update(users) + .set({ plan: "pro" }) + .where(eq(users.id, reg.userId)); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + + const page2 = nanoid(); + await performCreatePage({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + body: { + parentPageId: reg.pageId, + pageId: page2, + pageEncryptedSymmetricKeyring: rand32(), + pageEncryptedRelativeTitle: rand32(), + pageEncryptedAbsoluteTitle: rand32(), + }, + }); + + await performPageBump({ + db, + env, + accessCookie: access, + pageId: page2, + parentPageId: reg.pageId, + }); + const [u1] = await db + .select({ + starting: users.startingPageId, + recent: users.recentPageIds, + }) + .from(users) + .where(eq(users.id, reg.userId)); + expect(u1?.starting).toBe(page2); + expect(u1?.recent[0]).toBe(page2); + + const page3 = nanoid(); + await performCreatePage({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + body: { + parentPageId: reg.pageId, + pageId: page3, + pageEncryptedSymmetricKeyring: rand32(), + pageEncryptedRelativeTitle: rand32(), + pageEncryptedAbsoluteTitle: rand32(), + }, + }); + + await performPageBacklinkCreate({ + db, + env, + accessCookie: access, + targetPageId: page2, + sourcePageId: page3, + }); + const [bl] = await db + .select() + .from(pageLinks) + .where( + and( + eq(pageLinks.targetPageId, page2), + eq(pageLinks.sourcePageId, page3), + ), + ); + expect(bl?.sourcePageId).toBe(page3); + + await performPageBacklinkDelete({ + db, + env, + accessCookie: access, + sourcePageId: page3, + targetPageId: page2, + }); + const blAfter = await db + .select() + .from(pageLinks) + .where( + and( + eq(pageLinks.targetPageId, page2), + eq(pageLinks.sourcePageId, page3), + ), + ); + expect(blAfter.length).toBe(0); + + const kSym = rand32(); + const kData = rand32(); + const { snapshotId } = await performPageSnapshotSave({ + db, + env, + accessCookie: access, + pageId: page2, + encryptedSymmetricKey: kSym, + encryptedData: kData, + }); + const loaded = await performPageSnapshotLoad({ + db, + env, + accessCookie: access, + pageId: page2, + snapshotId, + }); + expect(loaded.encryptedData.equals(Buffer.from(kData))).toBe(true); + expect(loaded.encryptedSymmetricKey?.equals(Buffer.from(kSym))).toBe( + true, + ); + + await performPageSnapshotDelete({ + db, + env, + accessCookie: access, + pageId: page2, + snapshotId, + }); + + await performPageSoftDelete({ + db, + env, + accessCookie: access, + pageId: page2, + }); + const [rowDel] = await db + .select({ d: pages.permanentDeletionDate }) + .from(pages) + .where(eq(pages.id, page2)); + expect(rowDel?.d).toBeDefined(); + + await performPageRestore({ + db, + env, + accessCookie: access, + pageId: page2, + }); + const [rowOk] = await db + .select({ d: pages.permanentDeletionDate }) + .from(pages) + .where(eq(pages.id, page2)); + expect(rowOk?.d).toBeNull(); + + await performPageSoftDelete({ + db, + env, + accessCookie: access, + pageId: page2, + }); + await performPagePurge({ + db, + env, + accessCookie: access, + pageId: page2, + }); + const [rowP] = await db + .select({ d: pages.permanentDeletionDate }) + .from(pages) + .where(eq(pages.id, page2)); + expect(new Date(rowP!.d!).getTime()).toBeLessThan(Date.now()); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); }, ); diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 7a6ffcf0..aa770a0c 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -74,3 +74,14 @@ export { performGroupPrivacySetJoinRequestsAllowed, } from "./group-privacy.js"; export type { GroupPrivacyPrivatePayload } from "./group-privacy.js"; +export { + performPageBacklinkCreate, + performPageBacklinkDelete, + performPageBump, + performPagePurge, + performPageRestore, + performPageSnapshotDelete, + performPageSnapshotLoad, + performPageSnapshotSave, + performPageSoftDelete, +} from "./page-operations.js"; diff --git a/new-deepnotes/packages/session/src/page-operations.ts b/new-deepnotes/packages/session/src/page-operations.ts new file mode 100644 index 00000000..7c2f71cf --- /dev/null +++ b/new-deepnotes/packages/session/src/page-operations.ts @@ -0,0 +1,708 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { + groupMembers, + groups, + pageLinks, + pageSnapshots, + pages, + users, + usersPages, +} from "@deepnotes/db/schema"; +import { and, desc, eq, inArray, isNull } from "drizzle-orm"; + +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { userHasGroupPermission } from "./group-permissions.js"; +import { assertUserProPlan } from "./user-plan.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; + +function tsString(d: Date): string { + return d.toISOString(); +} + +function addMonths(d: Date, m: number): Date { + const x = new Date(d.getTime()); + x.setMonth(x.getMonth() + m); + return x; +} + +function addDays(d: Date, n: number): Date { + const x = new Date(d.getTime()); + x.setDate(x.getDate() + n); + return x; +} + +function toBuf(u: Uint8Array): Buffer { + return Buffer.from(u); +} + +function bumpStringIdList(ids: string[], itemId: string, max: number): string[] { + const rest = ids.filter((id) => id !== itemId); + return [itemId, ...rest].slice(0, max); +} + +/** + * `pages.bump` — recents, optional breadcrumb parent under personal main, activity dates. + * Replaces legacy KeyDB + partial best-effort SQL updates. + */ +export async function performPageBump(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageId: string; + parentPageId?: string | undefined; +}): Promise { + const { userId, personalGroupId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + + const [pageRow] = await input.db + .select({ + id: pages.id, + groupId: pages.groupId, + }) + .from(pages) + .where( + and(eq(pages.id, input.pageId), isNull(pages.permanentDeletionDate)), + ) + .limit(1); + + if (pageRow == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + + const canView = await userHasGroupPermission({ + db: input.db, + userId, + groupId: pageRow.groupId, + permission: "viewGroupPages", + }); + if (!canView) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const now = tsString(new Date()); + + if (input.parentPageId != null) { + const [personal] = await input.db + .select({ mainPageId: groups.mainPageId }) + .from(groups) + .where(eq(groups.id, personalGroupId)) + .limit(1); + if (personal == null) { + throw new SessionError(404, "NOT_FOUND", "Personal group not found."); + } + + const visited = new Set([input.pageId]); + let walkId: string | undefined = input.parentPageId; + let rootPageId: string | undefined; + while (walkId != null) { + if (visited.has(walkId)) { + return; + } + visited.add(walkId); + rootPageId = walkId; + const [up] = await input.db + .select({ lastParentId: usersPages.lastParentId }) + .from(usersPages) + .where( + and( + eq(usersPages.userId, userId), + eq(usersPages.pageId, walkId), + ), + ) + .limit(1); + walkId = up?.lastParentId ?? undefined; + } + + if (rootPageId !== personal.mainPageId) { + throw new SessionError(400, "BAD_REQUEST", "Invalid parent page."); + } + + await input.db + .insert(usersPages) + .values({ + userId, + pageId: input.pageId, + lastParentId: input.parentPageId, + }) + .onConflictDoUpdate({ + target: [usersPages.userId, usersPages.pageId], + set: { lastParentId: input.parentPageId }, + }); + + try { + await input.db + .insert(pageLinks) + .values({ + targetPageId: input.pageId, + sourcePageId: input.parentPageId, + lastActivityDate: now, + }) + .onConflictDoUpdate({ + target: [pageLinks.sourcePageId, pageLinks.targetPageId], + set: { lastActivityDate: now }, + }); + } catch { + // ignore: legacy ignored backlink errors + } + } + + const [urow] = await input.db + .select({ + recentPageIds: users.recentPageIds, + recentGroupIds: users.recentGroupIds, + }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + if (urow == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + + const nextRecentPages = bumpStringIdList(urow.recentPageIds, input.pageId, 50); + const nextRecentGroups = bumpStringIdList( + urow.recentGroupIds, + pageRow.groupId, + 50, + ); + + await input.db + .update(users) + .set({ + startingPageId: input.pageId, + recentPageIds: nextRecentPages, + recentGroupIds: nextRecentGroups, + }) + .where(eq(users.id, userId)); + + await input.db + .update(pages) + .set({ lastActivityDate: now }) + .where(eq(pages.id, input.pageId)); + + await input.db + .update(groupMembers) + .set({ lastActivityDate: now }) + .where( + and( + eq(groupMembers.groupId, pageRow.groupId), + eq(groupMembers.userId, userId), + ), + ); +} + +export async function performPageBacklinkCreate(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + targetPageId: string; + sourcePageId: string; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + + if (input.sourcePageId === input.targetPageId) { + throw new SessionError(400, "BAD_REQUEST", "Source and target must differ."); + } + + const [src, tgt] = await Promise.all([ + input.db + .select({ groupId: pages.groupId }) + .from(pages) + .where( + and( + eq(pages.id, input.sourcePageId), + isNull(pages.permanentDeletionDate), + ), + ) + .limit(1), + input.db + .select({ groupId: pages.groupId }) + .from(pages) + .where( + and( + eq(pages.id, input.targetPageId), + isNull(pages.permanentDeletionDate), + ), + ) + .limit(1), + ]); + + if (src[0] == null || tgt[0] == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + + const canSource = await userHasGroupPermission({ + db: input.db, + userId, + groupId: src[0].groupId, + permission: "editGroupPages", + }); + const canTarget = await userHasGroupPermission({ + db: input.db, + userId, + groupId: tgt[0].groupId, + permission: "editGroupPages", + }); + if (!canSource || !canTarget) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const now = tsString(new Date()); + await input.db + .insert(pageLinks) + .values({ + targetPageId: input.targetPageId, + sourcePageId: input.sourcePageId, + lastActivityDate: now, + }) + .onConflictDoUpdate({ + target: [pageLinks.sourcePageId, pageLinks.targetPageId], + set: { lastActivityDate: now }, + }); +} + +export async function performPageBacklinkDelete(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + targetPageId: string; + sourcePageId: string; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + + const [tgt] = await input.db + .select({ groupId: pages.groupId }) + .from(pages) + .where( + and( + eq(pages.id, input.targetPageId), + isNull(pages.permanentDeletionDate), + ), + ) + .limit(1); + if (tgt == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + + const can = await userHasGroupPermission({ + db: input.db, + userId, + groupId: tgt.groupId, + permission: "editGroupPages", + }); + if (!can) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const del = await input.db + .delete(pageLinks) + .where( + and( + eq(pageLinks.sourcePageId, input.sourcePageId), + eq(pageLinks.targetPageId, input.targetPageId), + ), + ) + .returning({ s: pageLinks.sourcePageId }); + if (del.length === 0) { + throw new SessionError(404, "NOT_FOUND", "Backlink not found."); + } +} + +export async function performPageSnapshotSave(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageId: string; + encryptedSymmetricKey: Uint8Array; + encryptedData: Uint8Array; + preRestore?: boolean | undefined; +}): Promise<{ snapshotId: string }> { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await assertUserProPlan({ db: input.db, userId }); + + const [pageRow] = await input.db + .select({ groupId: pages.groupId }) + .from(pages) + .where( + and(eq(pages.id, input.pageId), isNull(pages.permanentDeletionDate)), + ) + .limit(1); + if (pageRow == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + const can = await userHasGroupPermission({ + db: input.db, + userId, + groupId: pageRow.groupId, + permission: "editGroupPages", + }); + if (!can) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const type = input.preRestore === true ? "pre-restore" : "manual"; + + return input.db.transaction(async (tx) => { + const [row] = await tx + .insert(pageSnapshots) + .values({ + pageId: input.pageId, + authorId: userId, + encryptedSymmetricKey: toBuf(input.encryptedSymmetricKey), + encryptedData: toBuf(input.encryptedData), + type, + }) + .returning({ id: pageSnapshots.id }); + + if (row == null) { + throw new SessionError(500, "SERVER_MISCONFIG", "Snapshot insert failed."); + } + + const ordered = await tx + .select({ id: pageSnapshots.id, creationDate: pageSnapshots.creationDate }) + .from(pageSnapshots) + .where(eq(pageSnapshots.pageId, input.pageId)) + .orderBy(desc(pageSnapshots.creationDate)); + + const list = [...ordered]; + const toDelete: string[] = []; + while ( + list.length > 10 && + new Date() > + addDays(new Date(list[list.length - 1]!.creationDate), 14) + ) { + toDelete.push(list.pop()!.id); + } + if (toDelete.length > 0) { + await tx + .delete(pageSnapshots) + .where(inArray(pageSnapshots.id, toDelete)); + } + + return { snapshotId: row.id }; + }); +} + +export async function performPageSnapshotLoad(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageId: string; + snapshotId: string; +}): Promise<{ + encryptedSymmetricKey: Buffer | null; + encryptedData: Buffer; +}> { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await assertUserProPlan({ db: input.db, userId }); + + const [pageRow] = await input.db + .select({ groupId: pages.groupId }) + .from(pages) + .where( + and(eq(pages.id, input.pageId), isNull(pages.permanentDeletionDate)), + ) + .limit(1); + if (pageRow == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + const can = await userHasGroupPermission({ + db: input.db, + userId, + groupId: pageRow.groupId, + permission: "editGroupPages", + }); + if (!can) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const [snap] = await input.db + .select({ + encryptedSymmetricKey: pageSnapshots.encryptedSymmetricKey, + encryptedData: pageSnapshots.encryptedData, + }) + .from(pageSnapshots) + .where( + and( + eq(pageSnapshots.id, input.snapshotId), + eq(pageSnapshots.pageId, input.pageId), + ), + ) + .limit(1); + if (snap == null) { + throw new SessionError(404, "NOT_FOUND", "Snapshot not found."); + } + + return { + encryptedSymmetricKey: snap.encryptedSymmetricKey, + encryptedData: snap.encryptedData, + }; +} + +export async function performPageSnapshotDelete(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageId: string; + snapshotId: string; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + + const [pageRow] = await input.db + .select({ groupId: pages.groupId }) + .from(pages) + .where( + and(eq(pages.id, input.pageId), isNull(pages.permanentDeletionDate)), + ) + .limit(1); + if (pageRow == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + const can = await userHasGroupPermission({ + db: input.db, + userId, + groupId: pageRow.groupId, + permission: "editGroupPages", + }); + if (!can) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const removed = await input.db + .delete(pageSnapshots) + .where( + and( + eq(pageSnapshots.id, input.snapshotId), + eq(pageSnapshots.pageId, input.pageId), + ), + ) + .returning({ id: pageSnapshots.id }); + if (removed.length === 0) { + throw new SessionError(404, "NOT_FOUND", "Snapshot not found."); + } +} + +export async function performPageSoftDelete(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageId: string; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + + const [pre] = await input.db + .select({ + groupId: pages.groupId, + permanentDeletionDate: pages.permanentDeletionDate, + }) + .from(pages) + .where(eq(pages.id, input.pageId)) + .limit(1); + if (pre == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + if (pre.permanentDeletionDate != null) { + throw new SessionError(400, "BAD_REQUEST", "Page is already deleted."); + } + + const can = await userHasGroupPermission({ + db: input.db, + userId, + groupId: pre.groupId, + permission: "editGroupPages", + }); + if (!can) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const [g] = await input.db + .select({ mainPageId: groups.mainPageId }) + .from(groups) + .where(eq(groups.id, pre.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (g.mainPageId === input.pageId) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Cannot delete a group's main page, either replace the main page first or delete the whole group.", + ); + } + + await input.db + .update(pages) + .set({ permanentDeletionDate: tsString(addMonths(new Date(), 1)) }) + .where(eq(pages.id, input.pageId)); +} + +export async function performPageRestore(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageId: string; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + + const [pre] = await input.db + .select({ + groupId: pages.groupId, + permanentDeletionDate: pages.permanentDeletionDate, + free: pages.free, + }) + .from(pages) + .where(eq(pages.id, input.pageId)) + .limit(1); + if (pre == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + if (pre.permanentDeletionDate == null) { + throw new SessionError(400, "BAD_REQUEST", "Page is not deleted."); + } + + const can = await userHasGroupPermission({ + db: input.db, + userId, + groupId: pre.groupId, + permission: "editGroupPages", + }); + if (!can) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + if ( + new Date() > new Date(pre.permanentDeletionDate) && + pre.free === true + ) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Cannot restore a permanently deleted free page.", + ); + } + + await input.db + .update(pages) + .set({ permanentDeletionDate: null }) + .where(eq(pages.id, input.pageId)); +} + +export async function performPagePurge(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageId: string; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + + const [pre] = await input.db + .select({ + groupId: pages.groupId, + permanentDeletionDate: pages.permanentDeletionDate, + free: pages.free, + }) + .from(pages) + .where(eq(pages.id, input.pageId)) + .limit(1); + if (pre == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + + if ( + pre.permanentDeletionDate != null && + new Date() > new Date(pre.permanentDeletionDate) + ) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Page is already permanently deleted.", + ); + } + + const can = await userHasGroupPermission({ + db: input.db, + userId, + groupId: pre.groupId, + permission: "editGroupPages", + }); + if (!can) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const [g] = await input.db + .select({ mainPageId: groups.mainPageId }) + .from(groups) + .where(eq(groups.id, pre.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (g.mainPageId === input.pageId) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Cannot delete a group's main page, either replace the main page first or delete the whole group.", + ); + } + + let numForUser: number | undefined; + if (pre.free === true) { + const [u] = await input.db + .select({ numFreePages: users.numFreePages }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + if (u != null) { + numForUser = u.numFreePages + 1; + } + } + + await input.db.transaction(async (tx) => { + if (numForUser != null) { + await tx + .update(users) + .set({ numFreePages: numForUser }) + .where(eq(users.id, userId)); + } + await tx + .update(pages) + .set({ permanentDeletionDate: tsString(addDays(new Date(), -1)) }) + .where(eq(pages.id, input.pageId)); + }); +} From 94e3141a35255a57afad97a614c733f4ed48bc38 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 10:10:01 -0300 Subject: [PATCH 050/243] feat(new-deepnotes): move pages between groups --- new-deepnotes/PLAN_PROGRESS.md | 42 +- .../apps/api-worker/src/index.test.ts | 1 + new-deepnotes/apps/api-worker/src/index.ts | 109 ++++++ new-deepnotes/docs/TRPC_REST_MAP.md | 2 +- new-deepnotes/packages/api/src/index.ts | 1 + .../packages/api/src/openapi.test.ts | 1 + new-deepnotes/packages/api/src/openapi.ts | 30 ++ .../packages/api/src/schemas/pages-groups.ts | 50 +++ .../src/account-flows.integration.test.ts | 158 ++++++++ new-deepnotes/packages/session/src/index.ts | 8 + .../packages/session/src/page-move.ts | 358 ++++++++++++++++++ 11 files changed, 746 insertions(+), 14 deletions(-) create mode 100644 new-deepnotes/packages/session/src/page-move.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index fc9481e3..46c0a60a 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); **[pages — bump / backlinks / snapshots / deletion (slice 6)](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)**. **Still ahead:** [`POST /api/pages/:pageId/move`](./docs/TRPC_REST_MAP.md) (legacy two-step WS + Yjs/updates; collab data), optional **`groupCreation`** on `POST /api/groups/:id/pages`, WS or REST for **join invitations / requests / member role** ([TRPC map](./docs/TRPC_REST_MAP.md)), **realtime / collab**, **Stripe**. | +| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [slice 6 (page router)](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); **[slice 7 — page move](#pages-rest--slice-7-move--group-creation)** (`POST /api/pages/:pageId/move`). **Still ahead:** optional **`groupCreation`** on `POST /api/groups/:id/pages` (standalone create, not only via move), WS or REST for **join invitations / requests / member role** ([TRPC map](./docs/TRPC_REST_MAP.md)), **realtime / collab**, **Stripe**. | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — **21** cases when DB env set — register / email / password / **login + refresh** / **2FA** / **groups + pages** / **user page prefs** / **[slice 4–5 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion)** / **[slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)** (`bump` + `users` recents/starting, backlinks create/delete, Pro snapshots save/load/delete, page soft delete → restore → purge). `@deepnotes/db` `template-db.test.ts` — clone + FK matrix. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — **22** cases when DB env set — register / email / password / **login + refresh** / **2FA** / **groups + pages** / **user page prefs** / **[slice 4–5 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion)** / **[slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)** / **[slice 7 page move](#pages-rest--slice-7-move--group-creation)**. `@deepnotes/db` `template-db.test.ts` — clone + FK matrix. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) @@ -46,7 +46,7 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` **How to run locally:** ensure `.env` at `new-deepnotes/.env` has `DATABASE_URL` and (for template create/drop) `DATABASE_ADMIN_URL` with a role that can `CREATE DATABASE`. Then: -- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**21** cases when DB env set — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)) +- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**22** cases when DB env set — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7](#pages-rest--slice-7-move--group-creation)) - `pnpm --filter @deepnotes/db exec vitest run src/template-db.test.ts` CI should set the same vars against the workflow Postgres service (role with `CREATEDB`). @@ -78,6 +78,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **Group password, privacy, deletion (slice 4)** | `UPDATE users` → `plan = pro` on registered user; `performGroupPasswordEnable` / `Change` / `Disable` on **personal** group; `performGroupPrivacyMakePublic` after `accessKeyring` cleared; `performGroupPrivacySetJoinRequestsAllowed` **false**; `performGroupSoftDelete` (future `permanent_deletion_date`) → `Restore` (null) → `SoftDelete` + `Purge` (past date); `Restore` after purge **400** | PHC + server-side encrypt via `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`; `group-permissions` includes **`editGroupSettings`** (owner/admin only). | | **Privacy make-private (slice 5)** | Register with **public** personal group (`access_keyring` set); Pro; `performGroupPrivacyMakePrivate` with full re-key payload (single member, empty invites/requests, one page); assert `access_keyring` **null** + page `encrypted_symmetric_keyring` updated; second call **400** “already private”; re-public in DB then payload with **extra** page id → **400** keyset mismatch | Mirrors legacy `groupKeyRotationSchema` key sets; **no** `next_key_rotation_date` writes (RESTART_PLAN). | | **Page bump / backlinks / snapshots / deletion (slice 6)** | Pro; `performCreatePage` (second + third child); `performPageBump` (child, parent = main’s child chain); `users.starting_page_id` + recent; `performPageBacklinkCreate`+`Delete` (scoped query — bump may add separate `page_links` row); `performPageSnapshotSave`+`Load`+`Delete`; `performPageSoftDelete`+`Restore`+`SoftDelete`+`Purge` | Postgres-only links; Pro for snapshot save/load; not main page for delete. | +| **Page move (slice 7)** | Pro; no-op same dest + `!setAsMainPage` **400**; main page **400**; `setAsMainPage` in personal group (no `reencrypt`); `groupCreation` + cross-group reencrypt, `page_updates` index 0 | [performPageMove](packages/session/src/page-move.ts); no Redis collab key delete (RESTART_PLAN). | **`@deepnotes/db` real Postgres (`template-db.test.ts`):** @@ -194,23 +195,37 @@ CI should set the same vars against the workflow Postgres service (role with `CR ### Pages REST — slice 6 (bump, backlinks, snapshots, deletion) -**Goal:** legacy `pagesRouter` **except** `pages.move` and optional **`groupCreation`** on create. No KeyDB mirrors for links/snapshots — only **`page_links`** / **`page_snapshots`**. +**Goal:** legacy `pagesRouter` **except** `pages.move` (handled in [slice 7](#pages-rest--slice-7-move--group-creation)) and optional **`groupCreation`** on `POST` create only. No KeyDB mirrors for links/snapshots — only **`page_links`** / **`page_snapshots`**. | Layer | What shipped | |-------|----------------| | **`@deepnotes/session`** | `page-operations.ts` — `performPageBump` (personal-main breadcrumb validation, `users.*` recents, optional `page_links` on bump, activity timestamps), `performPageBacklinkCreate` / `performPageBacklinkDelete`, `performPageSnapshotSave` (Pro, legacy trim: >10 and oldest >14d), `performPageSnapshotLoad` (Pro), `performPageSnapshotDelete`, `performPageSoftDelete` / `performPageRestore` / `performPagePurge` (not group **main** page). | | **`@deepnotes/api`** | `pageIdPathSchema`, `pageTargetPagePathSchema`, `pageSnapshotPathSchema`, `pageBumpRequestSchema`, `pageBacklinkCreateRequestSchema`, `pageSnapshot*`, OpenAPI paths under `/api/pages/...`. | -| **`@deepnotes/api-worker`** | Routes listed in [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) **Pages** table; `503` matrix **54** session routes. | +| **`@deepnotes/api-worker`** | Routes listed in [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) **Pages** table; with [slice 7](#pages-rest--slice-7-move--group-creation) move route, `503` matrix **55** session routes. | | **Tests** | [Phase 3 detail table](#phase-3-test-coverage-detail) **slice 6** row; integration case **pages: bump, backlinks, …** | **Path semantics:** `POST /api/pages/:pageId/backlinks` — `pageId` = **target**; `DELETE /api/pages/:sourcePageId/backlinks/:targetPageId` — **source** first (legacy `sourcePageId` / `targetPageId`). +### Pages REST — slice 7 (move + group creation) + +**Goal:** legacy `websocket/pages/move` (tRPC `moveProcedureStep1` + `moveProcedureStep2`) in **one** `POST`, as in [TRPC map](./docs/TRPC_REST_MAP.md) — `editGroupSettings` on source, optional **new** shared group + owner row (`groupCreation` when `destGroupId` is unused), `setAsMainPage` with personal-group `users_pages` swap, cross-group page + `page_snapshots` + replace `page_updates` with a single Yjs payload at index 0, bump `users.recent_group_ids` for the destination. **Intentional omission:** KeyDB/Redis `page-update-*` deletes (RESTART_PLAN: collab on a new path). + +| Layer | What shipped | +|-------|----------------| +| **`@deepnotes/session`** | `page-move.ts` — `performPageMove` (Pro, `user-plan.assertUserProPlan`); `SessionEnv` + optional Argon2 group password for `groupCreation` (same as register/demo) via `computeGroupPasswordPhc` / `encryptGroupRehashedPasswordHash` | +| **`@deepnotes/api`** | `pageMoveRequestSchema` (+ nested reencrypt + `pageMoveGroupCreationRequestSchema`); `GET` OpenAPI **not** used for a separate “read step” — client must supply prepared ciphertext in one `POST` (or call existing read APIs first) | +| **`@deepnotes/api-worker`** | `POST /api/pages/:pageId/move` — `503` matrix **+1** (**55** total session routes) | +| **Tests** | Integration: [slice 7 row](#phase-3-test-coverage-detail) — `pages: move (set main, groupCreation, validation)`; `openapi.test.ts` + worker 503 list | + +**Client contract:** `reencrypt` is **required** when the page’s `group_id` changes (after optional `groupCreation` insert, the page still has the old `group_id` until the same transaction’s update block). Omit `reencrypt` when only `setAsMainPage` in the **same** group. Snapshots: `pageEncryptedSnapshots` is a map by snapshot id (empty `{}` if none). + ### Not started (Phase 3 — pages, groups, infra) - [x] **Groups (REST, slice 4):** [password, privacy, soft delete, restore, purge](#pagesgroups-rest--slice-4-group-password-privacy-deletion) — `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY` on `SessionEnv` / worker bindings. - [x] **Groups (REST, slice 5):** [make-private / re-key](#pagesgroups-rest--slice-5-privacy-private-re-key) — `POST /api/groups/:groupId/privacy/private`; OpenAPI + [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md). -- [x] **Pages (REST, slice 6):** [bump / backlinks / snapshots / page deletion](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) — **`pages.move` not** implemented. -- [ ] **`POST /api/pages/:pageId/move`** — legacy [websocket/pages/move](../apps/app-server/src/websocket/pages/move.ts) (two steps, Yjs `page_updates`, reencrypt snapshots, Redis cache bust); optional **`groupCreation`** in step 1. +- [x] **Pages (REST, slice 6):** [bump / backlinks / snapshots / page deletion](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). +- [x] **Pages (REST, slice 7):** [move + optional `groupCreation`](#pages-rest--slice-7-move--group-creation) — `POST /api/pages/:pageId/move` (`page-move.ts`). +- [ ] **Optional** **`groupCreation`** on **`POST /api/groups/:id/pages`** without move (if product wants “create group + first page” without moving an existing page). - [ ] **WS / REST:** group join/invite, member role, etc. (see [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) WebSocket table). - [ ] **Realtime / collab** (new or adapted protocols; no key rotation). - [ ] **Stripe:** `POST /api/webhooks/stripe`, checkout/portal (no RevenueCat); wire **`deleteStripeCustomer`** from account delete when keys exist. @@ -262,9 +277,9 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**21** cases when DB env set) — … + [slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; **`POST /api/pages/:pageId/move`**; optional `groupCreation` on create | -| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (session routes + [slice 6 `/api/pages/...` paths](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)); **`schemas/users.test.ts`**; **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; more Zod edge cases for new page schemas | -| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **54** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) page routes | **200** tests with stub `SessionEnv` + template DB (heavier) | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**22** cases when DB env set) — … + [slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 move](#pages-rest--slice-7-move--group-creation); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; optional `groupCreation` on create-only `POST` | +| **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (session routes + [slice 6/7 `/api/pages/...` paths](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)); **`schemas/users.test.ts`**; **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; more Zod edge cases for new page schemas | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **55** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + `/api/pages/{pageId}/move` | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -285,8 +300,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Drizzle migrations from empty DB documented for production upgrades. - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). -- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (account + **2FA** + **groups/pages** + prefs + [slice 4–5](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 6 page router](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) — **21** `account-flows` + **6** `@deepnotes/db` when DB set; [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) page router. **Next:** **Redis** failed-login against real Redis; move route; **Stripe** when billing exists. +- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (account + **2FA** + **groups/pages** + prefs + [slice 4–5](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 6 page router](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 page move](#pages-rest--slice-7-move--group-creation)). +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) — **22** `account-flows` + **6** `@deepnotes/db` when DB set; [slice 6 + 7](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). **Next:** **Redis** failed-login against real Redis; **Stripe** when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -297,7 +312,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Phase 3 working order (suggested) -Use this when resuming: **(done)** through [slice 6 — page bump / backlinks / snapshots / deletion](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). **(next)** **`POST /api/pages/:pageId/move`** (two-step collab/updates parity, see legacy `websocket/pages/move`); optional **`groupCreation`** on page create. **(then)** WS or REST for **join invitations**, **join requests**, **member role** / remove. **(then)** **realtime + collab** and **Stripe** + **`deleteStripeCustomer`**. +Use this when resuming: **(done)** through [slice 7 — page move](#pages-rest--slice-7-move--group-creation). **(next)** optional **`groupCreation`** on `POST /api/groups/:id/pages` only (if desired); **(then)** WS or REST for **join invitations**, **join requests**, **member role** / remove. **(then)** **realtime + collab** and **Stripe** + **`deleteStripeCustomer`**. --- @@ -305,6 +320,7 @@ Use this when resuming: **(done)** through [slice 6 — page bump / backlinks / | Date | Change | |------|--------| +| 2026-04-27 | **Phase 3 — pages slice 7 (move):** `page-move.ts` `performPageMove` (Pro; optional `groupCreation` + reencrypt; `setAsMainPage` + `users_pages` swap for personal; `page_updates` + `page_snapshots` on cross-group); `pageMoveRequestSchema` in `@deepnotes/api`; `POST /api/pages/:pageId/move` + OpenAPI; worker **503** **55** tests; `account-flows` **22** cases; [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) `websocket/pages/move` row; [slice 7 section](#pages-rest--slice-7-move--group-creation). **Next:** group invites / requests REST or join routes; **realtime + collab**; **Stripe**. | | 2026-04-27 | **Phase 3 — pages router slice 6 (bump, backlinks, snapshots, page deletion):** `page-operations.ts` (`performPageBump` … `performPagePurge`); `page_links` / `page_snapshots` + Postgres-only (no `page-backlinks` KeyDB); OpenAPI + Hono; worker **503** matrix **54** tests; `account-flows` **21** integration cases; [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) `pages.*` rows; [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). **Next:** `POST /api/pages/:pageId/move` (WS parity + `page_updates` / collab cache). | | 2026-04-27 | **Phase 3 — groups slice 5 (`POST …/privacy/private`):** `performGroupPrivacyMakePrivate` + `groupPrivacyPrivateRequestSchema` (legacy `groupKeyRotationSchema` shape, base64 JSON); Hono + OpenAPI; worker **503** **45** tests; `account-flows` **20** integration cases (make private, already-private **400**, bad page keyset **400**); **TRPC_REST_MAP** Groups + WS `make-private` rows; [slice 5 section](#pagesgroups-rest--slice-5-privacy-private-re-key); [working order](#phase-3-working-order-suggested) now leads with **`pagesRouter`**. | | 2026-04-27 | **Phase 3 — groups slice 4 (password, privacy, deletion):** `editGroupSettings` in `group-permissions.ts`; `SESSION` `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`; `user-plan.ts` (`assertUserProPlan`); `group-password.ts` / `group-privacy.ts` / `group-deletion.ts`; Hono + OpenAPI + Zod; worker **503** **44** tests; `account-flows` **19** integration cases; **TRPC_REST_MAP** updated; [slice 4 section](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + not-started `privacy/private`. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index e0d55ebe..8079ec53 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -88,6 +88,7 @@ describe("api-worker", () => { "/api/groups/aaaaaaaaaaaaaaaaaaaaa/restore", ], ["POST", "/api/groups/aaaaaaaaaaaaaaaaaaaaa/purge"], + ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/move"], ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/bump"], ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/backlinks"], [ diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index d154a0ab..27ca925d 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -6,6 +6,7 @@ import { groupPagesListQuerySchema, pageBacklinkCreateRequestSchema, pageBumpRequestSchema, + pageMoveRequestSchema, pageIdPathSchema, pageSnapshotCreateResponseSchema, pageSnapshotSaveRequestSchema, @@ -33,6 +34,7 @@ import { } from "@deepnotes/api"; import type { ContentfulStatusCode } from "hono/utils/http-status"; import { Hono } from "hono"; +import type { PageMoveBody } from "@deepnotes/session"; import { getDbForConnectionString } from "./db-pool.js"; import { readCookieHeader } from "./cookies.js"; @@ -1371,6 +1373,113 @@ app.post("/api/groups/:groupId/pages", async (c) => { } }); +app.post("/api/pages/:pageId/move", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const pParams = pageIdPathSchema.safeParse({ pageId: c.req.param("pageId") }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + const parsed = pageMoveRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const d = parsed.data; + const moveBody: PageMoveBody = { + destGroupId: d.destGroupId, + setAsMainPage: d.setAsMainPage, + groupCreation: + d.groupCreation == null + ? undefined + : { + groupEncryptedName: d.groupCreation.groupEncryptedName, + groupPasswordHash: d.groupCreation.groupPasswordHash, + groupIsPublic: d.groupCreation.groupIsPublic, + groupAccessKeyring: d.groupCreation.groupAccessKeyring, + groupEncryptedInternalKeyring: + d.groupCreation.groupEncryptedInternalKeyring, + groupEncryptedContentKeyring: d.groupCreation.groupEncryptedContentKeyring, + groupPublicKeyring: d.groupCreation.groupPublicKeyring, + groupEncryptedPrivateKeyring: d.groupCreation.groupEncryptedPrivateKeyring, + groupOwnerEncryptedName: d.groupCreation.groupOwnerEncryptedName, + }, + reencrypt: + d.reencrypt == null + ? undefined + : { + pageEncryptedSymmetricKeyring: + d.reencrypt.pageEncryptedSymmetricKeyring, + pageEncryptedRelativeTitle: d.reencrypt.pageEncryptedRelativeTitle, + pageEncryptedAbsoluteTitle: d.reencrypt.pageEncryptedAbsoluteTitle, + pageEncryptedUpdate: d.reencrypt.pageEncryptedUpdate, + pageEncryptedSnapshots: Object.fromEntries( + Object.entries(d.reencrypt.pageEncryptedSnapshots).map( + ([id, snap]) => [ + id, + { + encryptedSymmetricKey: snap.encryptedSymmetricKey, + encryptedData: snap.encryptedData, + }, + ], + ), + ), + }, + }; + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performPageMove } = await import("@deepnotes/session"); + await performPageMove({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageId: pParams.data.pageId, + body: moveBody, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.post("/api/pages/:pageId/bump", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index 0e2f9c8f..1ad471ed 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -89,7 +89,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `websocket/groups/remove-user` | `DELETE /api/groups/:groupId/members/:userId` | | | `websocket/groups/privacy/make-private` | `POST /api/groups/:groupId/privacy/private` | **implemented** — see `groups.privacy.makePrivate` row above | | `websocket/groups/rotate-keys` | — | **removed** per RESTART_PLAN | -| `websocket/pages/move` | `POST /api/pages/:pageId/move` | | +| `websocket/pages/move` | `POST /api/pages/:pageId/move` (**implemented** — `pageMoveRequestSchema`; `performPageMove`: Pro, optional `groupCreation`, `reencrypt` when changing group) | | | `websocket/users/account/change-password` | `POST /api/users/me/password` | **implemented** in `@deepnotes/session` (`performUserPasswordChange`) | | `websocket/users/account/email-change/finish` | `POST /api/users/me/email-change/confirm` | **implemented** — one call: `oldLoginHash`, `emailVerificationCode` (6 digits), `newLoginHash`, `userEncryptedPrivateKeyring`, `userEncryptedSymmetricKeyring` (b64; same as register/password); 204, clears cookies; optional Stripe in worker | | `websocket/users/account/rotate-keys` | — | **removed** | diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index b0cc003c..02ba00e1 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -39,6 +39,7 @@ export { groupPrivacyPublicRequestSchema, pageBacklinkCreateRequestSchema, pageBumpRequestSchema, + pageMoveRequestSchema, pageIdPathSchema, pageSnapshotCreateResponseSchema, pageSnapshotLoadResponseSchema, diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 7b85a5fd..84ec5d7d 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -75,6 +75,7 @@ describe("getOpenApiDocument", () => { expect(doc.paths?.["/api/groups/{groupId}"]?.delete).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/restore"]?.post).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/purge"]?.post).toBeDefined(); + expect(doc.paths?.["/api/pages/{pageId}/move"]?.post).toBeDefined(); expect(doc.paths?.["/api/pages/{pageId}/bump"]?.post).toBeDefined(); expect( doc.paths?.["/api/pages/{pageId}/backlinks"]?.post, diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index 5b0448e4..d0224266 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -32,6 +32,7 @@ import { groupPrivacyPublicRequestSchema, pageBacklinkCreateRequestSchema, pageBumpRequestSchema, + pageMoveRequestSchema, pageIdPathSchema, pageSnapshotCreateResponseSchema, pageSnapshotLoadResponseSchema, @@ -886,6 +887,35 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/pages/{pageId}/move", + summary: "Move page (optionally create group, re-key, set main)", + description: + "Replaces `websocket/pages/move` — Pro-only; `editGroupSettings` on the page's current group, `editGroupPages` on destination unless `groupCreation` creates it. `reencrypt` is required when the page changes group (Yjs `page_updates` replaced with a single index-0 row; snapshots updated by id).", + request: { + params: pageIdPathSchema, + body: { + content: { + "application/json": { + schema: pageMoveRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Move completed." }, + 400: { + description: "No-op move, or invalid payload.", + content: { "application/json": { schema: sessionErrorResponseSchema } }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "post", path: "/api/pages/{pageId}/bump", diff --git a/new-deepnotes/packages/api/src/schemas/pages-groups.ts b/new-deepnotes/packages/api/src/schemas/pages-groups.ts index d600e9a4..11a4536d 100644 --- a/new-deepnotes/packages/api/src/schemas/pages-groups.ts +++ b/new-deepnotes/packages/api/src/schemas/pages-groups.ts @@ -211,6 +211,56 @@ export const pageBumpRequestSchema = z }) .openapi("PageBumpRequest"); +/** Ciphertext to persist after a cross-group move (legacy `pageKeyRotationSchema` + Yjs update). */ +export const pageMoveReencryptRequestSchema = z + .object({ + pageEncryptedSymmetricKeyring: byteB64, + pageEncryptedRelativeTitle: byteB64, + pageEncryptedAbsoluteTitle: byteB64, + pageEncryptedUpdate: byteB64, + pageEncryptedSnapshots: z + .record( + nanoidRecordKeySchema, + z.object({ + encryptedSymmetricKey: byteB64, + encryptedData: byteB64, + }), + ) + .default({}), + }) + .openapi("PageMoveReencryptRequest"); + +/** Create a new shared group in the same call as a move (legacy WS step 1 `groupCreation`). */ +export const pageMoveGroupCreationRequestSchema = z + .object({ + groupEncryptedName: byteB64, + groupPasswordHash: byteB64.optional(), + groupIsPublic: z.boolean(), + groupAccessKeyring: byteB64, + groupEncryptedInternalKeyring: byteB64, + groupEncryptedContentKeyring: byteB64, + groupPublicKeyring: byteB64, + groupEncryptedPrivateKeyring: byteB64, + groupOwnerEncryptedName: byteB64, + }) + .openapi("PageMoveGroupCreationRequest"); + +/** + * Replaces `websocket/pages/move` (two tRPC steps) with one `POST` (optional `reencrypt` when + * `sourceGroupId !== destGroupId`). + */ +export const pageMoveRequestSchema = z + .object({ + destGroupId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi(nanoidIdOpenapi), + setAsMainPage: z.boolean(), + groupCreation: pageMoveGroupCreationRequestSchema.optional(), + reencrypt: pageMoveReencryptRequestSchema.optional(), + }) + .openapi("PageMoveRequest"); + export const pageBacklinkCreateRequestSchema = z .object({ sourcePageId: z diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index fb838729..53100d0f 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -25,6 +25,7 @@ import { groups, notifications, pageLinks, + pageUpdates, pages, sessions, users, @@ -71,6 +72,7 @@ import { performPageBacklinkCreate, performPageBacklinkDelete, performPageBump, + performPageMove, performPagePurge, performPageRestore, performPageSnapshotDelete, @@ -2074,5 +2076,161 @@ describe.skipIf(resolveTemplateContext() == null)( } } }); + + it("pages: move (set main, groupCreation, validation)", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `pmove-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + await db + .update(users) + .set({ plan: "pro" }) + .where(eq(users.id, reg.userId)); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + + const childId = nanoid(); + await performCreatePage({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + body: { + parentPageId: reg.pageId, + pageId: childId, + pageEncryptedSymmetricKeyring: rand32(), + pageEncryptedRelativeTitle: rand32(), + pageEncryptedAbsoluteTitle: rand32(), + }, + }); + + await expect( + performPageMove({ + db, + env, + accessCookie: access, + pageId: childId, + body: { + destGroupId: reg.groupId, + setAsMainPage: false, + }, + }), + ).rejects.toMatchObject({ + message: "No changes were requested on page move.", + }); + + await expect( + performPageMove({ + db, + env, + accessCookie: access, + pageId: reg.pageId, + body: { + destGroupId: reg.groupId, + setAsMainPage: true, + }, + }), + ).rejects.toMatchObject({ + message: + "Cannot move main page of a group. Please set another page as main page first.", + }); + + await performPageMove({ + db, + env, + accessCookie: access, + pageId: childId, + body: { + destGroupId: reg.groupId, + setAsMainPage: true, + }, + }); + const [mPersonal] = await db + .select({ mainPageId: groups.mainPageId, userId: groups.userId }) + .from(groups) + .where(eq(groups.id, reg.groupId)); + expect(mPersonal?.mainPageId).toBe(childId); + expect(mPersonal?.userId).toBe(reg.userId); + + const child2 = nanoid(); + await performCreatePage({ + db, + env, + accessCookie: access, + groupId: reg.groupId, + body: { + parentPageId: childId, + pageId: child2, + pageEncryptedSymmetricKeyring: rand32(), + pageEncryptedRelativeTitle: rand32(), + pageEncryptedAbsoluteTitle: rand32(), + }, + }); + const gc = { + groupEncryptedName: rand32(), + groupIsPublic: true, + groupAccessKeyring: rand32(), + groupEncryptedInternalKeyring: rand32(), + groupEncryptedContentKeyring: rand32(), + groupPublicKeyring: rand32(), + groupEncryptedPrivateKeyring: rand32(), + groupOwnerEncryptedName: rand32(), + }; + const newGid = nanoid(); + const up = rand32(); + await performPageMove({ + db, + env, + accessCookie: access, + pageId: child2, + body: { + destGroupId: newGid, + setAsMainPage: false, + groupCreation: gc, + reencrypt: { + pageEncryptedSymmetricKeyring: rand32(), + pageEncryptedRelativeTitle: rand32(), + pageEncryptedAbsoluteTitle: rand32(), + pageEncryptedUpdate: up, + pageEncryptedSnapshots: {}, + }, + }, + }); + const [prow] = await db + .select({ groupId: pages.groupId }) + .from(pages) + .where(eq(pages.id, child2)); + expect(prow?.groupId).toBe(newGid); + const [upRow] = await db + .select() + .from(pageUpdates) + .where(eq(pageUpdates.pageId, child2)); + expect(upRow?.index).toBe(0); + expect(upRow?.encryptedData.equals(Buffer.from(up))).toBe(true); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); }, ); diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index aa770a0c..e8e6dfde 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -85,3 +85,11 @@ export { performPageSnapshotSave, performPageSoftDelete, } from "./page-operations.js"; +export { + performPageMove, +} from "./page-move.js"; +export type { + PageMoveBody, + PageMoveGroupCreation, + PageMoveReencrypt, +} from "./page-move.js"; diff --git a/new-deepnotes/packages/session/src/page-move.ts b/new-deepnotes/packages/session/src/page-move.ts new file mode 100644 index 00000000..c032012b --- /dev/null +++ b/new-deepnotes/packages/session/src/page-move.ts @@ -0,0 +1,358 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { + groupMembers, + groups, + pageSnapshots, + pages, + pageUpdates, + users, + usersPages, +} from "@deepnotes/db/schema"; +import { and, eq, isNull } from "drizzle-orm"; + +import { + computeGroupPasswordPhc, + encryptGroupRehashedPasswordHash, + ensureSodiumReady, +} from "./crypto/session-crypto.js"; +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { userHasGroupPermission } from "./group-permissions.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; +import { assertUserProPlan } from "./user-plan.js"; + +function toBuf(u: Uint8Array): Buffer { + return Buffer.from(u); +} + +function bumpStringIdList(ids: string[], itemId: string, max: number): string[] { + const rest = ids.filter((id) => id !== itemId); + return [itemId, ...rest].slice(0, max); +} + +export type PageMoveGroupCreation = { + groupEncryptedName: Uint8Array; + groupPasswordHash?: Uint8Array; + groupIsPublic: boolean; + groupAccessKeyring: Uint8Array; + groupEncryptedInternalKeyring: Uint8Array; + groupEncryptedContentKeyring: Uint8Array; + groupPublicKeyring: Uint8Array; + groupEncryptedPrivateKeyring: Uint8Array; + groupOwnerEncryptedName: Uint8Array; +}; + +export type PageMoveReencrypt = { + pageEncryptedSymmetricKeyring: Uint8Array; + pageEncryptedRelativeTitle: Uint8Array; + pageEncryptedAbsoluteTitle: Uint8Array; + pageEncryptedUpdate: Uint8Array; + pageEncryptedSnapshots: Record< + string, + { encryptedSymmetricKey: Uint8Array; encryptedData: Uint8Array } + >; +}; + +export type PageMoveBody = { + destGroupId: string; + setAsMainPage: boolean; + groupCreation?: PageMoveGroupCreation; + reencrypt?: PageMoveReencrypt; +}; + +/** + * Replaces legacy WebSocket `pages.move` (two tRPC steps) with one transaction: + * optional new group, optional set-as-main (personal `users_pages` swap), and + * cross-group ciphertext + `page_updates` + snapshot rows when the page changes group. + * Collab/KeyDB cache bust is not performed (RESTART_PLAN: new collab path). + */ +export async function performPageMove(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageId: string; + body: PageMoveBody; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + await assertUserProPlan({ db: input.db, userId }); + await ensureSodiumReady(); + + const destGroupId = input.body.destGroupId; + const setAsMainPage = input.body.setAsMainPage; + const groupCreation = input.body.groupCreation; + const reencrypt = input.body.reencrypt; + + const [pageRow] = await input.db + .select({ + id: pages.id, + groupId: pages.groupId, + }) + .from(pages) + .where( + and(eq(pages.id, input.pageId), isNull(pages.permanentDeletionDate)), + ) + .limit(1); + if (pageRow == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + + const sourceGroupId = pageRow.groupId; + + if (sourceGroupId === destGroupId && !setAsMainPage) { + throw new SessionError( + 400, + "BAD_REQUEST", + "No changes were requested on page move.", + ); + } + + const [srcG] = await input.db + .select({ mainPageId: groups.mainPageId }) + .from(groups) + .where(eq(groups.id, sourceGroupId)) + .limit(1); + if (srcG == null) { + throw new SessionError(404, "NOT_FOUND", "Source group not found."); + } + if (input.pageId === srcG.mainPageId) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Cannot move main page of a group. Please set another page as main page first.", + ); + } + + await requireEditGroupSettings({ db: input.db, userId, groupId: sourceGroupId }); + + if (groupCreation == null) { + const [destExists] = await input.db + .select({ id: groups.id }) + .from(groups) + .where(eq(groups.id, destGroupId)) + .limit(1); + if (destExists == null) { + throw new SessionError(404, "NOT_FOUND", "Destination group not found."); + } + const canEditDest = await userHasGroupPermission({ + db: input.db, + userId, + groupId: destGroupId, + permission: "editGroupPages", + }); + if (!canEditDest) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + } else { + const [destExists] = await input.db + .select({ id: groups.id }) + .from(groups) + .where(eq(groups.id, destGroupId)) + .limit(1); + if (destExists != null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "A group with this id already exists.", + ); + } + } + + if (sourceGroupId !== destGroupId && reencrypt == null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "reencrypt payload is required when moving to another group.", + ); + } + + return await input.db.transaction(async (tx) => { + if (groupCreation != null) { + let encryptedRehashed: Buffer | undefined; + if ( + groupCreation.groupPasswordHash != null && + groupCreation.groupPasswordHash.byteLength > 0 + ) { + const phc = computeGroupPasswordPhc(groupCreation.groupPasswordHash); + const enc = encryptGroupRehashedPasswordHash( + phc, + input.env.GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY, + ); + encryptedRehashed = Buffer.from(enc); + } + await tx.insert(groups).values({ + id: destGroupId, + mainPageId: input.pageId, + encryptedName: toBuf(groupCreation.groupEncryptedName), + userId: null, + publicKeyring: toBuf(groupCreation.groupPublicKeyring), + encryptedPrivateKeyring: toBuf( + groupCreation.groupEncryptedPrivateKeyring, + ), + encryptedContentKeyring: toBuf(groupCreation.groupEncryptedContentKeyring), + accessKeyring: groupCreation.groupIsPublic + ? toBuf(groupCreation.groupAccessKeyring) + : null, + encryptedRehashedPasswordHash: encryptedRehashed, + }); + await tx.insert(groupMembers).values({ + groupId: destGroupId, + userId, + role: "owner", + encryptedAccessKeyring: groupCreation.groupIsPublic + ? null + : toBuf(groupCreation.groupAccessKeyring), + encryptedInternalKeyring: toBuf( + groupCreation.groupEncryptedInternalKeyring, + ), + encryptedName: toBuf(groupCreation.groupOwnerEncryptedName), + }); + } + + if (setAsMainPage) { + const [destG] = await tx + .select({ + mainPageId: groups.mainPageId, + userId: groups.userId, + }) + .from(groups) + .where(eq(groups.id, destGroupId)) + .limit(1); + if (destG == null) { + throw new SessionError(404, "NOT_FOUND", "Destination group not found."); + } + const destGroupOldMainPageId = destG.mainPageId; + const destGroupMemberId = destG.userId ?? null; + + const [upOld] = await tx + .select({ lastParentId: usersPages.lastParentId }) + .from(usersPages) + .where( + and( + eq(usersPages.userId, userId), + eq(usersPages.pageId, destGroupOldMainPageId), + ), + ) + .limit(1); + const oldLastParentId = upOld?.lastParentId ?? null; + + if ( + destGroupMemberId != null && + userId === destGroupMemberId && + input.pageId !== destGroupOldMainPageId + ) { + await tx + .insert(usersPages) + .values({ + userId, + pageId: input.pageId, + lastParentId: oldLastParentId, + }) + .onConflictDoUpdate({ + target: [usersPages.userId, usersPages.pageId], + set: { lastParentId: oldLastParentId }, + }); + await tx + .insert(usersPages) + .values({ + userId, + pageId: destGroupOldMainPageId, + lastParentId: input.pageId, + }) + .onConflictDoUpdate({ + target: [usersPages.userId, usersPages.pageId], + set: { lastParentId: input.pageId }, + }); + } + + await tx + .update(groups) + .set({ mainPageId: input.pageId }) + .where(eq(groups.id, destGroupId)); + } + + if (sourceGroupId !== destGroupId) { + if (reencrypt == null) { + throw new SessionError( + 500, + "SERVER_ERROR", + "Missing reencrypt payload for cross-group move.", + ); + } + await tx + .update(pages) + .set({ + groupId: destGroupId, + encryptedSymmetricKeyring: toBuf( + reencrypt.pageEncryptedSymmetricKeyring, + ), + encryptedRelativeTitle: toBuf(reencrypt.pageEncryptedRelativeTitle), + encryptedAbsoluteTitle: toBuf(reencrypt.pageEncryptedAbsoluteTitle), + }) + .where(eq(pages.id, input.pageId)); + + for (const [snapshotId, snap] of Object.entries( + reencrypt.pageEncryptedSnapshots, + )) { + await tx + .update(pageSnapshots) + .set({ + encryptedSymmetricKey: toBuf(snap.encryptedSymmetricKey), + encryptedData: toBuf(snap.encryptedData), + }) + .where( + and( + eq(pageSnapshots.id, snapshotId), + eq(pageSnapshots.pageId, input.pageId), + ), + ); + } + + await tx.delete(pageUpdates).where(eq(pageUpdates.pageId, input.pageId)); + await tx.insert(pageUpdates).values({ + pageId: input.pageId, + index: 0, + encryptedData: toBuf(reencrypt.pageEncryptedUpdate), + }); + + const [urow] = await tx + .select({ + recentGroupIds: users.recentGroupIds, + }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + if (urow == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + const next = bumpStringIdList( + urow.recentGroupIds, + destGroupId, + 50, + ); + await tx + .update(users) + .set({ recentGroupIds: next }) + .where(eq(users.id, userId)); + } + }); +} + +async function requireEditGroupSettings(input: { + db: DeepnotesDb; + userId: string; + groupId: string; +}): Promise { + const allowed = await userHasGroupPermission({ + db: input.db, + userId: input.userId, + groupId: input.groupId, + permission: "editGroupSettings", + }); + if (!allowed) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } +} From 4ef6a46c59f3d13531e7fa6d6a2e8a0b413292b6 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 10:33:41 -0300 Subject: [PATCH 051/243] feat(new-deepnotes): group creation helpers and page list refactors --- new-deepnotes/PLAN_PROGRESS.md | 44 ++++++--- new-deepnotes/docs/TRPC_REST_MAP.md | 2 +- new-deepnotes/packages/api/src/openapi.ts | 2 +- .../packages/api/src/schemas/pages-groups.ts | 33 ++++--- .../src/account-flows.integration.test.ts | 97 +++++++++++++++++++ .../session/src/group-creation-shared.ts | 71 ++++++++++++++ .../packages/session/src/group-pages.ts | 86 ++++++++++++---- .../packages/session/src/page-move.ts | 65 +++---------- 8 files changed, 296 insertions(+), 104 deletions(-) create mode 100644 new-deepnotes/packages/session/src/group-creation-shared.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 46c0a60a..50fa4e8a 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [slice 6 (page router)](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); **[slice 7 — page move](#pages-rest--slice-7-move--group-creation)** (`POST /api/pages/:pageId/move`). **Still ahead:** optional **`groupCreation`** on `POST /api/groups/:id/pages` (standalone create, not only via move), WS or REST for **join invitations / requests / member role** ([TRPC map](./docs/TRPC_REST_MAP.md)), **realtime / collab**, **Stripe**. | +| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); [slice 7 — page move](#pages-rest--slice-7-move--group-creation); **[slice 8 — create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation)** (`POST /api/groups/:groupId/pages` with optional new shared group). **Still ahead:** REST for **group join flows** (invites, requests, member role, remove) ([TRPC map](./docs/TRPC_REST_MAP.md) WebSocket table), **realtime / collab**, **Stripe** (`POST /api/webhooks/stripe`, portal/checkout). | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — **22** cases when DB env set — register / email / password / **login + refresh** / **2FA** / **groups + pages** / **user page prefs** / **[slice 4–5 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion)** / **[slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)** / **[slice 7 page move](#pages-rest--slice-7-move--group-creation)**. `@deepnotes/db` `template-db.test.ts` — clone + FK matrix. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — **23** cases when DB env set — register / email / password / **login + refresh** / **2FA** / **groups + pages** / **[slice 8 `groupCreation` create](#pagesgroups-rest--slice-8-create--groupcreation)** / **user page prefs** / **[slice 4–5 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion)** / **[slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)** / **[slice 7 page move](#pages-rest--slice-7-move--group-creation)**. `@deepnotes/db` `template-db.test.ts` — clone + FK matrix. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) @@ -46,7 +46,7 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` **How to run locally:** ensure `.env` at `new-deepnotes/.env` has `DATABASE_URL` and (for template create/drop) `DATABASE_ADMIN_URL` with a role that can `CREATE DATABASE`. Then: -- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**22** cases when DB env set — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7](#pages-rest--slice-7-move--group-creation)) +- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**23** cases when DB env set — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7](#pages-rest--slice-7-move--group-creation) + [slice 8](#pagesgroups-rest--slice-8-create--groupcreation)) - `pnpm --filter @deepnotes/db exec vitest run src/template-db.test.ts` CI should set the same vars against the workflow Postgres service (role with `CREATEDB`). @@ -79,6 +79,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **Privacy make-private (slice 5)** | Register with **public** personal group (`access_keyring` set); Pro; `performGroupPrivacyMakePrivate` with full re-key payload (single member, empty invites/requests, one page); assert `access_keyring` **null** + page `encrypted_symmetric_keyring` updated; second call **400** “already private”; re-public in DB then payload with **extra** page id → **400** keyset mismatch | Mirrors legacy `groupKeyRotationSchema` key sets; **no** `next_key_rotation_date` writes (RESTART_PLAN). | | **Page bump / backlinks / snapshots / deletion (slice 6)** | Pro; `performCreatePage` (second + third child); `performPageBump` (child, parent = main’s child chain); `users.starting_page_id` + recent; `performPageBacklinkCreate`+`Delete` (scoped query — bump may add separate `page_links` row); `performPageSnapshotSave`+`Load`+`Delete`; `performPageSoftDelete`+`Restore`+`SoftDelete`+`Purge` | Postgres-only links; Pro for snapshot save/load; not main page for delete. | | **Page move (slice 7)** | Pro; no-op same dest + `!setAsMainPage` **400**; main page **400**; `setAsMainPage` in personal group (no `reencrypt`); `groupCreation` + cross-group reencrypt, `page_updates` index 0 | [performPageMove](packages/session/src/page-move.ts); no Redis collab key delete (RESTART_PLAN). | +| **Create + `groupCreation` (slice 8)** | Pro; path `groupId` = unused nanoid; `parentPageId` in **personal** group; [insertSharedGroupForOwnerInTx](packages/session/src/group-creation-shared.ts) then `pages` + `users_pages` | Parity with legacy `pages.create` + `groupCreation` (tRPC). | **`@deepnotes/db` real Postgres (`template-db.test.ts`):** @@ -141,7 +142,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **`@deepnotes/api`** | `schemas/pages-groups.ts` — path/query/body/response Zod + OpenAPI; wired in `openapi.ts`. | | **`@deepnotes/api-worker`** | Hono: `GET /api/users/me/groups`, `GET /api/groups/:groupId/pages`, `POST /api/groups/:groupId/pages` (**201** on create). | -**Intentional gaps (next slices):** `pages.create` optional **`groupCreation`** (new non-personal group + first page) not exposed yet; no Redis locks (legacy redlock); **`GET /api/groups/:groupId/pages`** is **auth-only** (legacy allowed optional auth for public read — can add later). +**Intentional gaps (later):** no Redis locks (legacy redlock); **`GET /api/groups/:groupId/pages`** is **auth-only** (legacy allowed optional auth for public read — can add later). Optional **`groupCreation`** on create is [slice 8](#pagesgroups-rest--slice-8-create--groupcreation). ### Users/pages REST — slice 2 @@ -219,16 +220,29 @@ CI should set the same vars against the workflow Postgres service (role with `CR **Client contract:** `reencrypt` is **required** when the page’s `group_id` changes (after optional `groupCreation` insert, the page still has the old `group_id` until the same transaction’s update block). Omit `reencrypt` when only `setAsMainPage` in the **same** group. Snapshots: `pageEncryptedSnapshots` is a map by snapshot id (empty `{}` if none). +### Pages/groups REST — slice 8 (create + `groupCreation`) + +**Goal:** legacy tRPC `pages.create` with optional `groupCreation` (new non-personal `groupId` + first page) without using **move** — same ciphertext shape as [slice 7 `groupCreation`](#pages-rest--slice-7-move--group-creation) / `PageMoveGroupCreationRequest`. + +| Layer | What shipped | +|-------|--------------| +| **`@deepnotes/session`** | `group-creation-shared.ts` — `GroupCreationCiphertext` + `insertSharedGroupForOwnerInTx` (shared with [page-move.ts](packages/session/src/page-move.ts)). `group-pages.ts` — `performCreatePage`: if `body.groupCreation` set, Pro + path `groupId` must not exist, `parentPageId` in user’s **personal** group, then in one tx: insert group + owner member (legacy `createGroup` order), `pages` + `users_pages`, `group_members.last_activity` for new group. | +| **`@deepnotes/api`** | `pageMoveGroupCreationRequestSchema` moved above `GroupPageCreateRequest`; `groupPageCreateRequestSchema` includes optional `groupCreation`. | +| **`@deepnotes/api-worker`** | `POST /api/groups/:groupId/pages` — same handler; Zod decodes nested base64. | +| **Tests** | Integration: [slice 8 row](#phase-3-test-coverage-detail); [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) `pages.create` row. | + +**Client contract:** Path parameter `groupId` is the **new** group id when `groupCreation` is present (client-generated nanoid, must not already exist). `parentPageId` is typically under the user’s **personal** group (breadcrumb parent), not the new group’s id. + ### Not started (Phase 3 — pages, groups, infra) - [x] **Groups (REST, slice 4):** [password, privacy, soft delete, restore, purge](#pagesgroups-rest--slice-4-group-password-privacy-deletion) — `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY` on `SessionEnv` / worker bindings. - [x] **Groups (REST, slice 5):** [make-private / re-key](#pagesgroups-rest--slice-5-privacy-private-re-key) — `POST /api/groups/:groupId/privacy/private`; OpenAPI + [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md). - [x] **Pages (REST, slice 6):** [bump / backlinks / snapshots / page deletion](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). - [x] **Pages (REST, slice 7):** [move + optional `groupCreation`](#pages-rest--slice-7-move--group-creation) — `POST /api/pages/:pageId/move` (`page-move.ts`). -- [ ] **Optional** **`groupCreation`** on **`POST /api/groups/:id/pages`** without move (if product wants “create group + first page” without moving an existing page). -- [ ] **WS / REST:** group join/invite, member role, etc. (see [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) WebSocket table). -- [ ] **Realtime / collab** (new or adapted protocols; no key rotation). -- [ ] **Stripe:** `POST /api/webhooks/stripe`, checkout/portal (no RevenueCat); wire **`deleteStripeCustomer`** from account delete when keys exist. +- [x] **Pages (REST, slice 8):** [create + optional `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) on **`POST /api/groups/:groupId/pages`** — legacy `pages.create` parity (`group-creation-shared.ts` + `performCreatePage`). +- [ ] **Group membership (REST):** model after [TRPC map WebSocket rows](docs/TRPC_REST_MAP.md) — e.g. `PATCH/DELETE /api/groups/{groupId}/members/{userId}` (role change, remove), plus **join invitations** and **join requests** CRUD (send/accept/reject/cancel) as REST first (or documented WS on `/api/ws/...` if you need streaming later). **Reference:** `apps/app-server/src/websocket/groups/join-*`. +- [ ] **Realtime / collab** (new or adapted protocols; no key rotation; Durable Object vs separate service per [RESTART_PLAN](../docs/RESTART_PLAN.md) §4.3 / hosting table). +- [ ] **Stripe:** `POST /api/webhooks/stripe`, `POST /api/billing/stripe/checkout-session`, `POST /api/billing/stripe/portal-session` (map rows in [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) Users / webhooks); wire **`deleteStripeCustomer`** and **`updateStripeCustomerEmail`** from account flows when secrets exist; no RevenueCat. --- @@ -277,7 +291,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**22** cases when DB env set) — … + [slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 move](#pages-rest--slice-7-move--group-creation); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; optional `groupCreation` on create-only `POST` | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**23** cases when DB env set) — … + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 move](#pages-rest--slice-7-move--group-creation) + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT** | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (session routes + [slice 6/7 `/api/pages/...` paths](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)); **`schemas/users.test.ts`**; **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; more Zod edge cases for new page schemas | | **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **55** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + `/api/pages/{pageId}/move` | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | @@ -300,8 +314,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Drizzle migrations from empty DB documented for production upgrades. - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). -- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (account + **2FA** + **groups/pages** + prefs + [slice 4–5](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 6 page router](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 page move](#pages-rest--slice-7-move--group-creation)). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) — **22** `account-flows` + **6** `@deepnotes/db` when DB set; [slice 6 + 7](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). **Next:** **Redis** failed-login against real Redis; **Stripe** when billing exists. +- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (account + **2FA** + **groups/pages** + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) + prefs + [slice 4–5](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7](#pages-rest--slice-7-move--group-creation)). +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) — **23** `account-flows` + **6** `@deepnotes/db` when DB set; slices [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)–[8](#pagesgroups-rest--slice-8-create--groupcreation). **Next:** **Redis** failed-login against real Redis; **Stripe** when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -312,7 +326,12 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Phase 3 working order (suggested) -Use this when resuming: **(done)** through [slice 7 — page move](#pages-rest--slice-7-move--group-creation). **(next)** optional **`groupCreation`** on `POST /api/groups/:id/pages` only (if desired); **(then)** WS or REST for **join invitations**, **join requests**, **member role** / remove. **(then)** **realtime + collab** and **Stripe** + **`deleteStripeCustomer`**. +| Order | Item | Rationale / notes | +|-------|------|----------------------| +| ✅ | Slices 1–8 (through [create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation)) | Account, pages/groups CRUD, move, new shared group via create. | +| **1** | **Group join + membership REST** | Unblocks team workflows: map `join-invitations` / `join-requests` / `change-user-role` / `remove-user` to REST + Zod; extend `userHasGroupPermission` and integration tests. | +| **2** | **Realtime** (JWT upgrade, msgpackr-style protocol) + **collab** (Yjs, no rotation) | Depends on clear HTTP session story; [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md); load-test before freeze. | +| **3** | **Stripe** webhooks + portal/checkout + account-delete / email-change hooks | [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) billing rows; `SessionEnv` secrets documented in [DEPLOY_CLOUDFLARE](docs/DEPLOY_CLOUDFLARE.md). | --- @@ -320,6 +339,7 @@ Use this when resuming: **(done)** through [slice 7 — page move](#pages-rest-- | Date | Change | |------|--------| +| 2026-04-27 | **Phase 3 — slice 8 (create + `groupCreation`):** [group-creation-shared.ts](packages/session/src/group-creation-shared.ts) + `performCreatePage` with optional `groupCreation` (Pro; path `groupId` = new id; `parentPageId` in personal group). `GroupPageCreateRequest` + OpenAPI; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) `pages.create`; `account-flows` **23** cases. [Slice 8 section](#pagesgroups-rest--slice-8-create--groupcreation); [working order](#phase-3-working-order-suggested) next = **group join + membership REST**. | | 2026-04-27 | **Phase 3 — pages slice 7 (move):** `page-move.ts` `performPageMove` (Pro; optional `groupCreation` + reencrypt; `setAsMainPage` + `users_pages` swap for personal; `page_updates` + `page_snapshots` on cross-group); `pageMoveRequestSchema` in `@deepnotes/api`; `POST /api/pages/:pageId/move` + OpenAPI; worker **503** **55** tests; `account-flows` **22** cases; [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) `websocket/pages/move` row; [slice 7 section](#pages-rest--slice-7-move--group-creation). **Next:** group invites / requests REST or join routes; **realtime + collab**; **Stripe**. | | 2026-04-27 | **Phase 3 — pages router slice 6 (bump, backlinks, snapshots, page deletion):** `page-operations.ts` (`performPageBump` … `performPagePurge`); `page_links` / `page_snapshots` + Postgres-only (no `page-backlinks` KeyDB); OpenAPI + Hono; worker **503** matrix **54** tests; `account-flows` **21** integration cases; [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) `pages.*` rows; [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). **Next:** `POST /api/pages/:pageId/move` (WS parity + `page_updates` / collab cache). | | 2026-04-27 | **Phase 3 — groups slice 5 (`POST …/privacy/private`):** `performGroupPrivacyMakePrivate` + `groupPrivacyPrivateRequestSchema` (legacy `groupKeyRotationSchema` shape, base64 JSON); Hono + OpenAPI; worker **503** **45** tests; `account-flows` **20** integration cases (make private, already-private **400**, bad page keyset **400**); **TRPC_REST_MAP** Groups + WS `make-private` rows; [slice 5 section](#pagesgroups-rest--slice-5-privacy-private-re-key); [working order](#phase-3-working-order-suggested) now leads with **`pagesRouter`**. | diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index 1ad471ed..9cf4dfaf 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -68,7 +68,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | Legacy procedure | Proposed REST / notes | |------------------|----------------------| -| `pages.create` | `POST /api/groups/:groupId/pages` (**implemented** — `performCreatePage`; optional `groupCreation` not yet exposed; Pro + free-page rules per legacy) | +| `pages.create` | `POST /api/groups/:groupId/pages` (**implemented** — `performCreatePage`; optional `groupCreation` = new non-personal group + first page, same shape as `PageMoveGroupCreationRequest`, path `groupId` unused id; Pro + free-page rules per legacy) | | `pages.bump` | `POST /api/pages/:pageId/bump` (**implemented** — `performPageBump`; path `pageId`, optional body `{ "parentPageId" }` must chain to personal main page) | | `pages.backlinks.create` | `POST /api/pages/:pageId/backlinks` (**implemented** — `performPageBacklinkCreate`; path `pageId` = **target**; body `{ "sourcePageId" }`) | | `pages.backlinks.delete` | `DELETE /api/pages/:pageId/backlinks/:targetPageId` (**implemented** — `performPageBacklinkDelete`; path `pageId` = **source**; `targetPageId` = link target) | diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index d0224266..b065ba43 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -608,7 +608,7 @@ registry.registerPath({ path: "/api/groups/{groupId}/pages", summary: "Create a page in a group", description: - "Replaces legacy `pages.create` for an existing group (optional `groupCreation` path not yet exposed). Enforces `editGroupPages`, Pro subscription when `groupId` is not the user’s personal group, and the 50 free-page cap for non‑Pro users.", + "Replaces legacy `pages.create`. For an **existing** group, `parentPageId` must be a page in that group and the caller needs `editGroupPages`. With optional `groupCreation`, path `groupId` is a **new** nanoid (no row yet), `parentPageId` is a page in the user’s **personal** group, and the body includes the same ciphertext as `PageMoveGroupCreationRequest` — Pro only; creates the `groups` + owner `group_members` rows then the first page (legacy parity). The 50 free-page cap applies to non‑Pro users for normal creates.", request: { params: groupIdPathSchema, body: { diff --git a/new-deepnotes/packages/api/src/schemas/pages-groups.ts b/new-deepnotes/packages/api/src/schemas/pages-groups.ts index 11a4536d..bdc07e1f 100644 --- a/new-deepnotes/packages/api/src/schemas/pages-groups.ts +++ b/new-deepnotes/packages/api/src/schemas/pages-groups.ts @@ -42,6 +42,21 @@ export const groupPagesListQuerySchema = z.object({ }), }); +/** Ciphertext to create a non-personal group (legacy `groupCreation` on `pages.create` / `pages.move`). */ +export const pageMoveGroupCreationRequestSchema = z + .object({ + groupEncryptedName: byteB64, + groupPasswordHash: byteB64.optional(), + groupIsPublic: z.boolean(), + groupAccessKeyring: byteB64, + groupEncryptedInternalKeyring: byteB64, + groupEncryptedContentKeyring: byteB64, + groupPublicKeyring: byteB64, + groupEncryptedPrivateKeyring: byteB64, + groupOwnerEncryptedName: byteB64, + }) + .openapi("PageMoveGroupCreationRequest"); + export const groupPageCreateRequestSchema = z .object({ parentPageId: z @@ -55,6 +70,7 @@ export const groupPageCreateRequestSchema = z pageEncryptedSymmetricKeyring: byteB64, pageEncryptedRelativeTitle: byteB64, pageEncryptedAbsoluteTitle: byteB64, + groupCreation: pageMoveGroupCreationRequestSchema.optional(), }) .openapi("GroupPageCreateRequest"); @@ -230,24 +246,9 @@ export const pageMoveReencryptRequestSchema = z }) .openapi("PageMoveReencryptRequest"); -/** Create a new shared group in the same call as a move (legacy WS step 1 `groupCreation`). */ -export const pageMoveGroupCreationRequestSchema = z - .object({ - groupEncryptedName: byteB64, - groupPasswordHash: byteB64.optional(), - groupIsPublic: z.boolean(), - groupAccessKeyring: byteB64, - groupEncryptedInternalKeyring: byteB64, - groupEncryptedContentKeyring: byteB64, - groupPublicKeyring: byteB64, - groupEncryptedPrivateKeyring: byteB64, - groupOwnerEncryptedName: byteB64, - }) - .openapi("PageMoveGroupCreationRequest"); - /** * Replaces `websocket/pages/move` (two tRPC steps) with one `POST` (optional `reencrypt` when - * `sourceGroupId !== destGroupId`). + * `sourceGroupId !== destGroupId`). Optional `groupCreation` uses `PageMoveGroupCreationRequest`. */ export const pageMoveRequestSchema = z .object({ diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index 53100d0f..511fce3e 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -1376,6 +1376,103 @@ describe.skipIf(resolveTemplateContext() == null)( } }); + it("pages: create with groupCreation (new shared group + first page)", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const email = `gcreate-${nanoid()}@example.com`; + const loginHash = rand32(); + const reg = await buildRegisterBody(email, loginHash); + await performUserRegister({ db, env, body: reg }); + await db + .update(users) + .set({ plan: "pro" }) + .where(eq(users.id, reg.userId)); + const access = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: reg.userId, + sessionId: nanoid(), + }); + + const newGroupId = nanoid(); + const newPageId = nanoid(); + const gc = { + groupEncryptedName: rand32(), + groupIsPublic: true, + groupAccessKeyring: rand32(), + groupEncryptedInternalKeyring: rand32(), + groupEncryptedContentKeyring: rand32(), + groupPublicKeyring: rand32(), + groupEncryptedPrivateKeyring: rand32(), + groupOwnerEncryptedName: rand32(), + }; + const out = await performCreatePage({ + db, + env, + accessCookie: access, + groupId: newGroupId, + body: { + parentPageId: reg.pageId, + pageId: newPageId, + pageEncryptedSymmetricKeyring: rand32(), + pageEncryptedRelativeTitle: rand32(), + pageEncryptedAbsoluteTitle: rand32(), + groupCreation: gc, + }, + }); + expect(out.pageId).toBe(newPageId); + const [gRow] = await db + .select({ + id: groups.id, + mainPageId: groups.mainPageId, + userId: groups.userId, + }) + .from(groups) + .where(eq(groups.id, newGroupId)); + expect(gRow?.mainPageId).toBe(newPageId); + expect(gRow?.userId).toBeNull(); + const [pRow] = await db + .select({ groupId: pages.groupId }) + .from(pages) + .where(eq(pages.id, newPageId)); + expect(pRow?.groupId).toBe(newGroupId); + const [mem] = await db + .select({ role: groupMembers.role, userId: groupMembers.userId }) + .from(groupMembers) + .where( + and( + eq(groupMembers.groupId, newGroupId), + eq(groupMembers.userId, reg.userId), + ), + ); + expect(mem?.role).toBe("owner"); + const { groupIds } = await performGetUserGroupIds({ + db, + env, + accessCookie: access, + }); + expect(groupIds.sort()).toEqual([reg.groupId, newGroupId].sort()); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + it("user page prefs: starting, path, favorites, recent, defaults, notifications", async () => { const env = testSessionEnv(); const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; diff --git a/new-deepnotes/packages/session/src/group-creation-shared.ts b/new-deepnotes/packages/session/src/group-creation-shared.ts new file mode 100644 index 00000000..9104e192 --- /dev/null +++ b/new-deepnotes/packages/session/src/group-creation-shared.ts @@ -0,0 +1,71 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { groupMembers, groups } from "@deepnotes/db/schema"; + +import { + computeGroupPasswordPhc, + encryptGroupRehashedPasswordHash, +} from "./crypto/session-crypto.js"; +import type { SessionEnv } from "./env.js"; + +function toBuf(u: Uint8Array): Buffer { + return Buffer.from(u); +} + +export type GroupCreationCiphertext = { + groupEncryptedName: Uint8Array; + groupPasswordHash?: Uint8Array; + groupIsPublic: boolean; + groupAccessKeyring: Uint8Array; + groupEncryptedInternalKeyring: Uint8Array; + groupEncryptedContentKeyring: Uint8Array; + groupPublicKeyring: Uint8Array; + groupEncryptedPrivateKeyring: Uint8Array; + groupOwnerEncryptedName: Uint8Array; +}; + +type Tx = Parameters[0]>[0]; + +/** + * Inserts a non-personal `groups` row + owner `group_members` (legacy `createGroup` + * with `groupIsPersonal: false`, same cipher shape as `pages.move` groupCreation). + */ +export async function insertSharedGroupForOwnerInTx( + tx: Tx, + input: { + env: SessionEnv; + userId: string; + groupId: string; + mainPageId: string; + groupCreation: GroupCreationCiphertext; + }, +): Promise { + const g = input.groupCreation; + let encryptedRehashed: Buffer | undefined; + if (g.groupPasswordHash != null && g.groupPasswordHash.byteLength > 0) { + const phc = computeGroupPasswordPhc(g.groupPasswordHash); + const enc = encryptGroupRehashedPasswordHash( + phc, + input.env.GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY, + ); + encryptedRehashed = Buffer.from(enc); + } + await tx.insert(groups).values({ + id: input.groupId, + mainPageId: input.mainPageId, + encryptedName: toBuf(g.groupEncryptedName), + userId: null, + publicKeyring: toBuf(g.groupPublicKeyring), + encryptedPrivateKeyring: toBuf(g.groupEncryptedPrivateKeyring), + encryptedContentKeyring: toBuf(g.groupEncryptedContentKeyring), + accessKeyring: g.groupIsPublic ? toBuf(g.groupAccessKeyring) : null, + encryptedRehashedPasswordHash: encryptedRehashed, + }); + await tx.insert(groupMembers).values({ + groupId: input.groupId, + userId: input.userId, + role: "owner", + encryptedAccessKeyring: g.groupIsPublic ? null : toBuf(g.groupAccessKeyring), + encryptedInternalKeyring: toBuf(g.groupEncryptedInternalKeyring), + encryptedName: toBuf(g.groupOwnerEncryptedName), + }); +} diff --git a/new-deepnotes/packages/session/src/group-pages.ts b/new-deepnotes/packages/session/src/group-pages.ts index 91409fb7..b4eaaf42 100644 --- a/new-deepnotes/packages/session/src/group-pages.ts +++ b/new-deepnotes/packages/session/src/group-pages.ts @@ -2,10 +2,16 @@ import type { DeepnotesDb } from "@deepnotes/db/client"; import { groupMembers, groups, pages, users, usersPages } from "@deepnotes/db/schema"; import { and, desc, eq, isNull, lt } from "drizzle-orm"; +import { ensureSodiumReady } from "./crypto/session-crypto.js"; import type { SessionEnv } from "./env.js"; import { SessionError } from "./errors.js"; +import { + insertSharedGroupForOwnerInTx, + type GroupCreationCiphertext, +} from "./group-creation-shared.js"; import { userHasGroupPermission } from "./group-permissions.js"; import { getAuthenticatedUserSummary } from "./user-me.js"; +import { assertUserProPlan } from "./user-plan.js"; function toBuf(u: Uint8Array): Buffer { return Buffer.from(u); @@ -90,11 +96,13 @@ export type CreatePageBody = { pageEncryptedSymmetricKeyring: Uint8Array; pageEncryptedRelativeTitle: Uint8Array; pageEncryptedAbsoluteTitle: Uint8Array; + groupCreation?: GroupCreationCiphertext; }; /** - * Replaces legacy `pages.create` without optional `groupCreation` (new shared - * group + first page). Pro subscription and free-page limits match legacy. + * Replaces legacy `pages.create` (optional `groupCreation`: new non-personal + * group + first page, same as tRPC’s `groupId` + `groupCreation` payload). Pro + * + free-page limits match legacy. */ export async function performCreatePage(input: { db: DeepnotesDb; @@ -104,6 +112,7 @@ export async function performCreatePage(input: { body: CreatePageBody; }): Promise<{ pageId: string; numFreePages?: number }> { const { userId, personalGroupId } = await getAuthenticatedUserSummary(input); + const groupCreation = input.body.groupCreation; const [parent] = await input.db .select({ id: pages.id, groupId: pages.groupId }) @@ -111,12 +120,26 @@ export async function performCreatePage(input: { .where(eq(pages.id, input.body.parentPageId)) .limit(1); - if (parent == null || parent.groupId !== input.groupId) { - throw new SessionError( - 400, - "BAD_REQUEST", - "parentPageId must refer to a page in this group.", - ); + if (parent == null) { + throw new SessionError(404, "NOT_FOUND", "Parent page not found."); + } + + if (groupCreation == null) { + if (parent.groupId !== input.groupId) { + throw new SessionError( + 400, + "BAD_REQUEST", + "parentPageId must refer to a page in this group.", + ); + } + } else { + if (parent.groupId !== personalGroupId) { + throw new SessionError( + 400, + "BAD_REQUEST", + "When creating a new shared group, parentPageId must be a page in your personal group.", + ); + } } const [groupRow] = await input.db @@ -125,22 +148,35 @@ export async function performCreatePage(input: { .where(eq(groups.id, input.groupId)) .limit(1); - if (groupRow == null) { - throw new SessionError(404, "NOT_FOUND", "Group not found."); + if (groupCreation == null) { + if (groupRow == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + } else { + if (groupRow != null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "A group with this id already exists.", + ); + } + await assertUserProPlan({ db: input.db, userId }); + await ensureSodiumReady(); } - const canEdit = await userHasGroupPermission({ - db: input.db, - userId, - groupId: input.groupId, - permission: "editGroupPages", - }); - if (!canEdit) { - throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + if (groupCreation == null) { + const canEdit = await userHasGroupPermission({ + db: input.db, + userId, + groupId: input.groupId, + permission: "editGroupPages", + }); + if (!canEdit) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } } - const mustSubscribe = - input.groupId !== personalGroupId; + const mustSubscribe = input.groupId !== personalGroupId || groupCreation != null; if (mustSubscribe) { const [u] = await input.db .select({ plan: users.plan }) @@ -188,6 +224,16 @@ export async function performCreatePage(input: { numFreePagesOut = next; } + if (groupCreation != null) { + await insertSharedGroupForOwnerInTx(tx, { + env: input.env, + userId, + groupId: input.groupId, + mainPageId: input.body.pageId, + groupCreation, + }); + } + await tx.insert(pages).values({ id: input.body.pageId, groupId: input.groupId, diff --git a/new-deepnotes/packages/session/src/page-move.ts b/new-deepnotes/packages/session/src/page-move.ts index c032012b..88bbc375 100644 --- a/new-deepnotes/packages/session/src/page-move.ts +++ b/new-deepnotes/packages/session/src/page-move.ts @@ -1,6 +1,5 @@ import type { DeepnotesDb } from "@deepnotes/db/client"; import { - groupMembers, groups, pageSnapshots, pages, @@ -10,13 +9,13 @@ import { } from "@deepnotes/db/schema"; import { and, eq, isNull } from "drizzle-orm"; -import { - computeGroupPasswordPhc, - encryptGroupRehashedPasswordHash, - ensureSodiumReady, -} from "./crypto/session-crypto.js"; +import { ensureSodiumReady } from "./crypto/session-crypto.js"; import type { SessionEnv } from "./env.js"; import { SessionError } from "./errors.js"; +import { + insertSharedGroupForOwnerInTx, + type GroupCreationCiphertext, +} from "./group-creation-shared.js"; import { userHasGroupPermission } from "./group-permissions.js"; import { getAuthenticatedUserSummary } from "./user-me.js"; import { assertUserProPlan } from "./user-plan.js"; @@ -30,17 +29,7 @@ function bumpStringIdList(ids: string[], itemId: string, max: number): string[] return [itemId, ...rest].slice(0, max); } -export type PageMoveGroupCreation = { - groupEncryptedName: Uint8Array; - groupPasswordHash?: Uint8Array; - groupIsPublic: boolean; - groupAccessKeyring: Uint8Array; - groupEncryptedInternalKeyring: Uint8Array; - groupEncryptedContentKeyring: Uint8Array; - groupPublicKeyring: Uint8Array; - groupEncryptedPrivateKeyring: Uint8Array; - groupOwnerEncryptedName: Uint8Array; -}; +export type PageMoveGroupCreation = GroupCreationCiphertext; export type PageMoveReencrypt = { pageEncryptedSymmetricKeyring: Uint8Array; @@ -171,44 +160,12 @@ export async function performPageMove(input: { return await input.db.transaction(async (tx) => { if (groupCreation != null) { - let encryptedRehashed: Buffer | undefined; - if ( - groupCreation.groupPasswordHash != null && - groupCreation.groupPasswordHash.byteLength > 0 - ) { - const phc = computeGroupPasswordPhc(groupCreation.groupPasswordHash); - const enc = encryptGroupRehashedPasswordHash( - phc, - input.env.GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY, - ); - encryptedRehashed = Buffer.from(enc); - } - await tx.insert(groups).values({ - id: destGroupId, - mainPageId: input.pageId, - encryptedName: toBuf(groupCreation.groupEncryptedName), - userId: null, - publicKeyring: toBuf(groupCreation.groupPublicKeyring), - encryptedPrivateKeyring: toBuf( - groupCreation.groupEncryptedPrivateKeyring, - ), - encryptedContentKeyring: toBuf(groupCreation.groupEncryptedContentKeyring), - accessKeyring: groupCreation.groupIsPublic - ? toBuf(groupCreation.groupAccessKeyring) - : null, - encryptedRehashedPasswordHash: encryptedRehashed, - }); - await tx.insert(groupMembers).values({ - groupId: destGroupId, + await insertSharedGroupForOwnerInTx(tx, { + env: input.env, userId, - role: "owner", - encryptedAccessKeyring: groupCreation.groupIsPublic - ? null - : toBuf(groupCreation.groupAccessKeyring), - encryptedInternalKeyring: toBuf( - groupCreation.groupEncryptedInternalKeyring, - ), - encryptedName: toBuf(groupCreation.groupOwnerEncryptedName), + groupId: destGroupId, + mainPageId: input.pageId, + groupCreation, }); } From ffc8e2ccd422dcfa86294084f9e669d97775ae37 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 10:41:15 -0300 Subject: [PATCH 052/243] feat(new-deepnotes): group membership, roles, and updates API --- new-deepnotes/PLAN_PROGRESS.md | 51 +- .../apps/api-worker/src/index.test.ts | 37 + new-deepnotes/apps/api-worker/src/index.ts | 523 ++++++++++++++ new-deepnotes/docs/TRPC_REST_MAP.md | 14 +- new-deepnotes/packages/api/src/index.ts | 7 + .../packages/api/src/openapi.test.ts | 28 + new-deepnotes/packages/api/src/openapi.ts | 253 +++++++ .../packages/api/src/schemas/pages-groups.ts | 57 ++ .../src/account-flows.integration.test.ts | 198 ++++++ .../packages/session/src/group-membership.ts | 636 ++++++++++++++++++ .../packages/session/src/group-role-ranks.ts | 49 ++ new-deepnotes/packages/session/src/index.ts | 12 + 12 files changed, 1849 insertions(+), 16 deletions(-) create mode 100644 new-deepnotes/packages/session/src/group-membership.ts create mode 100644 new-deepnotes/packages/session/src/group-role-ranks.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 50fa4e8a..769482c0 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); [slice 7 — page move](#pages-rest--slice-7-move--group-creation); **[slice 8 — create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation)** (`POST /api/groups/:groupId/pages` with optional new shared group). **Still ahead:** REST for **group join flows** (invites, requests, member role, remove) ([TRPC map](./docs/TRPC_REST_MAP.md) WebSocket table), **realtime / collab**, **Stripe** (`POST /api/webhooks/stripe`, portal/checkout). | +| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); [7 — page move](#pages-rest--slice-7-move--group-creation); [8 — create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation); **[slice 9 — membership + join flows](#pagesgroups-rest--slice-9-membership--join-invites--requests)** (invitations, join requests, `PATCH`/`DELETE` members). **Still ahead:** **realtime / collab**, **Stripe** (`POST /api/webhooks/stripe`, portal/checkout). | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — **23** cases when DB env set — register / email / password / **login + refresh** / **2FA** / **groups + pages** / **[slice 8 `groupCreation` create](#pagesgroups-rest--slice-8-create--groupcreation)** / **user page prefs** / **[slice 4–5 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion)** / **[slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)** / **[slice 7 page move](#pages-rest--slice-7-move--group-creation)**. `@deepnotes/db` `template-db.test.ts` — clone + FK matrix. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — **24** cases when DB env set — register / email / password / **login + refresh** / **2FA** / **groups + pages** / **[slice 8 `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation)** / **[slice 9 membership + joins](#pagesgroups-rest--slice-9-membership--join-invites--requests)** / **user page prefs** / **[slice 4–5 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion)** / **[slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)** / **[slice 7 page move](#pages-rest--slice-7-move--group-creation)**. `@deepnotes/db` `template-db.test.ts` — clone + FK matrix. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) @@ -46,7 +46,7 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` **How to run locally:** ensure `.env` at `new-deepnotes/.env` has `DATABASE_URL` and (for template create/drop) `DATABASE_ADMIN_URL` with a role that can `CREATE DATABASE`. Then: -- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**23** cases when DB env set — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7](#pages-rest--slice-7-move--group-creation) + [slice 8](#pagesgroups-rest--slice-8-create--groupcreation)) +- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**24** cases when DB env set — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7](#pages-rest--slice-7-move--group-creation) + [slice 8](#pagesgroups-rest--slice-8-create--groupcreation) + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests)) - `pnpm --filter @deepnotes/db exec vitest run src/template-db.test.ts` CI should set the same vars against the workflow Postgres service (role with `CREATEDB`). @@ -80,6 +80,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **Page bump / backlinks / snapshots / deletion (slice 6)** | Pro; `performCreatePage` (second + third child); `performPageBump` (child, parent = main’s child chain); `users.starting_page_id` + recent; `performPageBacklinkCreate`+`Delete` (scoped query — bump may add separate `page_links` row); `performPageSnapshotSave`+`Load`+`Delete`; `performPageSoftDelete`+`Restore`+`SoftDelete`+`Purge` | Postgres-only links; Pro for snapshot save/load; not main page for delete. | | **Page move (slice 7)** | Pro; no-op same dest + `!setAsMainPage` **400**; main page **400**; `setAsMainPage` in personal group (no `reencrypt`); `groupCreation` + cross-group reencrypt, `page_updates` index 0 | [performPageMove](packages/session/src/page-move.ts); no Redis collab key delete (RESTART_PLAN). | | **Create + `groupCreation` (slice 8)** | Pro; path `groupId` = unused nanoid; `parentPageId` in **personal** group; [insertSharedGroupForOwnerInTx](packages/session/src/group-creation-shared.ts) then `pages` + `users_pages` | Parity with legacy `pages.create` + `groupCreation` (tRPC). | +| **Membership + joins (slice 9)** | Two Pro users; shared **public** group via `groupCreation`; [performGroupJoinInvitationSend](packages/session/src/group-membership.ts) → invite row → [performGroupJoinInvitationAccept](packages/session/src/group-membership.ts); [performGroupMemberRoleChange](packages/session/src/group-membership.ts) to moderator; [performGroupMemberRemove](packages/session/src/group-membership.ts); [performGroupJoinRequestSend](packages/session/src/group-membership.ts) + [performGroupJoinRequestAccept](packages/session/src/group-membership.ts) with `viewer` | Covers legacy WS join-invitation / join-request / change-role / remove DB semantics; **no** push-notification step 2 (client-driven later). | **`@deepnotes/db` real Postgres (`template-db.test.ts`):** @@ -233,6 +234,32 @@ CI should set the same vars against the workflow Postgres service (role with `CR **Client contract:** Path parameter `groupId` is the **new** group id when `groupCreation` is present (client-generated nanoid, must not already exist). `parentPageId` is typically under the user’s **personal** group (breadcrumb parent), not the new group’s id. +### Pages/groups REST — slice 9 (membership + join invites + requests) + +**Goal:** Replace legacy WebSocket `groups.joinInvitations.*`, `groups.joinRequests.*`, `groups.changeUserRole`, `groups.removeUser` with REST + Drizzle. **DB parity** with step 1 of each legacy flow; **no** server-side replication of step 2 **encrypted notification fan-out** (clients can still write `notifications` / `users_notifications` using the same ciphertext patterns as today when needed). + +| Layer | What shipped | +|-------|----------------| +| **`@deepnotes/session`** | [group-role-ranks.ts](packages/session/src/group-role-ranks.ts) — role rank / `canManageRole` / `canChangeRole` / `manageLowerRanks` equivalent. [group-membership.ts](packages/session/src/group-membership.ts) — `performGroupJoinInvitationSend` / `Accept` / `Reject` / `Cancel`, `performGroupJoinRequestSend` / `Accept` / `Reject` / `Cancel`, `performGroupMemberRoleChange`, `performGroupMemberRemove`. Pro gating aligned with legacy `assertUserSubscribed` except **invitation reject** (legacy had no Pro check). | +| **`@deepnotes/api`** | `groupJoinInvitationSendRequestSchema`, `groupJoinInvitationAcceptRequestSchema`, `groupJoinRequestSendRequestSchema`, `groupJoinRequestAcceptRequestSchema`, `groupMemberRolePatchRequestSchema`, `groupMemberRoleSchema`, `groupUserIdPathSchema` in [schemas/pages-groups.ts](packages/api/src/schemas/pages-groups.ts); paths in [openapi.ts](packages/api/src/openapi.ts); exports from [index.ts](packages/api/src/index.ts). | +| **`@deepnotes/api-worker`** | Hono: `POST /api/groups/:groupId/join-invitations`, `POST …/join-invitations/me/accept`, `POST …/me/reject`, `DELETE …/join-invitations/:userId`; `POST …/join-requests`, `POST …/join-requests/me/cancel` (**registered before** `…/:userId/accept` so `me` is not captured), `POST …/:userId/accept`, `POST …/:userId/reject`; `PATCH` / `DELETE` `/api/groups/:groupId/members/:userId`. | +| **Tests** | [Integration](#phase-3-test-coverage-detail) row **slice 9**; [openapi.test.ts](packages/api/src/openapi.test.ts) path assertions; worker **503** **65** routes. | + +**HTTP summary (OpenAPI):** + +| Method + path | Role | +|---------------|------| +| `POST /api/groups/{groupId}/join-invitations` | Manager sends invite (`inviteeUserId`, `invitationRole`, ciphertext; `encryptedAccessKeyring` required iff group **private**) | +| `POST /api/groups/{groupId}/join-invitations/me/accept` | Invitee accepts (`userEncryptedName`) | +| `POST /api/groups/{groupId}/join-invitations/me/reject` | Invitee rejects | +| `DELETE /api/groups/{groupId}/join-invitations/{userId}` | Manager cancels invite to `userId` | +| `POST /api/groups/{groupId}/join-requests` | Requester asks to join (`are_join_requests_allowed`) | +| `POST /api/groups/{groupId}/join-requests/me/cancel` | Requester withdraws pending request | +| `POST /api/groups/{groupId}/join-requests/{userId}/accept` | Manager accepts (`targetRole`, keyrings; access keyring iff private) | +| `POST /api/groups/{groupId}/join-requests/{userId}/reject` | Manager rejects (`rejected = true` on row) | +| `PATCH /api/groups/{groupId}/members/{userId}` | `role` change | +| `DELETE /api/groups/{groupId}/members/{userId}` | Remove member or **leave** (self) | + ### Not started (Phase 3 — pages, groups, infra) - [x] **Groups (REST, slice 4):** [password, privacy, soft delete, restore, purge](#pagesgroups-rest--slice-4-group-password-privacy-deletion) — `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY` on `SessionEnv` / worker bindings. @@ -240,7 +267,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR - [x] **Pages (REST, slice 6):** [bump / backlinks / snapshots / page deletion](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). - [x] **Pages (REST, slice 7):** [move + optional `groupCreation`](#pages-rest--slice-7-move--group-creation) — `POST /api/pages/:pageId/move` (`page-move.ts`). - [x] **Pages (REST, slice 8):** [create + optional `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) on **`POST /api/groups/:groupId/pages`** — legacy `pages.create` parity (`group-creation-shared.ts` + `performCreatePage`). -- [ ] **Group membership (REST):** model after [TRPC map WebSocket rows](docs/TRPC_REST_MAP.md) — e.g. `PATCH/DELETE /api/groups/{groupId}/members/{userId}` (role change, remove), plus **join invitations** and **join requests** CRUD (send/accept/reject/cancel) as REST first (or documented WS on `/api/ws/...` if you need streaming later). **Reference:** `apps/app-server/src/websocket/groups/join-*`. +- [x] **Group membership + joins (REST, slice 9):** [invitations, requests, role, remove](#pagesgroups-rest--slice-9-membership--join-invites--requests) — `group-membership.ts` + [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) WebSocket rows. **Note:** legacy WS **step 2** encrypted push notifications are **not** replicated on the server; the SPA should continue to use `users_notifications` + existing notification types when product needs parity. - [ ] **Realtime / collab** (new or adapted protocols; no key rotation; Durable Object vs separate service per [RESTART_PLAN](../docs/RESTART_PLAN.md) §4.3 / hosting table). - [ ] **Stripe:** `POST /api/webhooks/stripe`, `POST /api/billing/stripe/checkout-session`, `POST /api/billing/stripe/portal-session` (map rows in [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) Users / webhooks); wire **`deleteStripeCustomer`** and **`updateStripeCustomerEmail`** from account flows when secrets exist; no RevenueCat. @@ -291,9 +318,9 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Package / app | Role | What runs today | Gaps (highest value next) | |---------------|------|------------------|---------------------------| | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | -| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**23** cases when DB env set) — … + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 move](#pages-rest--slice-7-move--group-creation) + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT** | +| **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**24** cases when DB env set) — … + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 move](#pages-rest--slice-7-move--group-creation) + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; optional: invitation **reject/cancel**, join-request **reject/cancel**, **private** group invite/request **access keyring** branches | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (session routes + [slice 6/7 `/api/pages/...` paths](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)); **`schemas/users.test.ts`**; **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; more Zod edge cases for new page schemas | -| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **55** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + `/api/pages/{pageId}/move` | **200** tests with stub `SessionEnv` + template DB (heavier) | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **65** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + `/api/pages/{pageId}/move` + [slice 9 join/member routes](#pagesgroups-rest--slice-9-membership--join-invites--requests) | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -314,8 +341,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Drizzle migrations from empty DB documented for production upgrades. - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). -- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (account + **2FA** + **groups/pages** + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) + prefs + [slice 4–5](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7](#pages-rest--slice-7-move--group-creation)). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) — **23** `account-flows` + **6** `@deepnotes/db` when DB set; slices [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)–[8](#pagesgroups-rest--slice-8-create--groupcreation). **Next:** **Redis** failed-login against real Redis; **Stripe** when billing exists. +- [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (account + **2FA** + **groups/pages** + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) + [slice 9 membership](#pagesgroups-rest--slice-9-membership--join-invites--requests) + prefs + [slice 4–5](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7](#pages-rest--slice-7-move--group-creation)). +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) — **24** `account-flows` + **6** `@deepnotes/db` when DB set; slices [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)–[9](#pagesgroups-rest--slice-9-membership--join-invites--requests). **Next:** **Redis** failed-login against real Redis; **Stripe** when billing exists. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -328,10 +355,9 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Order | Item | Rationale / notes | |-------|------|----------------------| -| ✅ | Slices 1–8 (through [create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation)) | Account, pages/groups CRUD, move, new shared group via create. | -| **1** | **Group join + membership REST** | Unblocks team workflows: map `join-invitations` / `join-requests` / `change-user-role` / `remove-user` to REST + Zod; extend `userHasGroupPermission` and integration tests. | -| **2** | **Realtime** (JWT upgrade, msgpackr-style protocol) + **collab** (Yjs, no rotation) | Depends on clear HTTP session story; [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md); load-test before freeze. | -| **3** | **Stripe** webhooks + portal/checkout + account-delete / email-change hooks | [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) billing rows; `SessionEnv` secrets documented in [DEPLOY_CLOUDFLARE](docs/DEPLOY_CLOUDFLARE.md). | +| ✅ | Slices 1–9 (through [membership + joins](#pagesgroups-rest--slice-9-membership--join-invites--requests)) | Account, pages/groups CRUD, move, create-with-new-group, invitations/requests/member role/remove. | +| **1** | **Realtime** (JWT upgrade, msgpackr-style protocol) + **collab** (Yjs, no rotation) | [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md); load-test before freeze. | +| **2** | **Stripe** webhooks + portal/checkout + account-delete / email-change hooks | [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) billing rows; `SessionEnv` secrets documented in [DEPLOY_CLOUDFLARE](docs/DEPLOY_CLOUDFLARE.md). | --- @@ -339,6 +365,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-27 | **Phase 3 — slice 9 (membership + join flows):** [group-role-ranks.ts](packages/session/src/group-role-ranks.ts) (`canManageRole` / `canChangeRole` / `manageLowerRanks` parity); [group-membership.ts](packages/session/src/group-membership.ts) — invitations send/accept/reject/cancel, join requests send/accept/reject/cancel, `PATCH`/`DELETE` members; Zod + OpenAPI + Hono; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) WS table; **`byteB64`** passthrough in worker (decoded `Uint8Array`). `account-flows` **24** cases; api-worker 503 matrix **65**. [Slice 9 section](#pagesgroups-rest--slice-9-membership--join-invites--requests). **Next:** [realtime + collab](#phase-3-working-order-suggested). | | 2026-04-27 | **Phase 3 — slice 8 (create + `groupCreation`):** [group-creation-shared.ts](packages/session/src/group-creation-shared.ts) + `performCreatePage` with optional `groupCreation` (Pro; path `groupId` = new id; `parentPageId` in personal group). `GroupPageCreateRequest` + OpenAPI; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) `pages.create`; `account-flows` **23** cases. [Slice 8 section](#pagesgroups-rest--slice-8-create--groupcreation); [working order](#phase-3-working-order-suggested) next = **group join + membership REST**. | | 2026-04-27 | **Phase 3 — pages slice 7 (move):** `page-move.ts` `performPageMove` (Pro; optional `groupCreation` + reencrypt; `setAsMainPage` + `users_pages` swap for personal; `page_updates` + `page_snapshots` on cross-group); `pageMoveRequestSchema` in `@deepnotes/api`; `POST /api/pages/:pageId/move` + OpenAPI; worker **503** **55** tests; `account-flows` **22** cases; [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) `websocket/pages/move` row; [slice 7 section](#pages-rest--slice-7-move--group-creation). **Next:** group invites / requests REST or join routes; **realtime + collab**; **Stripe**. | | 2026-04-27 | **Phase 3 — pages router slice 6 (bump, backlinks, snapshots, page deletion):** `page-operations.ts` (`performPageBump` … `performPagePurge`); `page_links` / `page_snapshots` + Postgres-only (no `page-backlinks` KeyDB); OpenAPI + Hono; worker **503** matrix **54** tests; `account-flows` **21** integration cases; [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) `pages.*` rows; [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). **Next:** `POST /api/pages/:pageId/move` (WS parity + `page_updates` / collab cache). | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 8079ec53..6c9450a2 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -88,6 +88,43 @@ describe("api-worker", () => { "/api/groups/aaaaaaaaaaaaaaaaaaaaa/restore", ], ["POST", "/api/groups/aaaaaaaaaaaaaaaaaaaaa/purge"], + [ + "POST", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/join-invitations", + ], + [ + "POST", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/join-invitations/me/accept", + ], + [ + "POST", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/join-invitations/me/reject", + ], + [ + "DELETE", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/join-invitations/bbbbbbbbbbbbbbbbbbbbb", + ], + ["POST", "/api/groups/aaaaaaaaaaaaaaaaaaaaa/join-requests"], + [ + "POST", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/join-requests/me/cancel", + ], + [ + "POST", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/join-requests/bbbbbbbbbbbbbbbbbbbbb/accept", + ], + [ + "POST", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/join-requests/bbbbbbbbbbbbbbbbbbbbb/reject", + ], + [ + "PATCH", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/members/bbbbbbbbbbbbbbbbbbbbb", + ], + [ + "DELETE", + "/api/groups/aaaaaaaaaaaaaaaaaaaaa/members/bbbbbbbbbbbbbbbbbbbbb", + ], ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/move"], ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/bump"], ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/backlinks"], diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 27ca925d..31707607 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -13,6 +13,11 @@ import { groupPasswordChangeRequestSchema, groupPasswordDisableRequestSchema, groupPasswordEnableRequestSchema, + groupJoinInvitationAcceptRequestSchema, + groupJoinInvitationSendRequestSchema, + groupJoinRequestAcceptRequestSchema, + groupJoinRequestSendRequestSchema, + groupMemberRolePatchRequestSchema, groupPrivacyJoinRequestsPatchSchema, groupPrivacyPrivateRequestSchema, groupPrivacyPublicRequestSchema, @@ -2501,6 +2506,524 @@ app.post("/api/groups/:groupId/purge", async (c) => { } }); +app.post("/api/groups/:groupId/join-invitations", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = groupJoinInvitationSendRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupJoinInvitationSend } = await import("@deepnotes/session"); + await performGroupJoinInvitationSend({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + inviteeUserId: parsed.data.inviteeUserId, + invitationRole: parsed.data.invitationRole, + encryptedAccessKeyring: parsed.data.encryptedAccessKeyring, + encryptedInternalKeyring: parsed.data.encryptedInternalKeyring, + userEncryptedName: parsed.data.userEncryptedName, + userEncryptedNameForUser: parsed.data.userEncryptedNameForUser, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/groups/:groupId/join-invitations/me/accept", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = groupJoinInvitationAcceptRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupJoinInvitationAccept } = await import("@deepnotes/session"); + await performGroupJoinInvitationAccept({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + userEncryptedName: parsed.data.userEncryptedName, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/groups/:groupId/join-invitations/me/reject", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupJoinInvitationReject } = await import("@deepnotes/session"); + await performGroupJoinInvitationReject({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.delete("/api/groups/:groupId/join-invitations/:userId", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + const inviteeUserId = c.req.param("userId"); + + try { + const { performGroupJoinInvitationCancel } = await import("@deepnotes/session"); + await performGroupJoinInvitationCancel({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + inviteeUserId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/groups/:groupId/join-requests", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = groupJoinRequestSendRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupJoinRequestSend } = await import("@deepnotes/session"); + await performGroupJoinRequestSend({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + encryptedUserName: parsed.data.encryptedUserName, + encryptedUserNameForUser: parsed.data.encryptedUserNameForUser, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/groups/:groupId/join-requests/me/cancel", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + + try { + const { performGroupJoinRequestCancel } = await import("@deepnotes/session"); + await performGroupJoinRequestCancel({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/groups/:groupId/join-requests/:userId/accept", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = groupJoinRequestAcceptRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + const requesterUserId = c.req.param("userId"); + + try { + const { performGroupJoinRequestAccept } = await import("@deepnotes/session"); + await performGroupJoinRequestAccept({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + requesterUserId, + targetRole: parsed.data.targetRole, + encryptedAccessKeyring: parsed.data.encryptedAccessKeyring, + encryptedInternalKeyring: parsed.data.encryptedInternalKeyring, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/groups/:groupId/join-requests/:userId/reject", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + const requesterUserId = c.req.param("userId"); + + try { + const { performGroupJoinRequestReject } = await import("@deepnotes/session"); + await performGroupJoinRequestReject({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + requesterUserId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.patch("/api/groups/:groupId/members/:userId", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + const parsed = groupMemberRolePatchRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + const targetUserId = c.req.param("userId"); + + try { + const { performGroupMemberRoleChange } = await import("@deepnotes/session"); + await performGroupMemberRoleChange({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + targetUserId, + requestedRole: parsed.data.role, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.delete("/api/groups/:groupId/members/:userId", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + const groupId = c.req.param("groupId"); + const targetUserId = c.req.param("userId"); + + try { + const { performGroupMemberRemove } = await import("@deepnotes/session"); + await performGroupMemberRemove({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + groupId, + targetUserId, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.get("/api/users/me", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index 9cf4dfaf..5420c351 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -83,10 +83,16 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | Legacy handler | New surface | Notes | |----------------|-------------|--------| -| `websocket/groups/join-invitations/*` | `WS /api/ws/groups/...` or REST for low-frequency | send / accept / reject / cancel | -| `websocket/groups/join-requests/*` | same | send / accept / reject / cancel | -| `websocket/groups/change-user-role` | `PATCH /api/groups/:groupId/members/:userId` | prefer REST if acceptable | -| `websocket/groups/remove-user` | `DELETE /api/groups/:groupId/members/:userId` | | +| `websocket/groups/join-invitations/send` | `POST /api/groups/:groupId/join-invitations` (**implemented** — `performGroupJoinInvitationSend`; Pro; public groups omit `encryptedAccessKeyring`) | | +| `websocket/groups/join-invitations/accept` | `POST /api/groups/:groupId/join-invitations/me/accept` (**implemented** — `performGroupJoinInvitationAccept`) | | +| `websocket/groups/join-invitations/reject` | `POST /api/groups/:groupId/join-invitations/me/reject` (**implemented** — `performGroupJoinInvitationReject`; no Pro in legacy) | | +| `websocket/groups/join-invitations/cancel` | `DELETE /api/groups/:groupId/join-invitations/:userId` (**implemented** — `performGroupJoinInvitationCancel`; path `userId` = invitee) | | +| `websocket/groups/join-requests/send` | `POST /api/groups/:groupId/join-requests` (**implemented** — `performGroupJoinRequestSend`; requires `are_join_requests_allowed`) | | +| `websocket/groups/join-requests/accept` | `POST /api/groups/:groupId/join-requests/:userId/accept` (**implemented** — `performGroupJoinRequestAccept`) | | +| `websocket/groups/join-requests/reject` | `POST /api/groups/:groupId/join-requests/:userId/reject` (**implemented** — `performGroupJoinRequestReject`; sets `rejected`) | | +| `websocket/groups/join-requests/cancel` | `POST /api/groups/:groupId/join-requests/me/cancel` (**implemented** — `performGroupJoinRequestCancel`) | | +| `websocket/groups/change-user-role` | `PATCH /api/groups/:groupId/members/:userId` (**implemented** — `performGroupMemberRoleChange`) | | +| `websocket/groups/remove-user` | `DELETE /api/groups/:groupId/members/:userId` (**implemented** — `performGroupMemberRemove`; self-remove allowed) | | | `websocket/groups/privacy/make-private` | `POST /api/groups/:groupId/privacy/private` | **implemented** — see `groups.privacy.makePrivate` row above | | `websocket/groups/rotate-keys` | — | **removed** per RESTART_PLAN | | `websocket/pages/move` | `POST /api/pages/:pageId/move` (**implemented** — `pageMoveRequestSchema`; `performPageMove`: Pro, optional `groupCreation`, `reencrypt` when changing group) | | diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index 02ba00e1..5a0fe923 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -34,9 +34,16 @@ export { groupPasswordChangeRequestSchema, groupPasswordDisableRequestSchema, groupPasswordEnableRequestSchema, + groupJoinInvitationAcceptRequestSchema, + groupJoinInvitationSendRequestSchema, + groupJoinRequestAcceptRequestSchema, + groupJoinRequestSendRequestSchema, + groupMemberRolePatchRequestSchema, + groupMemberRoleSchema, groupPrivacyJoinRequestsPatchSchema, groupPrivacyPrivateRequestSchema, groupPrivacyPublicRequestSchema, + groupUserIdPathSchema, pageBacklinkCreateRequestSchema, pageBumpRequestSchema, pageMoveRequestSchema, diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 84ec5d7d..99fefcec 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -75,6 +75,34 @@ describe("getOpenApiDocument", () => { expect(doc.paths?.["/api/groups/{groupId}"]?.delete).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/restore"]?.post).toBeDefined(); expect(doc.paths?.["/api/groups/{groupId}/purge"]?.post).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/join-invitations"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/join-invitations/me/accept"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/join-invitations/me/reject"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/join-invitations/{userId}"]?.delete, + ).toBeDefined(); + expect(doc.paths?.["/api/groups/{groupId}/join-requests"]?.post).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/join-requests/me/cancel"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/join-requests/{userId}/accept"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/join-requests/{userId}/reject"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/members/{userId}"]?.patch, + ).toBeDefined(); + expect( + doc.paths?.["/api/groups/{groupId}/members/{userId}"]?.delete, + ).toBeDefined(); expect(doc.paths?.["/api/pages/{pageId}/move"]?.post).toBeDefined(); expect(doc.paths?.["/api/pages/{pageId}/bump"]?.post).toBeDefined(); expect( diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index b065ba43..929f22e0 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -27,9 +27,15 @@ import { groupPasswordChangeRequestSchema, groupPasswordDisableRequestSchema, groupPasswordEnableRequestSchema, + groupJoinInvitationAcceptRequestSchema, + groupJoinInvitationSendRequestSchema, + groupJoinRequestAcceptRequestSchema, + groupJoinRequestSendRequestSchema, + groupMemberRolePatchRequestSchema, groupPrivacyJoinRequestsPatchSchema, groupPrivacyPrivateRequestSchema, groupPrivacyPublicRequestSchema, + groupUserIdPathSchema, pageBacklinkCreateRequestSchema, pageBumpRequestSchema, pageMoveRequestSchema, @@ -887,6 +893,253 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/join-invitations", + summary: "Send a group join invitation (Pro)", + description: + "Replaces legacy WS `groups.joinInvitations.send` step 1. Deletes a conflicting join request for the invitee. For private groups, `encryptedAccessKeyring` is required; for public groups it is stored as null.", + request: { + params: groupIdPathSchema, + body: { + content: { + "application/json": { + schema: groupJoinInvitationSendRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Invitation created." }, + 400: { + description: "Already invited, already a member, or missing keyring for private group.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/join-invitations/me/accept", + summary: "Accept a pending join invitation (Pro)", + description: "Replaces legacy WS `groups.joinInvitations.accept` step 1.", + request: { + params: groupIdPathSchema, + body: { + content: { + "application/json": { + schema: groupJoinInvitationAcceptRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Invitation consumed; user added to `group_members`." }, + 400: { + description: "Validation error.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/join-invitations/me/reject", + summary: "Reject a pending join invitation", + description: "Replaces legacy WS `groups.joinInvitations.reject` step 1 (no Pro check in legacy).", + request: { params: groupIdPathSchema }, + responses: { + 204: { description: "Invitation removed." }, + 400: { + description: "No pending invitation.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "delete", + path: "/api/groups/{groupId}/join-invitations/{userId}", + summary: "Cancel a join invitation (Pro)", + description: + "Replaces legacy WS `groups.joinInvitations.cancel` step 1. Path `userId` is the invitee. Requires permission to manage the invited role.", + request: { params: groupUserIdPathSchema }, + responses: { + 204: { description: "Invitation removed." }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/join-requests", + summary: "Send a join request (Pro)", + description: + "Replaces legacy WS `groups.joinRequests.send` step 1. Requires `are_join_requests_allowed` on the group.", + request: { + params: groupIdPathSchema, + body: { + content: { + "application/json": { + schema: groupJoinRequestSendRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Join request created." }, + 400: { + description: "Already pending.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/join-requests/{userId}/accept", + summary: "Accept a join request (Pro)", + description: "Replaces legacy WS `groups.joinRequests.accept` step 1. Path `userId` is the requester.", + request: { + params: groupUserIdPathSchema, + body: { + content: { + "application/json": { + schema: groupJoinRequestAcceptRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Requester added to `group_members`." }, + 400: { + description: "No pending request or missing access keyring for private group.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/join-requests/{userId}/reject", + summary: "Reject a join request (Pro)", + description: + "Replaces legacy WS `groups.joinRequests.reject` step 1. Sets `rejected` on the request (legacy does not delete the row).", + request: { params: groupUserIdPathSchema }, + responses: { + 204: { description: "Request marked rejected." }, + 400: { + description: "No pending request.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/groups/{groupId}/join-requests/me/cancel", + summary: "Cancel own join request (Pro)", + description: "Replaces legacy WS `groups.joinRequests.cancel` step 1.", + request: { params: groupIdPathSchema }, + responses: { + 204: { description: "Join request row deleted." }, + 400: { + description: "No pending request.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "patch", + path: "/api/groups/{groupId}/members/{userId}", + summary: "Change a member's role (Pro)", + description: "Replaces legacy WS `groups.changeUserRole` step 1.", + request: { + params: groupUserIdPathSchema, + body: { + content: { + "application/json": { + schema: groupMemberRolePatchRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "`group_members.role` updated." }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "delete", + path: "/api/groups/{groupId}/members/{userId}", + summary: "Remove a member (or leave)", + description: + "Replaces legacy WS `groups.removeUser` step 1. Callers may remove themselves without `canManageRole` on others.", + request: { params: groupUserIdPathSchema }, + responses: { + 204: { description: "Membership removed." }, + 400: { + description: "Cannot remove the last owner.", + content: { + "application/json": { schema: sessionErrorResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "post", path: "/api/pages/{pageId}/move", diff --git a/new-deepnotes/packages/api/src/schemas/pages-groups.ts b/new-deepnotes/packages/api/src/schemas/pages-groups.ts index bdc07e1f..0b8f093e 100644 --- a/new-deepnotes/packages/api/src/schemas/pages-groups.ts +++ b/new-deepnotes/packages/api/src/schemas/pages-groups.ts @@ -188,6 +188,63 @@ export type GroupPrivacyPrivateRequest = z.infer< typeof groupPrivacyPrivateRequestSchema >; +export const groupMemberRoleSchema = z + .enum(["owner", "admin", "moderator", "member", "viewer"]) + .openapi("GroupMemberRole"); + +export const groupJoinInvitationSendRequestSchema = z + .object({ + inviteeUserId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi(nanoidIdOpenapi), + invitationRole: groupMemberRoleSchema, + /** Required when the group is private (`access_keyring` is null). Omitted / ignored for public groups. */ + encryptedAccessKeyring: byteB64.optional(), + encryptedInternalKeyring: byteB64, + userEncryptedName: byteB64, + userEncryptedNameForUser: byteB64, + }) + .openapi("GroupJoinInvitationSendRequest"); + +export const groupJoinInvitationAcceptRequestSchema = z + .object({ + userEncryptedName: byteB64, + }) + .openapi("GroupJoinInvitationAcceptRequest"); + +export const groupJoinRequestSendRequestSchema = z + .object({ + encryptedUserName: byteB64, + encryptedUserNameForUser: byteB64, + }) + .openapi("GroupJoinRequestSendRequest"); + +export const groupJoinRequestAcceptRequestSchema = z + .object({ + targetRole: groupMemberRoleSchema, + encryptedAccessKeyring: byteB64.optional(), + encryptedInternalKeyring: byteB64, + }) + .openapi("GroupJoinRequestAcceptRequest"); + +export const groupMemberRolePatchRequestSchema = z + .object({ + role: groupMemberRoleSchema, + }) + .openapi("GroupMemberRolePatchRequest"); + +export const groupUserIdPathSchema = z.object({ + groupId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi({ ...nanoidIdOpenapi, param: { name: "groupId", in: "path" } }), + userId: z + .string() + .regex(/^[A-Za-z0-9_-]{21}$/) + .openapi({ ...nanoidIdOpenapi, param: { name: "userId", in: "path" } }), +}); + export const pageIdPathSchema = z.object({ pageId: z .string() diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index 511fce3e..fd57b165 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -21,6 +21,7 @@ import { import * as schema from "@deepnotes/db/schema"; import { devices, + groupJoinInvitations, groupMembers, groups, notifications, @@ -59,6 +60,14 @@ import { performGetGroupMainPageId, performGetGroupMemberUserIds, } from "./group-main-and-members.js"; +import { + performGroupJoinInvitationSend, + performGroupJoinInvitationAccept, + performGroupJoinRequestAccept, + performGroupJoinRequestSend, + performGroupMemberRemove, + performGroupMemberRoleChange, +} from "./group-membership.js"; import { performGroupPasswordChange, performGroupPasswordDisable, @@ -1473,6 +1482,195 @@ describe.skipIf(resolveTemplateContext() == null)( } }); + it("groups: join invitation, role change, remove; join request accept", async () => { + const env = testSessionEnv(); + const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; + const admin = postgres(ctx.adminUrl, { max: 1 }); + try { + await createDatabaseFromTemplate(admin, cloneName, ctx.templateName); + } finally { + await admin.end({ timeout: 5 }); + } + + const cloneUrl = withDatabaseName(baseCtx.appBaseUrl, cloneName); + const client = postgres(cloneUrl, { max: 1 }); + const db = drizzle(client, { schema }); + try { + const loginA = rand32(); + const loginB = rand32(); + const regA = await buildRegisterBody(`a-${nanoid()}@example.com`, loginA); + const regB = await buildRegisterBody(`b-${nanoid()}@example.com`, loginB); + await performUserRegister({ db, env, body: regA }); + await performUserRegister({ db, env, body: regB }); + await db + .update(users) + .set({ plan: "pro" }) + .where(eq(users.id, regA.userId)); + await db + .update(users) + .set({ plan: "pro" }) + .where(eq(users.id, regB.userId)); + + const accessA = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: regA.userId, + sessionId: nanoid(), + }); + const accessB = await signAccessToken({ + secret: env.ACCESS_SECRET, + userId: regB.userId, + sessionId: nanoid(), + }); + + const sharedGroupId = nanoid(); + const sharedPageId = nanoid(); + await performCreatePage({ + db, + env, + accessCookie: accessA, + groupId: sharedGroupId, + body: { + parentPageId: regA.pageId, + pageId: sharedPageId, + pageEncryptedSymmetricKeyring: rand32(), + pageEncryptedRelativeTitle: rand32(), + pageEncryptedAbsoluteTitle: rand32(), + groupCreation: { + groupEncryptedName: rand32(), + groupIsPublic: true, + groupAccessKeyring: rand32(), + groupEncryptedInternalKeyring: rand32(), + groupEncryptedContentKeyring: rand32(), + groupPublicKeyring: rand32(), + groupEncryptedPrivateKeyring: rand32(), + groupOwnerEncryptedName: rand32(), + }, + }, + }); + + const encInt = rand32(); + const nm = rand32(); + const nmUser = rand32(); + await performGroupJoinInvitationSend({ + db, + env, + accessCookie: accessA, + groupId: sharedGroupId, + inviteeUserId: regB.userId, + invitationRole: "member", + encryptedInternalKeyring: encInt, + userEncryptedName: nm, + userEncryptedNameForUser: nmUser, + }); + + const [invRow] = await db + .select({ role: groupJoinInvitations.role }) + .from(groupJoinInvitations) + .where( + and( + eq(groupJoinInvitations.groupId, sharedGroupId), + eq(groupJoinInvitations.userId, regB.userId), + ), + ); + expect(invRow?.role).toBe("member"); + + const acceptName = rand32(); + await performGroupJoinInvitationAccept({ + db, + env, + accessCookie: accessB, + groupId: sharedGroupId, + userEncryptedName: acceptName, + }); + + const [memAfter] = await db + .select({ role: groupMembers.role }) + .from(groupMembers) + .where( + and( + eq(groupMembers.groupId, sharedGroupId), + eq(groupMembers.userId, regB.userId), + ), + ); + expect(memAfter?.role).toBe("member"); + + await performGroupMemberRoleChange({ + db, + env, + accessCookie: accessA, + groupId: sharedGroupId, + targetUserId: regB.userId, + requestedRole: "moderator", + }); + const [memMod] = await db + .select({ role: groupMembers.role }) + .from(groupMembers) + .where( + and( + eq(groupMembers.groupId, sharedGroupId), + eq(groupMembers.userId, regB.userId), + ), + ); + expect(memMod?.role).toBe("moderator"); + + await performGroupMemberRemove({ + db, + env, + accessCookie: accessA, + groupId: sharedGroupId, + targetUserId: regB.userId, + }); + const [gone] = await db + .select({ userId: groupMembers.userId }) + .from(groupMembers) + .where( + and( + eq(groupMembers.groupId, sharedGroupId), + eq(groupMembers.userId, regB.userId), + ), + ); + expect(gone).toBeUndefined(); + + await performGroupJoinRequestSend({ + db, + env, + accessCookie: accessB, + groupId: sharedGroupId, + encryptedUserName: rand32(), + encryptedUserNameForUser: rand32(), + }); + + await performGroupJoinRequestAccept({ + db, + env, + accessCookie: accessA, + groupId: sharedGroupId, + requesterUserId: regB.userId, + targetRole: "viewer", + encryptedInternalKeyring: rand32(), + }); + + const [memJr] = await db + .select({ role: groupMembers.role }) + .from(groupMembers) + .where( + and( + eq(groupMembers.groupId, sharedGroupId), + eq(groupMembers.userId, regB.userId), + ), + ); + expect(memJr?.role).toBe("viewer"); + } finally { + await client.end({ timeout: 5 }); + const admin2 = postgres(ctx.adminUrl, { max: 1 }); + try { + await dropDatabaseIfExists(admin2, cloneName); + } finally { + await admin2.end({ timeout: 5 }); + } + } + }); + it("user page prefs: starting, path, favorites, recent, defaults, notifications", async () => { const env = testSessionEnv(); const cloneName = `dn_test_${randomBytes(8).toString("hex")}`; diff --git a/new-deepnotes/packages/session/src/group-membership.ts b/new-deepnotes/packages/session/src/group-membership.ts new file mode 100644 index 00000000..62916017 --- /dev/null +++ b/new-deepnotes/packages/session/src/group-membership.ts @@ -0,0 +1,636 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { + groupJoinInvitations, + groupJoinRequests, + groupMembers, + groups, +} from "@deepnotes/db/schema"; +import { and, count, eq } from "drizzle-orm"; +import { Buffer } from "node:buffer"; + +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { + canChangeRole, + canManageRole, + roleHasManageLowerRanks, +} from "./group-role-ranks.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; +import { assertUserProPlan } from "./user-plan.js"; + +async function requireGroup(input: { + db: DeepnotesDb; + groupId: string; +}): Promise<{ accessKeyring: Buffer | null }> { + const [g] = await input.db + .select({ accessKeyring: groups.accessKeyring }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + return { accessKeyring: g.accessKeyring }; +} + +async function getMemberRole(input: { + db: DeepnotesDb; + groupId: string; + userId: string; +}): Promise { + const [row] = await input.db + .select({ role: groupMembers.role }) + .from(groupMembers) + .where( + and( + eq(groupMembers.groupId, input.groupId), + eq(groupMembers.userId, input.userId), + ), + ) + .limit(1); + return row?.role ?? null; +} + +async function countOwners(input: { + db: DeepnotesDb; + groupId: string; +}): Promise { + const [row] = await input.db + .select({ n: count() }) + .from(groupMembers) + .where( + and( + eq(groupMembers.groupId, input.groupId), + eq(groupMembers.role, "owner"), + ), + ); + return Number(row?.n ?? 0); +} + +/** Legacy `groups.joinInvitations.send` step 1 (DB only). */ +export async function performGroupJoinInvitationSend(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + inviteeUserId: string; + invitationRole: string; + /** Omitted for public groups (`access_keyring` set). */ + encryptedAccessKeyring?: Uint8Array; + encryptedInternalKeyring: Uint8Array; + userEncryptedName: Uint8Array; + userEncryptedNameForUser: Uint8Array; +}): Promise { + const { userId: agentId } = await getAuthenticatedUserSummary(input); + await assertUserProPlan({ db: input.db, userId: agentId }); + + const { accessKeyring } = await requireGroup({ db: input.db, groupId: input.groupId }); + const isPublic = accessKeyring != null; + + const agentRole = await getMemberRole({ + db: input.db, + groupId: input.groupId, + userId: agentId, + }); + if (agentRole == null || !canManageRole(agentRole, input.invitationRole)) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions"); + } + + const [existingInv] = await input.db + .select({ userId: groupJoinInvitations.userId }) + .from(groupJoinInvitations) + .where( + and( + eq(groupJoinInvitations.groupId, input.groupId), + eq(groupJoinInvitations.userId, input.inviteeUserId), + ), + ) + .limit(1); + if (existingInv != null) { + throw new SessionError(400, "BAD_REQUEST", "Invitation already exists"); + } + + const targetMemberRole = await getMemberRole({ + db: input.db, + groupId: input.groupId, + userId: input.inviteeUserId, + }); + if (targetMemberRole != null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "User is already a member of the group.", + ); + } + + if (!isPublic && input.encryptedAccessKeyring === undefined) { + throw new SessionError( + 400, + "BAD_REQUEST", + "encryptedAccessKeyring is required for private groups.", + ); + } + + await input.db.transaction(async (tx) => { + await tx + .delete(groupJoinRequests) + .where( + and( + eq(groupJoinRequests.groupId, input.groupId), + eq(groupJoinRequests.userId, input.inviteeUserId), + ), + ); + await tx.insert(groupJoinInvitations).values({ + groupId: input.groupId, + userId: input.inviteeUserId, + inviterId: agentId, + role: input.invitationRole, + encryptedAccessKeyring: isPublic + ? null + : Buffer.from(input.encryptedAccessKeyring!), + encryptedInternalKeyring: Buffer.from(input.encryptedInternalKeyring), + encryptedName: Buffer.from(input.userEncryptedName), + encryptedNameForUser: Buffer.from(input.userEncryptedNameForUser), + }); + }); +} + +/** Legacy `groups.joinInvitations.accept` step 1. */ +export async function performGroupJoinInvitationAccept(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + userEncryptedName: Uint8Array; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + await assertUserProPlan({ db: input.db, userId }); + + await requireGroup({ db: input.db, groupId: input.groupId }); + + const [inv] = await input.db + .select() + .from(groupJoinInvitations) + .where( + and( + eq(groupJoinInvitations.groupId, input.groupId), + eq(groupJoinInvitations.userId, userId), + ), + ) + .limit(1); + + if (inv == null) { + throw new SessionError(403, "FORBIDDEN", "No pending invitation."); + } + + await input.db.transaction(async (tx) => { + await tx + .delete(groupJoinInvitations) + .where( + and( + eq(groupJoinInvitations.groupId, input.groupId), + eq(groupJoinInvitations.userId, userId), + ), + ); + await tx.insert(groupMembers).values({ + groupId: input.groupId, + userId, + role: inv.role, + encryptedAccessKeyring: inv.encryptedAccessKeyring, + encryptedInternalKeyring: inv.encryptedInternalKeyring, + encryptedName: Buffer.from(input.userEncryptedName), + encryptedNameForUser: inv.encryptedNameForUser, + }); + }); +} + +/** Legacy `groups.joinInvitations.reject` step 1. */ +export async function performGroupJoinInvitationReject(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + + await requireGroup({ db: input.db, groupId: input.groupId }); + + const [inv] = await input.db + .select({ userId: groupJoinInvitations.userId }) + .from(groupJoinInvitations) + .where( + and( + eq(groupJoinInvitations.groupId, input.groupId), + eq(groupJoinInvitations.userId, userId), + ), + ) + .limit(1); + + if (inv == null) { + throw new SessionError(400, "BAD_REQUEST", "No pending join invitation."); + } + + await input.db + .delete(groupJoinInvitations) + .where( + and( + eq(groupJoinInvitations.groupId, input.groupId), + eq(groupJoinInvitations.userId, userId), + ), + ); +} + +/** Legacy `groups.joinInvitations.cancel` step 1. */ +export async function performGroupJoinInvitationCancel(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + inviteeUserId: string; +}): Promise { + const { userId: agentId } = await getAuthenticatedUserSummary(input); + await assertUserProPlan({ db: input.db, userId: agentId }); + + await requireGroup({ db: input.db, groupId: input.groupId }); + + const agentRole = await getMemberRole({ + db: input.db, + groupId: input.groupId, + userId: agentId, + }); + if (agentRole == null) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const [inv] = await input.db + .select({ role: groupJoinInvitations.role }) + .from(groupJoinInvitations) + .where( + and( + eq(groupJoinInvitations.groupId, input.groupId), + eq(groupJoinInvitations.userId, input.inviteeUserId), + ), + ) + .limit(1); + + if (inv == null) { + throw new SessionError(403, "FORBIDDEN", "No pending invitation."); + } + + if (!canManageRole(agentRole, inv.role)) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + await input.db + .delete(groupJoinInvitations) + .where( + and( + eq(groupJoinInvitations.groupId, input.groupId), + eq(groupJoinInvitations.userId, input.inviteeUserId), + ), + ); +} + +/** Legacy `groups.joinRequests.send` step 1. */ +export async function performGroupJoinRequestSend(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + encryptedUserName: Uint8Array; + encryptedUserNameForUser: Uint8Array; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + await assertUserProPlan({ db: input.db, userId }); + + const [g] = await input.db + .select({ + allow: groups.areJoinRequestsAllowed, + }) + .from(groups) + .where(eq(groups.id, input.groupId)) + .limit(1); + if (g == null) { + throw new SessionError(404, "NOT_FOUND", "Group not found."); + } + if (!g.allow) { + throw new SessionError( + 403, + "FORBIDDEN", + "This group does not allow join requests.", + ); + } + + const [reqRow] = await input.db + .select({ rejected: groupJoinRequests.rejected }) + .from(groupJoinRequests) + .where( + and( + eq(groupJoinRequests.groupId, input.groupId), + eq(groupJoinRequests.userId, userId), + ), + ) + .limit(1); + + if (reqRow != null) { + if (reqRow.rejected) { + throw new SessionError( + 403, + "FORBIDDEN", + "Your join request has been rejected.", + ); + } + throw new SessionError(400, "BAD_REQUEST", "Join request already pending."); + } + + const memberRole = await getMemberRole({ + db: input.db, + groupId: input.groupId, + userId, + }); + if (memberRole != null) { + throw new SessionError( + 403, + "FORBIDDEN", + "You are already a member of this group.", + ); + } + + await input.db.insert(groupJoinRequests).values({ + groupId: input.groupId, + userId, + encryptedName: Buffer.from(input.encryptedUserName), + encryptedNameForUser: Buffer.from(input.encryptedUserNameForUser), + rejected: false, + }); +} + +/** Legacy `groups.joinRequests.accept` step 1. */ +export async function performGroupJoinRequestAccept(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + requesterUserId: string; + targetRole: string; + /** Omitted for public groups. */ + encryptedAccessKeyring?: Uint8Array; + encryptedInternalKeyring: Uint8Array; +}): Promise { + const { userId: agentId } = await getAuthenticatedUserSummary(input); + await assertUserProPlan({ db: input.db, userId: agentId }); + + const { accessKeyring } = await requireGroup({ db: input.db, groupId: input.groupId }); + const isPublic = accessKeyring != null; + + const agentRole = await getMemberRole({ + db: input.db, + groupId: input.groupId, + userId: agentId, + }); + if (agentRole == null || !canManageRole(agentRole, input.targetRole)) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions"); + } + + const [reqRow] = await input.db + .select() + .from(groupJoinRequests) + .where( + and( + eq(groupJoinRequests.groupId, input.groupId), + eq(groupJoinRequests.userId, input.requesterUserId), + ), + ) + .limit(1); + + if (reqRow == null || reqRow.rejected) { + throw new SessionError(400, "BAD_REQUEST", "No pending request"); + } + + if (!isPublic && input.encryptedAccessKeyring === undefined) { + throw new SessionError( + 400, + "BAD_REQUEST", + "encryptedAccessKeyring is required for private groups.", + ); + } + + await input.db.transaction(async (tx) => { + await tx + .delete(groupJoinRequests) + .where( + and( + eq(groupJoinRequests.groupId, input.groupId), + eq(groupJoinRequests.userId, input.requesterUserId), + ), + ); + await tx.insert(groupMembers).values({ + groupId: input.groupId, + userId: input.requesterUserId, + role: input.targetRole, + encryptedAccessKeyring: isPublic + ? null + : Buffer.from(input.encryptedAccessKeyring!), + encryptedInternalKeyring: Buffer.from(input.encryptedInternalKeyring), + encryptedName: reqRow.encryptedName, + encryptedNameForUser: reqRow.encryptedNameForUser, + }); + }); +} + +/** Legacy `groups.joinRequests.reject` step 1. */ +export async function performGroupJoinRequestReject(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + requesterUserId: string; +}): Promise { + const { userId: agentId } = await getAuthenticatedUserSummary(input); + await assertUserProPlan({ db: input.db, userId: agentId }); + + await requireGroup({ db: input.db, groupId: input.groupId }); + + const agentRole = await getMemberRole({ + db: input.db, + groupId: input.groupId, + userId: agentId, + }); + if (agentRole == null || !roleHasManageLowerRanks(agentRole)) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const [reqRow] = await input.db + .select({ rejected: groupJoinRequests.rejected }) + .from(groupJoinRequests) + .where( + and( + eq(groupJoinRequests.groupId, input.groupId), + eq(groupJoinRequests.userId, input.requesterUserId), + ), + ) + .limit(1); + + if (reqRow == null || reqRow.rejected) { + throw new SessionError(400, "BAD_REQUEST", "No pending join request"); + } + + await input.db + .update(groupJoinRequests) + .set({ rejected: true }) + .where( + and( + eq(groupJoinRequests.groupId, input.groupId), + eq(groupJoinRequests.userId, input.requesterUserId), + ), + ); +} + +/** Legacy `groups.joinRequests.cancel` step 1. */ +export async function performGroupJoinRequestCancel(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; +}): Promise { + const { userId } = await getAuthenticatedUserSummary(input); + await assertUserProPlan({ db: input.db, userId }); + + await requireGroup({ db: input.db, groupId: input.groupId }); + + const [reqRow] = await input.db + .select({ rejected: groupJoinRequests.rejected }) + .from(groupJoinRequests) + .where( + and( + eq(groupJoinRequests.groupId, input.groupId), + eq(groupJoinRequests.userId, userId), + ), + ) + .limit(1); + + if (reqRow == null || reqRow.rejected) { + throw new SessionError(400, "BAD_REQUEST", "No pending join request."); + } + + await input.db + .delete(groupJoinRequests) + .where( + and( + eq(groupJoinRequests.groupId, input.groupId), + eq(groupJoinRequests.userId, userId), + ), + ); +} + +/** Legacy `groups.changeUserRole` step 1. */ +export async function performGroupMemberRoleChange(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + targetUserId: string; + requestedRole: string; +}): Promise { + const { userId: agentId } = await getAuthenticatedUserSummary(input); + await assertUserProPlan({ db: input.db, userId: agentId }); + + await requireGroup({ db: input.db, groupId: input.groupId }); + + const agentRole = await getMemberRole({ + db: input.db, + groupId: input.groupId, + userId: agentId, + }); + const patientRole = await getMemberRole({ + db: input.db, + groupId: input.groupId, + userId: input.targetUserId, + }); + + if ( + agentRole == null || + patientRole == null || + !canChangeRole(agentRole, patientRole, input.requestedRole) + ) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + if ( + patientRole === "owner" && + input.requestedRole !== "owner" && + (await countOwners({ db: input.db, groupId: input.groupId })) <= 1 + ) { + throw new SessionError( + 403, + "FORBIDDEN", + "You cannot remove all group owners.", + ); + } + + await input.db + .update(groupMembers) + .set({ role: input.requestedRole }) + .where( + and( + eq(groupMembers.groupId, input.groupId), + eq(groupMembers.userId, input.targetUserId), + ), + ); +} + +/** Legacy `groups.removeUser` step 1. */ +export async function performGroupMemberRemove(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + groupId: string; + targetUserId: string; +}): Promise { + const { userId: agentId } = await getAuthenticatedUserSummary(input); + await assertUserProPlan({ db: input.db, userId: agentId }); + + await requireGroup({ db: input.db, groupId: input.groupId }); + + const agentRole = await getMemberRole({ + db: input.db, + groupId: input.groupId, + userId: agentId, + }); + const targetRole = await getMemberRole({ + db: input.db, + groupId: input.groupId, + userId: input.targetUserId, + }); + + if (targetRole == null) { + throw new SessionError(404, "NOT_FOUND", "Member not found."); + } + + if ( + agentId !== input.targetUserId && + !canManageRole(agentRole ?? "", targetRole) + ) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + if ( + targetRole === "owner" && + (await countOwners({ db: input.db, groupId: input.groupId })) <= 1 + ) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Cannot remove the all group owners.", + ); + } + + await input.db + .delete(groupMembers) + .where( + and( + eq(groupMembers.groupId, input.groupId), + eq(groupMembers.userId, input.targetUserId), + ), + ); +} diff --git a/new-deepnotes/packages/session/src/group-role-ranks.ts b/new-deepnotes/packages/session/src/group-role-ranks.ts new file mode 100644 index 00000000..6b31b98b --- /dev/null +++ b/new-deepnotes/packages/session/src/group-role-ranks.ts @@ -0,0 +1,49 @@ +/** + * Mirrors `@deeplib/misc` `canManageRole` / `canChangeRole` for group role ranks + * (no workspace dependency on legacy packages). + */ + +const RANK: Record = { + owner: 5, + admin: 4, + moderator: 3, + member: 2, + viewer: 1, +}; + +/** `manageLowerRanks` + `manageOwnRank` semantics from legacy `IGroupRole`. */ +export function canManageRole( + managerRole: string, + targetRole: string, +): boolean { + const mr = RANK[managerRole]; + const tr = RANK[targetRole]; + if (mr == null || tr == null) { + return false; + } + const manageLower = ["owner", "admin", "moderator"].includes(managerRole); + const manageOwn = ["owner", "admin"].includes(managerRole); + if (tr < mr) { + return manageLower; + } + if (tr <= mr) { + return manageOwn; + } + return false; +} + +export function canChangeRole( + managerRole: string, + targetOldRole: string, + targetNewRole: string, +): boolean { + return ( + canManageRole(managerRole, targetOldRole) && + canManageRole(managerRole, targetNewRole) + ); +} + +/** Legacy `manageLowerRanks` permission (reject join requests, etc.). */ +export function roleHasManageLowerRanks(role: string): boolean { + return ["owner", "admin", "moderator"].includes(role); +} diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index e8e6dfde..ee89c9ce 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -93,3 +93,15 @@ export type { PageMoveGroupCreation, PageMoveReencrypt, } from "./page-move.js"; +export { + performGroupJoinInvitationAccept, + performGroupJoinInvitationCancel, + performGroupJoinInvitationReject, + performGroupJoinInvitationSend, + performGroupJoinRequestAccept, + performGroupJoinRequestCancel, + performGroupJoinRequestReject, + performGroupJoinRequestSend, + performGroupMemberRemove, + performGroupMemberRoleChange, +} from "./group-membership.js"; From bcdc03e75259faa3e5ca05b42a7d128d0cb7bdbb Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 11:22:13 -0300 Subject: [PATCH 053/243] feat(new-deepnotes): Stripe billing in session and API --- new-deepnotes/PLAN_PROGRESS.md | 38 ++- new-deepnotes/apps/api-worker/package.json | 3 +- .../apps/api-worker/src/index.test.ts | 3 + new-deepnotes/apps/api-worker/src/index.ts | 194 ++++++++++- .../apps/api-worker/src/session-env.ts | 38 ++- new-deepnotes/docs/DEPLOY_CLOUDFLARE.md | 2 +- new-deepnotes/docs/TRPC_REST_MAP.md | 6 +- new-deepnotes/packages/api/src/index.ts | 5 + .../packages/api/src/openapi.test.ts | 7 + new-deepnotes/packages/api/src/openapi.ts | 82 +++++ .../packages/api/src/schemas/billing.ts | 24 ++ new-deepnotes/packages/session/package.json | 3 +- .../src/account-flows.integration.test.ts | 1 + new-deepnotes/packages/session/src/index.ts | 8 + .../packages/session/src/stripe-billing.ts | 313 ++++++++++++++++++ new-deepnotes/pnpm-lock.yaml | 178 ++++++++++ new-deepnotes/template.env | 6 +- 17 files changed, 888 insertions(+), 23 deletions(-) create mode 100644 new-deepnotes/packages/api/src/schemas/billing.ts create mode 100644 new-deepnotes/packages/session/src/stripe-billing.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 769482c0..cf083c7e 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); [7 — page move](#pages-rest--slice-7-move--group-creation); [8 — create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation); **[slice 9 — membership + join flows](#pagesgroups-rest--slice-9-membership--join-invites--requests)** (invitations, join requests, `PATCH`/`DELETE` members). **Still ahead:** **realtime / collab**, **Stripe** (`POST /api/webhooks/stripe`, portal/checkout). | +| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); [7 — page move](#pages-rest--slice-7-move--group-creation); [8 — create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation); **[slice 9 — membership + join flows](#pagesgroups-rest--slice-9-membership--join-invites--requests)**. **[Stripe / billing](#phase-3--stripe-billing--webhooks--account-hooks):** checkout, portal, signed webhooks, optional customer `del`/`update` hooks. **Still ahead:** **realtime / collab** (JWT upgrade, Yjs, no key rotation) per [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md). | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -260,16 +260,25 @@ CI should set the same vars against the workflow Postgres service (role with `CR | `PATCH /api/groups/{groupId}/members/{userId}` | `role` change | | `DELETE /api/groups/{groupId}/members/{userId}` | Remove member or **leave** (self) | -### Not started (Phase 3 — pages, groups, infra) +### Phase 3 — Stripe (billing + webhooks + account hooks) -- [x] **Groups (REST, slice 4):** [password, privacy, soft delete, restore, purge](#pagesgroups-rest--slice-4-group-password-privacy-deletion) — `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY` on `SessionEnv` / worker bindings. -- [x] **Groups (REST, slice 5):** [make-private / re-key](#pagesgroups-rest--slice-5-privacy-private-re-key) — `POST /api/groups/:groupId/privacy/private`; OpenAPI + [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md). -- [x] **Pages (REST, slice 6):** [bump / backlinks / snapshots / page deletion](#pages-rest--slice-6-bump-backlinks-snapshots-deletion). -- [x] **Pages (REST, slice 7):** [move + optional `groupCreation`](#pages-rest--slice-7-move--group-creation) — `POST /api/pages/:pageId/move` (`page-move.ts`). -- [x] **Pages (REST, slice 8):** [create + optional `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) on **`POST /api/groups/:groupId/pages`** — legacy `pages.create` parity (`group-creation-shared.ts` + `performCreatePage`). -- [x] **Group membership + joins (REST, slice 9):** [invitations, requests, role, remove](#pagesgroups-rest--slice-9-membership--join-invites--requests) — `group-membership.ts` + [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) WebSocket rows. **Note:** legacy WS **step 2** encrypted push notifications are **not** replicated on the server; the SPA should continue to use `users_notifications` + existing notification types when product needs parity. -- [ ] **Realtime / collab** (new or adapted protocols; no key rotation; Durable Object vs separate service per [RESTART_PLAN](../docs/RESTART_PLAN.md) §4.3 / hosting table). -- [ ] **Stripe:** `POST /api/webhooks/stripe`, `POST /api/billing/stripe/checkout-session`, `POST /api/billing/stripe/portal-session` (map rows in [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) Users / webhooks); wire **`deleteStripeCustomer`** and **`updateStripeCustomerEmail`** from account flows when secrets exist; no RevenueCat. +Replaces legacy Fastify `/stripe/webhook` and tRPC `users.account.stripe.*` using **Postgres** `users.customer_id` (no KeyDB `customer` → `user-id` hash). + +| Layer | What shipped | +|-------|----------------| +| **`@deepnotes/session`** | [stripe-billing.ts](packages/session/src/stripe-billing.ts) — `performStripeCreateCheckoutSession` (monthly/yearly `price_*`, `Origin` or `PUBLIC_APP_URL` for success URL), `performStripeCreatePortalSession`, `parseStripeWebhookEvent` + `processStripeWebhookEvent` (`customer.subscription.updated` / `deleted` → `plan` / `subscription_id`, same branching as legacy `stripe-webhook.ts`). `findUserIdByStripeCustomerId` for tests and ops. | +| **`@deepnotes/api`** | [schemas/billing.ts](packages/api/src/schemas/billing.ts); OpenAPI paths registered in [openapi.ts](packages/api/src/openapi.ts). | +| **`@deepnotes/api-worker`** | `getStripeBillingEnv` / `getStripeWebhookSecret` in [session-env.ts](apps/api-worker/src/session-env.ts); `POST /api/billing/stripe/checkout-session` (optional JSON body), `POST /api/billing/stripe/portal-session`, `POST /api/webhooks/stripe` (raw body, `Stripe-Signature`); when `STRIPE_SECRET_KEY` is set: **`deleteStripeCustomer`** on `DELETE /api/users/me`, **`updateStripeCustomerEmail`** on `POST /api/users/me/email-change/confirm`. | +| **Config** | [template.env](template.env) — `STRIPE_SECRET_KEY`, `STRIPE_MONTHLY_PRICE_ID`, `STRIPE_YEARLY_PRICE_ID`, `STRIPE_WEBHOOK_SECRET`; [DEPLOY_CLOUDFLARE.md](docs/DEPLOY_CLOUDFLARE.md). | +| **Tests** | Vitest 503 matrix includes the three new routes (**68** total); no live Stripe calls in CI yet. | + +**Gaps (optional follow-ups):** Integration tests with Stripe test clock / fixtures; `checkout.session.completed` if product needs it beyond `subscription.updated`. + +### Not started (Phase 3 — realtime / collab only) + +Sprints **1–9** (pages / groups / membership) and **Stripe** are tracked in the sections above. Remaining work: + +- [ ] **Realtime / collab** — new or adapted WebSocket protocols; **no** key rotation; Durable Object vs separate service per [RESTART_PLAN](../docs/RESTART_PLAN.md) §4.3 / hosting table; blocks editor MVP collab in Phase 4. --- @@ -320,7 +329,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | | **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**24** cases when DB env set) — … + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 move](#pages-rest--slice-7-move--group-creation) + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; optional: invitation **reject/cancel**, join-request **reject/cancel**, **private** group invite/request **access keyring** branches | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (session routes + [slice 6/7 `/api/pages/...` paths](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)); **`schemas/users.test.ts`**; **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; more Zod edge cases for new page schemas | -| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **65** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + `/api/pages/{pageId}/move` + [slice 9 join/member routes](#pagesgroups-rest--slice-9-membership--join-invites--requests) | **200** tests with stub `SessionEnv` + template DB (heavier) | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **68** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + `/api/pages/{pageId}/move` + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests) + [Stripe routes](#phase-3--stripe-billing--webhooks--account-hooks) (`/api/billing/stripe/*`, `/api/webhooks/stripe`) | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -342,7 +351,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). - [x] SQL-heavy paths: real Postgres tests; prefer **template DB** cloning (§5.7) — `@deepnotes/db` `template-db.test.ts` (clone + **sessions / devices / pages / `group_members`** FK rejects); `@deepnotes/session` `account-flows.integration.test.ts` (account + **2FA** + **groups/pages** + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) + [slice 9 membership](#pagesgroups-rest--slice-9-membership--join-invites--requests) + prefs + [slice 4–5](#pagesgroups-rest--slice-4-group-password-privacy-deletion) + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7](#pages-rest--slice-7-move--group-creation)). -- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) — **24** `account-flows` + **6** `@deepnotes/db` when DB set; slices [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)–[9](#pagesgroups-rest--slice-9-membership--join-invites--requests). **Next:** **Redis** failed-login against real Redis; **Stripe** when billing exists. +- [ ] Auth, crypto, Stripe: automated coverage beyond smoke; **no** generic repository layer (§5.0). **Progress:** [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail) — **24** `account-flows` + **6** `@deepnotes/db` when DB set; slices [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)–[9](#pagesgroups-rest--slice-9-membership--join-invites--requests). **Stripe:** [shipped](#phase-3--stripe-billing--webhooks--account-hooks); add integration tests (webhook + checkout) when CI secrets allow. **Next:** **Redis** failed-login against real Redis; Stripe E2E tests optional. - [x] No tRPC / superjson / RevenueCat / key-rotation in **this** tree (keep absent); product sign-off for IAP/Stripe when billing ships. - [x] Client: zero undocumented forks, or a short owned exception list — see [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). - [ ] Cloudflare: deploy runbook; Hyperdrive + Postgres + Redis proven in staging; collab/realtime topology chosen and load-tested. @@ -356,8 +365,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Order | Item | Rationale / notes | |-------|------|----------------------| | ✅ | Slices 1–9 (through [membership + joins](#pagesgroups-rest--slice-9-membership--join-invites--requests)) | Account, pages/groups CRUD, move, create-with-new-group, invitations/requests/member role/remove. | -| **1** | **Realtime** (JWT upgrade, msgpackr-style protocol) + **collab** (Yjs, no rotation) | [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md); load-test before freeze. | -| **2** | **Stripe** webhooks + portal/checkout + account-delete / email-change hooks | [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) billing rows; `SessionEnv` secrets documented in [DEPLOY_CLOUDFLARE](docs/DEPLOY_CLOUDFLARE.md). | +| **1** | **Realtime** (JWT upgrade, msgpackr-style protocol) + **collab** (Yjs, no rotation) | [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md); load-test before freeze; only major Phase 3 item left. | +| ✅ | **Stripe** — checkout, portal, webhooks, account hooks | [Phase 3 — Stripe](#phase-3--stripe-billing--webhooks--account-hooks); secrets in [template.env](./template.env) and [DEPLOY_CLOUDFLARE](docs/DEPLOY_CLOUDFLARE.md). | --- @@ -365,6 +374,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-27 | **Phase 3 — Stripe (billing):** [stripe-billing.ts](packages/session/src/stripe-billing.ts) — `performStripeCreateCheckoutSession` / `performStripeCreatePortalSession`, `processStripeWebhookEvent` (legacy `customer.subscription.updated` / `deleted` → `users.plan` + `subscription_id` via `users.customer_id`); [schemas/billing.ts](packages/api/src/schemas/billing.ts) + OpenAPI; Worker `POST /api/billing/stripe/checkout-session`, `…/portal-session`, `POST /api/webhooks/stripe` ([session-env](apps/api-worker/src/session-env.ts) `getStripeBillingEnv` / `getStripeWebhookSecret`); **`STRIPE_SECRET_KEY`** hooks: `deleteStripeCustomer` on `DELETE /api/users/me`, `updateStripeCustomerEmail` on email-change confirm. Dependencies: `stripe@^17.7` in session + api-worker. [TRPC_REST_MAP](docs/TRPC_REST_MAP.md); [template.env](template.env). Api-worker 503 matrix **68** tests. **Next:** [realtime + collab](#phase-3-working-order-suggested) only. | | 2026-04-27 | **Phase 3 — slice 9 (membership + join flows):** [group-role-ranks.ts](packages/session/src/group-role-ranks.ts) (`canManageRole` / `canChangeRole` / `manageLowerRanks` parity); [group-membership.ts](packages/session/src/group-membership.ts) — invitations send/accept/reject/cancel, join requests send/accept/reject/cancel, `PATCH`/`DELETE` members; Zod + OpenAPI + Hono; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) WS table; **`byteB64`** passthrough in worker (decoded `Uint8Array`). `account-flows` **24** cases; api-worker 503 matrix **65**. [Slice 9 section](#pagesgroups-rest--slice-9-membership--join-invites--requests). **Next:** [realtime + collab](#phase-3-working-order-suggested). | | 2026-04-27 | **Phase 3 — slice 8 (create + `groupCreation`):** [group-creation-shared.ts](packages/session/src/group-creation-shared.ts) + `performCreatePage` with optional `groupCreation` (Pro; path `groupId` = new id; `parentPageId` in personal group). `GroupPageCreateRequest` + OpenAPI; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) `pages.create`; `account-flows` **23** cases. [Slice 8 section](#pagesgroups-rest--slice-8-create--groupcreation); [working order](#phase-3-working-order-suggested) next = **group join + membership REST**. | | 2026-04-27 | **Phase 3 — pages slice 7 (move):** `page-move.ts` `performPageMove` (Pro; optional `groupCreation` + reencrypt; `setAsMainPage` + `users_pages` swap for personal; `page_updates` + `page_snapshots` on cross-group); `pageMoveRequestSchema` in `@deepnotes/api`; `POST /api/pages/:pageId/move` + OpenAPI; worker **503** **55** tests; `account-flows` **22** cases; [TRPC_REST_MAP](./docs/TRPC_REST_MAP.md) `websocket/pages/move` row; [slice 7 section](#pages-rest--slice-7-move--group-creation). **Next:** group invites / requests REST or join routes; **realtime + collab**; **Stripe**. | diff --git a/new-deepnotes/apps/api-worker/package.json b/new-deepnotes/apps/api-worker/package.json index ae0d163f..8d2d96b5 100644 --- a/new-deepnotes/apps/api-worker/package.json +++ b/new-deepnotes/apps/api-worker/package.json @@ -15,7 +15,8 @@ "@deepnotes/db": "workspace:*", "@deepnotes/session": "workspace:*", "@upstash/redis": "^1.34.8", - "hono": "^4.7.7" + "hono": "^4.7.7", + "stripe": "^17.7.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20250426.0", diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index 6c9450a2..e4ff60f6 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -157,6 +157,9 @@ describe("api-worker", () => { ["POST", "/api/users/me/2fa/recovery-codes"], ["POST", "/api/users/me/2fa/devices/forget"], ["POST", "/api/users/me/2fa/disable"], + ["POST", "/api/billing/stripe/checkout-session"], + ["POST", "/api/billing/stripe/portal-session"], + ["POST", "/api/webhooks/stripe"], ] as const)("returns 503 for %s %s when auth env is not configured", async (method, path) => { const res = await app.request(`http://test${path}`, { method }); expect(res.status).toBe(503); diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 31707607..337bdc3e 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -36,15 +36,22 @@ import { userEmailChangeRequestSchema, userPasswordChangeRequestSchema, userRegisterRequestSchema, + stripeCheckoutSessionRequestSchema, } from "@deepnotes/api"; import type { ContentfulStatusCode } from "hono/utils/http-status"; import { Hono } from "hono"; import type { PageMoveBody } from "@deepnotes/session"; +import Stripe from "stripe"; import { getDbForConnectionString } from "./db-pool.js"; import { readCookieHeader } from "./cookies.js"; import { getSessionRedisPort } from "./redis-port.js"; -import { getSessionEnv, type WorkerSessionBindings } from "./session-env.js"; +import { + getSessionEnv, + getStripeBillingEnv, + getStripeWebhookSecret, + type WorkerSessionBindings, +} from "./session-env.js"; type Bindings = WorkerSessionBindings & { /** Wired in `wrangler.toml`; optional in unit tests that do not pass `env`. */ @@ -527,6 +534,7 @@ app.post("/api/users/me/email-change/confirm", async (c) => { try { const { performUserEmailChangeConfirm } = await import("@deepnotes/session"); + const stripeKey = c.env.STRIPE_SECRET_KEY; const { cookieLines } = await performUserEmailChangeConfirm({ db, env: sessionEnv, @@ -536,6 +544,13 @@ app.post("/api/users/me/email-change/confirm", async (c) => { newLoginHash: parsed.data.newLoginHash, newEncryptedPrivateKeyring: parsed.data.userEncryptedPrivateKeyring, newEncryptedSymmetricKeyring: parsed.data.userEncryptedSymmetricKeyring, + updateStripeCustomerEmail: + stripeKey != null && stripeKey.length > 0 + ? async (customerId: string, newEmail: string) => { + const stripe = new Stripe(stripeKey); + await stripe.customers.update(customerId, { email: newEmail }); + } + : undefined, }); const res = c.body(null, 204); appendSetCookies(res, cookieLines); @@ -603,11 +618,19 @@ app.delete("/api/users/me", async (c) => { try { const { performUserAccountDelete } = await import("@deepnotes/session"); + const stripeKey = c.env.STRIPE_SECRET_KEY; const { cookieLines } = await performUserAccountDelete({ db, env: sessionEnv, accessCookie: readCookieHeader(cookieHeader, "accessToken"), loginHash, + deleteStripeCustomer: + stripeKey != null && stripeKey.length > 0 + ? async (customerId: string) => { + const stripe = new Stripe(stripeKey); + await stripe.customers.del(customerId); + } + : undefined, }); const res = c.body(null, 204); appendSetCookies(res, cookieLines); @@ -3460,4 +3483,173 @@ app.post("/api/users/me/2fa/disable", async (c) => { } }); +const billingNotConfiguredBody = { + code: "SERVICE_UNAVAILABLE" as const, + message: + "Stripe billing is not configured. Set STRIPE_SECRET_KEY, STRIPE_MONTHLY_PRICE_ID, and STRIPE_YEARLY_PRICE_ID (Wrangler secrets / .dev.vars).", +} as const; + +const stripeWebhookNotConfiguredBody = { + code: "SERVICE_UNAVAILABLE" as const, + message: + "Stripe webhooks are not configured (STRIPE_WEBHOOK_SECRET).", +} as const; + +app.post("/api/billing/stripe/checkout-session", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + const billing = getStripeBillingEnv(c.env); + if (billing == null) { + return c.json(billingNotConfiguredBody, 503); + } + + let bodyJson: unknown = {}; + try { + const t = await c.req.text(); + if (t.length > 0) { + bodyJson = JSON.parse(t) as unknown; + } + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON object." }, 400); + } + const parsed = stripeCheckoutSessionRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performStripeCreateCheckoutSession } = await import( + "@deepnotes/session" + ); + const out = await performStripeCreateCheckoutSession({ + db, + env: sessionEnv, + billing, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + requestOrigin: c.req.header("Origin") ?? undefined, + billingFrequency: parsed.data.billingFrequency, + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/billing/stripe/portal-session", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + const billing = getStripeBillingEnv(c.env); + if (billing == null) { + return c.json(billingNotConfiguredBody, 503); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + try { + const { performStripeCreatePortalSession } = await import( + "@deepnotes/session" + ); + const out = await performStripeCreatePortalSession({ + db, + env: sessionEnv, + billing, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + }); + return c.json(out, 200); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/webhooks/stripe", async (c) => { + const hyper = c.env?.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + const webhookSecret = getStripeWebhookSecret(c.env); + if (webhookSecret == null) { + return c.json(stripeWebhookNotConfiguredBody, 503); + } + + const rawBody = await c.req.text(); + const db = getDbForConnectionString(hyper.connectionString); + + try { + const { + parseStripeWebhookEvent, + processStripeWebhookEvent, + } = await import("@deepnotes/session"); + const event = parseStripeWebhookEvent({ + rawBody, + signature: c.req.header("Stripe-Signature") ?? c.req.header("stripe-signature"), + webhookSecret, + }); + await processStripeWebhookEvent({ db, event }); + return c.body(null, 200); + } catch (e) { + if (e instanceof Stripe.errors.StripeSignatureVerificationError) { + return c.json( + { code: "BAD_REQUEST", message: "Invalid Stripe webhook signature." }, + 400, + ); + } + if (e instanceof Error && e.message === "Missing Stripe-Signature header.") { + return c.json({ code: "BAD_REQUEST", message: e.message }, 400); + } + throw e; + } +}); + export default app; diff --git a/new-deepnotes/apps/api-worker/src/session-env.ts b/new-deepnotes/apps/api-worker/src/session-env.ts index 5a275f27..ee51647d 100644 --- a/new-deepnotes/apps/api-worker/src/session-env.ts +++ b/new-deepnotes/apps/api-worker/src/session-env.ts @@ -1,4 +1,4 @@ -import type { SessionEnv } from "@deepnotes/session"; +import type { SessionEnv, StripeBillingEnv } from "@deepnotes/session"; export type WorkerSessionBindings = { ACCESS_SECRET?: string; @@ -22,8 +22,44 @@ export type WorkerSessionBindings = { /** Optional; when set with token, failed-login rate limits use Upstash REST Redis. */ UPSTASH_REDIS_REST_URL?: string; UPSTASH_REDIS_REST_TOKEN?: string; + /** Stripe (`stripe` package); checkout, portal, customer hooks when set. */ + STRIPE_SECRET_KEY?: string; + /** Webhook signing secret for `POST /api/webhooks/stripe`. */ + STRIPE_WEBHOOK_SECRET?: string; + STRIPE_MONTHLY_PRICE_ID?: string; + STRIPE_YEARLY_PRICE_ID?: string; }; +export function getStripeBillingEnv( + env: WorkerSessionBindings | undefined, +): StripeBillingEnv | null { + if ( + env?.STRIPE_SECRET_KEY == null || + env.STRIPE_SECRET_KEY === "" || + env.STRIPE_MONTHLY_PRICE_ID == null || + env.STRIPE_MONTHLY_PRICE_ID === "" || + env.STRIPE_YEARLY_PRICE_ID == null || + env.STRIPE_YEARLY_PRICE_ID === "" + ) { + return null; + } + return { + STRIPE_SECRET_KEY: env.STRIPE_SECRET_KEY, + STRIPE_MONTHLY_PRICE_ID: env.STRIPE_MONTHLY_PRICE_ID, + STRIPE_YEARLY_PRICE_ID: env.STRIPE_YEARLY_PRICE_ID, + }; +} + +export function getStripeWebhookSecret( + env: WorkerSessionBindings | undefined, +): string | null { + const s = env?.STRIPE_WEBHOOK_SECRET; + if (s == null || s === "") { + return null; + } + return s; +} + export function getSessionEnv( env: WorkerSessionBindings | undefined, ): SessionEnv | null { diff --git a/new-deepnotes/docs/DEPLOY_CLOUDFLARE.md b/new-deepnotes/docs/DEPLOY_CLOUDFLARE.md index 7e51f78f..e5a615e2 100644 --- a/new-deepnotes/docs/DEPLOY_CLOUDFLARE.md +++ b/new-deepnotes/docs/DEPLOY_CLOUDFLARE.md @@ -21,7 +21,7 @@ Set via **Wrangler secrets** or dashboard (never commit): - Database: Hyperdrive handles pooling; app reads Hyperdrive binding, not raw remote URL in Worker code paths that should use the binding. - `JWT_SECRET` (or `ACCESS_SECRET` / `REFRESH_SECRET` if split to match legacy semantics) -- `STRIPE_WEBHOOK_SECRET` when billing is wired +- **Stripe (subscriptions):** `STRIPE_SECRET_KEY`, `STRIPE_MONTHLY_PRICE_ID`, `STRIPE_YEARLY_PRICE_ID` for `POST /api/billing/stripe/checkout-session` and `…/portal-session`; `STRIPE_WEBHOOK_SECRET` for `POST /api/webhooks/stripe` (raw body + `Stripe-Signature`). Optional: same `STRIPE_SECRET_KEY` powers `customers.del` / `customers.update` after account delete and email change when wired in the Worker. - `REDIS_URL` or vendor-specific vars for rate limits / sessions ### Preview (per PR / branch) diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index 5420c351..b68ede63 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -25,8 +25,8 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `users.account.twoFactorAuth.generateRecoveryCodes` | `POST /api/users/me/2fa/recovery-codes` | | `users.account.twoFactorAuth.forgetTrustedDevices` | `POST /api/users/me/2fa/devices/forget` | | `users.account.twoFactorAuth.disable` | `POST /api/users/me/2fa/disable` | -| `users.account.stripe.createCheckoutSession` | `POST /api/billing/stripe/checkout-session` | -| `users.account.stripe.createPortalSession` | `POST /api/billing/stripe/portal-session` | +| `users.account.stripe.createCheckoutSession` | `POST /api/billing/stripe/checkout-session` (**implemented** — optional body `{ "billingFrequency"?: "monthly" \| "yearly" }`; **200** `{ "checkoutSessionUrl" }`; requires verified email; `STRIPE_*` + Hyperdrive in worker) | +| `users.account.stripe.createPortalSession` | `POST /api/billing/stripe/portal-session` (**implemented** — **200** `{ "portalSessionUrl" }`; requires `users.customer_id`) | | `users.account.delete` | `DELETE /api/users/me` (JSON body `{ "loginHash" }` base64; clears cookies on 204; optional `deleteStripeCustomer` in worker when billing is wired) | | (WS) `users.account.changePassword` step 1+2 | `POST /api/users/me/password` (JSON: `oldLoginHash`, `newLoginHash`, `userEncryptedPrivateKeyring`, `userEncryptedSymmetricKeyring` as base64; same keyring semantics as `POST /api/users`; 204 + clears cookies + invalidates all sessions) | @@ -104,7 +104,7 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | Legacy | New | |--------|-----| -| Stripe webhook (Fastify) | `POST /api/webhooks/stripe` | +| Stripe webhook (Fastify) | `POST /api/webhooks/stripe` (**implemented** — raw body + `Stripe-Signature`; `customer.subscription.updated` / `customer.subscription.deleted`; maps user by `users.customer_id`) | | RevenueCat webhook | **not implemented** | Reference routers: `apps/app-server/src/trpc/router.ts`, `apps/app-server/src/trpc/api/**`, `apps/app-server/src/websocket/**`. diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index 5a0fe923..a7ff5d71 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -67,6 +67,11 @@ export { userPagesPathQuerySchema, userStartingPageResponseSchema, } from "./schemas/user-pages.js"; +export { + stripeCheckoutSessionRequestSchema, + stripeCheckoutSessionResponseSchema, + stripePortalSessionResponseSchema, +} from "./schemas/billing.js"; export { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 99fefcec..0ffe4f0d 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -123,5 +123,12 @@ describe("getOpenApiDocument", () => { expect(doc.paths?.["/api/pages/{pageId}"]?.delete).toBeDefined(); expect(doc.paths?.["/api/pages/{pageId}/restore"]?.post).toBeDefined(); expect(doc.paths?.["/api/pages/{pageId}/purge"]?.post).toBeDefined(); + expect( + doc.paths?.["/api/billing/stripe/checkout-session"]?.post, + ).toBeDefined(); + expect( + doc.paths?.["/api/billing/stripe/portal-session"]?.post, + ).toBeDefined(); + expect(doc.paths?.["/api/webhooks/stripe"]?.post).toBeDefined(); }); }); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index 929f22e0..cb64b9da 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -57,6 +57,11 @@ import { userPagesPathQuerySchema, userStartingPageResponseSchema, } from "./schemas/user-pages.js"; +import { + stripeCheckoutSessionRequestSchema, + stripeCheckoutSessionResponseSchema, + stripePortalSessionResponseSchema, +} from "./schemas/billing.js"; import { emailVerificationConfirmRequestSchema, emailVerificationResendRequestSchema, @@ -1870,6 +1875,83 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "post", + path: "/api/billing/stripe/checkout-session", + summary: "Create Stripe Checkout (subscription) session", + description: + "Replaces legacy `users.account.stripe.createCheckoutSession`. Resolves or creates a Stripe customer from `users.customer_id` and decrypted account email, then returns a hosted Checkout URL. Requires verified email. Demo accounts receive **403**.", + request: { + body: { + content: { + "application/json": { + schema: stripeCheckoutSessionRequestSchema, + }, + }, + }, + }, + responses: { + 200: { + description: "Checkout session URL (hosted Stripe page).", + content: { + "application/json": { + schema: stripeCheckoutSessionResponseSchema, + }, + }, + }, + 400: { + description: "Already subscribed, or validation error.", + content: { "application/json": { schema: sessionErrorResponseSchema } }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/billing/stripe/portal-session", + summary: "Create Stripe Customer Portal session", + description: + "Replaces legacy `users.account.stripe.createPortalSession`. Requires a `users.customer_id` (create checkout first or migrate from legacy).", + responses: { + 200: { + description: "Portal session URL.", + content: { + "application/json": { + schema: stripePortalSessionResponseSchema, + }, + }, + }, + 400: { + description: "No Stripe customer on file.", + content: { "application/json": { schema: sessionErrorResponseSchema } }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/webhooks/stripe", + summary: "Stripe webhooks (signed raw body)", + description: + "Replaces the legacy Fastify `POST /stripe/webhook` handler. Send the **raw** request body; verification uses the `Stripe-Signature` header and `STRIPE_WEBHOOK_SECRET`. Handles `customer.subscription.updated` and `customer.subscription.deleted` by updating `users.plan` and `users.subscription_id` via `users.customer_id`.", + responses: { + 200: { description: "Event acknowledged." }, + 400: { + description: "Missing or invalid signature.", + content: { "application/json": { schema: sessionErrorResponseSchema } }, + }, + 503: sessionServiceUnavailable503, + }, +}); + const generator = new OpenApiGeneratorV3(registry.definitions); export function getOpenApiDocument(): OpenAPIObject { diff --git a/new-deepnotes/packages/api/src/schemas/billing.ts b/new-deepnotes/packages/api/src/schemas/billing.ts new file mode 100644 index 00000000..a00be4b9 --- /dev/null +++ b/new-deepnotes/packages/api/src/schemas/billing.ts @@ -0,0 +1,24 @@ +import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; +import { z } from "zod"; + +extendZodWithOpenApi(z); + +export const stripeCheckoutSessionRequestSchema = z + .object({ + billingFrequency: z.enum(["monthly", "yearly"]).optional().openapi({ + description: "Defaults to `monthly` when omitted (legacy tRPC).", + }), + }) + .openapi("StripeCheckoutSessionRequest"); + +export const stripeCheckoutSessionResponseSchema = z + .object({ + checkoutSessionUrl: z.string().url().openapi({ example: "https://checkout.stripe.com/c/pay/..." }), + }) + .openapi("StripeCheckoutSessionResponse"); + +export const stripePortalSessionResponseSchema = z + .object({ + portalSessionUrl: z.string().url().openapi({ example: "https://billing.stripe.com/p/session/..." }), + }) + .openapi("StripePortalSessionResponse"); diff --git a/new-deepnotes/packages/session/package.json b/new-deepnotes/packages/session/package.json index 21a802c6..72d5c6a5 100644 --- a/new-deepnotes/packages/session/package.json +++ b/new-deepnotes/packages/session/package.json @@ -22,7 +22,8 @@ "libsodium-wrappers-sumo": "^0.8.0", "msgpackr": "^1.11.2", "nanoid": "^5.1.5", - "otplib": "^12.0.1" + "otplib": "^12.0.1", + "stripe": "17.7.0" }, "devDependencies": { "@types/crypto-js": "^4.2.2", diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index fd57b165..0297a528 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -2528,4 +2528,5 @@ describe.skipIf(resolveTemplateContext() == null)( } }); }, + 30_000, ); diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index ee89c9ce..4ef35d16 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -105,3 +105,11 @@ export { performGroupMemberRemove, performGroupMemberRoleChange, } from "./group-membership.js"; +export type { StripeBillingEnv } from "./stripe-billing.js"; +export { + findUserIdByStripeCustomerId, + parseStripeWebhookEvent, + performStripeCreateCheckoutSession, + performStripeCreatePortalSession, + processStripeWebhookEvent, +} from "./stripe-billing.js"; diff --git a/new-deepnotes/packages/session/src/stripe-billing.ts b/new-deepnotes/packages/session/src/stripe-billing.ts new file mode 100644 index 00000000..eed2b566 --- /dev/null +++ b/new-deepnotes/packages/session/src/stripe-billing.ts @@ -0,0 +1,313 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { users } from "@deepnotes/db/schema"; +import { eq } from "drizzle-orm"; +import Stripe from "stripe"; + +import { decryptUserEmail } from "./encrypt-user-email.js"; +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; + +export type StripeBillingEnv = { + STRIPE_SECRET_KEY: string; + STRIPE_MONTHLY_PRICE_ID: string; + STRIPE_YEARLY_PRICE_ID: string; +}; + +function stripeClient(secret: string): Stripe { + return new Stripe(secret); +} + +function successUrl( + requestOrigin: string | undefined, + publicAppUrl: string | undefined, +): string { + const base = + requestOrigin != null && requestOrigin.length > 0 + ? requestOrigin.replace(/\/$/, "") + : (publicAppUrl ?? "https://deepnotes.app").replace(/\/$/, ""); + return `${base}/subscribed#/subscribed`; +} + +function subscriptionCustomerId(sub: Stripe.Subscription): string { + return typeof sub.customer === "string" + ? sub.customer + : sub.customer.id; +} + +/** + * @returns user id, or `undefined` if no `users` row has this Stripe customer id. + */ +export async function findUserIdByStripeCustomerId( + db: DeepnotesDb, + customerId: string, +): Promise { + const [row] = await db + .select({ id: users.id }) + .from(users) + .where(eq(users.customerId, customerId)) + .limit(1); + return row?.id; +} + +/** + * `users.account.stripe.createCheckoutSession` (legacy tRPC) — no KeyDB: customer id + * and email live on `users` + `decryptUserEmail`. + */ +export async function performStripeCreateCheckoutSession(input: { + db: DeepnotesDb; + env: SessionEnv; + billing: StripeBillingEnv; + accessCookie: string | undefined; + requestOrigin: string | undefined; + /** Defaults to monthly when omitted (legacy). */ + billingFrequency?: "monthly" | "yearly"; +}): Promise<{ checkoutSessionUrl: string }> { + const { userId, demo, emailVerified } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + if (demo) { + throw new SessionError( + 403, + "FORBIDDEN", + "This action is not available for demo accounts.", + ); + } + if (!emailVerified) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Verify your email before subscribing.", + ); + } + + const stripe = stripeClient(input.billing.STRIPE_SECRET_KEY); + const emailExceptions = input.env.EMAIL_CASE_SENSITIVITY_EXCEPTIONS ?? ""; + + const [userRow] = await input.db + .select({ + customerId: users.customerId, + encryptedEmail: users.encryptedEmail, + }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + if (userRow == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + + const email = decryptUserEmail( + new Uint8Array(userRow.encryptedEmail), + input.env.USER_EMAIL_ENCRYPTION_KEY, + emailExceptions, + ); + + let customer: Stripe.Customer | null = null; + + if (userRow.customerId != null && userRow.customerId.length > 0) { + try { + const retrieved = await stripe.customers.retrieve(userRow.customerId, { + expand: ["subscriptions"], + }); + if (!retrieved.deleted) { + customer = retrieved; + } + } catch { + customer = null; + } + } + + if (customer == null) { + const byEmail = await stripe.customers.list({ + email, + limit: 1, + expand: ["data.subscriptions"], + }); + const found = byEmail.data[0]; + customer = found ?? (await stripe.customers.create({ email })); + + await input.db + .update(users) + .set({ customerId: customer.id }) + .where(eq(users.id, userId)); + } + + if (customer.subscriptions?.data[0] != null) { + await input.db + .update(users) + .set({ + plan: "pro", + subscriptionId: customer.subscriptions.data[0].id, + }) + .where(eq(users.id, userId)); + throw new SessionError( + 400, + "BAD_REQUEST", + "You already have an active subscription.", + ); + } + + const priceId = + input.billingFrequency === "yearly" + ? input.billing.STRIPE_YEARLY_PRICE_ID + : input.billing.STRIPE_MONTHLY_PRICE_ID; + + const session = await stripe.checkout.sessions.create({ + mode: "subscription", + line_items: [{ price: priceId, quantity: 1 }], + customer: customer.id, + success_url: successUrl(input.requestOrigin, input.env.PUBLIC_APP_URL), + }); + + if (session.url == null) { + throw new SessionError( + 500, + "SERVER_MISCONFIG", + "Failed to create checkout session.", + ); + } + return { checkoutSessionUrl: session.url }; +} + +/** `users.account.stripe.createPortalSession` */ +export async function performStripeCreatePortalSession(input: { + db: DeepnotesDb; + env: SessionEnv; + billing: StripeBillingEnv; + accessCookie: string | undefined; +}): Promise<{ portalSessionUrl: string }> { + const { userId, demo } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + if (demo) { + throw new SessionError( + 403, + "FORBIDDEN", + "This action is not available for demo accounts.", + ); + } + + const [row] = await input.db + .select({ customerId: users.customerId }) + .from(users) + .where(eq(users.id, userId)) + .limit(1); + if (row == null) { + throw new SessionError(404, "NOT_FOUND", "User not found."); + } + if (row.customerId == null || row.customerId === "") { + throw new SessionError( + 400, + "BAD_REQUEST", + "No Stripe customer on file. Start checkout first.", + ); + } + + const stripe = stripeClient(input.billing.STRIPE_SECRET_KEY); + const portalSession = await stripe.billingPortal.sessions.create({ + customer: row.customerId, + }); + if (portalSession.url == null) { + throw new SessionError( + 500, + "SERVER_MISCONFIG", + "Failed to create customer portal session.", + ); + } + return { portalSessionUrl: portalSession.url }; +} + +/** + * Applies legacy `stripe-webhook` behavior: `customer.subscription.updated` and + * `customer.subscription.deleted` using `users.customer_id` (no KeyDB `customer` hash). + */ +export async function processStripeWebhookEvent(input: { + db: DeepnotesDb; + event: Stripe.Event; +}): Promise { + switch (input.event.type) { + case "customer.subscription.updated": + await onCustomerSubscriptionUpdated(input.db, input.event); + return; + case "customer.subscription.deleted": + await onCustomerSubscriptionDeleted(input.db, input.event); + return; + default: + return; + } +} + +function prevAttrs( + event: Stripe.Event, +): Record | null | undefined { + const data = event.data as { + previous_attributes?: Record | null; + }; + return data.previous_attributes; +} + +async function onCustomerSubscriptionUpdated( + db: DeepnotesDb, + event: Stripe.Event, +): Promise { + const previousAttributes = prevAttrs(event); + if (previousAttributes != null && "cancel_at" in previousAttributes) { + return; + } + + const subscription = event.data.object as Stripe.Subscription; + + if ( + previousAttributes != null && + "status" in previousAttributes && + subscription.status === "active" + ) { + const customerId = subscriptionCustomerId(subscription); + const userId = await findUserIdByStripeCustomerId(db, customerId); + if (userId == null) { + return; + } + await db + .update(users) + .set({ plan: "pro", subscriptionId: subscription.id }) + .where(eq(users.id, userId)); + } +} + +async function onCustomerSubscriptionDeleted( + db: DeepnotesDb, + event: Stripe.Event, +): Promise { + const subscription = event.data.object as Stripe.Subscription; + if (subscription.status !== "canceled") { + return; + } + const customerId = subscriptionCustomerId(subscription); + const userId = await findUserIdByStripeCustomerId(db, customerId); + if (userId == null) { + return; + } + await db + .update(users) + .set({ plan: "basic", subscriptionId: null }) + .where(eq(users.id, userId)); +} + +export function parseStripeWebhookEvent(input: { + rawBody: string; + signature: string | undefined; + webhookSecret: string; +}): Stripe.Event { + if (input.signature == null || input.signature === "") { + throw new Error("Missing Stripe-Signature header."); + } + return Stripe.webhooks.constructEvent( + input.rawBody, + input.signature, + input.webhookSecret, + ); +} diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml index 106cd1f4..0c8d6b49 100644 --- a/new-deepnotes/pnpm-lock.yaml +++ b/new-deepnotes/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: hono: specifier: ^4.7.7 version: 4.12.15 + stripe: + specifier: ^17.7.0 + version: 17.7.0 devDependencies: '@cloudflare/workers-types': specifier: ^4.20250426.0 @@ -159,6 +162,9 @@ importers: otplib: specifier: ^12.0.1 version: 12.0.1 + stripe: + specifier: 17.7.0 + version: 17.7.0 devDependencies: '@types/crypto-js': specifier: ^4.2.2 @@ -1550,6 +1556,14 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1717,6 +1731,10 @@ packages: sqlite3: optional: true + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1742,9 +1760,21 @@ packages: error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -1867,11 +1897,22 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gel@2.2.0: resolution: {integrity: sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==} engines: {node: '>= 18.0.0'} hasBin: true + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} @@ -1888,6 +1929,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + happy-dom@17.6.3: resolution: {integrity: sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==} engines: {node: '>=20.0.0'} @@ -1896,6 +1941,14 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -2006,6 +2059,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + miniflare@4.20260424.0: resolution: {integrity: sha512-B6MKBBd5TJ19daUc3Ae9rWctn1nDA/VCXykXfCsp9fTxyfGxnZY27tJs1caxgE9MWEMMKGbGHouqVtgKbKGxmw==} engines: {node: '>=18.0.0'} @@ -2061,6 +2118,10 @@ packages: engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} hasBin: true + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + openapi3-ts@4.5.0: resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==} @@ -2137,6 +2198,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2170,6 +2235,22 @@ packages: resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} engines: {node: '>= 0.4'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2217,6 +2298,10 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + stripe@17.7.0: + resolution: {integrity: sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==} + engines: {node: '>=12.*'} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -3480,6 +3565,16 @@ snapshots: cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} chai@5.3.3: @@ -3551,6 +3646,12 @@ snapshots: gel: 2.2.0 postgres: 3.4.9 + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + eastasianwidth@0.2.0: {} editorconfig@1.0.7: @@ -3571,8 +3672,16 @@ snapshots: error-stack-parser-es@1.0.5: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + esbuild@0.18.20: optionalDependencies: '@esbuild/android-arm': 0.18.20 @@ -3797,6 +3906,8 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + gel@2.2.0: dependencies: '@petamoriken/float16': 3.9.3 @@ -3809,6 +3920,24 @@ snapshots: - supports-color optional: true + get-intrinsic@1.3.0: + 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.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -3828,6 +3957,8 @@ snapshots: globals@14.0.0: {} + gopd@1.2.0: {} + happy-dom@17.6.3: dependencies: webidl-conversions: 7.0.0 @@ -3835,6 +3966,12 @@ snapshots: has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + he@1.2.0: {} hono@4.12.15: {} @@ -3926,6 +4063,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + miniflare@4.20260424.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -3987,6 +4126,8 @@ snapshots: dependencies: abbrev: 2.0.0 + object-inspect@1.13.4: {} + openapi3-ts@4.5.0: dependencies: yaml: 2.8.3 @@ -4055,6 +4196,10 @@ snapshots: punycode@2.3.1: {} + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4132,6 +4277,34 @@ snapshots: shell-quote@1.8.3: optional: true + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} signal-exit@4.1.0: {} @@ -4175,6 +4348,11 @@ snapshots: dependencies: js-tokens: 9.0.1 + stripe@17.7.0: + dependencies: + '@types/node': 22.19.17 + qs: 6.15.1 + supports-color@10.2.2: {} supports-color@7.2.0: diff --git a/new-deepnotes/template.env b/new-deepnotes/template.env index f9872bd9..72a20646 100644 --- a/new-deepnotes/template.env +++ b/new-deepnotes/template.env @@ -26,4 +26,8 @@ REDIS_URL=redis://localhost:6380 # SEND_EMAILS=false # when unset, outbound mail is ON and registration/resend need RESEND_API_KEY # RESEND_API_KEY= # Resend.com; required if SEND_EMAILS is not false # PUBLIC_APP_URL=https://deepnotes.app # optional; verification link base -# STRIPE_WEBHOOK_SECRET= +# Stripe (subscriptions; see packages/session/src/stripe-billing.ts and api-worker routes) +# STRIPE_SECRET_KEY= # sk_live_… or sk_test_…; checkout, portal, optional customer hooks +# STRIPE_MONTHLY_PRICE_ID= # price_… +# STRIPE_YEARLY_PRICE_ID= # price_… +# STRIPE_WEBHOOK_SECRET= # whsec_… for POST /api/webhooks/stripe From b2684ad8ef34c96eb56debb33db97c5e5c4d6c1d Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 11:27:17 -0300 Subject: [PATCH 054/243] feat(new-deepnotes): page collaboration update pipeline --- new-deepnotes/PLAN_PROGRESS.md | 36 +++- .../apps/api-worker/src/index.test.ts | 2 + new-deepnotes/apps/api-worker/src/index.ts | 125 +++++++++++++ new-deepnotes/docs/TRPC_REST_MAP.md | 7 + new-deepnotes/packages/api/src/index.ts | 2 + .../packages/api/src/openapi.test.ts | 6 + new-deepnotes/packages/api/src/openapi.ts | 56 ++++++ .../packages/api/src/schemas/pages-groups.ts | 53 ++++++ .../src/account-flows.integration.test.ts | 75 ++++++++ new-deepnotes/packages/session/src/index.ts | 4 + .../session/src/page-collab-updates.ts | 171 ++++++++++++++++++ 11 files changed, 527 insertions(+), 10 deletions(-) create mode 100644 new-deepnotes/packages/session/src/page-collab-updates.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index cf083c7e..170a763d 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -13,7 +13,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **0** — OpenAPI + Drizzle inventory | **Done** | tRPC→REST/WS map: [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). Drizzle + migration `0000_legacy_baseline` match `postgres-init.sql` core tables. Auth/CORS/forks: [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md), [docs/CLIENT_FORKS.md](./docs/CLIENT_FORKS.md). | | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | -| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); [7 — page move](#pages-rest--slice-7-move--group-creation); [8 — create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation); **[slice 9 — membership + join flows](#pagesgroups-rest--slice-9-membership--join-invites--requests)**. **[Stripe / billing](#phase-3--stripe-billing--webhooks--account-hooks):** checkout, portal, signed webhooks, optional customer `del`/`update` hooks. **Still ahead:** **realtime / collab** (JWT upgrade, Yjs, no key rotation) per [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md). | +| **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); [7 — page move](#pages-rest--slice-7-move--group-creation); [8 — create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation); **[slice 9 — membership + join flows](#pagesgroups-rest--slice-9-membership--join-invites--requests)**. **[Stripe / billing](#phase-3--stripe-billing--webhooks--account-hooks).** **[Slice 10 — collab Postgres bootstrap](#pages-rest--slice-10-collab-updates-rest):** `GET`/`POST …/collab-updates`. **Still ahead:** **collab + realtime WebSockets** (JWT upgrade, binary fan-out, optional Redis buffer like legacy) per [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md). | | **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | @@ -38,7 +38,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ - [x] **Email change mailer:** `sendEmailChangeVerificationEmail` (dev skip, missing API key, Resend errors/success via mocked `fetch`). - [x] **HTTP contracts:** OpenAPI path presence; Zod for `userEmailChange*`, password change, **2FA** bodies + finish TOTP (`schemas/users.test.ts`). - [x] **Worker smoke:** `503` when env/DB not configured for `/api/users/me/email-change` (+ confirm), alongside other session routes. -- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — **24** cases when DB env set — register / email / password / **login + refresh** / **2FA** / **groups + pages** / **[slice 8 `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation)** / **[slice 9 membership + joins](#pagesgroups-rest--slice-9-membership--join-invites--requests)** / **user page prefs** / **[slice 4–5 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion)** / **[slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)** / **[slice 7 page move](#pages-rest--slice-7-move--group-creation)**. `@deepnotes/db` `template-db.test.ts` — clone + FK matrix. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). +- [x] **DB integration (template Postgres):** `account-flows.integration.test.ts` — **24** `it()` blocks when DB env set — register / email / password / **login + refresh** / **2FA** / **groups + pages** (includes [slice 10 collab GET/append](#pages-rest--slice-10-collab-updates-rest) inside the same case) / **[slice 8 `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation)** / **[slice 9 membership + joins](#pagesgroups-rest--slice-9-membership--join-invites--requests)** / **user page prefs** / **[slice 4–5 group admin](#pagesgroups-rest--slice-4-group-password-privacy-deletion)** / **[slice 6 page ops](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)** / **[slice 7 page move](#pages-rest--slice-7-move--group-creation)**. `@deepnotes/db` `template-db.test.ts` — clone + FK matrix. See [Phase 3 test coverage (detail)](#phase-3-test-coverage-detail). ### Phase 3 test coverage (detail) @@ -46,7 +46,7 @@ Integration tests use `describe.skipIf` when `DATABASE_URL` (and admin URL for ` **How to run locally:** ensure `.env` at `new-deepnotes/.env` has `DATABASE_URL` and (for template create/drop) `DATABASE_ADMIN_URL` with a role that can `CREATE DATABASE`. Then: -- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**24** cases when DB env set — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7](#pages-rest--slice-7-move--group-creation) + [slice 8](#pagesgroups-rest--slice-8-create--groupcreation) + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests)) +- `pnpm --filter @deepnotes/session exec vitest run src/account-flows.integration.test.ts` (**24** `it()` blocks when DB env set — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7](#pages-rest--slice-7-move--group-creation) + [slice 8](#pagesgroups-rest--slice-8-create--groupcreation) + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests) + [slice 10 collab](#pages-rest--slice-10-collab-updates-rest) bundled in **groups + pages**) - `pnpm --filter @deepnotes/db exec vitest run src/template-db.test.ts` CI should set the same vars against the workflow Postgres service (role with `CREATEDB`). @@ -73,7 +73,7 @@ CI should set the same vars against the workflow Postgres service (role with `CR | **2FA login, bad TOTP** | `authenticatorToken: "111111"` | **401** “Invalid authenticator token.” | | **2FA login with recovery code** | `performUserTwoFactorEnableFinish` → `performSessionLogin` with `recoveryCode` (no TOTP) | **200**-equivalent (`sessionId`); `decryptRecoveryCodes` on row shows **5** hashes left (one consumed). | | **2FA recovery code reuse** | Second `performSessionLogin` with same plaintext recovery code, new IP/UA | **401** “Invalid recovery code.” | -| **Groups + pages (personal)** | `performUserRegister` then `performGetUserGroupIds` / `performListGroupPages` / `performGetGroupMainPageId` / `performGetGroupMemberUserIds` / `performCreatePage` (second page, `parentPageId` = initial page) | `groupIds` = `[personalGroupId]`; list returns initial `pageId`; **main page** = `reg.pageId` (matches `groups.main_page_id`); **members** = `[reg.userId]` (sole `group_members` row); create returns `numFreePages` **1** for default `plan`; `users.num_free_pages` = 1; two rows in `pages` for group; unknown `groupId` list → **404** `NOT_FOUND`. | +| **Groups + pages (personal)** | `performUserRegister` then `performGetUserGroupIds` / `performListGroupPages` / `performGetGroupMainPageId` / `performGetGroupMemberUserIds` / `performCreatePage` (second page, `parentPageId` = initial page); **[slice 10](#pages-rest--slice-10-collab-updates-rest)** `performGetPageCollabUpdates` + `performAppendPageCollabUpdates` on initial `reg.pageId` | `groupIds` = `[personalGroupId]`; list returns initial `pageId`; **main page** = `reg.pageId` (matches `groups.main_page_id`); **members** = `[reg.userId]` (sole `group_members` row); create returns `numFreePages` **1** for default `plan`; `users.num_free_pages` = 1; two rows in `pages` for group; unknown `groupId` list → **404** `NOT_FOUND`. **Collab:** empty GET → append index `0` + `1` → GET has `lastIndex` **1**; stale `expectedLastIndex` → **409** `CONFLICT`; gap in indices → **400**. | | **User page prefs** | Register + access JWT; `performGetStartingPageId`; `performGetCurrentPath` (root page + child); unknown page **404**; `performAddFavoritePages` / `performRemoveFavoritePages` (order on `users.favorite_page_ids`); `performRemoveRecentPages` bogus id **404** / missing child in recent **404** / remove root then `recent` empty + `performClearRecentPages`; `performPatchDefaultNote`; insert `notifications` + `users_notifications` → `performLoadNotifications` (base64 ciphertext) + `performMarkNotificationsRead` → `users.last_notification_read` | Matches legacy semantics where tested; favorites column from migration `0001_favorite_page_ids`. | | **Group password, privacy, deletion (slice 4)** | `UPDATE users` → `plan = pro` on registered user; `performGroupPasswordEnable` / `Change` / `Disable` on **personal** group; `performGroupPrivacyMakePublic` after `accessKeyring` cleared; `performGroupPrivacySetJoinRequestsAllowed` **false**; `performGroupSoftDelete` (future `permanent_deletion_date`) → `Restore` (null) → `SoftDelete` + `Purge` (past date); `Restore` after purge **400** | PHC + server-side encrypt via `GROUP_REHASHED_PASSWORD_HASH_ENCRYPTION_KEY`; `group-permissions` includes **`editGroupSettings`** (owner/admin only). | | **Privacy make-private (slice 5)** | Register with **public** personal group (`access_keyring` set); Pro; `performGroupPrivacyMakePrivate` with full re-key payload (single member, empty invites/requests, one page); assert `access_keyring` **null** + page `encrypted_symmetric_keyring` updated; second call **400** “already private”; re-public in DB then payload with **extra** page id → **400** keyset mismatch | Mirrors legacy `groupKeyRotationSchema` key sets; **no** `next_key_rotation_date` writes (RESTART_PLAN). | @@ -260,6 +260,20 @@ CI should set the same vars against the workflow Postgres service (role with `CR | `PATCH /api/groups/{groupId}/members/{userId}` | `role` change | | `DELETE /api/groups/{groupId}/members/{userId}` | Remove member or **leave** (self) | +### Pages REST — slice 10 (collab updates REST) + +**Goal:** Persist and load encrypted Yjs update blobs in **`page_updates`** over HTTP so the SPA can hydrate the editor **without** legacy collab-server or Redis. Matches legacy DB shape used after flush from Redis; **does not** replace live multi-user collab (WebSocket track still required for low-latency sync). + +| Layer | What shipped | +|-------|----------------| +| **`@deepnotes/session`** | [page-collab-updates.ts](packages/session/src/page-collab-updates.ts) — `performGetPageCollabUpdates` (`viewGroupPages`, ordered by `index`); `performAppendPageCollabUpdates` (`editGroupPages`, transaction + `max(index)` check, contiguous indices, **409** on stale `expectedLastIndex`) | +| **`@deepnotes/api`** | `pageCollabUpdatesGetResponseSchema`, `pageCollabUpdatesAppendRequestSchema` in [schemas/pages-groups.ts](packages/api/src/schemas/pages-groups.ts); OpenAPI `GET` + `POST` `/api/pages/{pageId}/collab-updates` | +| **`@deepnotes/api-worker`** | Hono handlers; JSON `encryptedData` as base64 | +| **Tests** | [account-flows.integration.test.ts](packages/session/src/account-flows.integration.test.ts) extends **groups + pages** case; [openapi.test.ts](packages/api/src/openapi.test.ts); worker 503 matrix **70** routes | +| **Docs** | [TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md) — **Collab bootstrap** table | + +**Client contract:** `GET` returns `{ lastIndex, updates: [{ index, encryptedData }] }` (`lastIndex` **null** if no rows). `POST` body `{ expectedLastIndex, updates: [{ index, encryptedData }] }` — first row must be index **0** when `expectedLastIndex` is **null**; subsequent appends must use contiguous indices. **Intentional:** no Redis cache invalidation (RESTART_PLAN); mixed legacy collab + new REST writers on the same page can diverge until cutover. + ### Phase 3 — Stripe (billing + webhooks + account hooks) Replaces legacy Fastify `/stripe/webhook` and tRPC `users.account.stripe.*` using **Postgres** `users.customer_id` (no KeyDB `customer` → `user-id` hash). @@ -270,15 +284,15 @@ Replaces legacy Fastify `/stripe/webhook` and tRPC `users.account.stripe.*` usin | **`@deepnotes/api`** | [schemas/billing.ts](packages/api/src/schemas/billing.ts); OpenAPI paths registered in [openapi.ts](packages/api/src/openapi.ts). | | **`@deepnotes/api-worker`** | `getStripeBillingEnv` / `getStripeWebhookSecret` in [session-env.ts](apps/api-worker/src/session-env.ts); `POST /api/billing/stripe/checkout-session` (optional JSON body), `POST /api/billing/stripe/portal-session`, `POST /api/webhooks/stripe` (raw body, `Stripe-Signature`); when `STRIPE_SECRET_KEY` is set: **`deleteStripeCustomer`** on `DELETE /api/users/me`, **`updateStripeCustomerEmail`** on `POST /api/users/me/email-change/confirm`. | | **Config** | [template.env](template.env) — `STRIPE_SECRET_KEY`, `STRIPE_MONTHLY_PRICE_ID`, `STRIPE_YEARLY_PRICE_ID`, `STRIPE_WEBHOOK_SECRET`; [DEPLOY_CLOUDFLARE.md](docs/DEPLOY_CLOUDFLARE.md). | -| **Tests** | Vitest 503 matrix includes the three new routes (**68** total); no live Stripe calls in CI yet. | +| **Tests** | Vitest 503 matrix **70** routes total (includes Stripe **+** [slice 10 `collab-updates`](#pages-rest--slice-10-collab-updates-rest)); no live Stripe calls in CI yet. | **Gaps (optional follow-ups):** Integration tests with Stripe test clock / fixtures; `checkout.session.completed` if product needs it beyond `subscription.updated`. -### Not started (Phase 3 — realtime / collab only) +### Not started (Phase 3 — realtime / collab WebSocket) -Sprints **1–9** (pages / groups / membership) and **Stripe** are tracked in the sections above. Remaining work: +Sprints **1–9** (pages / groups / membership), **Stripe**, and **[slice 10 — Postgres `page_updates` REST](#pages-rest--slice-10-collab-updates-rest)** are tracked above. Remaining work: -- [ ] **Realtime / collab** — new or adapted WebSocket protocols; **no** key rotation; Durable Object vs separate service per [RESTART_PLAN](../docs/RESTART_PLAN.md) §4.3 / hosting table; blocks editor MVP collab in Phase 4. +- [ ] **Collab WebSocket + realtime** — JWT cookie upgrade; binary protocol (legacy: lib0 + `@deeplib/misc` message kinds); **no** `next_key_rotation_date` / scheduled re-key; optional **Redis** `page-update-cache` / buffer like `@deeplib/data` `getAllPageUpdates` for parity; **Durable Object** vs separate Node service per [RESTART_PLAN](../docs/RESTART_PLAN.md) §4.3 / hosting table. REST [slice 10](#pages-rest--slice-10-collab-updates-rest) covers bootstrap + offline-style append only. --- @@ -329,7 +343,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | **`@deepnotes/db`** | Drizzle + migrations | `template-db.test.ts` (6 cases): clone template, empty `users`, **FK** rejects for orphan `sessions`, **`devices`→`users`**, **`pages`→`groups`**, **`group_members`→`users`**, **`group_members`→`groups`** | More paths when groups CRUD lands (join invites/requests, cascades from `groups` delete) | | **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**24** cases when DB env set) — … + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 move](#pages-rest--slice-7-move--group-creation) + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; optional: invitation **reject/cancel**, join-request **reject/cancel**, **private** group invite/request **access keyring** branches | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (session routes + [slice 6/7 `/api/pages/...` paths](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)); **`schemas/users.test.ts`**; **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; more Zod edge cases for new page schemas | -| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **68** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + `/api/pages/{pageId}/move` + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests) + [Stripe routes](#phase-3--stripe-billing--webhooks--account-hooks) (`/api/billing/stripe/*`, `/api/webhooks/stripe`) | **200** tests with stub `SessionEnv` + template DB (heavier) | +| **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **70** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + `/api/pages/{pageId}/move` + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests) + [slice 10 `…/collab-updates`](#pages-rest--slice-10-collab-updates-rest) (`GET` + `POST`) + [Stripe routes](#phase-3--stripe-billing--webhooks--account-hooks) (`/api/billing/stripe/*`, `/api/webhooks/stripe`) | **200** tests with stub `SessionEnv` + template DB (heavier) | | **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -365,7 +379,8 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Order | Item | Rationale / notes | |-------|------|----------------------| | ✅ | Slices 1–9 (through [membership + joins](#pagesgroups-rest--slice-9-membership--join-invites--requests)) | Account, pages/groups CRUD, move, create-with-new-group, invitations/requests/member role/remove. | -| **1** | **Realtime** (JWT upgrade, msgpackr-style protocol) + **collab** (Yjs, no rotation) | [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md); load-test before freeze; only major Phase 3 item left. | +| ✅ | **[Slice 10](#pages-rest--slice-10-collab-updates-rest)** — `page_updates` **GET** + append **POST** | Editor bootstrap + optimistic append on Postgres; no WS yet. | +| **1** | **Collab WebSocket + realtime** (JWT upgrade; Yjs/binary fan-out; no key rotation) | [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md); optional Redis buffer parity; DO vs separate service; load-test before freeze. | | ✅ | **Stripe** — checkout, portal, webhooks, account hooks | [Phase 3 — Stripe](#phase-3--stripe-billing--webhooks--account-hooks); secrets in [template.env](./template.env) and [DEPLOY_CLOUDFLARE](docs/DEPLOY_CLOUDFLARE.md). | --- @@ -374,6 +389,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-27 | **Phase 3 — slice 10 (collab Postgres REST):** [page-collab-updates.ts](packages/session/src/page-collab-updates.ts) — `performGetPageCollabUpdates` / `performAppendPageCollabUpdates`; `GET|POST /api/pages/:pageId/collab-updates`; OpenAPI + Zod; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) collab bootstrap table; integration extends **groups + pages**; api-worker 503 matrix **70**. **Next:** collab + realtime **WebSocket** only. | | 2026-04-27 | **Phase 3 — Stripe (billing):** [stripe-billing.ts](packages/session/src/stripe-billing.ts) — `performStripeCreateCheckoutSession` / `performStripeCreatePortalSession`, `processStripeWebhookEvent` (legacy `customer.subscription.updated` / `deleted` → `users.plan` + `subscription_id` via `users.customer_id`); [schemas/billing.ts](packages/api/src/schemas/billing.ts) + OpenAPI; Worker `POST /api/billing/stripe/checkout-session`, `…/portal-session`, `POST /api/webhooks/stripe` ([session-env](apps/api-worker/src/session-env.ts) `getStripeBillingEnv` / `getStripeWebhookSecret`); **`STRIPE_SECRET_KEY`** hooks: `deleteStripeCustomer` on `DELETE /api/users/me`, `updateStripeCustomerEmail` on email-change confirm. Dependencies: `stripe@^17.7` in session + api-worker. [TRPC_REST_MAP](docs/TRPC_REST_MAP.md); [template.env](template.env). Api-worker 503 matrix **68** tests. **Next:** [realtime + collab](#phase-3-working-order-suggested) only. | | 2026-04-27 | **Phase 3 — slice 9 (membership + join flows):** [group-role-ranks.ts](packages/session/src/group-role-ranks.ts) (`canManageRole` / `canChangeRole` / `manageLowerRanks` parity); [group-membership.ts](packages/session/src/group-membership.ts) — invitations send/accept/reject/cancel, join requests send/accept/reject/cancel, `PATCH`/`DELETE` members; Zod + OpenAPI + Hono; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) WS table; **`byteB64`** passthrough in worker (decoded `Uint8Array`). `account-flows` **24** cases; api-worker 503 matrix **65**. [Slice 9 section](#pagesgroups-rest--slice-9-membership--join-invites--requests). **Next:** [realtime + collab](#phase-3-working-order-suggested). | | 2026-04-27 | **Phase 3 — slice 8 (create + `groupCreation`):** [group-creation-shared.ts](packages/session/src/group-creation-shared.ts) + `performCreatePage` with optional `groupCreation` (Pro; path `groupId` = new id; `parentPageId` in personal group). `GroupPageCreateRequest` + OpenAPI; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) `pages.create`; `account-flows` **23** cases. [Slice 8 section](#pagesgroups-rest--slice-8-create--groupcreation); [working order](#phase-3-working-order-suggested) next = **group join + membership REST**. | diff --git a/new-deepnotes/apps/api-worker/src/index.test.ts b/new-deepnotes/apps/api-worker/src/index.test.ts index e4ff60f6..3f19f9c1 100644 --- a/new-deepnotes/apps/api-worker/src/index.test.ts +++ b/new-deepnotes/apps/api-worker/src/index.test.ts @@ -127,6 +127,8 @@ describe("api-worker", () => { ], ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/move"], ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/bump"], + ["GET", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/collab-updates"], + ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/collab-updates"], ["POST", "/api/pages/aaaaaaaaaaaaaaaaaaaaa/backlinks"], [ "DELETE", diff --git a/new-deepnotes/apps/api-worker/src/index.ts b/new-deepnotes/apps/api-worker/src/index.ts index 337bdc3e..e82d3164 100644 --- a/new-deepnotes/apps/api-worker/src/index.ts +++ b/new-deepnotes/apps/api-worker/src/index.ts @@ -6,6 +6,7 @@ import { groupPagesListQuerySchema, pageBacklinkCreateRequestSchema, pageBumpRequestSchema, + pageCollabUpdatesAppendRequestSchema, pageMoveRequestSchema, pageIdPathSchema, pageSnapshotCreateResponseSchema, @@ -1577,6 +1578,130 @@ app.post("/api/pages/:pageId/bump", async (c) => { } }); +app.get("/api/pages/:pageId/collab-updates", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + const pParams = pageIdPathSchema.safeParse({ pageId: c.req.param("pageId") }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performGetPageCollabUpdates } = await import("@deepnotes/session"); + const out = await performGetPageCollabUpdates({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageId: pParams.data.pageId, + }); + return c.json( + { + lastIndex: out.lastIndex, + updates: out.updates.map((u) => ({ + index: u.index, + encryptedData: u.encryptedData.toString("base64"), + })), + }, + 200, + ); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + +app.post("/api/pages/:pageId/collab-updates", async (c) => { + const sessionEnv = getSessionEnv(c.env); + if (sessionEnv == null) { + return c.json(serviceUnavailableBody, 503); + } + const hyper = c.env.HYPERDRIVE; + if (hyper == null) { + return c.json( + { + code: "SERVICE_UNAVAILABLE" as const, + message: "HYPERDRIVE binding is not configured.", + }, + 503, + ); + } + + let bodyJson: unknown; + try { + bodyJson = await c.req.json(); + } catch { + return c.json({ code: "BAD_REQUEST", message: "Expected JSON body." }, 400); + } + + const pParams = pageIdPathSchema.safeParse({ pageId: c.req.param("pageId") }); + if (!pParams.success) { + return c.json( + { code: "VALIDATION_ERROR", message: pParams.error.message }, + 400, + ); + } + const parsed = pageCollabUpdatesAppendRequestSchema.safeParse(bodyJson); + if (!parsed.success) { + return c.json( + { + code: "VALIDATION_ERROR", + message: parsed.error.flatten().formErrors.join("; "), + }, + 400, + ); + } + + const db = getDbForConnectionString(hyper.connectionString); + const cookieHeader = c.req.header("Cookie"); + + try { + const { performAppendPageCollabUpdates } = await import("@deepnotes/session"); + await performAppendPageCollabUpdates({ + db, + env: sessionEnv, + accessCookie: readCookieHeader(cookieHeader, "accessToken"), + pageId: pParams.data.pageId, + expectedLastIndex: parsed.data.expectedLastIndex, + updates: parsed.data.updates, + }); + return c.body(null, 204); + } catch (e) { + const { SessionError } = await import("@deepnotes/session"); + if (e instanceof SessionError) { + return c.json( + { code: e.code, message: e.message }, + e.status as ContentfulStatusCode, + ); + } + throw e; + } +}); + app.post("/api/pages/:pageId/backlinks", async (c) => { const sessionEnv = getSessionEnv(c.env); if (sessionEnv == null) { diff --git a/new-deepnotes/docs/TRPC_REST_MAP.md b/new-deepnotes/docs/TRPC_REST_MAP.md index b68ede63..c252d9f3 100644 --- a/new-deepnotes/docs/TRPC_REST_MAP.md +++ b/new-deepnotes/docs/TRPC_REST_MAP.md @@ -79,6 +79,13 @@ Working checklist for Phase 0 of [docs/RESTART_PLAN.md](../../docs/RESTART_PLAN. | `pages.deletion.restore` | `POST /api/pages/:pageId/restore` (**implemented** — `performPageRestore`) | | `pages.deletion.deletePermanently` | `POST /api/pages/:pageId/purge` (**implemented** — `performPagePurge`; `num_free_pages` +1 when `pages.free` and user not Pro) | +### Collab bootstrap (new; not legacy tRPC) + +| Capability | New surface | Notes | +|------------|-------------|--------| +| Load encrypted Yjs update chain from DB | `GET /api/pages/:pageId/collab-updates` (**implemented** — `performGetPageCollabUpdates`; `viewGroupPages`; Postgres `page_updates` only, no Redis cache) | Replaces initial `ALL_UPDATES_UNMERGED`-style payload for SPA bootstrap; binary collab WebSocket is still [Phase 3 — realtime/collab](../PLAN_PROGRESS.md#not-started-phase-3--realtime--collab-only). | +| Append updates (optimistic concurrency) | `POST /api/pages/:pageId/collab-updates` (**implemented** — `performAppendPageCollabUpdates`; `editGroupPages`; body `expectedLastIndex` + `updates[]`) | **409** when `expectedLastIndex` is stale. | + ## Legacy app-server WebSocket → target | Legacy handler | New surface | Notes | diff --git a/new-deepnotes/packages/api/src/index.ts b/new-deepnotes/packages/api/src/index.ts index a7ff5d71..ecef53f9 100644 --- a/new-deepnotes/packages/api/src/index.ts +++ b/new-deepnotes/packages/api/src/index.ts @@ -46,6 +46,8 @@ export { groupUserIdPathSchema, pageBacklinkCreateRequestSchema, pageBumpRequestSchema, + pageCollabUpdatesAppendRequestSchema, + pageCollabUpdatesGetResponseSchema, pageMoveRequestSchema, pageIdPathSchema, pageSnapshotCreateResponseSchema, diff --git a/new-deepnotes/packages/api/src/openapi.test.ts b/new-deepnotes/packages/api/src/openapi.test.ts index 0ffe4f0d..3b6275d5 100644 --- a/new-deepnotes/packages/api/src/openapi.test.ts +++ b/new-deepnotes/packages/api/src/openapi.test.ts @@ -105,6 +105,12 @@ describe("getOpenApiDocument", () => { ).toBeDefined(); expect(doc.paths?.["/api/pages/{pageId}/move"]?.post).toBeDefined(); expect(doc.paths?.["/api/pages/{pageId}/bump"]?.post).toBeDefined(); + expect( + doc.paths?.["/api/pages/{pageId}/collab-updates"]?.get, + ).toBeDefined(); + expect( + doc.paths?.["/api/pages/{pageId}/collab-updates"]?.post, + ).toBeDefined(); expect( doc.paths?.["/api/pages/{pageId}/backlinks"]?.post, ).toBeDefined(); diff --git a/new-deepnotes/packages/api/src/openapi.ts b/new-deepnotes/packages/api/src/openapi.ts index cb64b9da..23fae844 100644 --- a/new-deepnotes/packages/api/src/openapi.ts +++ b/new-deepnotes/packages/api/src/openapi.ts @@ -38,6 +38,8 @@ import { groupUserIdPathSchema, pageBacklinkCreateRequestSchema, pageBumpRequestSchema, + pageCollabUpdatesAppendRequestSchema, + pageCollabUpdatesGetResponseSchema, pageMoveRequestSchema, pageIdPathSchema, pageSnapshotCreateResponseSchema, @@ -1202,6 +1204,60 @@ registry.registerPath({ }, }); +registry.registerPath({ + method: "get", + path: "/api/pages/{pageId}/collab-updates", + summary: "List encrypted Yjs page updates (Postgres)", + description: + "Bootstrap for the editor: returns all `page_updates` rows for the page, ordered by `index`. Does not use legacy Redis collab cache — Postgres only. Full duplex collab remains a separate WebSocket track (Phase 3).", + request: { params: pageIdPathSchema }, + responses: { + 200: { + description: "Current ciphertext chain.", + content: { + "application/json": { schema: pageCollabUpdatesGetResponseSchema }, + }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 503: sessionServiceUnavailable503, + }, +}); + +registry.registerPath({ + method: "post", + path: "/api/pages/{pageId}/collab-updates", + summary: "Append page updates (optimistic concurrency)", + description: + "Appends ciphertext rows to `page_updates`. `expectedLastIndex` must match the current max index (or null when empty). **409** when another writer advanced the chain — client should re-GET and retry.", + request: { + params: pageIdPathSchema, + body: { + content: { + "application/json": { + schema: pageCollabUpdatesAppendRequestSchema, + }, + }, + }, + }, + responses: { + 204: { description: "Updates persisted." }, + 400: { + description: "Bad index sequence or wrong `expectedLastIndex` for an empty page.", + content: { "application/json": { schema: sessionErrorResponseSchema } }, + }, + 401: sessionUnauthorized401, + 403: sessionForbidden403, + 404: sessionNotFound404, + 409: { + description: "Stale `expectedLastIndex` (concurrent append).", + content: { "application/json": { schema: sessionErrorResponseSchema } }, + }, + 503: sessionServiceUnavailable503, + }, +}); + registry.registerPath({ method: "post", path: "/api/pages/{pageId}/backlinks", diff --git a/new-deepnotes/packages/api/src/schemas/pages-groups.ts b/new-deepnotes/packages/api/src/schemas/pages-groups.ts index 0b8f093e..c2395f8f 100644 --- a/new-deepnotes/packages/api/src/schemas/pages-groups.ts +++ b/new-deepnotes/packages/api/src/schemas/pages-groups.ts @@ -350,3 +350,56 @@ export const pageSnapshotLoadResponseSchema = z .openapi({ format: "byte", description: "Base64 ciphertext." }), }) .openapi("PageSnapshotLoadResponse"); + +const pageCollabUpdateItemInputSchema = z + .object({ + index: z + .number() + .int() + .nonnegative() + .openapi({ + description: + "Monotonic index (legacy collab / `page_updates.index`, often Yjs clock).", + }), + encryptedData: byteB64, + }) + .openapi("PageCollabUpdateItemInput"); + +export const pageCollabUpdatesAppendRequestSchema = z + .object({ + expectedLastIndex: z + .number() + .int() + .nonnegative() + .nullable() + .openapi({ + description: + "Must match `lastIndex` from GET (`null` when the page has no updates yet).", + }), + updates: z.array(pageCollabUpdateItemInputSchema).min(1), + }) + .openapi("PageCollabUpdatesAppendRequest"); + +export const pageCollabUpdatesGetResponseSchema = z + .object({ + lastIndex: z + .number() + .int() + .nonnegative() + .nullable() + .openapi({ + description: "Max `index` in the database, or null if there are no rows.", + }), + updates: z.array( + z.object({ + index: z.number().int().nonnegative(), + encryptedData: z + .string() + .openapi({ + format: "byte", + description: "Base64 ciphertext (`page_updates.encrypted_data`).", + }), + }), + ), + }) + .openapi("PageCollabUpdatesGetResponse"); diff --git a/new-deepnotes/packages/session/src/account-flows.integration.test.ts b/new-deepnotes/packages/session/src/account-flows.integration.test.ts index 0297a528..3e943050 100644 --- a/new-deepnotes/packages/session/src/account-flows.integration.test.ts +++ b/new-deepnotes/packages/session/src/account-flows.integration.test.ts @@ -88,6 +88,8 @@ import { performPageSnapshotLoad, performPageSnapshotSave, performPageSoftDelete, + performAppendPageCollabUpdates, + performGetPageCollabUpdates, } from "./index.js"; import { performCreatePage, @@ -1374,6 +1376,79 @@ describe.skipIf(resolveTemplateContext() == null)( groupId: "nononononononononono1", }), ).rejects.toMatchObject({ status: 404, code: "NOT_FOUND" }); + + const empty = await performGetPageCollabUpdates({ + db, + env, + accessCookie: access, + pageId: reg.pageId, + }); + expect(empty.lastIndex).toBeNull(); + expect(empty.updates).toEqual([]); + + const b0 = rand32(); + await performAppendPageCollabUpdates({ + db, + env, + accessCookie: access, + pageId: reg.pageId, + expectedLastIndex: null, + updates: [{ index: 0, encryptedData: b0 }], + }); + + const one = await performGetPageCollabUpdates({ + db, + env, + accessCookie: access, + pageId: reg.pageId, + }); + expect(one.lastIndex).toBe(0); + expect(one.updates).toHaveLength(1); + expect(one.updates[0]!.index).toBe(0); + expect(one.updates[0]!.encryptedData.equals(Buffer.from(b0))).toBe( + true, + ); + + const b1 = rand32(); + await performAppendPageCollabUpdates({ + db, + env, + accessCookie: access, + pageId: reg.pageId, + expectedLastIndex: 0, + updates: [{ index: 1, encryptedData: b1 }], + }); + + const two = await performGetPageCollabUpdates({ + db, + env, + accessCookie: access, + pageId: reg.pageId, + }); + expect(two.lastIndex).toBe(1); + expect(two.updates).toHaveLength(2); + + await expect( + performAppendPageCollabUpdates({ + db, + env, + accessCookie: access, + pageId: reg.pageId, + expectedLastIndex: 0, + updates: [{ index: 2, encryptedData: rand32() }], + }), + ).rejects.toMatchObject({ status: 409, code: "CONFLICT" }); + + await expect( + performAppendPageCollabUpdates({ + db, + env, + accessCookie: access, + pageId: reg.pageId, + expectedLastIndex: 1, + updates: [{ index: 3, encryptedData: rand32() }], + }), + ).rejects.toMatchObject({ status: 400, code: "BAD_REQUEST" }); } finally { await client.end({ timeout: 5 }); const admin2 = postgres(ctx.adminUrl, { max: 1 }); diff --git a/new-deepnotes/packages/session/src/index.ts b/new-deepnotes/packages/session/src/index.ts index 4ef35d16..0fc16dfc 100644 --- a/new-deepnotes/packages/session/src/index.ts +++ b/new-deepnotes/packages/session/src/index.ts @@ -85,6 +85,10 @@ export { performPageSnapshotSave, performPageSoftDelete, } from "./page-operations.js"; +export { + performAppendPageCollabUpdates, + performGetPageCollabUpdates, +} from "./page-collab-updates.js"; export { performPageMove, } from "./page-move.js"; diff --git a/new-deepnotes/packages/session/src/page-collab-updates.ts b/new-deepnotes/packages/session/src/page-collab-updates.ts new file mode 100644 index 00000000..f2e2dd7e --- /dev/null +++ b/new-deepnotes/packages/session/src/page-collab-updates.ts @@ -0,0 +1,171 @@ +import type { DeepnotesDb } from "@deepnotes/db/client"; +import { pageUpdates, pages } from "@deepnotes/db/schema"; +import { and, asc, eq, isNull, max } from "drizzle-orm"; + +import type { SessionEnv } from "./env.js"; +import { SessionError } from "./errors.js"; +import { userHasGroupPermission } from "./group-permissions.js"; +import { getAuthenticatedUserSummary } from "./user-me.js"; + +function toBuf(u: Uint8Array): Buffer { + return Buffer.from(u); +} + +/** + * Load encrypted Yjs page updates from Postgres (legacy `page_updates`), ordered by `index`. + * Complements future collab WebSocket: REST bootstrap without Redis cache. + */ +export async function performGetPageCollabUpdates(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageId: string; +}): Promise<{ + lastIndex: number | null; + updates: { index: number; encryptedData: Buffer }[]; +}> { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + + const [pageRow] = await input.db + .select({ id: pages.id, groupId: pages.groupId }) + .from(pages) + .where( + and(eq(pages.id, input.pageId), isNull(pages.permanentDeletionDate)), + ) + .limit(1); + + if (pageRow == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + + const canView = await userHasGroupPermission({ + db: input.db, + userId, + groupId: pageRow.groupId, + permission: "viewGroupPages", + }); + if (!canView) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const rows = await input.db + .select({ + index: pageUpdates.index, + encryptedData: pageUpdates.encryptedData, + }) + .from(pageUpdates) + .where(eq(pageUpdates.pageId, input.pageId)) + .orderBy(asc(pageUpdates.index)); + + if (rows.length === 0) { + return { lastIndex: null, updates: [] }; + } + + const lastIndex = rows[rows.length - 1]!.index; + return { + lastIndex, + updates: rows.map((r) => ({ + index: r.index, + encryptedData: Buffer.from(r.encryptedData), + })), + }; +} + +/** + * Append new `page_updates` rows with optimistic concurrency on the last index. + */ +export async function performAppendPageCollabUpdates(input: { + db: DeepnotesDb; + env: SessionEnv; + accessCookie: string | undefined; + pageId: string; + expectedLastIndex: number | null; + updates: { index: number; encryptedData: Uint8Array }[]; +}): Promise { + const { userId } = await getAuthenticatedUserSummary({ + db: input.db, + env: input.env, + accessCookie: input.accessCookie, + }); + + const [pageRow] = await input.db + .select({ id: pages.id, groupId: pages.groupId }) + .from(pages) + .where( + and(eq(pages.id, input.pageId), isNull(pages.permanentDeletionDate)), + ) + .limit(1); + + if (pageRow == null) { + throw new SessionError(404, "NOT_FOUND", "Page not found."); + } + + const canEdit = await userHasGroupPermission({ + db: input.db, + userId, + groupId: pageRow.groupId, + permission: "editGroupPages", + }); + if (!canEdit) { + throw new SessionError(403, "FORBIDDEN", "Insufficient permissions."); + } + + const sorted = [...input.updates].sort((a, b) => a.index - b.index); + if (sorted.length === 0) { + throw new SessionError(400, "BAD_REQUEST", "No updates to append."); + } + for (let i = 1; i < sorted.length; i++) { + if (sorted[i]!.index === sorted[i - 1]!.index) { + throw new SessionError(400, "BAD_REQUEST", "Duplicate update index."); + } + } + + await input.db.transaction(async (tx) => { + const [agg] = await tx + .select({ m: max(pageUpdates.index) }) + .from(pageUpdates) + .where(eq(pageUpdates.pageId, input.pageId)); + + const actualLast: number | null = agg?.m ?? null; + + if (actualLast === null) { + if (input.expectedLastIndex !== null) { + throw new SessionError( + 400, + "BAD_REQUEST", + "expectedLastIndex must be null when there are no updates.", + ); + } + } else if (input.expectedLastIndex !== actualLast) { + throw new SessionError( + 409, + "CONFLICT", + "Page updates were modified by another client.", + ); + } + + const start = actualLast === null ? -1 : actualLast; + for (let i = 0; i < sorted.length; i++) { + const expectedIdx = start + 1 + i; + if (sorted[i]!.index !== expectedIdx) { + throw new SessionError( + 400, + "BAD_REQUEST", + "Update indices must be contiguous after the current last index.", + ); + } + } + + await tx.insert(pageUpdates).values( + sorted.map((u) => ({ + pageId: input.pageId, + index: u.index, + encryptedData: toBuf(u.encryptedData), + })), + ); + }); +} From 4aa8c6ada34502683edd4ad6b37de0b39a165319 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 11:31:47 -0300 Subject: [PATCH 055/243] feat(new-deepnotes): web OpenAPI client and generated API types --- new-deepnotes/PLAN_PROGRESS.md | 34 +- new-deepnotes/apps/web/eslint.config.js | 7 +- new-deepnotes/apps/web/package.json | 4 + .../apps/web/scripts/generate-api-types.mts | 20 + .../apps/web/src/api/api-types.generated.ts | 6018 ++++++++++++++++ new-deepnotes/apps/web/src/api/client.test.ts | 28 + new-deepnotes/apps/web/src/api/client.ts | 23 + new-deepnotes/apps/web/src/api/index.ts | 6 + new-deepnotes/apps/web/src/api/openapi.json | 6253 +++++++++++++++++ new-deepnotes/apps/web/src/vite-env.d.ts | 9 + new-deepnotes/pnpm-lock.yaml | 211 +- 11 files changed, 12595 insertions(+), 18 deletions(-) create mode 100644 new-deepnotes/apps/web/scripts/generate-api-types.mts create mode 100644 new-deepnotes/apps/web/src/api/api-types.generated.ts create mode 100644 new-deepnotes/apps/web/src/api/client.test.ts create mode 100644 new-deepnotes/apps/web/src/api/client.ts create mode 100644 new-deepnotes/apps/web/src/api/index.ts create mode 100644 new-deepnotes/apps/web/src/api/openapi.json diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 170a763d..402f07ef 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -14,7 +14,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | | **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); [7 — page move](#pages-rest--slice-7-move--group-creation); [8 — create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation); **[slice 9 — membership + join flows](#pagesgroups-rest--slice-9-membership--join-invites--requests)**. **[Stripe / billing](#phase-3--stripe-billing--webhooks--account-hooks).** **[Slice 10 — collab Postgres bootstrap](#pages-rest--slice-10-collab-updates-rest):** `GET`/`POST …/collab-updates`. **Still ahead:** **collab + realtime WebSockets** (JWT upgrade, binary fan-out, optional Redis buffer like legacy) per [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md). | -| **4** — Client MVP | **Not started** | Auth → list → page → Yjs → groups; crypto/libs port as needed. **Parallel:** SPA structure, OpenAPI client, small E2E smoke—see [Frontend / UI track](#frontend--ui-track). | +| **4** — Client MVP | **In progress** | **Shipped:** [OpenAPI typed HTTP client](#phase-4--openapi-typed-client-bootstrap) in `@deepnotes/web` (`openapi-fetch` + generated `paths`). **Next:** routing + auth UI, then list → page → Yjs. **Parallel:** feature folders, E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | --- @@ -292,14 +292,35 @@ Replaces legacy Fastify `/stripe/webhook` and tRPC `users.account.stripe.*` usin Sprints **1–9** (pages / groups / membership), **Stripe**, and **[slice 10 — Postgres `page_updates` REST](#pages-rest--slice-10-collab-updates-rest)** are tracked above. Remaining work: -- [ ] **Collab WebSocket + realtime** — JWT cookie upgrade; binary protocol (legacy: lib0 + `@deeplib/misc` message kinds); **no** `next_key_rotation_date` / scheduled re-key; optional **Redis** `page-update-cache` / buffer like `@deeplib/data` `getAllPageUpdates` for parity; **Durable Object** vs separate Node service per [RESTART_PLAN](../docs/RESTART_PLAN.md) §4.3 / hosting table. REST [slice 10](#pages-rest--slice-10-collab-updates-rest) covers bootstrap + offline-style append only. +- [ ] **Collab WebSocket + realtime** — end-state: live Yjs-style sync with JWT-on-upgrade and **no** `next_key_rotation_date` / scheduled re-key ([RESTART_PLAN](../docs/RESTART_PLAN.md) §4.3). Suggested build order (can be parallelized after item 1): + 1. **Auth on upgrade:** reuse access JWT from `accessToken` cookie (same verification as HTTP); reject missing/invalid before accepting the socket; document subprotocol / first-message handshake if needed. + 2. **Room model:** one room per `pageId` (legacy pattern `…/page:{pageId}` or new versioned path); enforce `viewGroupPages` / `editGroupPages` from session + Drizzle before joining. + 3. **Wire format:** either **byte parity** with legacy collab-server (lib0 + `@deeplib/misc` message enums, golden fixtures) or **collab v2** with a semver’d protocol and a single cutover client—decide explicitly in code + short appendix next to OpenAPI. + 4. **Fan-out:** **Cloudflare Durable Object** per page (hibernatable WebSockets) vs dedicated Node/realtime process; REST [slice 10](#pages-rest--slice-10-collab-updates-rest) remains the Postgres source of truth for cold start / catch-up. + 5. **Optional Redis:** hot `page-update-*` buffer / pub-sub for multi-instance parity with legacy `@deeplib/data`—only if load tests or migration needs justify it (standard Redis commands only). + 6. **Tests:** at least one integration test per stream (collab + realtime) with Redis or in-memory doubles as required by the chosen topology ([RESTART_PLAN §8](../docs/RESTART_PLAN.md)). + +--- + +### Phase 4 — OpenAPI typed client (bootstrap) + +**Goal:** SPA uses a **small typed HTTP layer** (RESTART_PLAN §5.1 / §5.8): no `@deepnotes/api-worker`, `@deepnotes/db`, or Drizzle from `apps/web` source; session cookies via `credentials: "include"`. + +| Layer | What shipped | +|-------|----------------| +| **Codegen** | `pnpm --filter @deepnotes/web run generate:api-types` — `scripts/generate-api-types.mts` imports `getOpenApiDocument` from `@deepnotes/api` (dev-time only), writes `src/api/openapi.json`, runs `openapi-typescript` → `src/api/api-types.generated.ts`. **Re-run when `packages/api` OpenAPI paths change** (CI can add a drift check later: compare committed JSON to fresh dump). | +| **Runtime** | `openapi-fetch` + `createDeepnotesApiClient` / `resolveApiBaseUrl` in [apps/web/src/api/client.ts](apps/web/src/api/client.ts); optional `VITE_API_URL` (no trailing slash) in [vite-env.d.ts](apps/web/src/vite-env.d.ts) for cross-origin API during dev. | +| **Tests** | [apps/web/src/api/client.test.ts](apps/web/src/api/client.test.ts) — mocked `fetch` asserts `Request.credentials === "include"` and `/api/health` URL. | +| **Lint** | [apps/web/eslint.config.js](apps/web/eslint.config.js) ignores generated `api-types.generated.ts` and `openapi.json`. | + +**Intentional gaps:** no MSW/contract suite yet; no `import/no-restricted-paths` until more packages exist to accidentally import. --- ## Phase 4 checklist (client MVP) - [x] **Tooling (bootstrap):** Vitest + **happy-dom** + `@vue/test-utils` in `@deepnotes/web` (minimal `App` test); same Vite 6 pipeline via `vitest/config` `defineConfig` (RESTART_PLAN §5.8). -- [ ] **API client:** consume **OpenAPI** (generated types + `fetch`, or hey-api) from `@deepnotes/api` / published spec—**no** workspace dependency on Worker or DB packages from web source. +- [x] **API client (bootstrap):** typed client from the same OpenAPI document as the Worker—see [Phase 4 — OpenAPI typed client](#phase-4--openapi-typed-client-bootstrap). Runtime bundle does **not** import `@deepnotes/api` (only generated `api-types.generated.ts` + `openapi-fetch`); regenerate after OpenAPI changes. - [ ] **Routing + auth UI:** login / refresh / logout / 2FA flows aligned with [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md); composable or component tests + **E2E smoke** for cookie session. - [ ] **Pages:** list → open editor shell → integrate **Yjs** / collab when API is ready. - [ ] **Groups** subset and notifications UX as mapped from [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). @@ -325,7 +346,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ### Decoupling and layout (`@deepnotes/web`) -- [ ] **Forbidden imports:** no `@deepnotes/api-worker`, `@deepnotes/db`, or Drizzle from `apps/web` source; HTTP only via a small **API layer** (generated OpenAPI client or `fetch` + shared types from `@deepnotes/api`). +- [x] **API surface:** `src/api/` — generated `paths` + `createDeepnotesApiClient`; bundle does not depend on `@deepnotes/api` at runtime (codegen devDeps only). **Still to enforce:** ESLint `import/no-restricted-paths` banning `@deepnotes/api-worker`, `@deepnotes/db`, `drizzle-orm` from `apps/web/src/**` once rule config is added. - [ ] **Feature folders:** e.g. `src/features/auth`, `src/features/pages`, `src/shared/ui`—document the convention in `apps/web/README.md` (or link from repo root README). - [ ] **Thin Vue, fat composables:** session and crypto orchestration live in testable modules, not only in `.vue` files. @@ -344,7 +365,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**24** cases when DB env set) — … + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 move](#pages-rest--slice-7-move--group-creation) + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; optional: invitation **reject/cancel**, join-request **reject/cancel**, **private** group invite/request **access keyring** branches | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (session routes + [slice 6/7 `/api/pages/...` paths](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)); **`schemas/users.test.ts`**; **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; more Zod edge cases for new page schemas | | **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **70** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + `/api/pages/{pageId}/move` + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests) + [slice 10 `…/collab-updates`](#pages-rest--slice-10-collab-updates-rest) (`GET` + `POST`) + [Stripe routes](#phase-3--stripe-billing--webhooks--account-hooks) (`/api/billing/stripe/*`, `/api/webhooks/stripe`) | **200** tests with stub `SessionEnv` + template DB (heavier) | -| **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`) | Auth UI + API client as in §5.8 | +| **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`); **`client.test.ts`** (credentials + `/api/health` URL on mocked fetch) | Auth UI + composable tests; MSW/OpenAPI fixtures optional; run `generate:api-types` when `@deepnotes/api` OpenAPI changes | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -360,7 +381,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ## Success criteria (RESTART_PLAN §8) -- [ ] OpenAPI source of truth; client **generated** types or shared Zod. +- [x] OpenAPI source of truth; client **generated** types (`openapi-typescript`) + `openapi-fetch` in `@deepnotes/web` — [Phase 4 — OpenAPI typed client](#phase-4--openapi-typed-client-bootstrap). - [ ] Drizzle migrations from empty DB documented for production upgrades. - [ ] Cold API dev start under **2 s** (no `inspect-brk` by default) — validate on a typical laptop. - [ ] Collab + realtime: at least one integration test each (Redis + deps). @@ -389,6 +410,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-27 | **Phase 4 — OpenAPI typed client:** `@deepnotes/web` — `pnpm run generate:api-types` (`tsx` + `openapi-typescript`); committed `src/api/openapi.json` + `api-types.generated.ts`; `createDeepnotesApiClient` / `resolveApiBaseUrl` (`openapi-fetch`, `credentials: "include"`); `client.test.ts`; `VITE_API_URL`; eslint ignore for generated files. **Phase 3** collab WS backlog expanded (upgrade → room → wire → fan-out → Redis → tests). PLAN_PROGRESS Phase 4 snapshot → **In progress**. | | 2026-04-27 | **Phase 3 — slice 10 (collab Postgres REST):** [page-collab-updates.ts](packages/session/src/page-collab-updates.ts) — `performGetPageCollabUpdates` / `performAppendPageCollabUpdates`; `GET|POST /api/pages/:pageId/collab-updates`; OpenAPI + Zod; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) collab bootstrap table; integration extends **groups + pages**; api-worker 503 matrix **70**. **Next:** collab + realtime **WebSocket** only. | | 2026-04-27 | **Phase 3 — Stripe (billing):** [stripe-billing.ts](packages/session/src/stripe-billing.ts) — `performStripeCreateCheckoutSession` / `performStripeCreatePortalSession`, `processStripeWebhookEvent` (legacy `customer.subscription.updated` / `deleted` → `users.plan` + `subscription_id` via `users.customer_id`); [schemas/billing.ts](packages/api/src/schemas/billing.ts) + OpenAPI; Worker `POST /api/billing/stripe/checkout-session`, `…/portal-session`, `POST /api/webhooks/stripe` ([session-env](apps/api-worker/src/session-env.ts) `getStripeBillingEnv` / `getStripeWebhookSecret`); **`STRIPE_SECRET_KEY`** hooks: `deleteStripeCustomer` on `DELETE /api/users/me`, `updateStripeCustomerEmail` on email-change confirm. Dependencies: `stripe@^17.7` in session + api-worker. [TRPC_REST_MAP](docs/TRPC_REST_MAP.md); [template.env](template.env). Api-worker 503 matrix **68** tests. **Next:** [realtime + collab](#phase-3-working-order-suggested) only. | | 2026-04-27 | **Phase 3 — slice 9 (membership + join flows):** [group-role-ranks.ts](packages/session/src/group-role-ranks.ts) (`canManageRole` / `canChangeRole` / `manageLowerRanks` parity); [group-membership.ts](packages/session/src/group-membership.ts) — invitations send/accept/reject/cancel, join requests send/accept/reject/cancel, `PATCH`/`DELETE` members; Zod + OpenAPI + Hono; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) WS table; **`byteB64`** passthrough in worker (decoded `Uint8Array`). `account-flows` **24** cases; api-worker 503 matrix **65**. [Slice 9 section](#pagesgroups-rest--slice-9-membership--join-invites--requests). **Next:** [realtime + collab](#phase-3-working-order-suggested). | diff --git a/new-deepnotes/apps/web/eslint.config.js b/new-deepnotes/apps/web/eslint.config.js index f6c5b17b..0f6b7636 100644 --- a/new-deepnotes/apps/web/eslint.config.js +++ b/new-deepnotes/apps/web/eslint.config.js @@ -1,3 +1,8 @@ import base from "../../eslint.config.js"; -export default [...base]; +export default [ + ...base, + { + ignores: ["src/api/api-types.generated.ts", "src/api/openapi.json"], + }, +]; diff --git a/new-deepnotes/apps/web/package.json b/new-deepnotes/apps/web/package.json index f28e79d1..06506d0b 100644 --- a/new-deepnotes/apps/web/package.json +++ b/new-deepnotes/apps/web/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "scripts": { + "generate:api-types": "tsx scripts/generate-api-types.mts", "build": "vite build", "dev": "vite", "lint": "eslint vite.config.ts \"src/**/*.ts\"", @@ -12,12 +13,15 @@ "preview": "vite preview" }, "dependencies": { + "openapi-fetch": "^0.17.0", "vue": "^3.5.13" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", "@vue/test-utils": "^2.4.6", "happy-dom": "^17.4.4", + "openapi-typescript": "^7.13.0", + "tsx": "^4.21.0", "typescript": "^5.8.3", "vite": "^6.3.3", "vitest": "^3.2.4", diff --git a/new-deepnotes/apps/web/scripts/generate-api-types.mts b/new-deepnotes/apps/web/scripts/generate-api-types.mts new file mode 100644 index 00000000..2daf4ad8 --- /dev/null +++ b/new-deepnotes/apps/web/scripts/generate-api-types.mts @@ -0,0 +1,20 @@ +import { execSync } from "node:child_process"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { getOpenApiDocument } from "../../../packages/api/src/openapi.ts"; + +const scriptDir = dirname(fileURLToPath(import.meta.url)); +const webRoot = join(scriptDir, ".."); +const outDir = join(webRoot, "src", "api"); + +mkdirSync(outDir, { recursive: true }); +const jsonPath = join(outDir, "openapi.json"); +const typesPath = join(outDir, "api-types.generated.ts"); + +writeFileSync(jsonPath, `${JSON.stringify(getOpenApiDocument(), null, 2)}\n`); + +execSync( + `pnpm exec openapi-typescript "${jsonPath}" -o "${typesPath}"`, + { cwd: webRoot, stdio: "inherit" }, +); diff --git a/new-deepnotes/apps/web/src/api/api-types.generated.ts b/new-deepnotes/apps/web/src/api/api-types.generated.ts new file mode 100644 index 00000000..aa523466 --- /dev/null +++ b/new-deepnotes/apps/web/src/api/api-types.generated.ts @@ -0,0 +1,6018 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/health": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Health check */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description API is reachable */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HealthResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create session (email + login hash) + * @description Replaces legacy `sessions.login`. Sets httpOnly cookies when implemented. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["SessionLoginRequest"]; + }; + }; + responses: { + /** @description Login succeeded; cookies set. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionLoginSuccess"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Too many failed login attempts (rate limited). */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Register a new account + * @description Replaces legacy `users.account.register`. Creates user, personal group, and first page; sets email verification unless `SEND_EMAILS=false` (then verifies immediately, legacy parity). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserRegisterRequest"]; + }; + }; + responses: { + /** @description User created. `emailVerified` is true when outbound mail is disabled (`SEND_EMAILS=false`). */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserRegisterResponse"]; + }; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource already exists (e.g. email already registered). */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Email send failed (e.g. Resend API error after user row was created; rare). */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Current user (from access cookie) + * @description Minimal account summary for the authenticated user (`accessToken` cookie). + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Authenticated user. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserMeResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + /** + * Delete current account (password confirmation) + * @description Replaces legacy `users.account.delete`. Requires `accessToken` cookie and correct `loginHash` in the JSON body. Clears session cookies on success. Optional Stripe customer deletion is handled by the deployment (not part of OpenAPI). + */ + delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserAccountDeleteRequest"]; + }; + }; + responses: { + /** @description Account removed; session cookies cleared (same names as login). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password, ownership constraint, or validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/groups": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List group IDs for the current user + * @description Replaces legacy `users.pages.getGroupIds`. Returns `group_id` values from `group_members` ordered by recent activity (desc). + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ordered group ids. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserGroupIdsResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/starting": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Starting page id for the current user + * @description Replaces legacy `users.pages.getStartingPageId` (reads `users.starting_page_id`). + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Nanoid of the user’s starting page. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserStartingPageResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/path": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Breadcrumb path from a page to the personal main page + * @description Replaces legacy `users.pages.getCurrentPath`. Uses `users_pages.last_parent_id` and may repair a missing parent link once (legacy KeyDB behavior). + */ + get: { + parameters: { + query: { + initialPageId: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ordered page ids from root (personal main) to `initialPageId`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserCurrentPathResponse"]; + }; + }; + /** @description Missing or invalid `initialPageId` query parameter. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/recent/remove": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Remove page ids from recent list + * @description Replaces legacy `users.pages.removeRecentPages`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserPageIdsBody"]; + }; + }; + responses: { + /** @description Updated `users.recent_page_ids`. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/recent/clear": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Clear recent pages + * @description Replaces legacy `users.pages.clearRecentPages`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Recent list emptied. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/favorites": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Add favorite pages + * @description Replaces legacy `users.pages.addFavoritePages`. Favorites are stored in Postgres (`users.favorite_page_ids`); legacy used KeyDB only. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserPageIdsBody"]; + }; + }; + responses: { + /** @description Favorites merged (order: new ids first, then existing). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/favorites/remove": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Remove favorite pages + * @description Replaces legacy `users.pages.removeFavoritePages`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserPageIdsBody"]; + }; + }; + responses: { + /** @description Favorites updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/pages/favorites/clear": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Clear favorite pages + * @description Replaces legacy `users.pages.clearFavoritePages`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Favorites emptied. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/defaults/note": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Update encrypted default note template + * @description Replaces legacy `users.pages.setEncryptedDefaultNote`. + */ + patch: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserDefaultNotePatch"]; + }; + }; + responses: { + /** @description `users.encrypted_default_note` updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + trace?: never; + }; + "/api/users/me/defaults/arrow": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Update encrypted default arrow template + * @description Replaces legacy `users.pages.setEncryptedDefaultArrow`. + */ + patch: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserDefaultArrowPatch"]; + }; + }; + responses: { + /** @description `users.encrypted_default_arrow` updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + trace?: never; + }; + "/api/users/me/notifications": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Load notifications for the current user + * @description Replaces legacy `users.pages.notifications.load`. Ciphertext fields are base64 in JSON. + */ + get: { + parameters: { + query?: { + lastNotificationId?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Window of notifications and optional `lastNotificationRead`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserNotificationsLoadResponse"]; + }; + }; + /** @description Invalid query parameters. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/notifications/read": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Mark all notifications as read + * @description Replaces legacy `users.pages.notifications.markAsRead`. Sets `users.last_notification_read` to the latest linked notification id. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Read cursor updated (no-op if user has no notifications). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/main-page": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get the group main page id + * @description Replaces legacy `groups.getMainPageId` (KeyDB `main-page-id`). Source: `groups.main_page_id`. Requires `viewGroupPages` (same as listing pages). + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Main page id for the group. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupMainPageResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/members": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List user ids (members, requests, invitations) + * @description Replaces legacy `groups.getUserIds`: union of `group_members`, `group_join_requests`, and `group_join_invitations` for the group. Requires `viewGroupMembers` (not granted for public read without membership). + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Distinct user ids (unordered). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupMemberUserIdsResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/pages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List page IDs in a group + * @description Replaces legacy `groups.getPages` (authenticated). Optional `lastPageId` cursor for pagination (newest `last_activity_date` first). Omits soft-deleted pages (`permanent_deletion_date` set). Public groups allow `viewGroupPages` without membership. + */ + get: { + parameters: { + query?: { + lastPageId?: string; + }; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Page id window (max 20) and `hasMore`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupPagesListResponse"]; + }; + }; + /** @description Invalid `lastPageId` (not in group). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + /** + * Create a page in a group + * @description Replaces legacy `pages.create`. For an **existing** group, `parentPageId` must be a page in that group and the caller needs `editGroupPages`. With optional `groupCreation`, path `groupId` is a **new** nanoid (no row yet), `parentPageId` is a page in the user’s **personal** group, and the body includes the same ciphertext as `PageMoveGroupCreationRequest` — Pro only; creates the `groups` + owner `group_members` rows then the first page (legacy parity). The 50 free-page cap applies to non‑Pro users for normal creates. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPageCreateRequest"]; + }; + }; + responses: { + /** @description Page and `users_pages` row created. */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["GroupPageCreateResponse"]; + }; + }; + /** @description Invalid parent page or body. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Enable group password (Pro) + * @description Replaces `groups.password.enable`. Argon2id is applied on the server to the provided `groupPasswordHash` material (base64) and stored encrypted. Requires `editGroupSettings` and a Pro plan. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPasswordEnableRequest"]; + }; + }; + responses: { + /** @description Password protection enabled. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already protected or bad password material. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + /** + * Disable group password (not Pro check in legacy for disable-only) + * @description Replaces `groups.password.disable`. Verifies the current group password, removes server-side group password, updates `groupEncryptedContentKeyring`. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPasswordDisableRequest"]; + }; + }; + responses: { + /** @description Password protection disabled. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password, or not protected. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + /** + * Change group password (Pro) + * @description Replaces `groups.password.change`. Verifies the current group password, then re-wraps the content keyring. + */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPasswordChangeRequest"]; + }; + }; + responses: { + /** @description Password updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password, or group not protected. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + trace?: never; + }; + "/api/groups/{groupId}/privacy/public": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Make group public (Pro) + * @description Replaces `groups.privacy.makePublic`. Sets `access_keyring` and clears member/invite `encrypted_access_keyring`. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPrivacyPublicRequest"]; + }; + }; + responses: { + /** @description Group is public. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already public. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/privacy/join-requests": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Allow or reject join requests (Pro) + * @description Replaces `groups.privacy.setJoinRequestsAllowed`. + */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPrivacyJoinRequestsPatch"]; + }; + }; + responses: { + /** @description Setting updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + trace?: never; + }; + "/api/groups/{groupId}/privacy/private": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Make group private (Pro) — full re-key payload + * @description Replaces legacy WS `groups.privacy.makePrivate` (step 2 `rotateGroupKeys`) in one request. Clears `access_keyring` when `groupAccessKeyring` is omitted. Member / invitation / request / page record keys must match the DB exactly. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupPrivacyPrivateRequest"]; + }; + }; + responses: { + /** @description Group is private; ciphertext updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already private or payload key sets do not match group. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Soft-delete group (grace period) + * @description Replaces `groups.deletion.delete`. Sets `permanent_deletion_date` ~1 month ahead. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion scheduled. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already soft-deleted. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Restore a soft-deleted group + * @description Replaces `groups.deletion.restore` during the grace period (`permanent_deletion_date` in the future). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Group removed from scheduled deletion. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not soft-deleted, or no longer in grace period. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/purge": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Permanently mark group deleted (purge active or grace state) + * @description Replaces `groups.deletion.deletePermanently` — `permanent_deletion_date` set in the past (legacy). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Purge recorded. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already purged. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-invitations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Send a group join invitation (Pro) + * @description Replaces legacy WS `groups.joinInvitations.send` step 1. Deletes a conflicting join request for the invitee. For private groups, `encryptedAccessKeyring` is required; for public groups it is stored as null. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupJoinInvitationSendRequest"]; + }; + }; + responses: { + /** @description Invitation created. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already invited, already a member, or missing keyring for private group. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-invitations/me/accept": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Accept a pending join invitation (Pro) + * @description Replaces legacy WS `groups.joinInvitations.accept` step 1. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupJoinInvitationAcceptRequest"]; + }; + }; + responses: { + /** @description Invitation consumed; user added to `group_members`. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-invitations/me/reject": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reject a pending join invitation + * @description Replaces legacy WS `groups.joinInvitations.reject` step 1 (no Pro check in legacy). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Invitation removed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No pending invitation. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-invitations/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Cancel a join invitation (Pro) + * @description Replaces legacy WS `groups.joinInvitations.cancel` step 1. Path `userId` is the invitee. Requires permission to manage the invited role. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Invitation removed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-requests": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Send a join request (Pro) + * @description Replaces legacy WS `groups.joinRequests.send` step 1. Requires `are_join_requests_allowed` on the group. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupJoinRequestSendRequest"]; + }; + }; + responses: { + /** @description Join request created. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already pending. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-requests/{userId}/accept": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Accept a join request (Pro) + * @description Replaces legacy WS `groups.joinRequests.accept` step 1. Path `userId` is the requester. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupJoinRequestAcceptRequest"]; + }; + }; + responses: { + /** @description Requester added to `group_members`. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No pending request or missing access keyring for private group. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-requests/{userId}/reject": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reject a join request (Pro) + * @description Replaces legacy WS `groups.joinRequests.reject` step 1. Sets `rejected` on the request (legacy does not delete the row). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Request marked rejected. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No pending request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/join-requests/me/cancel": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Cancel own join request (Pro) + * @description Replaces legacy WS `groups.joinRequests.cancel` step 1. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Join request row deleted. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No pending request. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/groups/{groupId}/members/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Remove a member (or leave) + * @description Replaces legacy WS `groups.removeUser` step 1. Callers may remove themselves without `canManageRole` on others. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Membership removed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Cannot remove the last owner. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + /** + * Change a member's role (Pro) + * @description Replaces legacy WS `groups.changeUserRole` step 1. + */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + groupId: string; + userId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["GroupMemberRolePatchRequest"]; + }; + }; + responses: { + /** @description `group_members.role` updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + trace?: never; + }; + "/api/pages/{pageId}/move": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Move page (optionally create group, re-key, set main) + * @description Replaces `websocket/pages/move` — Pro-only; `editGroupSettings` on the page's current group, `editGroupPages` on destination unless `groupCreation` creates it. `reencrypt` is required when the page changes group (Yjs `page_updates` replaced with a single index-0 row; snapshots updated by id). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PageMoveRequest"]; + }; + }; + responses: { + /** @description Move completed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No-op move, or invalid payload. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/bump": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Bump page (recents, activity, optional breadcrumb parent) + * @description Replaces `pages.bump` — `users` starting + recents, optional `users_pages.last_parent_id` when the parent chain ends at the personal main page (`lastParentId` walk). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PageBumpRequest"]; + }; + }; + responses: { + /** @description Bumped (best-effort; loop in chain exits without updating parent). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid parent (chain does not resolve to main page). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/collab-updates": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List encrypted Yjs page updates (Postgres) + * @description Bootstrap for the editor: returns all `page_updates` rows for the page, ordered by `index`. Does not use legacy Redis collab cache — Postgres only. Full duplex collab remains a separate WebSocket track (Phase 3). + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current ciphertext chain. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageCollabUpdatesGetResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + /** + * Append page updates (optimistic concurrency) + * @description Appends ciphertext rows to `page_updates`. `expectedLastIndex` must match the current max index (or null when empty). **409** when another writer advanced the chain — client should re-GET and retry. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PageCollabUpdatesAppendRequest"]; + }; + }; + responses: { + /** @description Updates persisted. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Bad index sequence or wrong `expectedLastIndex` for an empty page. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Stale `expectedLastIndex` (concurrent append). */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/backlinks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create page backlink (source → this page as target) + * @description Replaces `pages.backlinks.create`. Path `pageId` is the **target**; body has `sourcePageId`. + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PageBacklinkCreateRequest"]; + }; + }; + responses: { + /** @description Backlink created or activity updated (upsert). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Source and target identical. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/backlinks/{targetPageId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete backlink from source page to target page + * @description Replaces `pages.backlinks.delete`. Path `pageId` is **source**; `targetPageId` is the link target (legacy input names). + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + targetPageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Backlink removed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/snapshots": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Save encrypted page snapshot (Pro) + * @description Replaces `pages.snapshots.save` — asserts Pro plan (legacy `assertUserSubscribed`). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PageSnapshotSaveRequest"]; + }; + }; + responses: { + /** @description Snapshot id */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageSnapshotCreateResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/snapshots/{snapshotId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Load page snapshot ciphertext (Pro) */ + get: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + snapshotId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ciphertext (base64 fields). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PageSnapshotLoadResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + put?: never; + post?: never; + /** Delete a page snapshot */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + snapshotId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Snapshot removed. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Soft-delete page (grace period) */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deletion scheduled (not main page). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Already deleted, or is group main page. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Restore a soft-deleted page */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Page restored in grace. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not deleted, or free page past purge date. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/pages/{pageId}/purge": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Permanently mark page deleted; refunds free page when applicable */ + post: { + parameters: { + query?: never; + header?: never; + path: { + pageId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Purge recorded. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Is main page, or already purged. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Change password (re-wrap keyrings) + * @description Replaces legacy WebSocket `users.account.changePassword`. Requires `accessToken`; verifies `oldLoginHash`; stores keyrings encrypted with the new password (`userEncrypted*` are plaintext keyrings from the client, same as registration). Invalidates all sessions and clears cookies — client must log in again. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserPasswordChangeRequest"]; + }; + }; + responses: { + /** @description Password updated; all sessions invalidated; session cookies cleared. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong current password or invalid key material. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/email-change": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Request account email change (6-digit code email) + * @description Replaces legacy `users.account.emailChange.request`. Verifies `oldLoginHash` and that the new address is not already registered. When outbound email is enabled, sends a 6-digit code. When `SEND_EMAILS=false` (e.g. local), returns 200 with `emailVerificationCode` instead of emailing. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserEmailChangeRequest"]; + }; + }; + responses: { + /** @description Out-of-band dev response when `SEND_EMAILS=false` (verification code not emailed). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["UserEmailChangeRequestResponse"]; + }; + }; + /** @description Code emailed to the new address; pending change stored on the user row. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password, address in use, or validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Email send failed (e.g. Resend) after the pending state was written. */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/email-change/confirm": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Confirm email change (re-wrap keyrings, new password) + * @description Replaces legacy WebSocket `users.account.emailChange.finish` (step 1 + 2 in one). Verifies 6-digit code and `oldLoginHash`, then applies new email + new password-encrypted keyrings, invalidates sessions, clears cookies; optional Stripe customer email update in the deployment (not in OpenAPI). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["UserEmailChangeConfirmRequest"]; + }; + }; + responses: { + /** @description Email updated; sessions cleared; re-login required. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong code, wrong password, no pending change, or invalid keyrings. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/enable/request": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Start 2FA setup (TOTP secret + otpauth URI) + * @description Replaces `users.account.twoFactorAuth.enable.request`. Stores a pending encrypted authenticator secret; client shows QR from `keyUri` or `secret`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faPasswordBody"]; + }; + }; + responses: { + /** @description Secret generated; not yet enabled until `…/enable/finish`. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User2faEnableRequestResponse"]; + }; + }; + /** @description Validation error, or 2FA already fully enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/enable/finish": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Complete 2FA setup (TOTP + recovery codes) + * @description Replaces `users.account.twoFactorAuth.enable.finish`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faEnableFinishRequest"]; + }; + }; + responses: { + /** @description 2FA enabled; one-time recovery codes returned. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User2faRecoveryCodesResponse"]; + }; + }; + /** @description Wrong password, wrong TOTP, or already enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/load": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reveal TOTP secret and otpauth URI (after password check) + * @description Replaces `users.account.twoFactorAuth.load` (legacy tRPC had `loginHash` in the query; this API uses a JSON body on POST to avoid putting secrets in query strings or logs). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faPasswordBody"]; + }; + }; + responses: { + /** @description Secret and `keyUri` for re-provisioning an authenticator. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User2faEnableRequestResponse"]; + }; + }; + /** @description Wrong password or 2FA not enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/recovery-codes": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Regenerate recovery codes + * @description Replaces `users.account.twoFactorAuth.generateRecoveryCodes`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faPasswordBody"]; + }; + }; + responses: { + /** @description New recovery codes (previous codes invalidated). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User2faRecoveryCodesResponse"]; + }; + }; + /** @description Wrong password or 2FA not enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/devices/forget": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Mark all user devices as not trusted + * @description Replaces `users.account.twoFactorAuth.forgetTrustedDevices`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faPasswordBody"]; + }; + }; + responses: { + /** @description `devices.trusted` cleared for this user. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password or 2FA not enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/me/2fa/disable": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Disable 2FA + * @description Replaces `users.account.twoFactorAuth.disable`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["User2faPasswordBody"]; + }; + }; + responses: { + /** @description 2FA disabled; authenticator and recovery material cleared. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Wrong password or 2FA not enabled. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/email-verification/resend": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Resend email verification (public, by email) + * @description Replaces legacy `users.account.resendVerificationEmail`. Uses Resend when `SEND_EMAILS` is not `false`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["EmailVerificationResendRequest"]; + }; + }; + responses: { + /** @description Email sent (or accepted by provider). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Validation error or outbound email disabled for this environment. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource already exists (e.g. email already registered). */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Email provider (Resend) request failed. */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/users/email-verification/confirm": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Confirm email with nanoid code + * @description Replaces legacy `users.account.verifyEmail` (public). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["EmailVerificationConfirmRequest"]; + }; + }; + responses: { + /** @description Email verified; account updated. */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid or expired code. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/refresh": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Rotate access token using refresh cookie + * @description Replaces legacy `sessions.refresh`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description New session key and cookies. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionRefreshSuccess"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Invalidate session and clear cookies + * @description Replaces legacy `sessions.logout`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Logged out (cookies cleared). */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/sessions/demo": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create demo user and session + * @description Replaces legacy `sessions.startDemo`. Request body will match registration key material once defined. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["SessionDemoRequest"]; + }; + }; + responses: { + /** @description Demo user created; same response shape as login. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionLoginSuccess"]; + }; + }; + /** @description Validation error (e.g. unsupported group password on demo). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/billing/stripe/checkout-session": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create Stripe Checkout (subscription) session + * @description Replaces legacy `users.account.stripe.createCheckoutSession`. Resolves or creates a Stripe customer from `users.customer_id` and decrypted account email, then returns a hosted Checkout URL. Requires verified email. Demo accounts receive **403**. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["StripeCheckoutSessionRequest"]; + }; + }; + responses: { + /** @description Checkout session URL (hosted Stripe page). */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StripeCheckoutSessionResponse"]; + }; + }; + /** @description Already subscribed, or validation error. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/billing/stripe/portal-session": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create Stripe Customer Portal session + * @description Replaces legacy `users.account.stripe.createPortalSession`. Requires a `users.customer_id` (create checkout first or migrate from legacy). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Portal session URL. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["StripePortalSessionResponse"]; + }; + }; + /** @description No Stripe customer on file. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Invalid credentials, token, or session state. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Action not allowed for this account (e.g. demo user). */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Resource not found. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/webhooks/stripe": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Stripe webhooks (signed raw body) + * @description Replaces the legacy Fastify `POST /stripe/webhook` handler. Send the **raw** request body; verification uses the `Stripe-Signature` header and `STRIPE_WEBHOOK_SECRET`. Handles `customer.subscription.updated` and `customer.subscription.deleted` by updating `users.plan` and `users.subscription_id` via `users.customer_id`. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Event acknowledged. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Missing or invalid signature. */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SessionErrorResponse"]; + }; + }; + /** @description Required auth environment variables are not configured (local: copy template.env / .dev.vars). */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ServiceUnavailableResponse"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + HealthResponse: { + /** @enum {string} */ + status: "ok"; + service: string; + }; + SessionLoginSuccess: { + userId: string; + sessionId: string; + /** + * Format: byte + * @description Base64-encoded session symmetric key. + */ + sessionKey: string; + personalGroupId: string; + /** Format: byte */ + publicKeyring: string; + /** Format: byte */ + encryptedPrivateKeyring: string; + /** Format: byte */ + encryptedSymmetricKeyring: string; + }; + SessionErrorResponse: { + code: string; + message: string; + }; + ServiceUnavailableResponse: { + /** @enum {string} */ + code: "SERVICE_UNAVAILABLE"; + message: string; + }; + SessionLoginRequest: { + email: string | "demo"; + /** + * Format: byte + * @description Base64-encoded login hash (legacy wire used binary; prefer standard base64 in JSON). + */ + loginHash: string; + rememberSession: boolean; + authenticatorToken?: string; + rememberDevice?: boolean; + recoveryCode?: string; + }; + UserRegisterResponse: { + userId: string; + emailVerified: boolean; + }; + SessionDemoGroupCreation: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPasswordHash?: string; + groupIsPublic: boolean; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupAccessKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedInternalKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPublicKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedPrivateKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupOwnerEncryptedName: string; + }; + SessionDemoPageCreation: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedSymmetricKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedRelativeTitle: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedAbsoluteTitle: string; + }; + SessionDemoRequest: { + userId: string; + groupId: string; + pageId: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userPublicKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedPrivateKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedSymmetricKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedDefaultNote: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedDefaultArrow: string; + groupCreation: components["schemas"]["SessionDemoGroupCreation"]; + pageCreation: components["schemas"]["SessionDemoPageCreation"]; + }; + UserRegisterRequest: components["schemas"]["SessionDemoRequest"] & { + /** Format: email */ + email: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + loginHash: string; + }; + UserMeResponse: { + userId: string; + emailVerified: boolean; + demo: boolean; + personalGroupId: string; + }; + UserGroupIdsResponse: { + groupIds: string[]; + }; + UserStartingPageResponse: { + startingPageId: string; + }; + UserCurrentPathResponse: { + pathPageIds: string[]; + }; + UserPageIdsBody: { + pageIds: string[]; + }; + UserDefaultNotePatch: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedDefaultNote: string; + }; + UserDefaultArrowPatch: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedDefaultArrow: string; + }; + UserNotificationItem: { + id: number; + type: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedSymmetricKey: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedContent: string; + dateTime: string; + }; + UserNotificationsLoadResponse: { + items: components["schemas"]["UserNotificationItem"][]; + hasMore: boolean; + lastNotificationRead?: number | null; + }; + GroupMainPageResponse: { + mainPageId: string; + }; + GroupMemberUserIdsResponse: { + userIds: string[]; + }; + GroupPagesListResponse: { + pageIds: string[]; + hasMore: boolean; + }; + GroupPageCreateResponse: { + pageId: string; + numFreePages?: number; + }; + PageMoveGroupCreationRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPasswordHash?: string; + groupIsPublic: boolean; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupAccessKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedInternalKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPublicKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedPrivateKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupOwnerEncryptedName: string; + }; + GroupPageCreateRequest: { + /** + * @description 21-character nanoid (URL-safe alphabet). + * @example V1StGXR8_Z5jdHi6B-myT + */ + parentPageId: string; + /** + * @description 21-character nanoid (URL-safe alphabet). + * @example V1StGXR8_Z5jdHi6B-myT + */ + pageId: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedSymmetricKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedRelativeTitle: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedAbsoluteTitle: string; + groupCreation?: components["schemas"]["PageMoveGroupCreationRequest"]; + }; + GroupPasswordEnableRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPasswordHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + }; + GroupPasswordChangeRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupCurrentPasswordHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupNewPasswordHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + }; + GroupPasswordDisableRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPasswordHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + }; + GroupPrivacyPublicRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + accessKeyring: string; + }; + GroupPrivacyJoinRequestsPatch: { + areJoinRequestsAllowed: boolean; + }; + GroupPrivacyPrivateMember: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedAccessKeyring?: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedInternalKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedName: string | null; + }; + GroupPrivacyPrivateInvitation: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedAccessKeyring?: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedInternalKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedName: string; + }; + GroupPrivacyPrivateJoinRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedName: string; + }; + GroupPrivacyPrivatePage: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedSymmetricKeyring: string; + }; + GroupPrivacyPrivateRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupAccessKeyring?: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedContentKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupPublicKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + groupEncryptedPrivateKeyring: string; + groupMembers: { + [key: string]: components["schemas"]["GroupPrivacyPrivateMember"]; + }; + groupJoinInvitations: { + [key: string]: components["schemas"]["GroupPrivacyPrivateInvitation"]; + }; + groupJoinRequests: { + [key: string]: components["schemas"]["GroupPrivacyPrivateJoinRequest"]; + }; + groupPages: { + [key: string]: components["schemas"]["GroupPrivacyPrivatePage"]; + }; + }; + /** @enum {string} */ + GroupMemberRole: "owner" | "admin" | "moderator" | "member" | "viewer"; + GroupJoinInvitationSendRequest: { + /** + * @description 21-character nanoid (URL-safe alphabet). + * @example V1StGXR8_Z5jdHi6B-myT + */ + inviteeUserId: string; + invitationRole: components["schemas"]["GroupMemberRole"]; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedAccessKeyring?: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedInternalKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedNameForUser: string; + }; + GroupJoinInvitationAcceptRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedName: string; + }; + GroupJoinRequestSendRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedUserName: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedUserNameForUser: string; + }; + GroupJoinRequestAcceptRequest: { + targetRole: components["schemas"]["GroupMemberRole"]; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedAccessKeyring?: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedInternalKeyring: string; + }; + GroupMemberRolePatchRequest: { + role: components["schemas"]["GroupMemberRole"]; + }; + PageMoveReencryptRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedSymmetricKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedRelativeTitle: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedAbsoluteTitle: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + pageEncryptedUpdate: string; + /** @default {} */ + pageEncryptedSnapshots: { + [key: string]: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedSymmetricKey: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedData: string; + }; + }; + }; + PageMoveRequest: { + /** + * @description 21-character nanoid (URL-safe alphabet). + * @example V1StGXR8_Z5jdHi6B-myT + */ + destGroupId: string; + setAsMainPage: boolean; + groupCreation?: components["schemas"]["PageMoveGroupCreationRequest"]; + reencrypt?: components["schemas"]["PageMoveReencryptRequest"]; + }; + PageBumpRequest: { + parentPageId?: string; + }; + PageCollabUpdatesGetResponse: { + /** @description Max `index` in the database, or null if there are no rows. */ + lastIndex: number | null; + updates: { + index: number; + /** + * Format: byte + * @description Base64 ciphertext (`page_updates.encrypted_data`). + */ + encryptedData: string; + }[]; + }; + PageCollabUpdateItemInput: { + /** @description Monotonic index (legacy collab / `page_updates.index`, often Yjs clock). */ + index: number; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedData: string; + }; + PageCollabUpdatesAppendRequest: { + /** @description Must match `lastIndex` from GET (`null` when the page has no updates yet). */ + expectedLastIndex: number | null; + updates: components["schemas"]["PageCollabUpdateItemInput"][]; + }; + PageBacklinkCreateRequest: { + /** + * @description 21-character nanoid (URL-safe alphabet). + * @example V1StGXR8_Z5jdHi6B-myT + */ + sourcePageId: string; + }; + PageSnapshotCreateResponse: { + snapshotId: string; + }; + PageSnapshotSaveRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedSymmetricKey: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + encryptedData: string; + preRestore?: boolean; + }; + PageSnapshotLoadResponse: { + encryptedSymmetricKey: string | null; + /** + * Format: byte + * @description Base64 ciphertext. + */ + encryptedData: string; + }; + UserPasswordChangeRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + oldLoginHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + newLoginHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedPrivateKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedSymmetricKeyring: string; + }; + UserEmailChangeRequestResponse: { + emailVerificationCode: string; + }; + UserEmailChangeRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + oldLoginHash: string; + /** Format: email */ + newEmail: string; + }; + UserEmailChangeConfirmRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + oldLoginHash: string; + emailVerificationCode: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + newLoginHash: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedPrivateKeyring: string; + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + userEncryptedSymmetricKeyring: string; + }; + UserAccountDeleteRequest: { + /** + * Format: byte + * @description Base64-encoded login hash (same semantics as `POST /api/sessions/login`). + */ + loginHash: string; + }; + User2faEnableRequestResponse: { + secret: string; + /** @description otpauth:// URI for authenticator apps. */ + keyUri: string; + }; + User2faPasswordBody: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + loginHash: string; + }; + User2faRecoveryCodesResponse: { + recoveryCodes: string[]; + }; + User2faEnableFinishRequest: { + /** + * Format: byte + * @description Standard base64-encoded binary (legacy tRPC used raw bytes). + */ + loginHash: string; + authenticatorToken: string; + }; + EmailVerificationResendRequest: { + /** Format: email */ + email: string; + }; + EmailVerificationConfirmRequest: { + emailVerificationCode: string; + }; + SessionRefreshSuccess: { + /** Format: byte */ + oldSessionKey: string; + /** Format: byte */ + newSessionKey: string; + }; + StripeCheckoutSessionResponse: { + /** + * Format: uri + * @example https://checkout.stripe.com/c/pay/... + */ + checkoutSessionUrl: string; + }; + StripeCheckoutSessionRequest: { + /** + * @description Defaults to `monthly` when omitted (legacy tRPC). + * @enum {string} + */ + billingFrequency?: "monthly" | "yearly"; + }; + StripePortalSessionResponse: { + /** + * Format: uri + * @example https://billing.stripe.com/p/session/... + */ + portalSessionUrl: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export type operations = Record; diff --git a/new-deepnotes/apps/web/src/api/client.test.ts b/new-deepnotes/apps/web/src/api/client.test.ts new file mode 100644 index 00000000..16d43ed9 --- /dev/null +++ b/new-deepnotes/apps/web/src/api/client.test.ts @@ -0,0 +1,28 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { createDeepnotesApiClient } from "./client"; + +describe("createDeepnotesApiClient", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("sends credentials: include for cookie session auth", async () => { + const fetchMock = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ status: "ok" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + vi.stubGlobal("fetch", fetchMock); + + const client = createDeepnotesApiClient("https://api.example"); + const res = await client.GET("/api/health"); + + expect(res.response.status).toBe(200); + expect(fetchMock).toHaveBeenCalledTimes(1); + const [input] = fetchMock.mock.calls[0] as [Request]; + expect(input.credentials).toBe("include"); + expect(input.url).toContain("/api/health"); + }); +}); diff --git a/new-deepnotes/apps/web/src/api/client.ts b/new-deepnotes/apps/web/src/api/client.ts new file mode 100644 index 00000000..e8d0781b --- /dev/null +++ b/new-deepnotes/apps/web/src/api/client.ts @@ -0,0 +1,23 @@ +import createClient from "openapi-fetch"; + +import type { paths } from "./api-types.generated"; + +/** + * Base URL for the DeepNotes HTTP API (no trailing slash). Empty string uses the + * current origin (Vite dev server proxy or Pages same-origin API). + */ +export function resolveApiBaseUrl(): string { + const raw = import.meta.env.VITE_API_URL ?? ""; + return raw.replace(/\/$/, ""); +} + +/** Typed OpenAPI client with `credentials: "include"` for httpOnly session cookies. */ +export function createDeepnotesApiClient(baseUrl?: string) { + const root = baseUrl ?? resolveApiBaseUrl(); + return createClient({ + baseUrl: root, + credentials: "include", + }); +} + +export type DeepnotesApiClient = ReturnType; diff --git a/new-deepnotes/apps/web/src/api/index.ts b/new-deepnotes/apps/web/src/api/index.ts new file mode 100644 index 00000000..87a461dc --- /dev/null +++ b/new-deepnotes/apps/web/src/api/index.ts @@ -0,0 +1,6 @@ +export { + createDeepnotesApiClient, + resolveApiBaseUrl, + type DeepnotesApiClient, +} from "./client"; +export type { components, paths } from "./api-types.generated"; diff --git a/new-deepnotes/apps/web/src/api/openapi.json b/new-deepnotes/apps/web/src/api/openapi.json new file mode 100644 index 00000000..2f9c130e --- /dev/null +++ b/new-deepnotes/apps/web/src/api/openapi.json @@ -0,0 +1,6253 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "DeepNotes API", + "version": "0.0.0", + "description": "Greenfield HTTP API (REST + OpenAPI). Legacy /trpc is not a compatibility target." + }, + "servers": [ + { + "url": "/" + } + ], + "components": { + "schemas": { + "HealthResponse": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "ok" + ] + }, + "service": { + "type": "string" + } + }, + "required": [ + "status", + "service" + ] + }, + "SessionLoginSuccess": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "sessionId": { + "type": "string" + }, + "sessionKey": { + "type": "string", + "format": "byte", + "description": "Base64-encoded session symmetric key." + }, + "personalGroupId": { + "type": "string" + }, + "publicKeyring": { + "type": "string", + "format": "byte" + }, + "encryptedPrivateKeyring": { + "type": "string", + "format": "byte" + }, + "encryptedSymmetricKeyring": { + "type": "string", + "format": "byte" + } + }, + "required": [ + "userId", + "sessionId", + "sessionKey", + "personalGroupId", + "publicKeyring", + "encryptedPrivateKeyring", + "encryptedSymmetricKeyring" + ] + }, + "SessionErrorResponse": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ] + }, + "ServiceUnavailableResponse": { + "type": "object", + "properties": { + "code": { + "type": "string", + "enum": [ + "SERVICE_UNAVAILABLE" + ] + }, + "message": { + "type": "string" + } + }, + "required": [ + "code", + "message" + ] + }, + "SessionLoginRequest": { + "type": "object", + "properties": { + "email": { + "anyOf": [ + { + "type": "string", + "format": "email" + }, + { + "type": "string", + "enum": [ + "demo" + ] + } + ] + }, + "loginHash": { + "type": "string", + "format": "byte", + "description": "Base64-encoded login hash (legacy wire used binary; prefer standard base64 in JSON)." + }, + "rememberSession": { + "type": "boolean" + }, + "authenticatorToken": { + "type": "string" + }, + "rememberDevice": { + "type": "boolean" + }, + "recoveryCode": { + "type": "string", + "pattern": "^[a-f0-9]{32}$" + } + }, + "required": [ + "email", + "loginHash", + "rememberSession" + ] + }, + "UserRegisterResponse": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "emailVerified": { + "type": "boolean" + } + }, + "required": [ + "userId", + "emailVerified" + ] + }, + "SessionDemoGroupCreation": { + "type": "object", + "properties": { + "groupEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupIsPublic": { + "type": "boolean" + }, + "groupAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupPublicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupOwnerEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupEncryptedName", + "groupIsPublic", + "groupAccessKeyring", + "groupEncryptedInternalKeyring", + "groupEncryptedContentKeyring", + "groupPublicKeyring", + "groupEncryptedPrivateKeyring", + "groupOwnerEncryptedName" + ] + }, + "SessionDemoPageCreation": { + "type": "object", + "properties": { + "pageEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedRelativeTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedAbsoluteTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "pageEncryptedSymmetricKeyring", + "pageEncryptedRelativeTitle", + "pageEncryptedAbsoluteTitle" + ] + }, + "SessionDemoRequest": { + "type": "object", + "properties": { + "userId": { + "type": "string", + "minLength": 21, + "maxLength": 21, + "pattern": "^[A-Za-z0-9_-]{21}$" + }, + "groupId": { + "type": "string", + "minLength": 21, + "maxLength": 21, + "pattern": "^[A-Za-z0-9_-]{21}$" + }, + "pageId": { + "type": "string", + "minLength": 21, + "maxLength": 21, + "pattern": "^[A-Za-z0-9_-]{21}$" + }, + "userPublicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedDefaultNote": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedDefaultArrow": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupCreation": { + "$ref": "#/components/schemas/SessionDemoGroupCreation" + }, + "pageCreation": { + "$ref": "#/components/schemas/SessionDemoPageCreation" + } + }, + "required": [ + "userId", + "groupId", + "pageId", + "userPublicKeyring", + "userEncryptedPrivateKeyring", + "userEncryptedSymmetricKeyring", + "userEncryptedName", + "userEncryptedDefaultNote", + "userEncryptedDefaultArrow", + "groupCreation", + "pageCreation" + ] + }, + "UserRegisterRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/SessionDemoRequest" + }, + { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + }, + "loginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "email", + "loginHash" + ] + } + ] + }, + "UserMeResponse": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "emailVerified": { + "type": "boolean" + }, + "demo": { + "type": "boolean" + }, + "personalGroupId": { + "type": "string" + } + }, + "required": [ + "userId", + "emailVerified", + "demo", + "personalGroupId" + ] + }, + "UserGroupIdsResponse": { + "type": "object", + "properties": { + "groupIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "groupIds" + ] + }, + "UserStartingPageResponse": { + "type": "object", + "properties": { + "startingPageId": { + "type": "string" + } + }, + "required": [ + "startingPageId" + ] + }, + "UserCurrentPathResponse": { + "type": "object", + "properties": { + "pathPageIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "pathPageIds" + ] + }, + "UserPageIdsBody": { + "type": "object", + "properties": { + "pageIds": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "minItems": 1 + } + }, + "required": [ + "pageIds" + ] + }, + "UserDefaultNotePatch": { + "type": "object", + "properties": { + "userEncryptedDefaultNote": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "userEncryptedDefaultNote" + ] + }, + "UserDefaultArrowPatch": { + "type": "object", + "properties": { + "userEncryptedDefaultArrow": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "userEncryptedDefaultArrow" + ] + }, + "UserNotificationItem": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "type": { + "type": "string" + }, + "encryptedSymmetricKey": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedContent": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "dateTime": { + "type": "string" + } + }, + "required": [ + "id", + "type", + "encryptedSymmetricKey", + "encryptedContent", + "dateTime" + ] + }, + "UserNotificationsLoadResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserNotificationItem" + } + }, + "hasMore": { + "type": "boolean" + }, + "lastNotificationRead": { + "type": "integer", + "nullable": true + } + }, + "required": [ + "items", + "hasMore" + ] + }, + "GroupMainPageResponse": { + "type": "object", + "properties": { + "mainPageId": { + "type": "string" + } + }, + "required": [ + "mainPageId" + ] + }, + "GroupMemberUserIdsResponse": { + "type": "object", + "properties": { + "userIds": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "userIds" + ] + }, + "GroupPagesListResponse": { + "type": "object", + "properties": { + "pageIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "hasMore": { + "type": "boolean" + } + }, + "required": [ + "pageIds", + "hasMore" + ] + }, + "GroupPageCreateResponse": { + "type": "object", + "properties": { + "pageId": { + "type": "string" + }, + "numFreePages": { + "type": "integer" + } + }, + "required": [ + "pageId" + ] + }, + "PageMoveGroupCreationRequest": { + "type": "object", + "properties": { + "groupEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupIsPublic": { + "type": "boolean" + }, + "groupAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupPublicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupOwnerEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupEncryptedName", + "groupIsPublic", + "groupAccessKeyring", + "groupEncryptedInternalKeyring", + "groupEncryptedContentKeyring", + "groupPublicKeyring", + "groupEncryptedPrivateKeyring", + "groupOwnerEncryptedName" + ] + }, + "GroupPageCreateRequest": { + "type": "object", + "properties": { + "parentPageId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "pageId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "pageEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedRelativeTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedAbsoluteTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupCreation": { + "$ref": "#/components/schemas/PageMoveGroupCreationRequest" + } + }, + "required": [ + "parentPageId", + "pageId", + "pageEncryptedSymmetricKeyring", + "pageEncryptedRelativeTitle", + "pageEncryptedAbsoluteTitle" + ] + }, + "GroupPasswordEnableRequest": { + "type": "object", + "properties": { + "groupPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupPasswordHash", + "groupEncryptedContentKeyring" + ] + }, + "GroupPasswordChangeRequest": { + "type": "object", + "properties": { + "groupCurrentPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupNewPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupCurrentPasswordHash", + "groupNewPasswordHash", + "groupEncryptedContentKeyring" + ] + }, + "GroupPasswordDisableRequest": { + "type": "object", + "properties": { + "groupPasswordHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "groupPasswordHash", + "groupEncryptedContentKeyring" + ] + }, + "GroupPrivacyPublicRequest": { + "type": "object", + "properties": { + "accessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "accessKeyring" + ] + }, + "GroupPrivacyJoinRequestsPatch": { + "type": "object", + "properties": { + "areJoinRequestsAllowed": { + "type": "boolean" + } + }, + "required": [ + "areJoinRequestsAllowed" + ] + }, + "GroupPrivacyPrivateMember": { + "type": "object", + "properties": { + "encryptedAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedName": { + "type": "string", + "nullable": true, + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedInternalKeyring", + "encryptedName" + ] + }, + "GroupPrivacyPrivateInvitation": { + "type": "object", + "properties": { + "encryptedAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedInternalKeyring", + "encryptedName" + ] + }, + "GroupPrivacyPrivateJoinRequest": { + "type": "object", + "properties": { + "encryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedName" + ] + }, + "GroupPrivacyPrivatePage": { + "type": "object", + "properties": { + "encryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedSymmetricKeyring" + ] + }, + "GroupPrivacyPrivateRequest": { + "type": "object", + "properties": { + "groupAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedContentKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupPublicKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "groupMembers": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyPrivateMember" + } + }, + "groupJoinInvitations": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyPrivateInvitation" + } + }, + "groupJoinRequests": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyPrivateJoinRequest" + } + }, + "groupPages": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/GroupPrivacyPrivatePage" + } + } + }, + "required": [ + "groupEncryptedName", + "groupEncryptedContentKeyring", + "groupPublicKeyring", + "groupEncryptedPrivateKeyring", + "groupMembers", + "groupJoinInvitations", + "groupJoinRequests", + "groupPages" + ] + }, + "GroupMemberRole": { + "type": "string", + "enum": [ + "owner", + "admin", + "moderator", + "member", + "viewer" + ] + }, + "GroupJoinInvitationSendRequest": { + "type": "object", + "properties": { + "inviteeUserId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "invitationRole": { + "$ref": "#/components/schemas/GroupMemberRole" + }, + "encryptedAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedNameForUser": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "inviteeUserId", + "invitationRole", + "encryptedInternalKeyring", + "userEncryptedName", + "userEncryptedNameForUser" + ] + }, + "GroupJoinInvitationAcceptRequest": { + "type": "object", + "properties": { + "userEncryptedName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "userEncryptedName" + ] + }, + "GroupJoinRequestSendRequest": { + "type": "object", + "properties": { + "encryptedUserName": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedUserNameForUser": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedUserName", + "encryptedUserNameForUser" + ] + }, + "GroupJoinRequestAcceptRequest": { + "type": "object", + "properties": { + "targetRole": { + "$ref": "#/components/schemas/GroupMemberRole" + }, + "encryptedAccessKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedInternalKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "targetRole", + "encryptedInternalKeyring" + ] + }, + "GroupMemberRolePatchRequest": { + "type": "object", + "properties": { + "role": { + "$ref": "#/components/schemas/GroupMemberRole" + } + }, + "required": [ + "role" + ] + }, + "PageMoveReencryptRequest": { + "type": "object", + "properties": { + "pageEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedRelativeTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedAbsoluteTitle": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedUpdate": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "pageEncryptedSnapshots": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "encryptedSymmetricKey": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedData": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "encryptedSymmetricKey", + "encryptedData" + ] + }, + "default": {} + } + }, + "required": [ + "pageEncryptedSymmetricKeyring", + "pageEncryptedRelativeTitle", + "pageEncryptedAbsoluteTitle", + "pageEncryptedUpdate" + ] + }, + "PageMoveRequest": { + "type": "object", + "properties": { + "destGroupId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "setAsMainPage": { + "type": "boolean" + }, + "groupCreation": { + "$ref": "#/components/schemas/PageMoveGroupCreationRequest" + }, + "reencrypt": { + "$ref": "#/components/schemas/PageMoveReencryptRequest" + } + }, + "required": [ + "destGroupId", + "setAsMainPage" + ] + }, + "PageBumpRequest": { + "type": "object", + "properties": { + "parentPageId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$" + } + } + }, + "PageCollabUpdatesGetResponse": { + "type": "object", + "properties": { + "lastIndex": { + "type": "integer", + "nullable": true, + "minimum": 0, + "description": "Max `index` in the database, or null if there are no rows." + }, + "updates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "index": { + "type": "integer", + "minimum": 0 + }, + "encryptedData": { + "type": "string", + "format": "byte", + "description": "Base64 ciphertext (`page_updates.encrypted_data`)." + } + }, + "required": [ + "index", + "encryptedData" + ] + } + } + }, + "required": [ + "lastIndex", + "updates" + ] + }, + "PageCollabUpdateItemInput": { + "type": "object", + "properties": { + "index": { + "type": "integer", + "minimum": 0, + "description": "Monotonic index (legacy collab / `page_updates.index`, often Yjs clock)." + }, + "encryptedData": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "index", + "encryptedData" + ] + }, + "PageCollabUpdatesAppendRequest": { + "type": "object", + "properties": { + "expectedLastIndex": { + "type": "integer", + "nullable": true, + "minimum": 0, + "description": "Must match `lastIndex` from GET (`null` when the page has no updates yet)." + }, + "updates": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PageCollabUpdateItemInput" + }, + "minItems": 1 + } + }, + "required": [ + "expectedLastIndex", + "updates" + ] + }, + "PageBacklinkCreateRequest": { + "type": "object", + "properties": { + "sourcePageId": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + } + }, + "required": [ + "sourcePageId" + ] + }, + "PageSnapshotCreateResponse": { + "type": "object", + "properties": { + "snapshotId": { + "type": "string" + } + }, + "required": [ + "snapshotId" + ] + }, + "PageSnapshotSaveRequest": { + "type": "object", + "properties": { + "encryptedSymmetricKey": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "encryptedData": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "preRestore": { + "type": "boolean" + } + }, + "required": [ + "encryptedSymmetricKey", + "encryptedData" + ] + }, + "PageSnapshotLoadResponse": { + "type": "object", + "properties": { + "encryptedSymmetricKey": { + "type": "string", + "nullable": true + }, + "encryptedData": { + "type": "string", + "format": "byte", + "description": "Base64 ciphertext." + } + }, + "required": [ + "encryptedSymmetricKey", + "encryptedData" + ] + }, + "UserPasswordChangeRequest": { + "type": "object", + "properties": { + "oldLoginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "newLoginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "oldLoginHash", + "newLoginHash", + "userEncryptedPrivateKeyring", + "userEncryptedSymmetricKeyring" + ] + }, + "UserEmailChangeRequestResponse": { + "type": "object", + "properties": { + "emailVerificationCode": { + "type": "string", + "pattern": "^\\d{6}$" + } + }, + "required": [ + "emailVerificationCode" + ] + }, + "UserEmailChangeRequest": { + "type": "object", + "properties": { + "oldLoginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "newEmail": { + "type": "string", + "format": "email" + } + }, + "required": [ + "oldLoginHash", + "newEmail" + ] + }, + "UserEmailChangeConfirmRequest": { + "type": "object", + "properties": { + "oldLoginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "emailVerificationCode": { + "type": "string", + "pattern": "^\\d{6}$" + }, + "newLoginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedPrivateKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "userEncryptedSymmetricKeyring": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "oldLoginHash", + "emailVerificationCode", + "newLoginHash", + "userEncryptedPrivateKeyring", + "userEncryptedSymmetricKeyring" + ] + }, + "UserAccountDeleteRequest": { + "type": "object", + "properties": { + "loginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Base64-encoded login hash (same semantics as `POST /api/sessions/login`)." + } + }, + "required": [ + "loginHash" + ] + }, + "User2faEnableRequestResponse": { + "type": "object", + "properties": { + "secret": { + "type": "string" + }, + "keyUri": { + "type": "string", + "description": "otpauth:// URI for authenticator apps." + } + }, + "required": [ + "secret", + "keyUri" + ] + }, + "User2faPasswordBody": { + "type": "object", + "properties": { + "loginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + } + }, + "required": [ + "loginHash" + ] + }, + "User2faRecoveryCodesResponse": { + "type": "object", + "properties": { + "recoveryCodes": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-f0-9]{32}$" + } + } + }, + "required": [ + "recoveryCodes" + ] + }, + "User2faEnableFinishRequest": { + "type": "object", + "properties": { + "loginHash": { + "type": "string", + "minLength": 1, + "format": "byte", + "description": "Standard base64-encoded binary (legacy tRPC used raw bytes)." + }, + "authenticatorToken": { + "type": "string", + "pattern": "^\\d{6}$" + } + }, + "required": [ + "loginHash", + "authenticatorToken" + ] + }, + "EmailVerificationResendRequest": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email" + } + }, + "required": [ + "email" + ] + }, + "EmailVerificationConfirmRequest": { + "type": "object", + "properties": { + "emailVerificationCode": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9_-]{21}$" + } + }, + "required": [ + "emailVerificationCode" + ] + }, + "SessionRefreshSuccess": { + "type": "object", + "properties": { + "oldSessionKey": { + "type": "string", + "format": "byte" + }, + "newSessionKey": { + "type": "string", + "format": "byte" + } + }, + "required": [ + "oldSessionKey", + "newSessionKey" + ] + }, + "StripeCheckoutSessionResponse": { + "type": "object", + "properties": { + "checkoutSessionUrl": { + "type": "string", + "format": "uri", + "example": "https://checkout.stripe.com/c/pay/..." + } + }, + "required": [ + "checkoutSessionUrl" + ] + }, + "StripeCheckoutSessionRequest": { + "type": "object", + "properties": { + "billingFrequency": { + "type": "string", + "enum": [ + "monthly", + "yearly" + ], + "description": "Defaults to `monthly` when omitted (legacy tRPC)." + } + } + }, + "StripePortalSessionResponse": { + "type": "object", + "properties": { + "portalSessionUrl": { + "type": "string", + "format": "uri", + "example": "https://billing.stripe.com/p/session/..." + } + }, + "required": [ + "portalSessionUrl" + ] + } + }, + "parameters": {} + }, + "paths": { + "/api/health": { + "get": { + "summary": "Health check", + "responses": { + "200": { + "description": "API is reachable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HealthResponse" + } + } + } + } + } + } + }, + "/api/sessions/login": { + "post": { + "summary": "Create session (email + login hash)", + "description": "Replaces legacy `sessions.login`. Sets httpOnly cookies when implemented.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionLoginRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Login succeeded; cookies set.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionLoginSuccess" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "429": { + "description": "Too many failed login attempts (rate limited).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users": { + "post": { + "summary": "Register a new account", + "description": "Replaces legacy `users.account.register`. Creates user, personal group, and first page; sets email verification unless `SEND_EMAILS=false` (then verifies immediately, legacy parity).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRegisterRequest" + } + } + } + }, + "responses": { + "201": { + "description": "User created. `emailVerified` is true when outbound mail is disabled (`SEND_EMAILS=false`).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserRegisterResponse" + } + } + } + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "409": { + "description": "Resource already exists (e.g. email already registered).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "502": { + "description": "Email send failed (e.g. Resend API error after user row was created; rare).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me": { + "get": { + "summary": "Current user (from access cookie)", + "description": "Minimal account summary for the authenticated user (`accessToken` cookie).", + "responses": { + "200": { + "description": "Authenticated user.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserMeResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "delete": { + "summary": "Delete current account (password confirmation)", + "description": "Replaces legacy `users.account.delete`. Requires `accessToken` cookie and correct `loginHash` in the JSON body. Clears session cookies on success. Optional Stripe customer deletion is handled by the deployment (not part of OpenAPI).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserAccountDeleteRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Account removed; session cookies cleared (same names as login)." + }, + "400": { + "description": "Wrong password, ownership constraint, or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/groups": { + "get": { + "summary": "List group IDs for the current user", + "description": "Replaces legacy `users.pages.getGroupIds`. Returns `group_id` values from `group_members` ordered by recent activity (desc).", + "responses": { + "200": { + "description": "Ordered group ids.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserGroupIdsResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/starting": { + "get": { + "summary": "Starting page id for the current user", + "description": "Replaces legacy `users.pages.getStartingPageId` (reads `users.starting_page_id`).", + "responses": { + "200": { + "description": "Nanoid of the user’s starting page.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserStartingPageResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/path": { + "get": { + "summary": "Breadcrumb path from a page to the personal main page", + "description": "Replaces legacy `users.pages.getCurrentPath`. Uses `users_pages.last_parent_id` and may repair a missing parent link once (legacy KeyDB behavior).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "Page to resolve toward the personal group main page.", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "initialPageId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Ordered page ids from root (personal main) to `initialPageId`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserCurrentPathResponse" + } + } + } + }, + "400": { + "description": "Missing or invalid `initialPageId` query parameter.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/recent/remove": { + "post": { + "summary": "Remove page ids from recent list", + "description": "Replaces legacy `users.pages.removeRecentPages`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPageIdsBody" + } + } + } + }, + "responses": { + "204": { + "description": "Updated `users.recent_page_ids`." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/recent/clear": { + "post": { + "summary": "Clear recent pages", + "description": "Replaces legacy `users.pages.clearRecentPages`.", + "responses": { + "204": { + "description": "Recent list emptied." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/favorites": { + "post": { + "summary": "Add favorite pages", + "description": "Replaces legacy `users.pages.addFavoritePages`. Favorites are stored in Postgres (`users.favorite_page_ids`); legacy used KeyDB only.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPageIdsBody" + } + } + } + }, + "responses": { + "204": { + "description": "Favorites merged (order: new ids first, then existing)." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/favorites/remove": { + "post": { + "summary": "Remove favorite pages", + "description": "Replaces legacy `users.pages.removeFavoritePages`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPageIdsBody" + } + } + } + }, + "responses": { + "204": { + "description": "Favorites updated." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/pages/favorites/clear": { + "post": { + "summary": "Clear favorite pages", + "description": "Replaces legacy `users.pages.clearFavoritePages`.", + "responses": { + "204": { + "description": "Favorites emptied." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/defaults/note": { + "patch": { + "summary": "Update encrypted default note template", + "description": "Replaces legacy `users.pages.setEncryptedDefaultNote`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDefaultNotePatch" + } + } + } + }, + "responses": { + "204": { + "description": "`users.encrypted_default_note` updated." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/defaults/arrow": { + "patch": { + "summary": "Update encrypted default arrow template", + "description": "Replaces legacy `users.pages.setEncryptedDefaultArrow`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserDefaultArrowPatch" + } + } + } + }, + "responses": { + "204": { + "description": "`users.encrypted_default_arrow` updated." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/notifications": { + "get": { + "summary": "Load notifications for the current user", + "description": "Replaces legacy `users.pages.notifications.load`. Ciphertext fields are base64 in JSON.", + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "description": "Return notifications strictly older than this id (legacy pagination)." + }, + "required": false, + "name": "lastNotificationId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Window of notifications and optional `lastNotificationRead`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserNotificationsLoadResponse" + } + } + } + }, + "400": { + "description": "Invalid query parameters.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/notifications/read": { + "post": { + "summary": "Mark all notifications as read", + "description": "Replaces legacy `users.pages.notifications.markAsRead`. Sets `users.last_notification_read` to the latest linked notification id.", + "responses": { + "204": { + "description": "Read cursor updated (no-op if user has no notifications)." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/main-page": { + "get": { + "summary": "Get the group main page id", + "description": "Replaces legacy `groups.getMainPageId` (KeyDB `main-page-id`). Source: `groups.main_page_id`. Requires `viewGroupPages` (same as listing pages).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Main page id for the group.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupMainPageResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/members": { + "get": { + "summary": "List user ids (members, requests, invitations)", + "description": "Replaces legacy `groups.getUserIds`: union of `group_members`, `group_join_requests`, and `group_join_invitations` for the group. Requires `viewGroupMembers` (not granted for public read without membership).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Distinct user ids (unordered).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupMemberUserIdsResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/pages": { + "get": { + "summary": "List page IDs in a group", + "description": "Replaces legacy `groups.getPages` (authenticated). Optional `lastPageId` cursor for pagination (newest `last_activity_date` first). Omits soft-deleted pages (`permanent_deletion_date` set). Public groups allow `viewGroupPages` without membership.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "Pagination cursor: return pages older than this page's activity (legacy `lastPageId`)." + }, + "required": false, + "name": "lastPageId", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Page id window (max 20) and `hasMore`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPagesListResponse" + } + } + } + }, + "400": { + "description": "Invalid `lastPageId` (not in group).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "post": { + "summary": "Create a page in a group", + "description": "Replaces legacy `pages.create`. For an **existing** group, `parentPageId` must be a page in that group and the caller needs `editGroupPages`. With optional `groupCreation`, path `groupId` is a **new** nanoid (no row yet), `parentPageId` is a page in the user’s **personal** group, and the body includes the same ciphertext as `PageMoveGroupCreationRequest` — Pro only; creates the `groups` + owner `group_members` rows then the first page (legacy parity). The 50 free-page cap applies to non‑Pro users for normal creates.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPageCreateRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Page and `users_pages` row created.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPageCreateResponse" + } + } + } + }, + "400": { + "description": "Invalid parent page or body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/password": { + "post": { + "summary": "Enable group password (Pro)", + "description": "Replaces `groups.password.enable`. Argon2id is applied on the server to the provided `groupPasswordHash` material (base64) and stored encrypted. Requires `editGroupSettings` and a Pro plan.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPasswordEnableRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Password protection enabled." + }, + "400": { + "description": "Already protected or bad password material.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "patch": { + "summary": "Change group password (Pro)", + "description": "Replaces `groups.password.change`. Verifies the current group password, then re-wraps the content keyring.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPasswordChangeRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Password updated." + }, + "400": { + "description": "Wrong password, or group not protected.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "delete": { + "summary": "Disable group password (not Pro check in legacy for disable-only)", + "description": "Replaces `groups.password.disable`. Verifies the current group password, removes server-side group password, updates `groupEncryptedContentKeyring`.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPasswordDisableRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Password protection disabled." + }, + "400": { + "description": "Wrong password, or not protected.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/privacy/public": { + "post": { + "summary": "Make group public (Pro)", + "description": "Replaces `groups.privacy.makePublic`. Sets `access_keyring` and clears member/invite `encrypted_access_keyring`.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPrivacyPublicRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Group is public." + }, + "400": { + "description": "Already public.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/privacy/join-requests": { + "patch": { + "summary": "Allow or reject join requests (Pro)", + "description": "Replaces `groups.privacy.setJoinRequestsAllowed`.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPrivacyJoinRequestsPatch" + } + } + } + }, + "responses": { + "204": { + "description": "Setting updated." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/privacy/private": { + "post": { + "summary": "Make group private (Pro) — full re-key payload", + "description": "Replaces legacy WS `groups.privacy.makePrivate` (step 2 `rotateGroupKeys`) in one request. Clears `access_keyring` when `groupAccessKeyring` is omitted. Member / invitation / request / page record keys must match the DB exactly.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupPrivacyPrivateRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Group is private; ciphertext updated." + }, + "400": { + "description": "Already private or payload key sets do not match group.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}": { + "delete": { + "summary": "Soft-delete group (grace period)", + "description": "Replaces `groups.deletion.delete`. Sets `permanent_deletion_date` ~1 month ahead.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Deletion scheduled." + }, + "400": { + "description": "Already soft-deleted.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/restore": { + "post": { + "summary": "Restore a soft-deleted group", + "description": "Replaces `groups.deletion.restore` during the grace period (`permanent_deletion_date` in the future).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Group removed from scheduled deletion." + }, + "400": { + "description": "Not soft-deleted, or no longer in grace period.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/purge": { + "post": { + "summary": "Permanently mark group deleted (purge active or grace state)", + "description": "Replaces `groups.deletion.deletePermanently` — `permanent_deletion_date` set in the past (legacy).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Purge recorded." + }, + "400": { + "description": "Already purged.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-invitations": { + "post": { + "summary": "Send a group join invitation (Pro)", + "description": "Replaces legacy WS `groups.joinInvitations.send` step 1. Deletes a conflicting join request for the invitee. For private groups, `encryptedAccessKeyring` is required; for public groups it is stored as null.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupJoinInvitationSendRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Invitation created." + }, + "400": { + "description": "Already invited, already a member, or missing keyring for private group.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-invitations/me/accept": { + "post": { + "summary": "Accept a pending join invitation (Pro)", + "description": "Replaces legacy WS `groups.joinInvitations.accept` step 1.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupJoinInvitationAcceptRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Invitation consumed; user added to `group_members`." + }, + "400": { + "description": "Validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-invitations/me/reject": { + "post": { + "summary": "Reject a pending join invitation", + "description": "Replaces legacy WS `groups.joinInvitations.reject` step 1 (no Pro check in legacy).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Invitation removed." + }, + "400": { + "description": "No pending invitation.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-invitations/{userId}": { + "delete": { + "summary": "Cancel a join invitation (Pro)", + "description": "Replaces legacy WS `groups.joinInvitations.cancel` step 1. Path `userId` is the invitee. Requires permission to manage the invited role.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Invitation removed." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-requests": { + "post": { + "summary": "Send a join request (Pro)", + "description": "Replaces legacy WS `groups.joinRequests.send` step 1. Requires `are_join_requests_allowed` on the group.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupJoinRequestSendRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Join request created." + }, + "400": { + "description": "Already pending.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-requests/{userId}/accept": { + "post": { + "summary": "Accept a join request (Pro)", + "description": "Replaces legacy WS `groups.joinRequests.accept` step 1. Path `userId` is the requester.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupJoinRequestAcceptRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Requester added to `group_members`." + }, + "400": { + "description": "No pending request or missing access keyring for private group.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-requests/{userId}/reject": { + "post": { + "summary": "Reject a join request (Pro)", + "description": "Replaces legacy WS `groups.joinRequests.reject` step 1. Sets `rejected` on the request (legacy does not delete the row).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Request marked rejected." + }, + "400": { + "description": "No pending request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/join-requests/me/cancel": { + "post": { + "summary": "Cancel own join request (Pro)", + "description": "Replaces legacy WS `groups.joinRequests.cancel` step 1.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Join request row deleted." + }, + "400": { + "description": "No pending request.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/groups/{groupId}/members/{userId}": { + "patch": { + "summary": "Change a member's role (Pro)", + "description": "Replaces legacy WS `groups.changeUserRole` step 1.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GroupMemberRolePatchRequest" + } + } + } + }, + "responses": { + "204": { + "description": "`group_members.role` updated." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "delete": { + "summary": "Remove a member (or leave)", + "description": "Replaces legacy WS `groups.removeUser` step 1. Callers may remove themselves without `canManageRole` on others.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "groupId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "userId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Membership removed." + }, + "400": { + "description": "Cannot remove the last owner.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/move": { + "post": { + "summary": "Move page (optionally create group, re-key, set main)", + "description": "Replaces `websocket/pages/move` — Pro-only; `editGroupSettings` on the page's current group, `editGroupPages` on destination unless `groupCreation` creates it. `reencrypt` is required when the page changes group (Yjs `page_updates` replaced with a single index-0 row; snapshots updated by id).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageMoveRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Move completed." + }, + "400": { + "description": "No-op move, or invalid payload.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/bump": { + "post": { + "summary": "Bump page (recents, activity, optional breadcrumb parent)", + "description": "Replaces `pages.bump` — `users` starting + recents, optional `users_pages.last_parent_id` when the parent chain ends at the personal main page (`lastParentId` walk).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageBumpRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Bumped (best-effort; loop in chain exits without updating parent)." + }, + "400": { + "description": "Invalid parent (chain does not resolve to main page).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/collab-updates": { + "get": { + "summary": "List encrypted Yjs page updates (Postgres)", + "description": "Bootstrap for the editor: returns all `page_updates` rows for the page, ordered by `index`. Does not use legacy Redis collab cache — Postgres only. Full duplex collab remains a separate WebSocket track (Phase 3).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Current ciphertext chain.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageCollabUpdatesGetResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "post": { + "summary": "Append page updates (optimistic concurrency)", + "description": "Appends ciphertext rows to `page_updates`. `expectedLastIndex` must match the current max index (or null when empty). **409** when another writer advanced the chain — client should re-GET and retry.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageCollabUpdatesAppendRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Updates persisted." + }, + "400": { + "description": "Bad index sequence or wrong `expectedLastIndex` for an empty page.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "409": { + "description": "Stale `expectedLastIndex` (concurrent append).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/backlinks": { + "post": { + "summary": "Create page backlink (source → this page as target)", + "description": "Replaces `pages.backlinks.create`. Path `pageId` is the **target**; body has `sourcePageId`.", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageBacklinkCreateRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Backlink created or activity updated (upsert)." + }, + "400": { + "description": "Source and target identical.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/backlinks/{targetPageId}": { + "delete": { + "summary": "Delete backlink from source page to target page", + "description": "Replaces `pages.backlinks.delete`. Path `pageId` is **source**; `targetPageId` is the link target (legacy input names).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "targetPageId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Backlink removed." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/snapshots": { + "post": { + "summary": "Save encrypted page snapshot (Pro)", + "description": "Replaces `pages.snapshots.save` — asserts Pro plan (legacy `assertUserSubscribed`).", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageSnapshotSaveRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Snapshot id", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageSnapshotCreateResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/snapshots/{snapshotId}": { + "get": { + "summary": "Load page snapshot ciphertext (Pro)", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "snapshotId", + "in": "path" + } + ], + "responses": { + "200": { + "description": "Ciphertext (base64 fields).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PageSnapshotLoadResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + }, + "delete": { + "summary": "Delete a page snapshot", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + }, + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "snapshotId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Snapshot removed." + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}": { + "delete": { + "summary": "Soft-delete page (grace period)", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Deletion scheduled (not main page)." + }, + "400": { + "description": "Already deleted, or is group main page.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/restore": { + "post": { + "summary": "Restore a soft-deleted page", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Page restored in grace." + }, + "400": { + "description": "Not deleted, or free page past purge date.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/pages/{pageId}/purge": { + "post": { + "summary": "Permanently mark page deleted; refunds free page when applicable", + "parameters": [ + { + "schema": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]{21}$", + "description": "21-character nanoid (URL-safe alphabet).", + "example": "V1StGXR8_Z5jdHi6B-myT" + }, + "required": true, + "name": "pageId", + "in": "path" + } + ], + "responses": { + "204": { + "description": "Purge recorded." + }, + "400": { + "description": "Is main page, or already purged.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/password": { + "post": { + "summary": "Change password (re-wrap keyrings)", + "description": "Replaces legacy WebSocket `users.account.changePassword`. Requires `accessToken`; verifies `oldLoginHash`; stores keyrings encrypted with the new password (`userEncrypted*` are plaintext keyrings from the client, same as registration). Invalidates all sessions and clears cookies — client must log in again.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserPasswordChangeRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Password updated; all sessions invalidated; session cookies cleared." + }, + "400": { + "description": "Wrong current password or invalid key material.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/email-change": { + "post": { + "summary": "Request account email change (6-digit code email)", + "description": "Replaces legacy `users.account.emailChange.request`. Verifies `oldLoginHash` and that the new address is not already registered. When outbound email is enabled, sends a 6-digit code. When `SEND_EMAILS=false` (e.g. local), returns 200 with `emailVerificationCode` instead of emailing.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserEmailChangeRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Out-of-band dev response when `SEND_EMAILS=false` (verification code not emailed).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserEmailChangeRequestResponse" + } + } + } + }, + "204": { + "description": "Code emailed to the new address; pending change stored on the user row." + }, + "400": { + "description": "Wrong password, address in use, or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "502": { + "description": "Email send failed (e.g. Resend) after the pending state was written.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/email-change/confirm": { + "post": { + "summary": "Confirm email change (re-wrap keyrings, new password)", + "description": "Replaces legacy WebSocket `users.account.emailChange.finish` (step 1 + 2 in one). Verifies 6-digit code and `oldLoginHash`, then applies new email + new password-encrypted keyrings, invalidates sessions, clears cookies; optional Stripe customer email update in the deployment (not in OpenAPI).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserEmailChangeConfirmRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Email updated; sessions cleared; re-login required." + }, + "400": { + "description": "Wrong code, wrong password, no pending change, or invalid keyrings.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/enable/request": { + "post": { + "summary": "Start 2FA setup (TOTP secret + otpauth URI)", + "description": "Replaces `users.account.twoFactorAuth.enable.request`. Stores a pending encrypted authenticator secret; client shows QR from `keyUri` or `secret`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faPasswordBody" + } + } + } + }, + "responses": { + "200": { + "description": "Secret generated; not yet enabled until `…/enable/finish`.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faEnableRequestResponse" + } + } + } + }, + "400": { + "description": "Validation error, or 2FA already fully enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/enable/finish": { + "post": { + "summary": "Complete 2FA setup (TOTP + recovery codes)", + "description": "Replaces `users.account.twoFactorAuth.enable.finish`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faEnableFinishRequest" + } + } + } + }, + "responses": { + "200": { + "description": "2FA enabled; one-time recovery codes returned.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faRecoveryCodesResponse" + } + } + } + }, + "400": { + "description": "Wrong password, wrong TOTP, or already enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/load": { + "post": { + "summary": "Reveal TOTP secret and otpauth URI (after password check)", + "description": "Replaces `users.account.twoFactorAuth.load` (legacy tRPC had `loginHash` in the query; this API uses a JSON body on POST to avoid putting secrets in query strings or logs).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faPasswordBody" + } + } + } + }, + "responses": { + "200": { + "description": "Secret and `keyUri` for re-provisioning an authenticator.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faEnableRequestResponse" + } + } + } + }, + "400": { + "description": "Wrong password or 2FA not enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/recovery-codes": { + "post": { + "summary": "Regenerate recovery codes", + "description": "Replaces `users.account.twoFactorAuth.generateRecoveryCodes`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faPasswordBody" + } + } + } + }, + "responses": { + "200": { + "description": "New recovery codes (previous codes invalidated).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faRecoveryCodesResponse" + } + } + } + }, + "400": { + "description": "Wrong password or 2FA not enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/devices/forget": { + "post": { + "summary": "Mark all user devices as not trusted", + "description": "Replaces `users.account.twoFactorAuth.forgetTrustedDevices`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faPasswordBody" + } + } + } + }, + "responses": { + "204": { + "description": "`devices.trusted` cleared for this user." + }, + "400": { + "description": "Wrong password or 2FA not enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/me/2fa/disable": { + "post": { + "summary": "Disable 2FA", + "description": "Replaces `users.account.twoFactorAuth.disable`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User2faPasswordBody" + } + } + } + }, + "responses": { + "204": { + "description": "2FA disabled; authenticator and recovery material cleared." + }, + "400": { + "description": "Wrong password or 2FA not enabled.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/email-verification/resend": { + "post": { + "summary": "Resend email verification (public, by email)", + "description": "Replaces legacy `users.account.resendVerificationEmail`. Uses Resend when `SEND_EMAILS` is not `false`.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailVerificationResendRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Email sent (or accepted by provider)." + }, + "400": { + "description": "Validation error or outbound email disabled for this environment.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "409": { + "description": "Resource already exists (e.g. email already registered).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "502": { + "description": "Email provider (Resend) request failed.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/users/email-verification/confirm": { + "post": { + "summary": "Confirm email with nanoid code", + "description": "Replaces legacy `users.account.verifyEmail` (public).", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EmailVerificationConfirmRequest" + } + } + } + }, + "responses": { + "204": { + "description": "Email verified; account updated." + }, + "400": { + "description": "Invalid or expired code.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + } + } + } + }, + "/api/sessions/refresh": { + "post": { + "summary": "Rotate access token using refresh cookie", + "description": "Replaces legacy `sessions.refresh`.", + "responses": { + "200": { + "description": "New session key and cookies.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionRefreshSuccess" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/sessions/logout": { + "post": { + "summary": "Invalidate session and clear cookies", + "description": "Replaces legacy `sessions.logout`.", + "responses": { + "204": { + "description": "Logged out (cookies cleared)." + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/sessions/demo": { + "post": { + "summary": "Create demo user and session", + "description": "Replaces legacy `sessions.startDemo`. Request body will match registration key material once defined.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionDemoRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Demo user created; same response shape as login.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionLoginSuccess" + } + } + } + }, + "400": { + "description": "Validation error (e.g. unsupported group password on demo).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/billing/stripe/checkout-session": { + "post": { + "summary": "Create Stripe Checkout (subscription) session", + "description": "Replaces legacy `users.account.stripe.createCheckoutSession`. Resolves or creates a Stripe customer from `users.customer_id` and decrypted account email, then returns a hosted Checkout URL. Requires verified email. Demo accounts receive **403**.", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StripeCheckoutSessionRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Checkout session URL (hosted Stripe page).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StripeCheckoutSessionResponse" + } + } + } + }, + "400": { + "description": "Already subscribed, or validation error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/billing/stripe/portal-session": { + "post": { + "summary": "Create Stripe Customer Portal session", + "description": "Replaces legacy `users.account.stripe.createPortalSession`. Requires a `users.customer_id` (create checkout first or migrate from legacy).", + "responses": { + "200": { + "description": "Portal session URL.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StripePortalSessionResponse" + } + } + } + }, + "400": { + "description": "No Stripe customer on file.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials, token, or session state.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "403": { + "description": "Action not allowed for this account (e.g. demo user).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "404": { + "description": "Resource not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + }, + "/api/webhooks/stripe": { + "post": { + "summary": "Stripe webhooks (signed raw body)", + "description": "Replaces the legacy Fastify `POST /stripe/webhook` handler. Send the **raw** request body; verification uses the `Stripe-Signature` header and `STRIPE_WEBHOOK_SECRET`. Handles `customer.subscription.updated` and `customer.subscription.deleted` by updating `users.plan` and `users.subscription_id` via `users.customer_id`.", + "responses": { + "200": { + "description": "Event acknowledged." + }, + "400": { + "description": "Missing or invalid signature.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionErrorResponse" + } + } + } + }, + "503": { + "description": "Required auth environment variables are not configured (local: copy template.env / .dev.vars).", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServiceUnavailableResponse" + } + } + } + } + } + } + } + } +} diff --git a/new-deepnotes/apps/web/src/vite-env.d.ts b/new-deepnotes/apps/web/src/vite-env.d.ts index 11f02fe2..a19c2d5f 100644 --- a/new-deepnotes/apps/web/src/vite-env.d.ts +++ b/new-deepnotes/apps/web/src/vite-env.d.ts @@ -1 +1,10 @@ /// + +interface ImportMetaEnv { + /** Origin of the HTTP API (no trailing slash). Omit for same-origin requests. */ + readonly VITE_API_URL?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml index 0c8d6b49..cf228355 100644 --- a/new-deepnotes/pnpm-lock.yaml +++ b/new-deepnotes/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: apps/web: dependencies: + openapi-fetch: + specifier: ^0.17.0 + version: 0.17.0 vue: specifier: ^3.5.13 version: 3.5.33(typescript@5.9.3) @@ -76,6 +79,12 @@ importers: happy-dom: specifier: ^17.4.4 version: 17.6.3 + openapi-typescript: + specifier: ^7.13.0 + version: 7.13.0(typescript@5.9.3) + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: ^5.8.3 version: 5.9.3 @@ -192,6 +201,10 @@ packages: peerDependencies: zod: ^3.20.2 + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -1149,6 +1162,16 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@redocly/ajv@8.11.2': + resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==} + + '@redocly/config@0.22.0': + resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==} + + '@redocly/openapi-core@1.34.13': + resolution: {integrity: sha512-4Tm4ysZkexx6ZTX7knqSZTqPlNgIvXc7Ha0pd30I694/GD0KtJE2xrElycfPds0vCLFAqoKyIzBtOF1xrLo8KA==} + engines: {node: '>=18.17.0', npm: '>=9.5.0'} + '@rollup/rollup-android-arm-eabi@4.60.2': resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} cpu: [arm] @@ -1500,12 +1523,20 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} alien-signals@1.0.13: resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1576,6 +1607,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + change-case@5.4.4: + resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -1587,6 +1621,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@1.4.0: + resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -1957,6 +1994,10 @@ packages: resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==} engines: {node: '>=16.9.0'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1973,6 +2014,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -2010,6 +2055,13 @@ packages: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} + js-levenshtein@1.1.6: + resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} + engines: {node: '>=0.10.0'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -2023,6 +2075,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2075,6 +2130,10 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + minimatch@9.0.9: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} @@ -2122,6 +2181,18 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + openapi-fetch@0.17.0: + resolution: {integrity: sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==} + + openapi-typescript-helpers@0.1.0: + resolution: {integrity: sha512-OKTGPthhivLw/fHz6c3OPtg72vi86qaMlqbJuVJ23qOvQ+53uw1n7HdmkJFibloF7QEjDrDkzJiOJuockM/ljw==} + + openapi-typescript@7.13.0: + resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==} + hasBin: true + peerDependencies: + typescript: ^5.x + openapi3-ts@4.5.0: resolution: {integrity: sha512-jaL+HgTq2Gj5jRcfdutgRGLosCy/hT8sQf6VOy+P+g36cZOjI1iukdPnijC+4CmeRzg/jEllJUboEic2FhxhtQ==} @@ -2147,6 +2218,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2179,6 +2254,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + postcss@8.5.12: resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} @@ -2202,6 +2281,10 @@ packages: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2358,6 +2441,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typescript-eslint@8.59.0: resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2383,6 +2470,9 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + uri-js-replace@1.0.1: + resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2541,11 +2631,18 @@ packages: utf-8-validate: optional: true + yaml-ast-parser@0.0.43: + resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} + yaml@2.8.3: resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} engines: {node: '>= 14.6'} hasBin: true + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2566,6 +2663,12 @@ snapshots: openapi3-ts: 4.5.0 zod: 3.25.76 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -2935,7 +3038,7 @@ snapshots: '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 3.1.5 transitivePeerDependencies: - supports-color @@ -2951,7 +3054,7 @@ snapshots: '@eslint/eslintrc@3.3.5': dependencies: ajv: 6.15.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 @@ -3162,6 +3265,29 @@ snapshots: '@poppinss/exception@1.2.3': {} + '@redocly/ajv@8.11.2': + dependencies: + fast-deep-equal: 3.1.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + uri-js-replace: 1.0.1 + + '@redocly/config@0.22.0': {} + + '@redocly/openapi-core@1.34.13(supports-color@10.2.2)': + dependencies: + '@redocly/ajv': 8.11.2 + '@redocly/config': 0.22.0 + colorette: 1.4.0 + https-proxy-agent: 7.0.6(supports-color@10.2.2) + js-levenshtein: 1.1.6 + js-yaml: 4.1.1 + minimatch: 5.1.9 + pluralize: 8.0.0 + yaml-ast-parser: 0.0.43 + transitivePeerDependencies: + - supports-color + '@rollup/rollup-android-arm-eabi@4.60.2': optional: true @@ -3298,7 +3424,7 @@ snapshots: '@typescript-eslint/types': 8.59.0 '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.59.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 9.39.4 typescript: 5.9.3 transitivePeerDependencies: @@ -3308,7 +3434,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) '@typescript-eslint/types': 8.59.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3327,7 +3453,7 @@ snapshots: '@typescript-eslint/types': 8.59.0 '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) eslint: 9.39.4 ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 @@ -3342,7 +3468,7 @@ snapshots: '@typescript-eslint/tsconfig-utils': 8.59.0(typescript@5.9.3) '@typescript-eslint/types': 8.59.0 '@typescript-eslint/visitor-keys': 8.59.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.5 semver: 7.7.4 tinyglobby: 0.2.16 @@ -3519,6 +3645,8 @@ snapshots: acorn@8.16.0: {} + agent-base@7.1.4: {} + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -3528,6 +3656,8 @@ snapshots: alien-signals@1.0.13: {} + ansi-colors@4.1.3: {} + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -3590,6 +3720,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + change-case@5.4.4: {} + check-error@2.1.3: {} color-convert@2.0.1: @@ -3598,6 +3730,8 @@ snapshots: color-name@1.1.4: {} + colorette@1.4.0: {} + commander@10.0.1: {} concat-map@0.0.1: {} @@ -3621,9 +3755,11 @@ snapshots: de-indent@1.0.2: {} - debug@4.4.3: + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 deep-eql@5.0.2: {} @@ -3824,7 +3960,7 @@ snapshots: ajv: 6.15.0 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -3911,7 +4047,7 @@ snapshots: gel@2.2.0: dependencies: '@petamoriken/float16': 3.9.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) env-paths: 3.0.0 semver: 7.7.4 shell-quote: 1.8.3 @@ -3976,6 +4112,13 @@ snapshots: hono@4.12.15: {} + https-proxy-agent@7.0.6(supports-color@10.2.2): + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + ignore@5.3.2: {} ignore@7.0.5: {} @@ -3987,6 +4130,8 @@ snapshots: imurmurhash@0.1.4: {} + index-to-position@1.2.0: {} + ini@1.3.8: {} is-extglob@2.1.1: {} @@ -4020,6 +4165,10 @@ snapshots: js-cookie@3.0.5: {} + js-levenshtein@1.1.6: {} + + js-tokens@4.0.0: {} + js-tokens@9.0.1: {} js-yaml@4.1.1: @@ -4030,6 +4179,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} keyv@4.5.4: @@ -4085,6 +4236,10 @@ snapshots: dependencies: brace-expansion: 1.1.14 + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.0 + minimatch@9.0.9: dependencies: brace-expansion: 2.1.0 @@ -4128,6 +4283,22 @@ snapshots: object-inspect@1.13.4: {} + openapi-fetch@0.17.0: + dependencies: + openapi-typescript-helpers: 0.1.0 + + openapi-typescript-helpers@0.1.0: {} + + openapi-typescript@7.13.0(typescript@5.9.3): + dependencies: + '@redocly/openapi-core': 1.34.13(supports-color@10.2.2) + ansi-colors: 4.1.3 + change-case: 5.4.4 + parse-json: 8.3.0 + supports-color: 10.2.2 + typescript: 5.9.3 + yargs-parser: 21.1.1 + openapi3-ts@4.5.0: dependencies: yaml: 2.8.3 @@ -4161,6 +4332,12 @@ snapshots: dependencies: callsites: 3.1.0 + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.29.0 + index-to-position: 1.2.0 + type-fest: 4.41.0 + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -4182,6 +4359,8 @@ snapshots: picomatch@4.0.4: {} + pluralize@8.0.0: {} + postcss@8.5.12: dependencies: nanoid: 3.3.11 @@ -4200,6 +4379,8 @@ snapshots: dependencies: side-channel: 1.1.0 + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4403,6 +4584,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@4.41.0: {} + typescript-eslint@8.59.0(eslint@9.39.4)(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) @@ -4426,6 +4609,8 @@ snapshots: dependencies: pathe: 2.0.3 + uri-js-replace@1.0.1: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -4433,7 +4618,7 @@ snapshots: vite-node@3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) @@ -4476,7 +4661,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.3.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) expect-type: 1.3.0 magic-string: 0.30.21 pathe: 2.0.3 @@ -4586,8 +4771,12 @@ snapshots: ws@8.18.0: {} + yaml-ast-parser@0.0.43: {} + yaml@2.8.3: {} + yargs-parser@21.1.1: {} + yocto-queue@0.1.0: {} youch-core@0.3.3: From d7e4c777c3d8078c8d3329511f56f524d26ebaa8 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 12:44:57 -0300 Subject: [PATCH 056/243] feat(new-deepnotes): Vue login, useSession, router, and home --- new-deepnotes/PLAN_PROGRESS.md | 28 +- new-deepnotes/apps/web/README.md | 22 ++ new-deepnotes/apps/web/package.json | 5 +- new-deepnotes/apps/web/src/App.vue | 127 +++++++- new-deepnotes/apps/web/src/app.test.ts | 24 +- .../apps/web/src/features/auth/LoginView.vue | 256 ++++++++++++++++ .../src/features/auth/build-demo-session.ts | 49 +++ .../apps/web/src/features/auth/bytes.test.ts | 17 ++ .../apps/web/src/features/auth/bytes.ts | 14 + .../apps/web/src/features/auth/cookies.ts | 12 + .../apps/web/src/features/auth/useSession.ts | 214 +++++++++++++ .../apps/web/src/features/home/HomeView.vue | 90 ++++++ new-deepnotes/apps/web/src/main.ts | 5 +- new-deepnotes/apps/web/src/router.ts | 20 ++ new-deepnotes/apps/web/vite.config.ts | 9 + new-deepnotes/pnpm-lock.yaml | 285 ++++++++++++++++++ 16 files changed, 1152 insertions(+), 25 deletions(-) create mode 100644 new-deepnotes/apps/web/README.md create mode 100644 new-deepnotes/apps/web/src/features/auth/LoginView.vue create mode 100644 new-deepnotes/apps/web/src/features/auth/build-demo-session.ts create mode 100644 new-deepnotes/apps/web/src/features/auth/bytes.test.ts create mode 100644 new-deepnotes/apps/web/src/features/auth/bytes.ts create mode 100644 new-deepnotes/apps/web/src/features/auth/cookies.ts create mode 100644 new-deepnotes/apps/web/src/features/auth/useSession.ts create mode 100644 new-deepnotes/apps/web/src/features/home/HomeView.vue create mode 100644 new-deepnotes/apps/web/src/router.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 402f07ef..d0c4ce19 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -14,7 +14,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | | **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); [7 — page move](#pages-rest--slice-7-move--group-creation); [8 — create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation); **[slice 9 — membership + join flows](#pagesgroups-rest--slice-9-membership--join-invites--requests)**. **[Stripe / billing](#phase-3--stripe-billing--webhooks--account-hooks).** **[Slice 10 — collab Postgres bootstrap](#pages-rest--slice-10-collab-updates-rest):** `GET`/`POST …/collab-updates`. **Still ahead:** **collab + realtime WebSockets** (JWT upgrade, binary fan-out, optional Redis buffer like legacy) per [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md). | -| **4** — Client MVP | **In progress** | **Shipped:** [OpenAPI typed HTTP client](#phase-4--openapi-typed-client-bootstrap) in `@deepnotes/web` (`openapi-fetch` + generated `paths`). **Next:** routing + auth UI, then list → page → Yjs. **Parallel:** feature folders, E2E smoke—see [Frontend / UI track](#frontend--ui-track). | +| **4** — Client MVP | **In progress** | **Shipped:** [OpenAPI typed client](#phase-4--openapi-typed-client-bootstrap) + [`vue-router`](#phase-4--routing--session-ui). **Next:** page list + editor shell → Yjs; `POST /api/users` register UI (must use [same password preimage as login](apps/web/README.md#sign-in-contract)). **Parallel:** E2E smoke—see [Frontend / UI track](#frontend--ui-track). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | --- @@ -315,14 +315,25 @@ Sprints **1–9** (pages / groups / membership), **Stripe**, and **[slice 10 — **Intentional gaps:** no MSW/contract suite yet; no `import/no-restricted-paths` until more packages exist to accidentally import. +### Phase 4 — routing + session UI + +| Layer | Shipped | +|-------|---------| +| **Router** | [`router.ts`](apps/web/src/router.ts) — `createWebHistory`, `/`, `/login` (lazy `HomeView` / `LoginView`). | +| **Session** | [`useSession`](apps/web/src/features/auth/useSession.ts) — `bootstrap` (single in-flight + `bootstrapped` gate), `fetchMe`, `loginWithPassword` + **2FA** branch, `loginWithDemo`, `logout`. Cookie hint via [`readDocumentCookie`](apps/web/src/features/auth/cookies.ts) (`loggedIn` only; access/refresh stay httpOnly). | +| **Demo** | [`buildSessionDemoRequest`](apps/web/src/features/auth/build-demo-session.ts) — `libsodium` + `nanoid`, base64 field shapes match OpenAPI. | +| **Auth preimage** | [`bytes.ts`](apps/web/src/features/auth/bytes.ts) — `loginPreimageFromPassword` = UTF-8 bytes of the password; **must** match future register UI. | +| **Vite** | [Proxy `→ 8787`](apps/web/vite.config.ts) for `wrangler dev`; `optimizeDeps` for `libsodium-wrappers-sumo`. | +| **Docs** | [apps/web/README.md](apps/web/README.md) — feature folders + sign-in contract. | + --- ## Phase 4 checklist (client MVP) - [x] **Tooling (bootstrap):** Vitest + **happy-dom** + `@vue/test-utils` in `@deepnotes/web` (minimal `App` test); same Vite 6 pipeline via `vitest/config` `defineConfig` (RESTART_PLAN §5.8). - [x] **API client (bootstrap):** typed client from the same OpenAPI document as the Worker—see [Phase 4 — OpenAPI typed client](#phase-4--openapi-typed-client-bootstrap). Runtime bundle does **not** import `@deepnotes/api` (only generated `api-types.generated.ts` + `openapi-fetch`); regenerate after OpenAPI changes. -- [ ] **Routing + auth UI:** login / refresh / logout / 2FA flows aligned with [docs/AUTH_AND_CORS.md](./docs/AUTH_AND_CORS.md); composable or component tests + **E2E smoke** for cookie session. -- [ ] **Pages:** list → open editor shell → integrate **Yjs** / collab when API is ready. +- [x] **Routing + session UI (slice a):** `vue-router` + [`App.vue` shell](apps/web/src/App.vue) (header, Sign in / Sign out). **`useSession`** ([`useSession.ts`](apps/web/src/features/auth/useSession.ts)): deduped `bootstrap()` = `POST /api/sessions/refresh` + `GET /api/users/me` when `loggedIn` document cookie; `loginWithPassword` (UTF-8 password → base64 `loginHash`, [README contract](apps/web/README.md#sign-in-contract)); 401 + `Requires two-factor authentication.` → 2FA fields; `loginWithDemo` ([`build-demo-session.ts`](apps/web/src/features/auth/build-demo-session.ts)); `POST /api/sessions/logout`. Views: [`/login`](apps/web/src/features/auth/LoginView.vue), [`/`](apps/web/src/features/home/HomeView.vue). Vite [proxy `/api` → 127.0.0.1:8787](apps/web/vite.config.ts). Tests: [`app.test.ts`](apps/web/src/app.test.ts) (router + bootstrap), [`bytes.test.ts`](apps/web/src/features/auth/bytes.test.ts). **Not done:** Playwright E2E; `POST /api/users` **register** form (must match login preimage). **CORS:** API must allow this app’s origin; proxy avoids cross-origin in local dev. +- [ ] **Pages / editor path:** list → open editor shell → **Yjs** + `GET`/`POST /api/pages/{id}/collab-updates` ([TRPC map](./docs/TRPC_REST_MAP.md) collab bootstrap; [Phase 3 — realtime / collab WebSocket](#not-started-phase-3--realtime--collab-websocket) still TBD for live collab). - [ ] **Groups** subset and notifications UX as mapped from [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). - [ ] **Native wrappers** (Capacitor / Tauri): only after web MVP and CI stable. @@ -347,13 +358,13 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ### Decoupling and layout (`@deepnotes/web`) - [x] **API surface:** `src/api/` — generated `paths` + `createDeepnotesApiClient`; bundle does not depend on `@deepnotes/api` at runtime (codegen devDeps only). **Still to enforce:** ESLint `import/no-restricted-paths` banning `@deepnotes/api-worker`, `@deepnotes/db`, `drizzle-orm` from `apps/web/src/**` once rule config is added. -- [ ] **Feature folders:** e.g. `src/features/auth`, `src/features/pages`, `src/shared/ui`—document the convention in `apps/web/README.md` (or link from repo root README). -- [ ] **Thin Vue, fat composables:** session and crypto orchestration live in testable modules, not only in `.vue` files. +- [x] **Feature folders (bootstrap):** `src/features/auth` (session, demo builder, bytes), `src/features/home` — [apps/web/README.md](./apps/web/README.md). **Still empty:** `src/shared/ui`, `src/features/pages` (list + editor). +- [x] **Session composable:** [`useSession.ts`](./apps/web/src/features/auth/useSession.ts) (testable) + thin [`LoginView.vue`](./apps/web/src/features/auth/LoginView.vue) / [`App.vue`](./apps/web/src/App.vue). **Later:** keyring + page crypto in dedicated modules (not in `.vue` only). ### Testing (see RESTART_PLAN §5.8) - [x] **Vitest** in `apps/web` with DOM environment (`happy-dom`) and `@vue/test-utils` aligned with Vite 6. -- [ ] **Component or composable tests** for the first **auth** / session flows (forms, validation, error mapping from API). +- [x] **Composable tests (bootstrap):** [`bytes.test.ts`](./apps/web/src/features/auth/bytes.test.ts) (base64 + UTF-8 preimage); [`app.test.ts`](./apps/web/src/app.test.ts) (router + bootstrap, no `loggedIn` cookie). **Not done:** `LoginView` / `useSession` MSW or mocked `fetch` matrix (401, 2FA, demo). - [ ] **Contract tests** for the fetch wrapper (MSW or recorded OpenAPI fixtures)—optional until multiple features consume the API. - [ ] **E2E smoke** (Playwright recommended): login or session refresh with **httpOnly cookies** against **local compose** or **Cloudflare preview**—add CI job when stable enough (can start `manual`/`workflow_dispatch` if cost is a concern). @@ -365,7 +376,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**24** cases when DB env set) — … + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 move](#pages-rest--slice-7-move--group-creation) + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; optional: invitation **reject/cancel**, join-request **reject/cancel**, **private** group invite/request **access keyring** branches | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (session routes + [slice 6/7 `/api/pages/...` paths](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)); **`schemas/users.test.ts`**; **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; more Zod edge cases for new page schemas | | **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **70** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + `/api/pages/{pageId}/move` + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests) + [slice 10 `…/collab-updates`](#pages-rest--slice-10-collab-updates-rest) (`GET` + `POST`) + [Stripe routes](#phase-3--stripe-billing--webhooks--account-hooks) (`/api/billing/stripe/*`, `/api/webhooks/stripe`) | **200** tests with stub `SessionEnv` + template DB (heavier) | -| **`@deepnotes/web`** | SPA | `app.test.ts` (mount `App.vue`); **`client.test.ts`** (credentials + `/api/health` URL on mocked fetch) | Auth UI + composable tests; MSW/OpenAPI fixtures optional; run `generate:api-types` when `@deepnotes/api` OpenAPI changes | +| **`@deepnotes/web`** | SPA | `app.test.ts` (router + `App`, bootstrap); **`client.test.ts`**; **`bytes.test.ts`**; Vitest [include](apps/web/vite.config.ts) `src/**/*.test.ts` | MSW/contract for login + 2FA; `generate:api-types` when OpenAPI changes; Playwright (see Phase 4 checklist) | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -375,7 +386,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte |------------------------|--------------------------------| | Quasar + Vite 2, 4GB heap builds | Vite 6 + Vue 3.5, Vitest + happy-dom in CI | | Imports `AppRouter`, server websocket paths | Must use **OpenAPI** + documented WS only | -| No automated UI tests | **Done:** real `test` script + one component test | +| No automated UI tests | **Done:** `test` + `app.test` (router) + `client.test` + `bytes.test`; auth UI in [`LoginView`](./apps/web/src/features/auth/LoginView.vue) | --- @@ -410,6 +421,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-27 | **Phase 4 — routing + session UI:** `vue-router` (`/`, `/login`); `useSession` (refresh + me bootstrap, email/password + 2FA, demo, logout); `build-demo-session` + `libsodium`/`nanoid`; Vite proxy `/api` → `127.0.0.1:8787`; `App` shell + `HomeView` + `LoginView`; `bytes.test.ts` + updated `app.test.ts`; [apps/web/README.md](./apps/web/README.md). **Next (Phase 4):** page list, editor, register account form (same `loginHash` preimage as login). **Phase 3** still: collab + realtime [WebSocket](#not-started-phase-3--realtime--collab-websocket). | | 2026-04-27 | **Phase 4 — OpenAPI typed client:** `@deepnotes/web` — `pnpm run generate:api-types` (`tsx` + `openapi-typescript`); committed `src/api/openapi.json` + `api-types.generated.ts`; `createDeepnotesApiClient` / `resolveApiBaseUrl` (`openapi-fetch`, `credentials: "include"`); `client.test.ts`; `VITE_API_URL`; eslint ignore for generated files. **Phase 3** collab WS backlog expanded (upgrade → room → wire → fan-out → Redis → tests). PLAN_PROGRESS Phase 4 snapshot → **In progress**. | | 2026-04-27 | **Phase 3 — slice 10 (collab Postgres REST):** [page-collab-updates.ts](packages/session/src/page-collab-updates.ts) — `performGetPageCollabUpdates` / `performAppendPageCollabUpdates`; `GET|POST /api/pages/:pageId/collab-updates`; OpenAPI + Zod; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) collab bootstrap table; integration extends **groups + pages**; api-worker 503 matrix **70**. **Next:** collab + realtime **WebSocket** only. | | 2026-04-27 | **Phase 3 — Stripe (billing):** [stripe-billing.ts](packages/session/src/stripe-billing.ts) — `performStripeCreateCheckoutSession` / `performStripeCreatePortalSession`, `processStripeWebhookEvent` (legacy `customer.subscription.updated` / `deleted` → `users.plan` + `subscription_id` via `users.customer_id`); [schemas/billing.ts](packages/api/src/schemas/billing.ts) + OpenAPI; Worker `POST /api/billing/stripe/checkout-session`, `…/portal-session`, `POST /api/webhooks/stripe` ([session-env](apps/api-worker/src/session-env.ts) `getStripeBillingEnv` / `getStripeWebhookSecret`); **`STRIPE_SECRET_KEY`** hooks: `deleteStripeCustomer` on `DELETE /api/users/me`, `updateStripeCustomerEmail` on email-change confirm. Dependencies: `stripe@^17.7` in session + api-worker. [TRPC_REST_MAP](docs/TRPC_REST_MAP.md); [template.env](template.env). Api-worker 503 matrix **68** tests. **Next:** [realtime + collab](#phase-3-working-order-suggested) only. | diff --git a/new-deepnotes/apps/web/README.md b/new-deepnotes/apps/web/README.md new file mode 100644 index 00000000..35bd4549 --- /dev/null +++ b/new-deepnotes/apps/web/README.md @@ -0,0 +1,22 @@ +# @deepnotes/web + +Vue 3 SPA for the greenfield stack. The bundle talks to the API only through [`src/api/`](./src/api/) (OpenAPI-generated `paths` + `openapi-fetch`); it does not import server or Drizzle packages at runtime. + +## Layout + +- `src/api/` — `createDeepnotesApiClient`, generated types (`pnpm run generate:api-types` when `packages/api` changes). +- `src/features/auth/` — session bootstrap (`/api/sessions/refresh` + `GET /api/users/me` when the `loggedIn` cookie is set), demo login, email/password + 2FA step, shared helpers. +- `src/features/home/` — first shell screen after auth. +- `src/router.ts` — `vue-router` history routes. + +## Local dev + +- API base URL defaults to same origin. With `pnpm` dev for this app, Vite proxies `/api` to `http://127.0.0.1:8787` (run `wrangler dev` in `api-worker` there). +- Override with `VITE_API_URL` (no trailing slash) for a different host. + +## Sign-in contract + +- **Password login** sends `loginHash` as standard base64 over the UTF-8 bytes of the password. Any future `POST /api/users` registration UI must use the same preimage so Argon2 verification matches. +- **Demo** uses `POST /api/sessions/demo` with random ciphertext-shaped payloads (see `build-demo-session.ts`). + +See also [../docs/AUTH_AND_CORS.md](../docs/AUTH_AND_CORS.md). diff --git a/new-deepnotes/apps/web/package.json b/new-deepnotes/apps/web/package.json index 06506d0b..752879f2 100644 --- a/new-deepnotes/apps/web/package.json +++ b/new-deepnotes/apps/web/package.json @@ -13,8 +13,11 @@ "preview": "vite preview" }, "dependencies": { + "libsodium-wrappers-sumo": "^0.8.0", + "nanoid": "^5.1.5", "openapi-fetch": "^0.17.0", - "vue": "^3.5.13" + "vue": "^3.5.13", + "vue-router": "^5.0.6" }, "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", diff --git a/new-deepnotes/apps/web/src/App.vue b/new-deepnotes/apps/web/src/App.vue index 4c23bdea..944a73a8 100644 --- a/new-deepnotes/apps/web/src/App.vue +++ b/new-deepnotes/apps/web/src/App.vue @@ -1,24 +1,129 @@ diff --git a/new-deepnotes/apps/web/src/app.test.ts b/new-deepnotes/apps/web/src/app.test.ts index 8241f1ba..74a6388c 100644 --- a/new-deepnotes/apps/web/src/app.test.ts +++ b/new-deepnotes/apps/web/src/app.test.ts @@ -1,12 +1,28 @@ -import { mount } from "@vue/test-utils"; +import { mount, flushPromises } from "@vue/test-utils"; +import { nextTick } from "vue"; import { describe, expect, it } from "vitest"; import App from "./App.vue"; +import router from "./router"; describe("App", () => { - it("renders shell copy", () => { - const wrapper = mount(App); + it("renders shell after session bootstrap (no session cookie)", async () => { + await router.push("/"); + await router.isReady(); + + const wrapper = mount(App, { + global: { plugins: [router] }, + }); + for (let i = 0; i < 30; i++) { + await flushPromises(); + await nextTick(); + if (wrapper.find(".app").exists()) { + break; + } + } + + expect(wrapper.find(".app").exists()).toBe(true); expect(wrapper.text()).toContain("DeepNotes"); - expect(wrapper.text()).toContain("Greenfield SPA"); + expect(wrapper.text()).toContain("Sign in"); }); }); diff --git a/new-deepnotes/apps/web/src/features/auth/LoginView.vue b/new-deepnotes/apps/web/src/features/auth/LoginView.vue new file mode 100644 index 00000000..6ba0615f --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/LoginView.vue @@ -0,0 +1,256 @@ + + + + + diff --git a/new-deepnotes/apps/web/src/features/auth/build-demo-session.ts b/new-deepnotes/apps/web/src/features/auth/build-demo-session.ts new file mode 100644 index 00000000..35c4d0d2 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/build-demo-session.ts @@ -0,0 +1,49 @@ +import sodium from "libsodium-wrappers-sumo"; +import { nanoid } from "nanoid"; + +import type { components } from "../../api/api-types.generated"; +import { uint8ToBase64 } from "./bytes"; + +type SessionDemoRequest = components["schemas"]["SessionDemoRequest"]; + +function rand32(): Uint8Array { + return sodium.randombytes_buf(32); +} + +/** + * Random demo registration payload (parity with session integration tests / `POST /api/sessions/demo`). + */ +export async function buildSessionDemoRequest(): Promise { + await sodium.ready; + + const userId = nanoid(); + const groupId = nanoid(); + const pageId = nanoid(); + + return { + userId, + groupId, + pageId, + userPublicKeyring: uint8ToBase64(rand32()), + userEncryptedPrivateKeyring: uint8ToBase64(rand32()), + userEncryptedSymmetricKeyring: uint8ToBase64(rand32()), + userEncryptedName: uint8ToBase64(rand32()), + userEncryptedDefaultNote: uint8ToBase64(rand32()), + userEncryptedDefaultArrow: uint8ToBase64(rand32()), + groupCreation: { + groupEncryptedName: uint8ToBase64(rand32()), + groupIsPublic: true, + groupAccessKeyring: uint8ToBase64(rand32()), + groupEncryptedInternalKeyring: uint8ToBase64(rand32()), + groupEncryptedContentKeyring: uint8ToBase64(rand32()), + groupPublicKeyring: uint8ToBase64(rand32()), + groupEncryptedPrivateKeyring: uint8ToBase64(rand32()), + groupOwnerEncryptedName: uint8ToBase64(rand32()), + }, + pageCreation: { + pageEncryptedSymmetricKeyring: uint8ToBase64(rand32()), + pageEncryptedRelativeTitle: uint8ToBase64(rand32()), + pageEncryptedAbsoluteTitle: uint8ToBase64(rand32()), + }, + }; +} diff --git a/new-deepnotes/apps/web/src/features/auth/bytes.test.ts b/new-deepnotes/apps/web/src/features/auth/bytes.test.ts new file mode 100644 index 00000000..b9fc7a0d --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/bytes.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; + +import { loginPreimageFromPassword, uint8ToBase64 } from "./bytes"; + +describe("uint8ToBase64", () => { + it("encodes small buffers", () => { + const u = new Uint8Array([0, 1, 2, 255]); + expect(uint8ToBase64(u)).toBe("AAEC/w=="); + }); +}); + +describe("loginPreimageFromPassword", () => { + it("uses UTF-8 bytes", () => { + const b = loginPreimageFromPassword("héllo"); + expect(b).toEqual(new TextEncoder().encode("héllo")); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/auth/bytes.ts b/new-deepnotes/apps/web/src/features/auth/bytes.ts new file mode 100644 index 00000000..7dd0e7a2 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/bytes.ts @@ -0,0 +1,14 @@ +/** Standard base64 (OpenAPI `format: byte`) for JSON session bodies. */ +export function uint8ToBase64(bytes: Uint8Array): string { + let binary = ""; + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]!); + } + return btoa(binary); +} + +/** Argon2 preimage: UTF-8 bytes of the account password (same at register and login). */ +export function loginPreimageFromPassword(password: string): Uint8Array { + return new TextEncoder().encode(password); +} diff --git a/new-deepnotes/apps/web/src/features/auth/cookies.ts b/new-deepnotes/apps/web/src/features/auth/cookies.ts new file mode 100644 index 00000000..2d4a6ca2 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/cookies.ts @@ -0,0 +1,12 @@ +/** Read non–httpOnly cookie (e.g. `loggedIn` client hint per AUTH_AND_CORS.md). */ +export function readDocumentCookie(name: string): string | undefined { + if (typeof document === "undefined") return undefined; + const parts = document.cookie.split(";").map((p) => p.trim()); + const prefix = `${name}=`; + for (const part of parts) { + if (part.startsWith(prefix)) { + return decodeURIComponent(part.slice(prefix.length)); + } + } + return undefined; +} diff --git a/new-deepnotes/apps/web/src/features/auth/useSession.ts b/new-deepnotes/apps/web/src/features/auth/useSession.ts new file mode 100644 index 00000000..5ba2fac4 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/useSession.ts @@ -0,0 +1,214 @@ +import { computed, ref, type Ref } from "vue"; + +import { createDeepnotesApiClient } from "../../api/client"; +import type { components } from "../../api/api-types.generated"; +import { buildSessionDemoRequest } from "./build-demo-session"; +import { loginPreimageFromPassword, uint8ToBase64 } from "./bytes"; +import { readDocumentCookie } from "./cookies"; + +export type UserMe = components["schemas"]["UserMeResponse"]; +export type SessionErrorBody = components["schemas"]["SessionErrorResponse"]; + +const TWO_FACTOR_MESSAGE = "Requires two-factor authentication."; + +const client = createDeepnotesApiClient(); + +const user: Ref = ref(null); +const loading: Ref = ref(false); +const bootstrapped: Ref = ref(false); +const lastError: Ref = ref(null); +const twoFactorRequired: Ref = ref(false); + +let bootstrapInFlight: Promise | null = null; + +function setErrorFromBody(body: unknown, fallback: string) { + if ( + body && + typeof body === "object" && + "message" in body && + typeof (body as SessionErrorBody).message === "string" + ) { + lastError.value = (body as SessionErrorBody).message; + return; + } + lastError.value = fallback; +} + +export function useSession() { + const isAuthenticated = computed(() => user.value != null); + const loggedInHint = computed( + () => readDocumentCookie("loggedIn") === "true", + ); + + async function fetchMe() { + const { data, error, response } = await client.GET("/api/users/me", {}); + if (response.status === 200 && data) { + user.value = data; + return true; + } + if (error != null) { + setErrorFromBody(error, "Could not load account."); + } + user.value = null; + return false; + } + + /** + * If the `loggedIn` hint cookie is set, rotate refresh token then load `/me`. + * Safe to call from multiple components; concurrent callers share one run. + */ + async function bootstrap() { + if (bootstrapped.value) { + return; + } + if (bootstrapInFlight) { + await bootstrapInFlight; + return; + } + loading.value = true; + lastError.value = null; + bootstrapInFlight = (async () => { + try { + if (readDocumentCookie("loggedIn") !== "true") { + user.value = null; + return; + } + const refRes = await client.POST("/api/sessions/refresh", {}); + if (refRes.response.status === 401) { + user.value = null; + return; + } + if (refRes.error != null) { + user.value = null; + setErrorFromBody( + refRes.error, + "Session could not be refreshed. Try signing in again.", + ); + return; + } + await fetchMe(); + } finally { + loading.value = false; + bootstrapped.value = true; + bootstrapInFlight = null; + } + })(); + await bootstrapInFlight; + } + + async function loginWithDemo() { + loading.value = true; + twoFactorRequired.value = false; + lastError.value = null; + try { + const body = await buildSessionDemoRequest(); + const { data, error, response } = await client.POST( + "/api/sessions/demo", + { body }, + ); + if (response.status === 200 && data) { + user.value = null; + await fetchMe(); + return { ok: true as const }; + } + if (error != null) { + setErrorFromBody(error, "Demo session could not be created."); + } else { + lastError.value = "Demo session could not be created."; + } + return { ok: false as const }; + } finally { + loading.value = false; + } + } + + async function loginWithPassword(input: { + email: string; + password: string; + rememberSession: boolean; + authenticatorToken?: string; + recoveryCode?: string; + }): Promise<{ ok: boolean; needTwoFactor: boolean }> { + loading.value = true; + if (!input.authenticatorToken && !input.recoveryCode) { + twoFactorRequired.value = false; + } + lastError.value = null; + const preimage = loginPreimageFromPassword(input.password); + const body: components["schemas"]["SessionLoginRequest"] = { + email: input.email.trim().toLowerCase(), + loginHash: uint8ToBase64(preimage), + rememberSession: input.rememberSession, + ...(input.authenticatorToken + ? { authenticatorToken: input.authenticatorToken } + : {}), + ...(input.recoveryCode ? { recoveryCode: input.recoveryCode } : {}), + }; + try { + const { data, error, response } = await client.POST( + "/api/sessions/login", + { body }, + ); + if (response.status === 200 && data) { + twoFactorRequired.value = false; + user.value = null; + await fetchMe(); + return { ok: true, needTwoFactor: false }; + } + if (response.status === 401 && error) { + setErrorFromBody(error, "Sign-in failed."); + const errBody = error as SessionErrorBody; + if (errBody.message === TWO_FACTOR_MESSAGE) { + twoFactorRequired.value = true; + return { ok: false, needTwoFactor: true }; + } + return { ok: false, needTwoFactor: false }; + } + if (error != null) { + setErrorFromBody(error, "Sign-in failed."); + } else { + lastError.value = "Sign-in failed."; + } + return { ok: false, needTwoFactor: false }; + } finally { + loading.value = false; + } + } + + async function logout() { + loading.value = true; + lastError.value = null; + try { + const { response } = await client.POST("/api/sessions/logout", {}); + if (response.status === 204) { + user.value = null; + twoFactorRequired.value = false; + } else { + lastError.value = "Sign out failed."; + } + } finally { + loading.value = false; + } + } + + return { + client, + user, + loading, + bootstrapped, + lastError, + twoFactorRequired, + isAuthenticated, + loggedInHint, + bootstrap, + fetchMe, + loginWithDemo, + loginWithPassword, + logout, + clearError: () => { + lastError.value = null; + }, + }; +} + +export { TWO_FACTOR_MESSAGE }; diff --git a/new-deepnotes/apps/web/src/features/home/HomeView.vue b/new-deepnotes/apps/web/src/features/home/HomeView.vue new file mode 100644 index 00000000..94d4b4b1 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/home/HomeView.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/new-deepnotes/apps/web/src/main.ts b/new-deepnotes/apps/web/src/main.ts index 45573079..cb971f20 100644 --- a/new-deepnotes/apps/web/src/main.ts +++ b/new-deepnotes/apps/web/src/main.ts @@ -1,5 +1,8 @@ import { createApp } from "vue"; import App from "./App.vue"; +import router from "./router"; -createApp(App).mount("#app"); +const app = createApp(App); +app.use(router); +app.mount("#app"); diff --git a/new-deepnotes/apps/web/src/router.ts b/new-deepnotes/apps/web/src/router.ts new file mode 100644 index 00000000..7458ad46 --- /dev/null +++ b/new-deepnotes/apps/web/src/router.ts @@ -0,0 +1,20 @@ +import { createRouter, createWebHistory } from "vue-router"; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: "/", + name: "home", + component: () => import("./features/home/HomeView.vue"), + }, + { + path: "/login", + name: "login", + component: () => import("./features/auth/LoginView.vue"), + meta: { public: true }, + }, + ], +}); + +export default router; diff --git a/new-deepnotes/apps/web/vite.config.ts b/new-deepnotes/apps/web/vite.config.ts index 6ab7a83c..de737b08 100644 --- a/new-deepnotes/apps/web/vite.config.ts +++ b/new-deepnotes/apps/web/vite.config.ts @@ -5,6 +5,15 @@ export default defineConfig({ plugins: [vue()], server: { port: 5174, + proxy: { + "/api": { + target: "http://127.0.0.1:8787", + changeOrigin: true, + }, + }, + }, + optimizeDeps: { + include: ["libsodium-wrappers-sumo"], }, test: { environment: "happy-dom", diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml index cf228355..7438595c 100644 --- a/new-deepnotes/pnpm-lock.yaml +++ b/new-deepnotes/pnpm-lock.yaml @@ -63,12 +63,21 @@ importers: apps/web: dependencies: + libsodium-wrappers-sumo: + specifier: ^0.8.0 + version: 0.8.4 + nanoid: + specifier: ^5.1.5 + version: 5.1.9 openapi-fetch: specifier: ^0.17.0 version: 0.17.0 vue: specifier: ^3.5.13 version: 3.5.33(typescript@5.9.3) + vue-router: + specifier: ^5.0.6 + version: 5.0.6(@vue/compiler-sfc@3.5.33)(vue@3.5.33(typescript@5.9.3)) devDependencies: '@vitejs/plugin-vue': specifier: ^5.2.3 @@ -205,6 +214,10 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -1085,6 +1098,12 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -1092,6 +1111,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -1459,6 +1481,15 @@ packages: '@volar/typescript@2.4.15': resolution: {integrity: sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==} + '@vue-macros/common@3.1.2': + resolution: {integrity: sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==} + engines: {node: '>=20.19.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + '@vue/compiler-core@3.5.33': resolution: {integrity: sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==} @@ -1474,6 +1505,15 @@ packages: '@vue/compiler-vue2@2.7.16': resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==} + '@vue/devtools-api@8.1.1': + resolution: {integrity: sha512-bsDMJ07b3GN1puVwJb/fyFnj/U2imyswK5UQVLZwVl7O05jDrt6BHxeG5XffmOOdasOj/bOmIjxJvGPxU7pcqw==} + + '@vue/devtools-kit@8.1.1': + resolution: {integrity: sha512-gVBaBv++i+adg4JpH71k9ppl4soyR7Y2McEqO5YNgv0BI1kMZ7BDX5gnwkZ5COYgiCyhejZG+yGNrBAjj6Coqg==} + + '@vue/devtools-shared@8.1.1': + resolution: {integrity: sha512-+h4ttmJYl/txpxHKaoZcaKpC+pvckgLzIDiSQlaQ7kKthKh8KuwoLW2D8hPJEnqKzXOvu15UHEoGyngAXCz0EQ==} + '@vue/language-core@2.2.12': resolution: {integrity: sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==} peerDependencies: @@ -1560,6 +1600,14 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-kit@2.2.0: + resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} + engines: {node: '>=20.19.0'} + + ast-walker-scope@0.8.3: + resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} + engines: {node: '>=20.19.0'} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1567,6 +1615,9 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} @@ -1614,6 +1665,10 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1631,6 +1686,12 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -1892,6 +1953,9 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1994,6 +2058,9 @@ packages: resolution: {integrity: sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==} engines: {node: '>=16.9.0'} + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -2069,6 +2136,11 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -2081,6 +2153,11 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2098,6 +2175,10 @@ packages: libsodium-wrappers-sumo@0.8.4: resolution: {integrity: sha512-ql7hcgulKZ3ekfa2DGAogcCKsWU0diA/0nArz1CFzh93WQdb46/Kj18ka/Hifq6uA3Ush34Pc6vU/6HXeRwUkg==} + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2111,6 +2192,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + magic-string-ast@1.0.3: + resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} + engines: {node: '>=20.19.0'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2142,6 +2227,9 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2247,6 +2335,9 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2254,6 +2345,12 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -2281,6 +2378,13 @@ packages: resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2297,6 +2401,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} @@ -2457,6 +2564,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} @@ -2470,6 +2580,14 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + + unplugin@3.0.0: + resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} + engines: {node: ^20.19.0 || >=22.12.0} + uri-js-replace@1.0.1: resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} @@ -2555,6 +2673,21 @@ packages: vue-component-type-helpers@3.2.7: resolution: {integrity: sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==} + vue-router@5.0.6: + resolution: {integrity: sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w==} + peerDependencies: + '@pinia/colada': '>=0.21.2' + '@vue/compiler-sfc': ^3.5.17 + pinia: ^3.0.4 + vue: ^3.5.0 + peerDependenciesMeta: + '@pinia/colada': + optional: true + '@vue/compiler-sfc': + optional: true + pinia: + optional: true + vue-tsc@2.2.12: resolution: {integrity: sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==} hasBin: true @@ -2573,6 +2706,9 @@ packages: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} @@ -2669,6 +2805,14 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -3195,10 +3339,25 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 @@ -3556,6 +3715,16 @@ snapshots: path-browserify: 1.0.1 vscode-uri: 3.1.0 + '@vue-macros/common@3.1.2(vue@3.5.33(typescript@5.9.3))': + dependencies: + '@vue/compiler-sfc': 3.5.33 + ast-kit: 2.2.0 + local-pkg: 1.1.2 + magic-string-ast: 1.0.3 + unplugin-utils: 0.3.1 + optionalDependencies: + vue: 3.5.33(typescript@5.9.3) + '@vue/compiler-core@3.5.33': dependencies: '@babel/parser': 7.29.2 @@ -3591,6 +3760,19 @@ snapshots: de-indent: 1.0.2 he: 1.2.0 + '@vue/devtools-api@8.1.1': + dependencies: + '@vue/devtools-kit': 8.1.1 + + '@vue/devtools-kit@8.1.1': + dependencies: + '@vue/devtools-shared': 8.1.1 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@8.1.1': {} + '@vue/language-core@2.2.12(typescript@5.9.3)': dependencies: '@volar/language-core': 2.4.15 @@ -3672,10 +3854,22 @@ snapshots: assertion-error@2.0.1: {} + ast-kit@2.2.0: + dependencies: + '@babel/parser': 7.29.2 + pathe: 2.0.3 + + ast-walker-scope@0.8.3: + dependencies: + '@babel/parser': 7.29.2 + ast-kit: 2.2.0 + balanced-match@1.0.2: {} balanced-match@4.0.4: {} + birpc@2.9.0: {} + blake3-wasm@2.1.5: {} brace-expansion@1.1.14: @@ -3724,6 +3918,10 @@ snapshots: check-error@2.1.3: {} + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3736,6 +3934,10 @@ snapshots: concat-map@0.0.1: {} + confbox@0.1.8: {} + + confbox@0.2.4: {} + config-chain@1.1.13: dependencies: ini: 1.3.8 @@ -4008,6 +4210,8 @@ snapshots: expect-type@1.3.0: {} + exsolve@1.0.8: {} + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -4112,6 +4316,8 @@ snapshots: hono@4.12.15: {} + hookable@5.5.3: {} + https-proxy-agent@7.0.6(supports-color@10.2.2): dependencies: agent-base: 7.1.4 @@ -4175,6 +4381,8 @@ snapshots: dependencies: argparse: 2.0.1 + jsesc@3.1.0: {} + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} @@ -4183,6 +4391,8 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -4200,6 +4410,12 @@ snapshots: dependencies: libsodium-sumo: 0.8.4 + local-pkg@1.1.2: + dependencies: + mlly: 1.8.2 + pkg-types: 2.3.1 + quansync: 0.2.11 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -4210,6 +4426,10 @@ snapshots: lru-cache@10.4.3: {} + magic-string-ast@1.0.3: + dependencies: + magic-string: 0.30.21 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4246,6 +4466,13 @@ snapshots: minipass@7.1.3: {} + mlly@1.8.2: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + ms@2.1.3: {} msgpackr-extract@3.0.3: @@ -4355,10 +4582,24 @@ snapshots: pathval@2.0.1: {} + perfect-debounce@2.1.0: {} + picocolors@1.1.1: {} picomatch@4.0.4: {} + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.0.8 + pathe: 2.0.3 + pluralize@8.0.0: {} postcss@8.5.12: @@ -4379,6 +4620,10 @@ snapshots: dependencies: side-channel: 1.1.0 + quansync@0.2.11: {} + + readdirp@5.0.0: {} + require-from-string@2.0.2: {} resolve-from@4.0.0: {} @@ -4416,6 +4661,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.2 fsevents: 2.3.3 + scule@1.3.0: {} + semver@7.7.4: {} sharp@0.34.5: @@ -4599,6 +4846,8 @@ snapshots: typescript@5.9.3: {} + ufo@1.6.3: {} + uncrypto@0.1.3: {} undici-types@6.21.0: {} @@ -4609,6 +4858,17 @@ snapshots: dependencies: pathe: 2.0.3 + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.4 + + unplugin@3.0.0: + dependencies: + '@jridgewell/remapping': 2.3.5 + picomatch: 4.0.4 + webpack-virtual-modules: 0.6.2 + uri-js-replace@1.0.1: {} uri-js@4.4.1: @@ -4696,6 +4956,29 @@ snapshots: vue-component-type-helpers@3.2.7: {} + vue-router@5.0.6(@vue/compiler-sfc@3.5.33)(vue@3.5.33(typescript@5.9.3)): + dependencies: + '@babel/generator': 7.29.1 + '@vue-macros/common': 3.1.2(vue@3.5.33(typescript@5.9.3)) + '@vue/devtools-api': 8.1.1 + ast-walker-scope: 0.8.3 + chokidar: 5.0.0 + json5: 2.2.3 + local-pkg: 1.1.2 + magic-string: 0.30.21 + mlly: 1.8.2 + muggle-string: 0.4.1 + pathe: 2.0.3 + picomatch: 4.0.4 + scule: 1.3.0 + tinyglobby: 0.2.16 + unplugin: 3.0.0 + unplugin-utils: 0.3.1 + vue: 3.5.33(typescript@5.9.3) + yaml: 2.8.3 + optionalDependencies: + '@vue/compiler-sfc': 3.5.33 + vue-tsc@2.2.12(typescript@5.9.3): dependencies: '@volar/typescript': 2.4.15 @@ -4714,6 +4997,8 @@ snapshots: webidl-conversions@7.0.0: {} + webpack-virtual-modules@0.6.2: {} + whatwg-mimetype@3.0.0: {} which@2.0.2: From 3c6b2d5ac0a6ab7f9c47176cacb6df2d13c77075 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 13:36:27 -0300 Subject: [PATCH 057/243] feat(new-deepnotes): shadcn-vue UI, layout, and app shell --- new-deepnotes/PLAN_PROGRESS.md | 2 + new-deepnotes/apps/web/README.md | 9 + new-deepnotes/apps/web/components.json | 25 + new-deepnotes/apps/web/eslint.config.js | 6 +- new-deepnotes/apps/web/package.json | 11 + new-deepnotes/apps/web/src/App.vue | 149 +- new-deepnotes/apps/web/src/app.test.ts | 4 +- .../web/src/components/ui/alert/Alert.vue | 21 + .../src/components/ui/alert/AlertAction.vue | 17 + .../components/ui/alert/AlertDescription.vue | 17 + .../src/components/ui/alert/AlertTitle.vue | 17 + .../apps/web/src/components/ui/alert/index.ts | 21 + .../web/src/components/ui/button/Button.vue | 31 + .../web/src/components/ui/button/index.ts | 35 + .../apps/web/src/components/ui/card/Card.vue | 21 + .../web/src/components/ui/card/CardAction.vue | 17 + .../src/components/ui/card/CardContent.vue | 17 + .../components/ui/card/CardDescription.vue | 17 + .../web/src/components/ui/card/CardFooter.vue | 17 + .../web/src/components/ui/card/CardHeader.vue | 17 + .../web/src/components/ui/card/CardTitle.vue | 17 + .../apps/web/src/components/ui/card/index.ts | 7 + .../src/components/ui/checkbox/Checkbox.vue | 34 + .../web/src/components/ui/checkbox/index.ts | 1 + .../web/src/components/ui/input/Input.vue | 31 + .../apps/web/src/components/ui/input/index.ts | 1 + .../web/src/components/ui/label/Label.vue | 26 + .../apps/web/src/components/ui/label/index.ts | 1 + .../apps/web/src/features/auth/LoginView.vue | 323 +- .../apps/web/src/features/home/HomeView.vue | 142 +- new-deepnotes/apps/web/src/lib/utils.ts | 7 + new-deepnotes/apps/web/src/main.ts | 1 + new-deepnotes/apps/web/src/styles/globals.css | 129 + new-deepnotes/apps/web/tsconfig.app.json | 4 + new-deepnotes/apps/web/tsconfig.json | 6 + new-deepnotes/apps/web/vite.config.ts | 13 +- new-deepnotes/pnpm-lock.yaml | 2764 ++++++++++++++++- 37 files changed, 3540 insertions(+), 438 deletions(-) create mode 100644 new-deepnotes/apps/web/components.json create mode 100644 new-deepnotes/apps/web/src/components/ui/alert/Alert.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/alert/AlertAction.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/alert/AlertDescription.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/alert/AlertTitle.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/alert/index.ts create mode 100644 new-deepnotes/apps/web/src/components/ui/button/Button.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/button/index.ts create mode 100644 new-deepnotes/apps/web/src/components/ui/card/Card.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/card/CardAction.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/card/CardContent.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/card/CardDescription.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/card/CardFooter.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/card/CardHeader.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/card/CardTitle.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/card/index.ts create mode 100644 new-deepnotes/apps/web/src/components/ui/checkbox/Checkbox.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/checkbox/index.ts create mode 100644 new-deepnotes/apps/web/src/components/ui/input/Input.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/input/index.ts create mode 100644 new-deepnotes/apps/web/src/components/ui/label/Label.vue create mode 100644 new-deepnotes/apps/web/src/components/ui/label/index.ts create mode 100644 new-deepnotes/apps/web/src/lib/utils.ts create mode 100644 new-deepnotes/apps/web/src/styles/globals.css diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index d0c4ce19..1a861319 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -360,6 +360,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte - [x] **API surface:** `src/api/` — generated `paths` + `createDeepnotesApiClient`; bundle does not depend on `@deepnotes/api` at runtime (codegen devDeps only). **Still to enforce:** ESLint `import/no-restricted-paths` banning `@deepnotes/api-worker`, `@deepnotes/db`, `drizzle-orm` from `apps/web/src/**` once rule config is added. - [x] **Feature folders (bootstrap):** `src/features/auth` (session, demo builder, bytes), `src/features/home` — [apps/web/README.md](./apps/web/README.md). **Still empty:** `src/shared/ui`, `src/features/pages` (list + editor). - [x] **Session composable:** [`useSession.ts`](./apps/web/src/features/auth/useSession.ts) (testable) + thin [`LoginView.vue`](./apps/web/src/features/auth/LoginView.vue) / [`App.vue`](./apps/web/src/App.vue). **Later:** keyring + page crypto in dedicated modules (not in `.vue` only). +- [x] **Tailwind + shadcn-vue:** Tailwind v4 (`@tailwindcss/vite`, [`globals.css`](./apps/web/src/styles/globals.css)); `npx shadcn-vue init` + `button` / `input` / `label` / `card` / `alert` / `checkbox`; shell uses utility classes + `@/components/ui/*` ([README — Styling](./apps/web/README.md#styling)). ESLint ignores generated [`src/components/ui`](./apps/web/src/components/ui). ### Testing (see RESTART_PLAN §5.8) @@ -421,6 +422,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-27 | **Stack:** `@deepnotes/web` — Tailwind CSS v4 + shadcn-vue (Reka), `components.json`, `@/*` alias, [`README` styling](./apps/web/README.md#styling). | | 2026-04-27 | **Phase 4 — routing + session UI:** `vue-router` (`/`, `/login`); `useSession` (refresh + me bootstrap, email/password + 2FA, demo, logout); `build-demo-session` + `libsodium`/`nanoid`; Vite proxy `/api` → `127.0.0.1:8787`; `App` shell + `HomeView` + `LoginView`; `bytes.test.ts` + updated `app.test.ts`; [apps/web/README.md](./apps/web/README.md). **Next (Phase 4):** page list, editor, register account form (same `loginHash` preimage as login). **Phase 3** still: collab + realtime [WebSocket](#not-started-phase-3--realtime--collab-websocket). | | 2026-04-27 | **Phase 4 — OpenAPI typed client:** `@deepnotes/web` — `pnpm run generate:api-types` (`tsx` + `openapi-typescript`); committed `src/api/openapi.json` + `api-types.generated.ts`; `createDeepnotesApiClient` / `resolveApiBaseUrl` (`openapi-fetch`, `credentials: "include"`); `client.test.ts`; `VITE_API_URL`; eslint ignore for generated files. **Phase 3** collab WS backlog expanded (upgrade → room → wire → fan-out → Redis → tests). PLAN_PROGRESS Phase 4 snapshot → **In progress**. | | 2026-04-27 | **Phase 3 — slice 10 (collab Postgres REST):** [page-collab-updates.ts](packages/session/src/page-collab-updates.ts) — `performGetPageCollabUpdates` / `performAppendPageCollabUpdates`; `GET|POST /api/pages/:pageId/collab-updates`; OpenAPI + Zod; [TRPC_REST_MAP](docs/TRPC_REST_MAP.md) collab bootstrap table; integration extends **groups + pages**; api-worker 503 matrix **70**. **Next:** collab + realtime **WebSocket** only. | diff --git a/new-deepnotes/apps/web/README.md b/new-deepnotes/apps/web/README.md index 35bd4549..69930da4 100644 --- a/new-deepnotes/apps/web/README.md +++ b/new-deepnotes/apps/web/README.md @@ -2,6 +2,15 @@ Vue 3 SPA for the greenfield stack. The bundle talks to the API only through [`src/api/`](./src/api/) (OpenAPI-generated `paths` + `openapi-fetch`); it does not import server or Drizzle packages at runtime. +## Styling + +- **Tailwind CSS v4** with the [Vite plugin](https://tailwindcss.com/docs/installation/framework-guides) (`@tailwindcss/vite` in [`vite.config.ts`](./vite.config.ts)), global entry [`src/styles/globals.css`](./src/styles/globals.css). +- **[shadcn-vue](https://www.shadcn-vue.com/)** (Reka + `components.json`); UI primitives live under [`src/components/ui/`](./src/components/ui). Add more with: + + `pnpm dlx shadcn-vue@latest add --yes` + +- **Imports:** Vite + `tsconfig` path alias `@` → [`src`](./tsconfig.app.json) (e.g. `@/components/ui/button`). + ## Layout - `src/api/` — `createDeepnotesApiClient`, generated types (`pnpm run generate:api-types` when `packages/api` changes). diff --git a/new-deepnotes/apps/web/components.json b/new-deepnotes/apps/web/components.json new file mode 100644 index 00000000..3a2ebf95 --- /dev/null +++ b/new-deepnotes/apps/web/components.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://shadcn-vue.com/schema.json", + "style": "reka-nova", + "font": "geist-sans", + "typescript": true, + "tailwind": { + "config": "", + "css": "src/styles/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "composables": "@/composables" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/new-deepnotes/apps/web/eslint.config.js b/new-deepnotes/apps/web/eslint.config.js index 0f6b7636..a09b51d1 100644 --- a/new-deepnotes/apps/web/eslint.config.js +++ b/new-deepnotes/apps/web/eslint.config.js @@ -3,6 +3,10 @@ import base from "../../eslint.config.js"; export default [ ...base, { - ignores: ["src/api/api-types.generated.ts", "src/api/openapi.json"], + ignores: [ + "src/api/api-types.generated.ts", + "src/api/openapi.json", + "src/components/ui/**", + ], }, ]; diff --git a/new-deepnotes/apps/web/package.json b/new-deepnotes/apps/web/package.json index 752879f2..28071da1 100644 --- a/new-deepnotes/apps/web/package.json +++ b/new-deepnotes/apps/web/package.json @@ -13,13 +13,24 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.2.4", + "@vueuse/core": "^14.2.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "libsodium-wrappers-sumo": "^0.8.0", + "lucide-vue-next": "^1.0.0", "nanoid": "^5.1.5", "openapi-fetch": "^0.17.0", + "reka-ui": "^2.9.6", + "shadcn-vue": "^2.6.2", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.2.4", + "tw-animate-css": "^1.4.0", "vue": "^3.5.13", "vue-router": "^5.0.6" }, "devDependencies": { + "@types/node": "^22.14.1", "@vitejs/plugin-vue": "^5.2.3", "@vue/test-utils": "^2.4.6", "happy-dom": "^17.4.4", diff --git a/new-deepnotes/apps/web/src/App.vue b/new-deepnotes/apps/web/src/App.vue index 944a73a8..e593355d 100644 --- a/new-deepnotes/apps/web/src/App.vue +++ b/new-deepnotes/apps/web/src/App.vue @@ -2,6 +2,8 @@ import { onMounted } from "vue"; import { RouterLink, RouterView } from "vue-router"; +import { Button } from "@/components/ui/button"; + import { useSession } from "./features/auth/useSession"; const { bootstrap, isAuthenticated, user, bootstrapped, loading, logout } = @@ -17,113 +19,52 @@ async function onLogout() { - - diff --git a/new-deepnotes/apps/web/src/app.test.ts b/new-deepnotes/apps/web/src/app.test.ts index 74a6388c..d2545607 100644 --- a/new-deepnotes/apps/web/src/app.test.ts +++ b/new-deepnotes/apps/web/src/app.test.ts @@ -16,12 +16,12 @@ describe("App", () => { for (let i = 0; i < 30; i++) { await flushPromises(); await nextTick(); - if (wrapper.find(".app").exists()) { + if (wrapper.find(".app-root").exists()) { break; } } - expect(wrapper.find(".app").exists()).toBe(true); + expect(wrapper.find(".app-root").exists()).toBe(true); expect(wrapper.text()).toContain("DeepNotes"); expect(wrapper.text()).toContain("Sign in"); }); diff --git a/new-deepnotes/apps/web/src/components/ui/alert/Alert.vue b/new-deepnotes/apps/web/src/components/ui/alert/Alert.vue new file mode 100644 index 00000000..916295ba --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/alert/Alert.vue @@ -0,0 +1,21 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/alert/AlertAction.vue b/new-deepnotes/apps/web/src/components/ui/alert/AlertAction.vue new file mode 100644 index 00000000..8fd2f1ec --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/alert/AlertAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/alert/AlertDescription.vue b/new-deepnotes/apps/web/src/components/ui/alert/AlertDescription.vue new file mode 100644 index 00000000..43b45a9a --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/alert/AlertDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/alert/AlertTitle.vue b/new-deepnotes/apps/web/src/components/ui/alert/AlertTitle.vue new file mode 100644 index 00000000..b9694a62 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/alert/AlertTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/alert/index.ts b/new-deepnotes/apps/web/src/components/ui/alert/index.ts new file mode 100644 index 00000000..4c797da0 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/alert/index.ts @@ -0,0 +1,21 @@ +import type { VariantProps } from 'class-variance-authority' +import { cva } from 'class-variance-authority' + +export { default as Alert } from './Alert.vue' +export { default as AlertAction } from './AlertAction.vue' +export { default as AlertDescription } from './AlertDescription.vue' +export { default as AlertTitle } from './AlertTitle.vue' + +export const alertVariants = cva('grid gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*=size-])]:size-4 group/alert relative w-full', { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: 'text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current', + }, + }, + defaultVariants: { + variant: 'default', + }, +}) + +export type AlertVariants = VariantProps diff --git a/new-deepnotes/apps/web/src/components/ui/button/Button.vue b/new-deepnotes/apps/web/src/components/ui/button/Button.vue new file mode 100644 index 00000000..1b6a5120 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/button/Button.vue @@ -0,0 +1,31 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/button/index.ts b/new-deepnotes/apps/web/src/components/ui/button/index.ts new file mode 100644 index 00000000..676a977f --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/button/index.ts @@ -0,0 +1,35 @@ +import type { VariantProps } from 'class-variance-authority' +import { cva } from 'class-variance-authority' + +export { default as Button } from './Button.vue' + +export const buttonVariants = cva( + 'focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-lg border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 active:not-aria-[haspopup]:translate-y-px [&_svg:not([class*=size-])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-all outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80', + outline: 'border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground', + ghost: 'hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground', + destructive: 'bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + 'default': 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + 'xs': 'h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3', + 'sm': 'h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*=size-])]:size-3.5', + 'lg': 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2', + 'icon': 'size-8', + 'icon-xs': 'size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*=size-])]:size-3', + 'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg', + 'icon-lg': 'size-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +) +export type ButtonVariants = VariantProps diff --git a/new-deepnotes/apps/web/src/components/ui/card/Card.vue b/new-deepnotes/apps/web/src/components/ui/card/Card.vue new file mode 100644 index 00000000..073e6516 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/Card.vue @@ -0,0 +1,21 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardAction.vue b/new-deepnotes/apps/web/src/components/ui/card/CardAction.vue new file mode 100644 index 00000000..c2beb206 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardAction.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardContent.vue b/new-deepnotes/apps/web/src/components/ui/card/CardContent.vue new file mode 100644 index 00000000..6270bc46 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardContent.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardDescription.vue b/new-deepnotes/apps/web/src/components/ui/card/CardDescription.vue new file mode 100644 index 00000000..722b2033 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardDescription.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardFooter.vue b/new-deepnotes/apps/web/src/components/ui/card/CardFooter.vue new file mode 100644 index 00000000..ca3936b4 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardFooter.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardHeader.vue b/new-deepnotes/apps/web/src/components/ui/card/CardHeader.vue new file mode 100644 index 00000000..27d56f7e --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardHeader.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/CardTitle.vue b/new-deepnotes/apps/web/src/components/ui/card/CardTitle.vue new file mode 100644 index 00000000..1f53990e --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/CardTitle.vue @@ -0,0 +1,17 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/card/index.ts b/new-deepnotes/apps/web/src/components/ui/card/index.ts new file mode 100644 index 00000000..73d985f2 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/card/index.ts @@ -0,0 +1,7 @@ +export { default as Card } from './Card.vue' +export { default as CardAction } from './CardAction.vue' +export { default as CardContent } from './CardContent.vue' +export { default as CardDescription } from './CardDescription.vue' +export { default as CardFooter } from './CardFooter.vue' +export { default as CardHeader } from './CardHeader.vue' +export { default as CardTitle } from './CardTitle.vue' diff --git a/new-deepnotes/apps/web/src/components/ui/checkbox/Checkbox.vue b/new-deepnotes/apps/web/src/components/ui/checkbox/Checkbox.vue new file mode 100644 index 00000000..52f45e08 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/checkbox/Checkbox.vue @@ -0,0 +1,34 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/checkbox/index.ts b/new-deepnotes/apps/web/src/components/ui/checkbox/index.ts new file mode 100644 index 00000000..8c28c286 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/checkbox/index.ts @@ -0,0 +1 @@ +export { default as Checkbox } from './Checkbox.vue' diff --git a/new-deepnotes/apps/web/src/components/ui/input/Input.vue b/new-deepnotes/apps/web/src/components/ui/input/Input.vue new file mode 100644 index 00000000..4ebb6aba --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/input/Input.vue @@ -0,0 +1,31 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/input/index.ts b/new-deepnotes/apps/web/src/components/ui/input/index.ts new file mode 100644 index 00000000..a691dd6c --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from './Input.vue' diff --git a/new-deepnotes/apps/web/src/components/ui/label/Label.vue b/new-deepnotes/apps/web/src/components/ui/label/Label.vue new file mode 100644 index 00000000..9d30cbbe --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/label/Label.vue @@ -0,0 +1,26 @@ + + + diff --git a/new-deepnotes/apps/web/src/components/ui/label/index.ts b/new-deepnotes/apps/web/src/components/ui/label/index.ts new file mode 100644 index 00000000..572c2f01 --- /dev/null +++ b/new-deepnotes/apps/web/src/components/ui/label/index.ts @@ -0,0 +1 @@ +export { default as Label } from './Label.vue' diff --git a/new-deepnotes/apps/web/src/features/auth/LoginView.vue b/new-deepnotes/apps/web/src/features/auth/LoginView.vue index 6ba0615f..fda411f0 100644 --- a/new-deepnotes/apps/web/src/features/auth/LoginView.vue +++ b/new-deepnotes/apps/web/src/features/auth/LoginView.vue @@ -2,6 +2,20 @@ import { onMounted, ref } from "vue"; import { RouterLink, useRoute, useRouter } from "vue-router"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + import { useSession } from "./useSession"; const router = useRouter(); @@ -60,197 +74,122 @@ async function onSubmit() { - - diff --git a/new-deepnotes/apps/web/src/features/home/HomeView.vue b/new-deepnotes/apps/web/src/features/home/HomeView.vue index 94d4b4b1..b14da144 100644 --- a/new-deepnotes/apps/web/src/features/home/HomeView.vue +++ b/new-deepnotes/apps/web/src/features/home/HomeView.vue @@ -1,90 +1,72 @@ - - diff --git a/new-deepnotes/apps/web/src/lib/utils.ts b/new-deepnotes/apps/web/src/lib/utils.ts new file mode 100644 index 00000000..c66a9d9c --- /dev/null +++ b/new-deepnotes/apps/web/src/lib/utils.ts @@ -0,0 +1,7 @@ +import type { ClassValue } from "clsx" +import { clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/new-deepnotes/apps/web/src/main.ts b/new-deepnotes/apps/web/src/main.ts index cb971f20..1df2e82e 100644 --- a/new-deepnotes/apps/web/src/main.ts +++ b/new-deepnotes/apps/web/src/main.ts @@ -1,5 +1,6 @@ import { createApp } from "vue"; +import "./styles/globals.css"; import App from "./App.vue"; import router from "./router"; diff --git a/new-deepnotes/apps/web/src/styles/globals.css b/new-deepnotes/apps/web/src/styles/globals.css new file mode 100644 index 00000000..e8c6dcdf --- /dev/null +++ b/new-deepnotes/apps/web/src/styles/globals.css @@ -0,0 +1,129 @@ + +@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap'); + +@import "tailwindcss"; + +@import "tw-animate-css"; + +@import "shadcn-vue/tailwind.css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --font-sans: 'Geist Variable', sans-serif; + --font-heading: var(--font-sans); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.87 0 0); + --chart-2: oklch(0.556 0 0); + --chart-3: oklch(0.439 0 0); + --chart-4: oklch(0.371 0 0); + --chart-5: oklch(0.269 0 0); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + @apply font-sans; + } +} \ No newline at end of file diff --git a/new-deepnotes/apps/web/tsconfig.app.json b/new-deepnotes/apps/web/tsconfig.app.json index 4ff66ac8..83799647 100644 --- a/new-deepnotes/apps/web/tsconfig.app.json +++ b/new-deepnotes/apps/web/tsconfig.app.json @@ -1,5 +1,9 @@ { "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, "target": "ES2022", "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", diff --git a/new-deepnotes/apps/web/tsconfig.json b/new-deepnotes/apps/web/tsconfig.json index 1ffef600..08d70bd1 100644 --- a/new-deepnotes/apps/web/tsconfig.json +++ b/new-deepnotes/apps/web/tsconfig.json @@ -1,5 +1,11 @@ { "files": [], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, "references": [ { "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" } diff --git a/new-deepnotes/apps/web/vite.config.ts b/new-deepnotes/apps/web/vite.config.ts index de737b08..b40794cb 100644 --- a/new-deepnotes/apps/web/vite.config.ts +++ b/new-deepnotes/apps/web/vite.config.ts @@ -1,8 +1,19 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import tailwindcss from "@tailwindcss/vite"; import vue from "@vitejs/plugin-vue"; import { defineConfig } from "vitest/config"; +const appRoot = fileURLToPath(new URL(".", import.meta.url)); + export default defineConfig({ - plugins: [vue()], + plugins: [vue(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(appRoot, "src"), + }, + }, server: { port: 5174, proxy: { diff --git a/new-deepnotes/pnpm-lock.yaml b/new-deepnotes/pnpm-lock.yaml index 7438595c..f029cbb2 100644 --- a/new-deepnotes/pnpm-lock.yaml +++ b/new-deepnotes/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 9.39.4 eslint: specifier: ^9.25.0 - version: 9.39.4 + version: 9.39.4(jiti@2.6.1) turbo: specifier: ^2.5.0 version: 2.9.6 @@ -25,7 +25,7 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.31.0 - version: 8.59.0(eslint@9.39.4)(typescript@5.9.3) + version: 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) apps/api-worker: dependencies: @@ -56,22 +56,52 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.1 - version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) wrangler: specifier: ^4.12.0 version: 4.85.0(@cloudflare/workers-types@4.20260426.1) apps/web: dependencies: + '@tailwindcss/vite': + specifier: ^4.2.4 + version: 4.2.4(vite@6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3)) + '@vueuse/core': + specifier: ^14.2.1 + version: 14.2.1(vue@3.5.33(typescript@5.9.3)) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 libsodium-wrappers-sumo: specifier: ^0.8.0 version: 0.8.4 + lucide-vue-next: + specifier: ^1.0.0 + version: 1.0.0(vue@3.5.33(typescript@5.9.3)) nanoid: specifier: ^5.1.5 version: 5.1.9 openapi-fetch: specifier: ^0.17.0 version: 0.17.0 + reka-ui: + specifier: ^2.9.6 + version: 2.9.6(vue@3.5.33(typescript@5.9.3)) + shadcn-vue: + specifier: ^2.6.2 + version: 2.6.2(eslint@9.39.4(jiti@2.6.1))(vue@3.5.33(typescript@5.9.3)) + tailwind-merge: + specifier: ^3.5.0 + version: 3.5.0 + tailwindcss: + specifier: ^4.2.4 + version: 4.2.4 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 vue: specifier: ^3.5.13 version: 3.5.33(typescript@5.9.3) @@ -79,9 +109,12 @@ importers: specifier: ^5.0.6 version: 5.0.6(@vue/compiler-sfc@3.5.33)(vue@3.5.33(typescript@5.9.3)) devDependencies: + '@types/node': + specifier: ^22.14.1 + version: 22.19.17 '@vitejs/plugin-vue': specifier: ^5.2.3 - version: 5.2.4(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.33(typescript@5.9.3)) + version: 5.2.4(vite@6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.33(typescript@5.9.3)) '@vue/test-utils': specifier: ^2.4.6 version: 2.4.8(@vue/compiler-dom@3.5.33)(@vue/server-renderer@3.5.33(vue@3.5.33(typescript@5.9.3)))(vue@3.5.33(typescript@5.9.3)) @@ -99,10 +132,10 @@ importers: version: 5.9.3 vite: specifier: ^6.3.3 - version: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + version: 6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) vue-tsc: specifier: ^2.2.10 version: 2.2.12(typescript@5.9.3) @@ -127,7 +160,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.1.1 - version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) packages/db: dependencies: @@ -152,7 +185,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) packages/session: dependencies: @@ -201,7 +234,7 @@ importers: version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3) + version: 3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) packages: @@ -214,10 +247,68 @@ packages: resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + '@babel/generator@7.29.1': resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -226,11 +317,62 @@ packages: resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + '@babel/parser@7.29.2': resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@8.0.0-alpha.12': + resolution: {integrity: sha512-AzWmrp4uJ+DcXVH0uoUpJVhRqxNirC0BbXsZ82AQuVod41CoaV5G+cwcvtYusrIIxv7BIJb6ce0dQ9L0wAl1iA==} + engines: {node: ^18.20.0 || ^20.10.0 || >=21.0.0} + hasBin: true + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} @@ -285,9 +427,19 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@dotenvx/dotenvx@1.64.0': + resolution: {integrity: sha512-6+xRpZaWuHXEqnhBjae+VmQI9Uaqw5Uzu/ScpO+W7ww9Zp3lHSNBoNjFcUxhrCyc7pRGQzyDjhKzloqrPHERiQ==} + hasBin: true + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@ecies/ciphers@0.2.6': + resolution: {integrity: sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==} + engines: {bun: '>=1', deno: '>=2.7.10', node: '>=16'} + peerDependencies: + '@noble/ciphers': ^1.0.0 + '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} @@ -937,6 +1089,24 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@floating-ui/vue@1.1.11': + resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==} + + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} engines: {node: '>=18.18.0'} @@ -1094,10 +1264,20 @@ packages: cpu: [x64] os: [win32] + '@internationalized/date@3.12.1': + resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==} + + '@internationalized/number@3.6.6': + resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/cliui@9.0.0': + resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} + engines: {node: '>=18'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1117,6 +1297,16 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -1147,6 +1337,30 @@ packages: cpu: [x64] os: [win32] + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} @@ -1326,6 +1540,110 @@ packages: '@speed-highlight/core@1.2.15': resolution: {integrity: sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==} + '@swc/helpers@0.5.21': + resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==} + + '@tailwindcss/node@4.2.4': + resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==} + + '@tailwindcss/oxide-android-arm64@4.2.4': + resolution: {integrity: sha512-e7MOr1SAn9U8KlZzPi1ZXGZHeC5anY36qjNwmZv9pOJ8E4Q6jmD1vyEHkQFmNOIN7twGPEMXRHmitN4zCMN03g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + resolution: {integrity: sha512-tSC/Kbqpz/5/o/C2sG7QvOxAKqyd10bq+ypZNf+9Fi2TvbVbv1zNpcEptcsU7DPROaSbVgUXmrzKhurFvo5eDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.4': + resolution: {integrity: sha512-yPyUXn3yO/ufR6+Kzv0t4fCg2qNr90jxXc5QqBpjlPNd0NqyDXcmQb/6weunH/MEDXW5dhyEi+agTDiqa3WsGg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + resolution: {integrity: sha512-BoMIB4vMQtZsXdGLVc2z+P9DbETkiopogfWZKbWwM8b/1Vinbs4YcUwo+kM/KeLkX3Ygrf4/PsRndKaYhS8Eiw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + resolution: {integrity: sha512-7pIHBLTHYRAlS7V22JNuTh33yLH4VElwKtB3bwchK/UaKUPpQ0lPQiOWcbm4V3WP2I6fNIJ23vABIvoy2izdwA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + resolution: {integrity: sha512-+E4wxJ0ZGOzSH325reXTWB48l42i93kQqMvDyz5gqfRzRZ7faNhnmvlV4EPGJU3QJM/3Ab5jhJ5pCRUsKn6OQw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + resolution: {integrity: sha512-bBADEGAbo4ASnppIziaQJelekCxdMaxisrk+fB7Thit72IBnALp9K6ffA2G4ruj90G9XRS2VQ6q2bCKbfFV82g==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + resolution: {integrity: sha512-7Mx25E4WTfnht0TVRTyC00j3i0M+EeFe7wguMDTlX4mRxafznw0CA8WJkFjWYH5BlgELd1kSjuU2JiPnNZbJDA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + resolution: {integrity: sha512-2wwJRF7nyhOR0hhHoChc04xngV3iS+akccHTGtz965FwF0up4b2lOdo6kI1EbDaEXKgvcrFBYcYQQ/rrnWFVfA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + resolution: {integrity: sha512-FQsqApeor8Fo6gUEklzmaa9994orJZZDBAlQpK2Mq+DslRKFJeD6AjHpBQ0kZFQohVr8o85PPh8eOy86VlSCmw==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + resolution: {integrity: sha512-L9BXqxC4ToVgwMFqj3pmZRqyHEztulpUJzCxUtLjobMCzTPsGt1Fa9enKbOpY2iIyVtaHNeNvAK8ERP/64sqGQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + resolution: {integrity: sha512-ESlKG0EpVJQwRjXDDa9rLvhEAh0mhP1sF7sap9dNZT0yyl9SAG6T7gdP09EH0vIv0UNTlo6jPWyujD6559fZvw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.4': + resolution: {integrity: sha512-9El/iI069DKDSXwTvB9J4BwdO5JhRrOweGaK25taBAvBXyXqJAX+Jqdvs8r8gKpsI/1m0LeJLyQYTf/WLrBT1Q==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.2.4': + resolution: {integrity: sha512-pCvohwOCspk3ZFn6eJzrrX3g4n2JY73H6MmYC87XfGPyTty4YsCjYTMArRZm/zOI8dIt3+EcrLHAFPe5A4bgtw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/virtual-core@3.14.0': + resolution: {integrity: sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==} + + '@tanstack/vue-virtual@3.13.24': + resolution: {integrity: sha512-A0k2qF0zFSUStXSZkGXABouXr2Tw2Ztl/cVIYG9qy84uR8W7UNjAcX3DvzBS3YnDcwvLxab8v7dbmYBZ39itDA==} + peerDependencies: + vue: ^2.7.0 || ^3.0.0 + + '@ts-morph/common@0.28.1': + resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} + '@turbo/darwin-64@2.9.6': resolution: {integrity: sha512-X/56SnVXIQZBLKwniGTwEQTGmtE5brSACnKMBWpY3YafuxVYefrC2acamfjgxP7BG5w3I+6jf0UrLoSzgPcSJg==} cpu: [x64] @@ -1374,6 +1692,9 @@ packages: '@types/node@22.19.17': resolution: {integrity: sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@typescript-eslint/eslint-plugin@8.59.0': resolution: {integrity: sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1433,6 +1754,11 @@ packages: resolution: {integrity: sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@unovue/detypes@0.8.5': + resolution: {integrity: sha512-Yz4JeWOHGa+w/3YudVdng8hgN/VGW9cvp8xmFkmPPFzalGblLPPSpIRiwVo853yLstMZO2LLwe0vOoLAQsUQXw==} + engines: {node: '>=18'} + hasBin: true + '@upstash/redis@1.37.0': resolution: {integrity: sha512-LqOJ3+XWPLSZ2rGSed5DYG3ixybxb8EhZu3yQqF7MdZX1wLBG/FRcI6xcUZXHy/SS7mmXWyadrud0HJHkOc+uw==} @@ -1549,10 +1875,30 @@ packages: '@vue/server-renderer': optional: true + '@vuedx/template-ast-types@0.7.1': + resolution: {integrity: sha512-Mqugk/F0lFN2u9bhimH6G1kSu2hhLi2WoqgCVxrMvgxm2kDc30DtdvVGRq+UgEmKVP61OudcMtZqkUoGQeFBUQ==} + + '@vueuse/core@14.2.1': + resolution: {integrity: sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.2.1': + resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==} + + '@vueuse/shared@14.2.1': + resolution: {integrity: sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==} + peerDependencies: + vue: ^3.5.0 + abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1567,9 +1913,20 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + alien-signals@1.0.13: resolution: {integrity: sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==} @@ -1596,6 +1953,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1604,10 +1965,23 @@ packages: resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} engines: {node: '>=20.19.0'} + ast-types-x@1.18.0: + resolution: {integrity: sha512-ZtfIlyTCmnAXPCQo4mSDtFsHL7L3q0sJfpVYPmy5uYPjs+fynzOuc1Cg6yQ9fF6h61RjEWtOlRFwV1Kc80Qs6A==} + engines: {node: '>=4'} + ast-walker-scope@0.8.3: resolution: {integrity: sha512-cbdCP0PGOBq0ASG+sjnKIoYkWMKhhz+F/h9pRexUdX2Hd38+WOlBkRKlqkGOSm0YQpcFMQBJeK4WspUAkwsEdg==} engines: {node: '>=20.19.0'} + astral-regex@2.0.0: + resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} + engines: {node: '>=8'} + + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1615,12 +1989,24 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + baseline-browser-mapping@2.10.23: + resolution: {integrity: sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==} + engines: {node: '>=6.0.0'} + hasBin: true + birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@1.1.14: resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} @@ -1631,9 +2017,34 @@ packages: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1650,6 +2061,9 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + caniuse-lite@1.0.30001791: + resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} @@ -1658,6 +2072,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} @@ -1669,6 +2087,31 @@ packages: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-progress@3.12.0: + resolution: {integrity: sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==} + engines: {node: '>=4'} + + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1683,6 +2126,14 @@ packages: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1695,10 +2146,41 @@ packages: config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-hrtime@5.0.0: + resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} + engines: {node: '>=12'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + cookie@1.1.1: resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} engines: {node: '>=18'} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1706,6 +2188,21 @@ packages: crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + css@3.0.0: + resolution: {integrity: sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1721,6 +2218,22 @@ packages: supports-color: optional: true + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-diff@1.0.2: + resolution: {integrity: sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1728,14 +2241,61 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@16.6.1: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + drizzle-kit@0.31.10: resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} hasBin: true @@ -1836,17 +2396,39 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + eciesjs@0.4.18: + resolution: {integrity: sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==} + engines: {bun: '>=1', deno: '>=2', node: '>=16'} + editorconfig@1.0.7: resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} engines: {node: '>=14'} hasBin: true + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.344: + resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.21.0: + resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -1893,6 +2475,13 @@ packages: engines: {node: '>=18'} hasBin: true + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1949,22 +2538,61 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.4.1: + resolution: {integrity: sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1978,6 +2606,14 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1993,6 +2629,21 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@11.3.4: + resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} + engines: {node: '>=14.14'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2001,22 +2652,53 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function-timeout@1.0.2: + resolution: {integrity: sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==} + engines: {node: '>=18'} + + fuzzysort@3.1.0: + resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + gel@2.2.0: resolution: {integrity: sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==} engines: {node: '>= 18.0.0'} hasBin: true + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-own-enumerable-keys@1.0.0: + resolution: {integrity: sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA==} + engines: {node: '>=14.16'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + giget@3.2.0: + resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} + hasBin: true + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -2026,14 +2708,32 @@ packages: deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} + engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + gonzales-pe@4.3.0: + resolution: {integrity: sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==} + engines: {node: '>=0.6.0'} + hasBin: true + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + happy-dom@17.6.3: resolution: {integrity: sha512-UVIHeVhxmxedbWPCfgS55Jg2rDfwf2BCKeylcPSqazLz5w3Kri7Q4xdBJubsr/+VUzFLh0VjIvh13RaDA2/Xug==} engines: {node: '>=20.0.0'} @@ -2061,10 +2761,26 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + identifier-regex@1.0.1: + resolution: {integrity: sha512-ZrYyM0sozNPZlvBvE7Oq9Bn44n0qKGrYu5sQ0JzMUnjIhpgWYE2JB6aBoFwEYdPjqj7jPyxXTMJiHDOxDfd8yw==} + engines: {node: '>=18'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2085,9 +2801,29 @@ packages: resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} engines: {node: '>=18'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2100,6 +2836,46 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-identifier@1.0.1: + resolution: {integrity: sha512-HQ5v4rEJ7REUV54bCd2l5FaD299SGDEn2UPoVXaTHAyGviLq2menVUD2udi3trQ32uvB6LdAh/0ck2EuizrtpA==} + engines: {node: '>=18'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-obj@3.0.0: + resolution: {integrity: sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ==} + engines: {node: '>=12'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2110,9 +2886,20 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.2.3: + resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} + engines: {node: 20 || >=22} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + js-beautify@1.15.4: resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} engines: {node: '>=14'} @@ -2150,6 +2937,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2158,9 +2948,16 @@ packages: engines: {node: '>=6'} hasBin: true + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -2175,6 +2972,76 @@ packages: libsodium-wrappers-sumo@0.8.4: resolution: {integrity: sha512-ql7hcgulKZ3ekfa2DGAogcCKsWU0diA/0nArz1CFzh93WQdb46/Kj18ka/Hifq6uA3Ush34Pc6vU/6HXeRwUkg==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + local-pkg@1.1.2: resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} engines: {node: '>=14'} @@ -2183,15 +3050,40 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.sortedlastindex@4.1.0: + resolution: {integrity: sha512-s8xEQdsp2Tu5zUqVdFSe9C0kR8YlnAJYLqMdkh+pIRBRxF6/apWseLdHl3/+jv2I61dhPwtI/Ff+EqvCpc+N8w==} + + lodash.truncate@4.4.2: + resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==} + + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-vue-next@1.0.0: + resolution: {integrity: sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg==} + peerDependencies: + vue: '>=3.0.1' + magic-string-ast@1.0.3: resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==} engines: {node: '>=20.19.0'} @@ -2199,10 +3091,49 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-asynchronous@1.1.0: + resolution: {integrity: sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==} + engines: {node: '>=18'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + miniflare@4.20260424.0: resolution: {integrity: sha512-B6MKBBd5TJ19daUc3Ae9rWctn1nDA/VCXykXfCsp9fTxyfGxnZY27tJs1caxgE9MWEMMKGbGHouqVtgKbKGxmw==} engines: {node: '>=18.0.0'} @@ -2223,6 +3154,9 @@ packages: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -2256,19 +3190,77 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true + node-html-parser@7.1.0: + resolution: {integrity: sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + nopt@7.2.1: resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} hasBin: true + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + nypm@0.6.6: + resolution: {integrity: sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==} + engines: {node: '>=18'} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-treeify@1.1.33: + resolution: {integrity: sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A==} + engines: {node: '>= 10'} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + openapi-fetch@0.17.0: resolution: {integrity: sha512-PsbZR1wAPcG91eEthKhN+Zn92FMHxv+/faECIwjXdxfTODGSGegYv0sc1Olz+HYPvKOuoXfp+0pA2XVt2cI0Ig==} @@ -2288,9 +3280,17 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + ora@9.4.0: + resolution: {integrity: sha512-84cglkRILFxdtA8hAvLNdMrtBpPNBTrQ9/ulg0FA7xLMnD6mifv+enAIeRmvtv+WgdCE+LPGOfQmtJRrVaIVhQ==} + engines: {node: '>=20'} + otplib@12.0.1: resolution: {integrity: sha512-xDGvUOQjop7RDgxTQ+o4pOol0/3xSZzawTiPKRrHnQWAy0WjhNs/5HdIDJCrqC4MBynmjXgULc6YfioaxZeFgg==} + p-event@6.0.1: + resolution: {integrity: sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==} + engines: {node: '>=16.17'} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2299,6 +3299,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-timeout@6.1.4: + resolution: {integrity: sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==} + engines: {node: '>=14.16'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -2310,6 +3314,10 @@ packages: resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} engines: {node: '>=18'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2317,6 +3325,10 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2325,9 +3337,16 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2341,10 +3360,18 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -2355,6 +3382,30 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} + postcss-less@6.0.0: + resolution: {integrity: sha512-FPX16mQLyEjLzEuuJtxA8X3ejDLNGGEG503d2YGZR5Ask1SpDN8KmZUMpzCvyalWRywAn1n1VOA5dcqfCLo5rg==} + engines: {node: '>=12'} + peerDependencies: + postcss: ^8.3.5 + + postcss-sass@0.5.0: + resolution: {integrity: sha512-qtu8awh1NMF3o9j/x9j3EZnd+BlF66X6NZYl12BdKoG2Z4hmydOt/dZj2Nq+g0kfk2pQy3jeYFBmvG9DBwynGQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss-styl@0.12.3: + resolution: {integrity: sha512-8I7Cd8sxiEITIp32xBK4K/Aj1ukX6vuWnx8oY/oAH35NfQI4OZaY5nd68Yx8HeN5S49uhQ6DL0rNk0ZBu/TaLg==} + engines: {node: ^8.10.0 || ^10.13.0 || ^11.10.1 || >=12.13.0} + postcss@8.5.12: resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} @@ -2367,9 +3418,22 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2381,14 +3445,41 @@ packages: quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc9@3.0.1: + resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + recast-x@1.0.5: + resolution: {integrity: sha512-CkfWKhQiYsMQYaWUkHdERXUxT2jJLBoa5y7zFv3dUAE7Ly5oU/0hsqrENyEfrCL03pDsQYbnoz17Cbagx/c2OA==} + engines: {node: '>= 4'} + + reka-ui@2.9.6: + resolution: {integrity: sha512-K6bL457owpvWONc7hsjFxo3HDC9s6IzhRqShW0w9JSKelPGfRbkHD558UQTn/NH1cvrXVHygKyC7fExFmRketg==} + peerDependencies: + vue: '>= 3.4.0' + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + reserved-identifiers@1.2.0: + resolution: {integrity: sha512-yE7KUfFvaBFzGPs5H3Ops1RevfUEsDc5Iz65rOwWg4lE8HJSYtle77uul3+573457oHvBKuHYDl/xqUkKpEEdw==} + engines: {node: '>=18'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2396,19 +3487,63 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rollup@4.60.2: resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + semver@7.7.4: resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shadcn-vue@2.6.2: + resolution: {integrity: sha512-5IGP8qaLyaRDs3g+ap9epeZ/KoaUKlv9cjeAxdkdIaOUDwu5ktI3ntfk+dgfk2EGBdZDrXLG3jiOEdygVifh0w==} + hasBin: true + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -2444,14 +3579,28 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slice-ansi@4.0.0: + resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} + engines: {node: '>=10'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-resolve@0.6.0: + resolution: {integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} @@ -2459,12 +3608,24 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + engines: {node: '>=18'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -2473,6 +3634,14 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + engines: {node: '>=20'} + + stringify-object@6.0.0: + resolution: {integrity: sha512-6f94vIED6vmJJfh3lyVsVWxCYSfI5uM+16ntED/Ql37XIyV6kj0mRAAiTeMMc/QLYIaizC3bUprQ8pQnDDrKfA==} + engines: {node: '>=20'} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2481,6 +3650,10 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2492,6 +3665,14 @@ packages: resolution: {integrity: sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==} engines: {node: '>=12.*'} + stylus@0.57.0: + resolution: {integrity: sha512-yOI6G8WYfr0q8v8rRvE91wbxFU+rJPo760Va4MF6K0I6BZjO4r+xSynkvyPBP9tV1CIEUeRsiidjIs2rzb1CnQ==} + hasBin: true + + super-regex@1.1.0: + resolution: {integrity: sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==} + engines: {node: '>=18'} + supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} @@ -2500,16 +3681,41 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + table@6.9.0: + resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} + engines: {node: '>=10.0.0'} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@4.2.4: + resolution: {integrity: sha512-HhKppgO81FQof5m6TEnuBWCZGgfRAWbaeOaGT00KOy/Pf/j6oUihdvBpA7ltCeAvZpFhW3j0PTclkxsd4IXYDA==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + thirty-two@1.0.2: resolution: {integrity: sha512-OEI0IWCe+Dw46019YLl6V10Us5bi574EvlJEOcAkB29IzQ/mYD1A6RyNHLjZPiHCmuodxvgF6U+vZO1L15lxVA==} engines: {node: '>=0.2.6'} + time-span@5.1.0: + resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} + engines: {node: '>=12'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} + engines: {node: '>=18'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} @@ -2526,12 +3732,23 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' + ts-morph@27.0.2: + resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2544,6 +3761,9 @@ packages: resolution: {integrity: sha512-+v2QJey7ZUeUiuigkU+uFfklvNUyPI2VO2vBpMYJA+a1hKFLFiKtUYlRHdb3P9CrAvMzi0upbjI4WT+zKtqkBg==} hasBin: true + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2552,6 +3772,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript-eslint@8.59.0: resolution: {integrity: sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2580,6 +3804,14 @@ packages: unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + unplugin-utils@0.3.1: resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} engines: {node: '>=20.19.0'} @@ -2588,12 +3820,29 @@ packages: resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==} engines: {node: ^20.19.0 || >=22.12.0} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + uri-js-replace@1.0.1: resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==} uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2673,17 +3922,38 @@ packages: vue-component-type-helpers@3.2.7: resolution: {integrity: sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==} - vue-router@5.0.6: - resolution: {integrity: sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w==} + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true peerDependencies: - '@pinia/colada': '>=0.21.2' - '@vue/compiler-sfc': ^3.5.17 - pinia: ^3.0.4 - vue: ^3.5.0 + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 peerDependenciesMeta: - '@pinia/colada': + '@vue/composition-api': optional: true - '@vue/compiler-sfc': + + vue-eslint-parser@10.4.0: + resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + + vue-metamorph@3.3.4: + resolution: {integrity: sha512-WZ1xzHrmYh9UiZ7OC9eG1ASzgSybEB10jhop+k5KzMY9I1JmRKdreqUYzbV3hOnOMvLhyDn7y6f62mLE2jHFSg==} + hasBin: true + + vue-router@5.0.6: + resolution: {integrity: sha512-9+kmUTGbKMyW9Asoy98IXXYIzrTMT7JDAdpDDeEkorHvybpUvBI2wsrSM5jFOXrFydpzRFJ9vAh+80DN2PGu9w==} + peerDependencies: + '@pinia/colada': '>=0.21.2' + '@vue/compiler-sfc': ^3.5.17 + pinia: ^3.0.4 + vue: ^3.5.0 + peerDependenciesMeta: + '@pinia/colada': + optional: true + '@vue/compiler-sfc': optional: true pinia: optional: true @@ -2702,6 +3972,9 @@ packages: typescript: optional: true + web-worker@1.5.0: + resolution: {integrity: sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} @@ -2755,6 +4028,9 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -2767,6 +4043,13 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml-ast-parser@0.0.43: resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==} @@ -2783,12 +4066,25 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-spinner@1.1.0: + resolution: {integrity: sha512-/BY0AUXnS7IKO354uLLA2eRcWiqDifEbd6unXCsOxkFDAkhgUL3PH9X2bFoaU0YchnDXsF+iKleeTLJGckbXfA==} + engines: {node: '>=18.19'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + youch-core@0.3.3: resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} youch@4.1.0-beta.10: resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} @@ -2805,6 +4101,28 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@10.2.2) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.2 @@ -2813,14 +4131,153 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + '@babel/parser@7.29.2': dependencies: '@babel/types': 7.29.0 + '@babel/parser@8.0.0-alpha.12': {} + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3(supports-color@10.2.2) + transitivePeerDependencies: + - supports-color + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.27.1 @@ -2855,8 +4312,25 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@dotenvx/dotenvx@1.64.0': + dependencies: + commander: 11.1.0 + dotenv: 17.4.2 + eciesjs: 0.4.18 + execa: 5.1.1 + fdir: 6.5.0(picomatch@4.0.4) + ignore: 5.3.2 + object-treeify: 1.1.33 + picomatch: 4.0.4 + which: 4.0.0 + yocto-spinner: 1.1.0 + '@drizzle-team/brocli@0.10.2': {} + '@ecies/ciphers@0.2.6(@noble/ciphers@1.3.0)': + dependencies: + '@noble/ciphers': 1.3.0 + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 @@ -3172,9 +4646,9 @@ snapshots: '@esbuild/win32-x64@0.27.7': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.6.1))': dependencies: - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -3218,6 +4692,30 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@floating-ui/vue@1.1.11(vue@3.5.33(typescript@5.9.3))': + dependencies: + '@floating-ui/dom': 1.7.6 + '@floating-ui/utils': 0.2.11 + vue-demi: 0.14.10(vue@3.5.33(typescript@5.9.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@hono/node-server@1.19.14(hono@4.12.15)': + dependencies: + hono: 4.12.15 + '@humanfs/core@0.19.2': dependencies: '@humanfs/types': 0.15.0 @@ -3330,6 +4828,14 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@internationalized/date@3.12.1': + dependencies: + '@swc/helpers': 0.5.21 + + '@internationalized/number@3.6.6': + dependencies: + '@swc/helpers': 0.5.21 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3339,6 +4845,8 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/cliui@9.0.0': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3363,6 +4871,28 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.15) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.4.1(express@5.2.1) + hono: 4.12.15 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true @@ -3381,6 +4911,26 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@one-ini/wasm@0.1.1': {} '@otplib/core@12.0.1': {} @@ -3526,6 +5076,91 @@ snapshots: '@speed-highlight/core@1.2.15': {} + '@swc/helpers@0.5.21': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.2.4': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.0 + jiti: 2.6.1 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.4 + + '@tailwindcss/oxide-android-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.4': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.4': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.4': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.4': + optional: true + + '@tailwindcss/oxide@4.2.4': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-arm64': 4.2.4 + '@tailwindcss/oxide-darwin-x64': 4.2.4 + '@tailwindcss/oxide-freebsd-x64': 4.2.4 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.4 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.4 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.4 + '@tailwindcss/oxide-linux-x64-musl': 4.2.4 + '@tailwindcss/oxide-wasm32-wasi': 4.2.4 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.4 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.4 + + '@tailwindcss/vite@4.2.4(vite@6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@tailwindcss/node': 4.2.4 + '@tailwindcss/oxide': 4.2.4 + tailwindcss: 4.2.4 + vite: 6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) + + '@tanstack/virtual-core@3.14.0': {} + + '@tanstack/vue-virtual@3.13.24(vue@3.5.33(typescript@5.9.3))': + dependencies: + '@tanstack/virtual-core': 3.14.0 + vue: 3.5.33(typescript@5.9.3) + + '@ts-morph/common@0.28.1': + dependencies: + minimatch: 10.2.5 + path-browserify: 1.0.1 + tinyglobby: 0.2.16 + '@turbo/darwin-64@2.9.6': optional: true @@ -3561,15 +5196,17 @@ snapshots: dependencies: undici-types: 6.21.0 - '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': + '@types/web-bluetooth@0.0.21': {} + + '@typescript-eslint/eslint-plugin@8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.59.0 - '@typescript-eslint/type-utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.59.0 - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -3577,14 +5214,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.59.0 '@typescript-eslint/types': 8.59.0 '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.59.0 debug: 4.4.3(supports-color@10.2.2) - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3607,13 +5244,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.59.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.59.0 '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3(supports-color@10.2.2) - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -3636,13 +5273,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.0(eslint@9.39.4)(typescript@5.9.3)': + '@typescript-eslint/utils@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.59.0 '@typescript-eslint/types': 8.59.0 '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3652,13 +5289,26 @@ snapshots: '@typescript-eslint/types': 8.59.0 eslint-visitor-keys: 5.0.1 + '@unovue/detypes@0.8.5': + dependencies: + '@babel/core': 7.29.0 + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@vue/compiler-dom': 3.5.33 + '@vue/compiler-sfc': 3.5.33 + '@vuedx/template-ast-types': 0.7.1 + fast-glob: 3.3.3 + prettier: 3.8.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@upstash/redis@1.37.0': dependencies: uncrypto: 0.1.3 - '@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.33(typescript@5.9.3))': + '@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.33(typescript@5.9.3))': dependencies: - vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) vue: 3.5.33(typescript@5.9.3) '@vitest/expect@3.2.4': @@ -3669,13 +5319,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3))': + '@vitest/mocker@3.2.4(vite@6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) '@vitest/pretty-format@3.2.4': dependencies: @@ -3819,8 +5469,30 @@ snapshots: optionalDependencies: '@vue/server-renderer': 3.5.33(vue@3.5.33(typescript@5.9.3)) + '@vuedx/template-ast-types@0.7.1': + dependencies: + '@vue/compiler-core': 3.5.33 + + '@vueuse/core@14.2.1(vue@3.5.33(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.2.1 + '@vueuse/shared': 14.2.1(vue@3.5.33(typescript@5.9.3)) + vue: 3.5.33(typescript@5.9.3) + + '@vueuse/metadata@14.2.1': {} + + '@vueuse/shared@14.2.1(vue@3.5.33(typescript@5.9.3))': + dependencies: + vue: 3.5.33(typescript@5.9.3) + abbrev@2.0.0: {} + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -3829,6 +5501,10 @@ snapshots: agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -3836,6 +5512,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + alien-signals@1.0.13: {} ansi-colors@4.1.3: {} @@ -3852,6 +5535,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + assertion-error@2.0.1: {} ast-kit@2.2.0: @@ -3859,19 +5546,45 @@ snapshots: '@babel/parser': 7.29.2 pathe: 2.0.3 + ast-types-x@1.18.0: + dependencies: + tslib: 2.8.1 + ast-walker-scope@0.8.3: dependencies: '@babel/parser': 7.29.2 ast-kit: 2.2.0 + astral-regex@2.0.0: {} + + atob@2.1.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} + baseline-browser-mapping@2.10.23: {} + birpc@2.9.0: {} blake3-wasm@2.1.5: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3(supports-color@10.2.2) + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + boolbase@1.0.0: {} + brace-expansion@1.1.14: dependencies: balanced-match: 1.0.2 @@ -3885,8 +5598,41 @@ snapshots: dependencies: balanced-match: 4.0.4 + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.23 + caniuse-lite: 1.0.30001791 + electron-to-chromium: 1.5.344 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-from@1.1.2: {} + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bytes@3.1.2: {} + + c12@3.3.4: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 + exsolve: 1.0.8 + giget: 3.2.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.1 + rc9: 3.0.1 + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -3901,6 +5647,8 @@ snapshots: callsites@3.1.0: {} + caniuse-lite@1.0.30001791: {} + chai@5.3.3: dependencies: assertion-error: 2.0.1 @@ -3914,6 +5662,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + chalk@5.6.2: {} + change-case@5.4.4: {} check-error@2.1.3: {} @@ -3922,16 +5672,40 @@ snapshots: dependencies: readdirp: 5.0.0 - color-convert@2.0.1: + citty@0.2.2: {} + + class-variance-authority@0.7.1: dependencies: - color-name: 1.1.4 + clsx: 2.1.1 - color-name@1.1.4: {} + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-progress@3.12.0: + dependencies: + string-width: 4.2.3 + + cli-spinners@3.4.0: {} + + clsx@2.1.1: {} + + code-block-writer@13.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} colorette@1.4.0: {} commander@10.0.1: {} + commander@11.1.0: {} + + commander@14.0.3: {} + concat-map@0.0.1: {} confbox@0.1.8: {} @@ -3943,8 +5717,27 @@ snapshots: ini: 1.3.8 proto-list: 1.2.4 + consola@3.4.2: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + convert-hrtime@5.0.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + cookie@1.1.1: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -3953,6 +5746,24 @@ snapshots: crypto-js@4.2.0: {} + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + css@3.0.0: + dependencies: + inherits: 2.0.4 + source-map: 0.6.1 + source-map-resolve: 0.6.0 + + cssesc@3.0.0: {} + csstype@3.2.3: {} de-indent@1.0.2: {} @@ -3963,14 +5774,59 @@ snapshots: optionalDependencies: supports-color: 10.2.2 + decode-uri-component@0.2.2: {} + + dedent@1.7.2: {} + + deep-diff@1.0.2: {} + deep-eql@5.0.2: {} deep-is@0.1.4: {} + deepmerge@4.3.1: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + defu@6.1.7: {} + + depd@2.0.0: {} + + destr@2.0.5: {} + detect-libc@2.1.2: {} + diff@8.0.4: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dotenv@16.6.1: {} + dotenv@17.4.2: {} + drizzle-kit@0.31.10: dependencies: '@drizzle-team/brocli': 0.10.2 @@ -3992,6 +5848,13 @@ snapshots: eastasianwidth@0.2.0: {} + eciesjs@0.4.18: + dependencies: + '@ecies/ciphers': 0.2.6(@noble/ciphers@1.3.0) + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + editorconfig@1.0.7: dependencies: '@one-ini/wasm': 0.1.1 @@ -3999,10 +5862,23 @@ snapshots: minimatch: 9.0.9 semver: 7.7.4 + ee-first@1.1.1: {} + + electron-to-chromium@1.5.344: {} + emoji-regex@8.0.0: {} emoji-regex@9.2.2: {} + encodeurl@2.0.0: {} + + enhanced-resolve@5.21.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@4.5.0: {} + entities@7.0.1: {} env-paths@3.0.0: @@ -4132,6 +6008,10 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + escalade@3.2.0: {} + + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} eslint-scope@8.4.0: @@ -4145,9 +6025,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.4: + eslint@9.39.4(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 @@ -4181,6 +6061,8 @@ snapshots: minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -4208,16 +6090,90 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + + eventsource-parser@3.0.8: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.8 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + expect-type@1.3.0: {} + express-rate-limit@8.4.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@10.2.2) + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.8: {} fast-deep-equal@3.1.3: {} + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fast-uri@3.1.0: {} + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -4226,6 +6182,21 @@ snapshots: dependencies: flat-cache: 4.0.1 + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -4243,11 +6214,27 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@11.3.4: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true function-bind@1.1.2: {} + function-timeout@1.0.2: {} + + fuzzysort@3.1.0: {} + gel@2.2.0: dependencies: '@petamoriken/float16': 3.9.3 @@ -4260,6 +6247,10 @@ snapshots: - supports-color optional: true + gensync@1.0.0-beta.2: {} + + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -4273,15 +6264,25 @@ snapshots: hasown: 2.0.3 math-intrinsics: 1.1.0 + get-own-enumerable-keys@1.0.0: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@6.0.1: {} + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 + giget@3.2.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -4295,10 +6296,34 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@11.1.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.2.3 + minimatch: 10.2.5 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.2 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + globals@14.0.0: {} + gonzales-pe@4.3.0: + dependencies: + minimist: 1.2.8 + gopd@1.2.0: {} + graceful-fs@4.2.11: {} + happy-dom@17.6.3: dependencies: webidl-conversions: 7.0.0 @@ -4318,6 +6343,14 @@ snapshots: hookable@5.5.3: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + https-proxy-agent@7.0.6(supports-color@10.2.2): dependencies: agent-base: 7.1.4 @@ -4325,6 +6358,16 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@2.1.0: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + identifier-regex@1.0.1: + dependencies: + reserved-identifiers: 1.2.0 + ignore@5.3.2: {} ignore@7.0.5: {} @@ -4338,8 +6381,21 @@ snapshots: index-to-position@1.2.0: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + ini@1.3.8: {} + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + + is-docker@3.0.0: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -4348,10 +6404,36 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-identifier@1.0.1: + dependencies: + identifier-regex: 1.0.1 + super-regex: 1.1.0 + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-number@7.0.0: {} + + is-obj@3.0.0: {} + + is-promise@4.0.0: {} + + is-regexp@3.1.0: {} + + is-stream@2.0.1: {} + + is-unicode-supported@2.1.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isexe@2.0.0: {} - isexe@3.1.5: - optional: true + isexe@3.1.5: {} jackspeak@3.4.3: dependencies: @@ -4359,8 +6441,16 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.2.3: + dependencies: + '@isaacs/cliui': 9.0.0 + + jiti@2.6.1: {} + jose@5.10.0: {} + jose@6.2.3: {} + js-beautify@1.15.4: dependencies: config-chain: 1.1.13 @@ -4389,14 +6479,24 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@2.2.3: {} + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 + kleur@3.0.3: {} + kleur@4.1.5: {} levn@0.4.1: @@ -4410,6 +6510,55 @@ snapshots: dependencies: libsodium-sumo: 0.8.4 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + local-pkg@1.1.2: dependencies: mlly: 1.8.2 @@ -4420,12 +6569,33 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash-es@4.18.1: {} + lodash.merge@4.6.2: {} + lodash.sortedlastindex@4.1.0: {} + + lodash.truncate@4.4.2: {} + + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.1.2 + loupe@3.2.1: {} lru-cache@10.4.3: {} + lru-cache@11.3.5: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-vue-next@1.0.0(vue@3.5.33(typescript@5.9.3)): + dependencies: + vue: 3.5.33(typescript@5.9.3) + magic-string-ast@1.0.3: dependencies: magic-string: 0.30.21 @@ -4434,8 +6604,37 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + make-asynchronous@1.1.0: + dependencies: + p-event: 6.0.1 + type-fest: 4.41.0 + web-worker: 1.5.0 + math-intrinsics@1.1.0: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mimic-fn@2.1.0: {} + + mimic-function@5.0.1: {} + miniflare@4.20260424.0: dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -4464,6 +6663,8 @@ snapshots: dependencies: brace-expansion: 2.1.0 + minimist@1.2.8: {} + minipass@7.1.3: {} mlly@1.8.2: @@ -4499,17 +6700,77 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + + node-fetch-native@1.6.7: {} + node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.2 optional: true + node-html-parser@7.1.0: + dependencies: + css-select: 5.2.2 + he: 1.2.0 + + node-releases@2.0.38: {} + nopt@7.2.1: dependencies: abbrev: 2.0.0 + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + nypm@0.6.6: + dependencies: + citty: 0.2.2 + pathe: 2.0.3 + tinyexec: 1.1.1 + + object-assign@4.1.1: {} + object-inspect@1.13.4: {} + object-treeify@1.1.33: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.3 + + ohash@2.0.11: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + openapi-fetch@0.17.0: dependencies: openapi-typescript-helpers: 0.1.0 @@ -4539,12 +6800,27 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@9.4.0: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.2.1 + otplib@12.0.1: dependencies: '@otplib/core': 12.0.1 '@otplib/preset-default': 12.0.1 '@otplib/preset-v11': 12.0.1 + p-event@6.0.1: + dependencies: + p-timeout: 6.1.4 + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -4553,6 +6829,8 @@ snapshots: dependencies: p-limit: 3.1.0 + p-timeout@6.1.4: {} + package-json-from-dist@1.0.1: {} parent-module@1.0.1: @@ -4565,10 +6843,14 @@ snapshots: index-to-position: 1.2.0 type-fest: 4.41.0 + parseurl@1.3.3: {} + path-browserify@1.0.1: {} path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-scurry@1.11.1: @@ -4576,8 +6858,15 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.3 + path-scurry@2.0.2: + dependencies: + lru-cache: 11.3.5 + minipass: 7.1.3 + path-to-regexp@6.3.0: {} + path-to-regexp@8.4.2: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -4586,8 +6875,12 @@ snapshots: picocolors@1.1.1: {} + picomatch@2.3.2: {} + picomatch@4.0.4: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -4602,6 +6895,34 @@ snapshots: pluralize@8.0.0: {} + postcss-less@6.0.0(postcss@8.5.12): + dependencies: + postcss: 8.5.12 + + postcss-sass@0.5.0: + dependencies: + gonzales-pe: 4.3.0 + postcss: 8.5.12 + + postcss-scss@4.0.9(postcss@8.5.12): + dependencies: + postcss: 8.5.12 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-styl@0.12.3: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + fast-diff: 1.3.0 + lodash.sortedlastindex: 4.1.0 + postcss: 8.5.12 + stylus: 0.57.0 + transitivePeerDependencies: + - supports-color + postcss@8.5.12: dependencies: nanoid: 3.3.11 @@ -4612,8 +6933,20 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.8.3: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + proto-list@1.2.4: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} qs@6.15.1: @@ -4622,14 +6955,62 @@ snapshots: quansync@0.2.11: {} + queue-microtask@1.2.3: {} + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + + rc9@3.0.1: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + readdirp@5.0.0: {} + recast-x@1.0.5: + dependencies: + ast-types: ast-types-x@1.18.0 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + + reka-ui@2.9.6(vue@3.5.33(typescript@5.9.3)): + dependencies: + '@floating-ui/dom': 1.7.6 + '@floating-ui/vue': 1.1.11(vue@3.5.33(typescript@5.9.3)) + '@internationalized/date': 3.12.1 + '@internationalized/number': 3.6.6 + '@tanstack/vue-virtual': 3.13.24(vue@3.5.33(typescript@5.9.3)) + '@vueuse/core': 14.2.1(vue@3.5.33(typescript@5.9.3)) + '@vueuse/shared': 14.2.1(vue@3.5.33(typescript@5.9.3)) + aria-hidden: 1.2.6 + defu: 6.1.7 + ohash: 2.0.11 + vue: 3.5.33(typescript@5.9.3) + transitivePeerDependencies: + - '@vue/composition-api' + require-from-string@2.0.2: {} + reserved-identifiers@1.2.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.1.0: {} + rollup@4.60.2: dependencies: '@types/estree': 1.0.8 @@ -4661,10 +7042,105 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.2 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + sax@1.2.4: {} + scule@1.3.0: {} + semver@6.3.1: {} + semver@7.7.4: {} + send@1.2.1: + dependencies: + debug: 4.4.3(supports-color@10.2.2) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shadcn-vue@2.6.2(eslint@9.39.4(jiti@2.6.1))(vue@3.5.33(typescript@5.9.3)): + dependencies: + '@dotenvx/dotenvx': 1.64.0 + '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) + '@unovue/detypes': 0.8.5 + '@vue/compiler-sfc': 3.5.33 + c12: 3.3.4 + commander: 14.0.3 + consola: 3.4.2 + dedent: 1.7.2 + deepmerge: 4.3.1 + diff: 8.0.4 + fs-extra: 11.3.4 + fuzzysort: 3.1.0 + get-tsconfig: 4.14.0 + giget: 3.2.0 + magic-string: 0.30.21 + nypm: 0.6.6 + ofetch: 1.5.1 + open: 10.2.0 + ora: 9.4.0 + pathe: 2.0.3 + postcss: 8.5.12 + postcss-selector-parser: 7.1.1 + prompts: 2.4.2 + reka-ui: 2.9.6(vue@3.5.33(typescript@5.9.3)) + semver: 7.7.4 + stringify-object: 6.0.0 + tailwindcss: 4.2.4 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + ts-morph: 27.0.2 + undici: 7.24.8 + validate-npm-package-name: 5.0.1 + vue-metamorph: 3.3.4(eslint@9.39.4(jiti@2.6.1)) + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@vue/composition-api' + - babel-plugin-macros + - eslint + - magicast + - supports-color + - vue + sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -4735,10 +7211,25 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} + sisteransi@1.0.5: {} + + slice-ansi@4.0.0: + dependencies: + ansi-styles: 4.3.0 + astral-regex: 2.0.0 + is-fullwidth-code-point: 3.0.0 + source-map-js@1.2.1: {} + source-map-resolve@0.6.0: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 @@ -4746,10 +7237,16 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.6: {} + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} + stdin-discarder@0.3.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -4762,6 +7259,18 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.2.0 + string-width@8.2.1: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + stringify-object@6.0.0: + dependencies: + get-own-enumerable-keys: 1.0.0 + is-identifier: 1.0.1 + is-obj: 3.0.0 + is-regexp: 3.1.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -4770,6 +7279,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-final-newline@2.0.0: {} + strip-json-comments@3.1.1: {} strip-literal@3.1.0: @@ -4781,18 +7292,57 @@ snapshots: '@types/node': 22.19.17 qs: 6.15.1 + stylus@0.57.0: + dependencies: + css: 3.0.0 + debug: 4.4.3(supports-color@10.2.2) + glob: 7.2.3 + safer-buffer: 2.1.2 + sax: 1.2.4 + source-map: 0.7.6 + transitivePeerDependencies: + - supports-color + + super-regex@1.1.0: + dependencies: + function-timeout: 1.0.2 + make-asynchronous: 1.1.0 + time-span: 5.1.0 + supports-color@10.2.2: {} supports-color@7.2.0: dependencies: has-flag: 4.0.0 + table@6.9.0: + dependencies: + ajv: 8.20.0 + lodash.truncate: 4.4.2 + slice-ansi: 4.0.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + tailwind-merge@3.5.0: {} + + tailwindcss@4.2.4: {} + + tapable@2.3.3: {} + thirty-two@1.0.2: {} + time-span@5.1.0: + dependencies: + convert-hrtime: 5.0.0 + + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} + tinyexec@1.1.1: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -4804,12 +7354,22 @@ snapshots: tinyspy@4.0.4: {} + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + ts-api-utils@2.5.0(typescript@5.9.3): dependencies: typescript: 5.9.3 - tslib@2.8.1: - optional: true + ts-morph@27.0.2: + dependencies: + '@ts-morph/common': 0.28.1 + code-block-writer: 13.0.3 + + tslib@2.8.1: {} tsx@4.21.0: dependencies: @@ -4827,19 +7387,27 @@ snapshots: '@turbo/windows-64': 2.9.6 '@turbo/windows-arm64': 2.9.6 + tw-animate-css@1.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 type-fest@4.41.0: {} - typescript-eslint@8.59.0(eslint@9.39.4)(typescript@5.9.3): + type-is@2.0.1: dependencies: - '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) - '@typescript-eslint/parser': 8.59.0(eslint@9.39.4)(typescript@5.9.3) + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript-eslint@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.0(@typescript-eslint/parser@8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.59.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.0(eslint@9.39.4)(typescript@5.9.3) - eslint: 9.39.4 + '@typescript-eslint/utils': 8.59.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -4858,6 +7426,10 @@ snapshots: dependencies: pathe: 2.0.3 + universalify@2.0.1: {} + + unpipe@1.0.0: {} + unplugin-utils@0.3.1: dependencies: pathe: 2.0.3 @@ -4869,19 +7441,31 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + uri-js-replace@1.0.1: {} uri-js@4.4.1: dependencies: punycode: 2.3.1 - vite-node@3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3): + util-deprecate@1.0.2: {} + + validate-npm-package-name@5.0.1: {} + + vary@1.1.2: {} + + vite-node@3.2.4(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: cac: 6.7.14 debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - jiti @@ -4896,7 +7480,7 @@ snapshots: - tsx - yaml - vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3): + vite@6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -4907,14 +7491,17 @@ snapshots: optionalDependencies: '@types/node': 22.19.17 fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.32.0 + stylus: 0.57.0 tsx: 4.21.0 yaml: 2.8.3 - vitest@3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(tsx@4.21.0)(yaml@2.8.3): + vitest@3.2.4(@types/node@22.19.17)(happy-dom@17.6.3)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/mocker': 3.2.4(vite@6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -4932,8 +7519,8 @@ snapshots: tinyglobby: 0.2.16 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.2(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) - vite-node: 3.2.4(@types/node@22.19.17)(tsx@4.21.0)(yaml@2.8.3) + vite: 6.4.2(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@22.19.17)(jiti@2.6.1)(lightningcss@1.32.0)(stylus@0.57.0)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.17 @@ -4956,6 +7543,48 @@ snapshots: vue-component-type-helpers@3.2.7: {} + vue-demi@0.14.10(vue@3.5.33(typescript@5.9.3)): + dependencies: + vue: 3.5.33(typescript@5.9.3) + + vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1)): + dependencies: + debug: 4.4.3(supports-color@10.2.2) + eslint: 9.39.4(jiti@2.6.1) + eslint-scope: 8.4.0 + eslint-visitor-keys: 5.0.1 + espree: 10.4.0 + esquery: 1.7.0 + semver: 7.7.4 + transitivePeerDependencies: + - supports-color + + vue-metamorph@3.3.4(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@babel/parser': 8.0.0-alpha.12 + ast-types-x: 1.18.0 + chalk: 5.6.2 + cli-progress: 3.12.0 + commander: 14.0.3 + deep-diff: 1.0.2 + fs-extra: 11.3.4 + glob: 11.1.0 + lodash-es: 4.18.1 + magic-string: 0.30.21 + micromatch: 4.0.8 + node-html-parser: 7.1.0 + postcss: 8.5.12 + postcss-less: 6.0.0(postcss@8.5.12) + postcss-sass: 0.5.0 + postcss-scss: 4.0.9(postcss@8.5.12) + postcss-styl: 0.12.3 + recast-x: 1.0.5 + table: 6.9.0 + vue-eslint-parser: 10.4.0(eslint@9.39.4(jiti@2.6.1)) + transitivePeerDependencies: + - eslint + - supports-color + vue-router@5.0.6(@vue/compiler-sfc@3.5.33)(vue@3.5.33(typescript@5.9.3)): dependencies: '@babel/generator': 7.29.1 @@ -4995,6 +7624,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 + web-worker@1.5.0: {} + webidl-conversions@7.0.0: {} webpack-virtual-modules@0.6.2: {} @@ -5008,7 +7639,6 @@ snapshots: which@4.0.0: dependencies: isexe: 3.1.5 - optional: true why-is-node-running@2.3.0: dependencies: @@ -5054,8 +7684,16 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.2.0 + wrappy@1.0.2: {} + ws@8.18.0: {} + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + yallist@3.1.1: {} + yaml-ast-parser@0.0.43: {} yaml@2.8.3: {} @@ -5064,6 +7702,12 @@ snapshots: yocto-queue@0.1.0: {} + yocto-spinner@1.1.0: + dependencies: + yoctocolors: 2.1.2 + + yoctocolors@2.1.2: {} + youch-core@0.3.3: dependencies: '@poppinss/exception': 1.2.3 @@ -5077,4 +7721,8 @@ snapshots: cookie: 1.1.1 youch-core: 0.3.3 + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod@3.25.76: {} From 7936bdb2294706da67dd7bce3ba468e1e35eb911 Mon Sep 17 00:00:00 2001 From: Gustavo Toyota Date: Mon, 27 Apr 2026 18:21:46 -0300 Subject: [PATCH 058/243] feat(new-deepnotes): register, page editor, and group pages in UI --- new-deepnotes/PLAN_PROGRESS.md | 29 +++- new-deepnotes/apps/web/package.json | 3 +- new-deepnotes/apps/web/src/App.vue | 11 +- .../apps/web/src/features/auth/LoginView.vue | 19 ++- .../web/src/features/auth/RegisterView.vue | 130 +++++++++++++++ .../features/auth/build-user-register.test.ts | 21 +++ .../src/features/auth/build-user-register.ts | 24 +++ .../apps/web/src/features/home/HomeView.vue | 58 ++++++- .../web/src/features/pages/PageEditorView.vue | 156 ++++++++++++++++++ .../web/src/features/pages/useGroupPages.ts | 68 ++++++++ new-deepnotes/apps/web/src/router.ts | 11 ++ new-deepnotes/pnpm-lock.yaml | 25 +++ 12 files changed, 537 insertions(+), 18 deletions(-) create mode 100644 new-deepnotes/apps/web/src/features/auth/RegisterView.vue create mode 100644 new-deepnotes/apps/web/src/features/auth/build-user-register.test.ts create mode 100644 new-deepnotes/apps/web/src/features/auth/build-user-register.ts create mode 100644 new-deepnotes/apps/web/src/features/pages/PageEditorView.vue create mode 100644 new-deepnotes/apps/web/src/features/pages/useGroupPages.ts diff --git a/new-deepnotes/PLAN_PROGRESS.md b/new-deepnotes/PLAN_PROGRESS.md index 1a861319..fe6ecc30 100644 --- a/new-deepnotes/PLAN_PROGRESS.md +++ b/new-deepnotes/PLAN_PROGRESS.md @@ -14,7 +14,7 @@ Living checklist for the greenfield work described in [docs/RESTART_PLAN.md](../ | **1** — Legacy repo hygiene | **Optional / n/a** | Parallel track only if still editing the old monorepo. | | **2** — Repo bootstrap | **Done** | Template DB integration test + CI `DATABASE_ADMIN_URL`; deploy doc: [docs/DEPLOY_CLOUDFLARE.md](./docs/DEPLOY_CLOUDFLARE.md). **`@deepnotes/web`:** Vitest + happy-dom + `@vue/test-utils`; `vite.config` uses `defineConfig` from `vitest/config`. Optional: Wrangler deploy job. | | **3** — REST + Drizzle features | **In progress** | Account + **2FA** complete. **Shipped:** slices [1](#pagesgroups-rest--slice-1)–[5](#pagesgroups-rest--slice-5-privacy-private-re-key); [6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion); [7 — page move](#pages-rest--slice-7-move--group-creation); [8 — create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation); **[slice 9 — membership + join flows](#pagesgroups-rest--slice-9-membership--join-invites--requests)**. **[Stripe / billing](#phase-3--stripe-billing--webhooks--account-hooks).** **[Slice 10 — collab Postgres bootstrap](#pages-rest--slice-10-collab-updates-rest):** `GET`/`POST …/collab-updates`. **Still ahead:** **collab + realtime WebSockets** (JWT upgrade, binary fan-out, optional Redis buffer like legacy) per [RESTART_PLAN §4.3](../docs/RESTART_PLAN.md). | -| **4** — Client MVP | **In progress** | **Shipped:** [OpenAPI typed client](#phase-4--openapi-typed-client-bootstrap) + [`vue-router`](#phase-4--routing--session-ui). **Next:** page list + editor shell → Yjs; `POST /api/users` register UI (must use [same password preimage as login](apps/web/README.md#sign-in-contract)). **Parallel:** E2E smoke—see [Frontend / UI track](#frontend--ui-track). | +| **4** — Client MVP | **In progress** | **Shipped:** [OpenAPI client](#phase-4--openapi-typed-client-bootstrap) + [routing + session UI](#phase-4--routing--session-ui) + [pages + editor + register](#phase-4--pages-list-editor--register). **Next:** decrypt collab blobs into Yjs (page keyring) + `POST` append; Tiptap or richer editor; **MSW/Playwright**; **groups** admin/notifications UX. **Phase 3** still: [collab + realtime WebSocket](#not-started-phase-3--realtime--collab-websocket). | | **5** — Cutover | **Not started** | Canary, redirect, retire `/trpc` when safe. | --- @@ -319,22 +319,32 @@ Sprints **1–9** (pages / groups / membership), **Stripe**, and **[slice 10 — | Layer | Shipped | |-------|---------| -| **Router** | [`router.ts`](apps/web/src/router.ts) — `createWebHistory`, `/`, `/login` (lazy `HomeView` / `LoginView`). | +| **Router** | [`router.ts`](apps/web/src/router.ts) — `createWebHistory`, `/`, [`/register`](apps/web/src/features/auth/RegisterView.vue), `/login` (lazy), [`/page/:pageId` → `PageEditorView`](apps/web/src/features/pages/PageEditorView.vue). | | **Session** | [`useSession`](apps/web/src/features/auth/useSession.ts) — `bootstrap` (single in-flight + `bootstrapped` gate), `fetchMe`, `loginWithPassword` + **2FA** branch, `loginWithDemo`, `logout`. Cookie hint via [`readDocumentCookie`](apps/web/src/features/auth/cookies.ts) (`loggedIn` only; access/refresh stay httpOnly). | | **Demo** | [`buildSessionDemoRequest`](apps/web/src/features/auth/build-demo-session.ts) — `libsodium` + `nanoid`, base64 field shapes match OpenAPI. | -| **Auth preimage** | [`bytes.ts`](apps/web/src/features/auth/bytes.ts) — `loginPreimageFromPassword` = UTF-8 bytes of the password; **must** match future register UI. | +| **Auth preimage** | [`bytes.ts`](apps/web/src/features/auth/bytes.ts) — `loginPreimageFromPassword` = UTF-8 bytes of the password; **must** match register + login. | | **Vite** | [Proxy `→ 8787`](apps/web/vite.config.ts) for `wrangler dev`; `optimizeDeps` for `libsodium-wrappers-sumo`. | | **Docs** | [apps/web/README.md](apps/web/README.md) — feature folders + sign-in contract. | +### Phase 4 — pages list, editor, register + +| Layer | Shipped | +|-------|---------| +| **Routes** | [`/page/:pageId`](apps/web/src/features/pages/PageEditorView.vue) (auth: redirect to login with `?redirect=`); guest [`/register`](apps/web/src/features/auth/RegisterView.vue). | +| **Registration** | [`buildUserRegisterRequest`](apps/web/src/features/auth/build-user-register.ts) = [`buildSessionDemoRequest`](apps/web/src/features/auth/build-demo-session.ts) + `email` + `loginHash` from [`uint8ToBase64(loginPreimage)`](apps/web/src/features/auth/bytes.ts); test [`build-user-register.test.ts`](apps/web/src/features/auth/build-user-register.test.ts). **201** → redirect to `/login?registered=1` + banner. | +| **Page list** | [`useGroupPages`](apps/web/src/features/pages/useGroupPages.ts): `GET /api/users/me/groups` then per group `GET /api/groups/{groupId}/pages` (first window, max 20); [HomeView](apps/web/src/features/home/HomeView.vue) shows links. | +| **Editor shell** | [`PageEditorView`](apps/web/src/features/pages/PageEditorView.vue): `GET /api/pages/{pageId}/collab-updates` (counts + `lastIndex` only — ciphertext not decoded); in-memory **`yjs`** `Y.Doc` + `Y.Text` bound to a textarea (local only until E2E pipeline exists). **Dependency:** `yjs` in [`package.json`](apps/web/package.json). | +| **Intentional gap** | No `POST` to append collab rows (needs encrypt); no Tiptap; [Phase 3 WS collab](#not-started-phase-3--realtime--collab-websocket) still the real-time path. | + --- ## Phase 4 checklist (client MVP) - [x] **Tooling (bootstrap):** Vitest + **happy-dom** + `@vue/test-utils` in `@deepnotes/web` (minimal `App` test); same Vite 6 pipeline via `vitest/config` `defineConfig` (RESTART_PLAN §5.8). - [x] **API client (bootstrap):** typed client from the same OpenAPI document as the Worker—see [Phase 4 — OpenAPI typed client](#phase-4--openapi-typed-client-bootstrap). Runtime bundle does **not** import `@deepnotes/api` (only generated `api-types.generated.ts` + `openapi-fetch`); regenerate after OpenAPI changes. -- [x] **Routing + session UI (slice a):** `vue-router` + [`App.vue` shell](apps/web/src/App.vue) (header, Sign in / Sign out). **`useSession`** ([`useSession.ts`](apps/web/src/features/auth/useSession.ts)): deduped `bootstrap()` = `POST /api/sessions/refresh` + `GET /api/users/me` when `loggedIn` document cookie; `loginWithPassword` (UTF-8 password → base64 `loginHash`, [README contract](apps/web/README.md#sign-in-contract)); 401 + `Requires two-factor authentication.` → 2FA fields; `loginWithDemo` ([`build-demo-session.ts`](apps/web/src/features/auth/build-demo-session.ts)); `POST /api/sessions/logout`. Views: [`/login`](apps/web/src/features/auth/LoginView.vue), [`/`](apps/web/src/features/home/HomeView.vue). Vite [proxy `/api` → 127.0.0.1:8787](apps/web/vite.config.ts). Tests: [`app.test.ts`](apps/web/src/app.test.ts) (router + bootstrap), [`bytes.test.ts`](apps/web/src/features/auth/bytes.test.ts). **Not done:** Playwright E2E; `POST /api/users` **register** form (must match login preimage). **CORS:** API must allow this app’s origin; proxy avoids cross-origin in local dev. -- [ ] **Pages / editor path:** list → open editor shell → **Yjs** + `GET`/`POST /api/pages/{id}/collab-updates` ([TRPC map](./docs/TRPC_REST_MAP.md) collab bootstrap; [Phase 3 — realtime / collab WebSocket](#not-started-phase-3--realtime--collab-websocket) still TBD for live collab). -- [ ] **Groups** subset and notifications UX as mapped from [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md). +- [x] **Routing + session UI (slice a):** `vue-router` + [`App.vue` shell](apps/web/src/App.vue) (header, Register / Sign in / Sign out). **`useSession`**: as below; Vite [proxy `/api` → 127.0.0.1:8787](apps/web/vite.config.ts). **CORS:** API must allow this app’s origin; proxy avoids cross-origin in local dev. **Not done:** Playwright E2E. +- [x] **Pages + editor + register (slice b):** [Phase 4 — pages, editor, register](#phase-4--pages-list-editor--register) — `GET` collab **metadata** on [`PageEditorView`](apps/web/src/features/pages/PageEditorView.vue) (`lastIndex`, update count); local **`yjs`** `Y.Text` draft (ciphertext not decrypted yet; no `POST` append). `POST /api/users` via [`buildUserRegisterRequest`](apps/web/src/features/auth/build-user-register.ts) (same `loginHash` preimage as login) + [`RegisterView` `/register`](apps/web/src/features/auth/RegisterView.vue). Home lists groups + first page id window. **Not done:** decrypt → `Y.applyUpdate`, `POST` collab-updates, [live collab WebSocket](#not-started-phase-3--realtime--collab-websocket). +- [ ] **Groups** subset and notifications UX as mapped from [docs/TRPC_REST_MAP.md](./docs/TRPC_REST_MAP.md) (API exists; thin SPA). - [ ] **Native wrappers** (Capacitor / Tauri): only after web MVP and CI stable. --- @@ -358,7 +368,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte ### Decoupling and layout (`@deepnotes/web`) - [x] **API surface:** `src/api/` — generated `paths` + `createDeepnotesApiClient`; bundle does not depend on `@deepnotes/api` at runtime (codegen devDeps only). **Still to enforce:** ESLint `import/no-restricted-paths` banning `@deepnotes/api-worker`, `@deepnotes/db`, `drizzle-orm` from `apps/web/src/**` once rule config is added. -- [x] **Feature folders (bootstrap):** `src/features/auth` (session, demo builder, bytes), `src/features/home` — [apps/web/README.md](./apps/web/README.md). **Still empty:** `src/shared/ui`, `src/features/pages` (list + editor). +- [x] **Feature folders (bootstrap):** `src/features/auth` (session, demo builder, bytes, [register builder](apps/web/src/features/auth/build-user-register.ts)), `src/features/home`, `src/features/pages` ([`useGroupPages`](apps/web/src/features/pages/useGroupPages.ts), [`PageEditorView`](apps/web/src/features/pages/PageEditorView.vue)) — [apps/web/README.md](./apps/web/README.md). **Optional later:** `src/shared/ui` re-exports if primitives grow; ESLint `import/no-restricted-paths` when enforced. - [x] **Session composable:** [`useSession.ts`](./apps/web/src/features/auth/useSession.ts) (testable) + thin [`LoginView.vue`](./apps/web/src/features/auth/LoginView.vue) / [`App.vue`](./apps/web/src/App.vue). **Later:** keyring + page crypto in dedicated modules (not in `.vue` only). - [x] **Tailwind + shadcn-vue:** Tailwind v4 (`@tailwindcss/vite`, [`globals.css`](./apps/web/src/styles/globals.css)); `npx shadcn-vue init` + `button` / `input` / `label` / `card` / `alert` / `checkbox`; shell uses utility classes + `@/components/ui/*` ([README — Styling](./apps/web/README.md#styling)). ESLint ignores generated [`src/components/ui`](./apps/web/src/components/ui). @@ -377,7 +387,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | **`@deepnotes/session`** | Auth, account, crypto orchestration | Unit: `login-rate-limit`, `encrypt-user-email`, `email-hash`, `send-email-change-code`. **Integration:** `account-flows.integration.test.ts` (**24** cases when DB env set) — … + [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + [slice 7 move](#pages-rest--slice-7-move--group-creation) + [slice 8 create + `groupCreation`](#pagesgroups-rest--slice-8-create--groupcreation) + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests); template `dn_test_tpl_session_email`, **`@deepnotes/db/testing/template-db`**. | **Redis** + `performSessionLogin` failed-login counters; refresh **expired JWT**; optional: invitation **reject/cancel**, join-request **reject/cancel**, **private** group invite/request **access keyring** branches | | **`@deepnotes/api`** | Zod + OpenAPI | `openapi.test.ts` (session routes + [slice 6/7 `/api/pages/...` paths](#pages-rest--slice-6-bump-backlinks-snapshots-deletion)); **`schemas/users.test.ts`**; **`schemas/pages-groups.ts`**, **`schemas/user-pages.ts`** | Optional OpenAPI **snapshot**; more Zod edge cases for new page schemas | | **`@deepnotes/api-worker`** | Hono on Worker | `index.test.ts`: **70** tests (503 matrix when env/Hyperdrive missing) — includes [slice 6](#pages-rest--slice-6-bump-backlinks-snapshots-deletion) + `/api/pages/{pageId}/move` + [slice 9](#pagesgroups-rest--slice-9-membership--join-invites--requests) + [slice 10 `…/collab-updates`](#pages-rest--slice-10-collab-updates-rest) (`GET` + `POST`) + [Stripe routes](#phase-3--stripe-billing--webhooks--account-hooks) (`/api/billing/stripe/*`, `/api/webhooks/stripe`) | **200** tests with stub `SessionEnv` + template DB (heavier) | -| **`@deepnotes/web`** | SPA | `app.test.ts` (router + `App`, bootstrap); **`client.test.ts`**; **`bytes.test.ts`**; Vitest [include](apps/web/vite.config.ts) `src/**/*.test.ts` | MSW/contract for login + 2FA; `generate:api-types` when OpenAPI changes; Playwright (see Phase 4 checklist) | +| **`@deepnotes/web`** | SPA | `app.test.ts` (router + `App`, bootstrap); **`client.test.ts`**; **`bytes.test.ts`**; **`build-user-register.test.ts`**; Vitest [include](apps/web/vite.config.ts) `src/**/*.test.ts` | MSW/contract for login + 2FA; `generate:api-types` when OpenAPI changes; Playwright (see Phase 4 checklist) | **Principle:** keep **fast unit tests** on pure crypto, Zod, and mail/HTTP branches; add **Postgres-backed** flows incrementally (same template pattern as `@deepnotes/db`) so Phase 3 routes do not regress silently. @@ -387,7 +397,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte |------------------------|--------------------------------| | Quasar + Vite 2, 4GB heap builds | Vite 6 + Vue 3.5, Vitest + happy-dom in CI | | Imports `AppRouter`, server websocket paths | Must use **OpenAPI** + documented WS only | -| No automated UI tests | **Done:** `test` + `app.test` (router) + `client.test` + `bytes.test`; auth UI in [`LoginView`](./apps/web/src/features/auth/LoginView.vue) | +| No automated UI tests | **Done:** `app.test` + `client.test` + `bytes.test` + `build-user-register.test`; pages/register routes + [`HomeView`](./apps/web/src/features/home/HomeView.vue) + [`PageEditorView`](./apps/web/src/features/pages/PageEditorView.vue) | --- @@ -422,6 +432,7 @@ Cross-cutting work so the new SPA does not repeat **legacy `apps/client`** patte | Date | Change | |------|--------| +| 2026-04-27 | **Phase 4 (pages + register + editor shell):** Routes `/register`, `/page/:pageId`; [`buildUserRegisterRequest`](./apps/web/src/features/auth/build-user-register.ts) + `RegisterView`; [`useGroupPages`](./apps/web/src/features/pages/useGroupPages.ts) + Home page links; [`PageEditorView`](./apps/web/src/features/pages/PageEditorView.vue) — `GET` collab **metadata** + local **`yjs`** draft; dependency **`yjs`**; test [`build-user-register.test.ts`](./apps/web/src/features/auth/build-user-register.test.ts). **Next (Phase 4):** decrypt + `Y.applyUpdate`, Tiptap, `POST` collab. **Phase 3:** [collab + realtime WebSocket](#not-started-phase-3--realtime--collab-websocket). | | 2026-04-27 | **Stack:** `@deepnotes/web` — Tailwind CSS v4 + shadcn-vue (Reka), `components.json`, `@/*` alias, [`README` styling](./apps/web/README.md#styling). | | 2026-04-27 | **Phase 4 — routing + session UI:** `vue-router` (`/`, `/login`); `useSession` (refresh + me bootstrap, email/password + 2FA, demo, logout); `build-demo-session` + `libsodium`/`nanoid`; Vite proxy `/api` → `127.0.0.1:8787`; `App` shell + `HomeView` + `LoginView`; `bytes.test.ts` + updated `app.test.ts`; [apps/web/README.md](./apps/web/README.md). **Next (Phase 4):** page list, editor, register account form (same `loginHash` preimage as login). **Phase 3** still: collab + realtime [WebSocket](#not-started-phase-3--realtime--collab-websocket). | | 2026-04-27 | **Phase 4 — OpenAPI typed client:** `@deepnotes/web` — `pnpm run generate:api-types` (`tsx` + `openapi-typescript`); committed `src/api/openapi.json` + `api-types.generated.ts`; `createDeepnotesApiClient` / `resolveApiBaseUrl` (`openapi-fetch`, `credentials: "include"`); `client.test.ts`; `VITE_API_URL`; eslint ignore for generated files. **Phase 3** collab WS backlog expanded (upgrade → room → wire → fan-out → Redis → tests). PLAN_PROGRESS Phase 4 snapshot → **In progress**. | diff --git a/new-deepnotes/apps/web/package.json b/new-deepnotes/apps/web/package.json index 28071da1..6ce0762f 100644 --- a/new-deepnotes/apps/web/package.json +++ b/new-deepnotes/apps/web/package.json @@ -27,7 +27,8 @@ "tailwindcss": "^4.2.4", "tw-animate-css": "^1.4.0", "vue": "^3.5.13", - "vue-router": "^5.0.6" + "vue-router": "^5.0.6", + "yjs": "^13.6.30" }, "devDependencies": { "@types/node": "^22.14.1", diff --git a/new-deepnotes/apps/web/src/App.vue b/new-deepnotes/apps/web/src/App.vue index e593355d..cabba88f 100644 --- a/new-deepnotes/apps/web/src/App.vue +++ b/new-deepnotes/apps/web/src/App.vue @@ -48,9 +48,14 @@ async function onLogout() { > {{ user.demo ? "Demo" : "Signed in" }} - + diff --git a/new-deepnotes/apps/web/src/features/auth/RegisterView.vue b/new-deepnotes/apps/web/src/features/auth/RegisterView.vue new file mode 100644 index 00000000..233c3ae1 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/RegisterView.vue @@ -0,0 +1,130 @@ + + + diff --git a/new-deepnotes/apps/web/src/features/auth/build-user-register.test.ts b/new-deepnotes/apps/web/src/features/auth/build-user-register.test.ts new file mode 100644 index 00000000..258e91ff --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/build-user-register.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; + +import { buildUserRegisterRequest } from "./build-user-register"; +import { loginPreimageFromPassword, uint8ToBase64 } from "./bytes"; + +describe("buildUserRegisterRequest", () => { + it("sets email, loginHash from UTF-8 password preimage, and demo-shaped fields", async () => { + const body = await buildUserRegisterRequest({ + email: " User@Example.com ", + password: "hunter2", + }); + expect(body.email).toBe("user@example.com"); + expect(body.loginHash).toBe( + uint8ToBase64(loginPreimageFromPassword("hunter2")), + ); + expect(body.userId).toMatch(/^[A-Za-z0-9_-]{21}$/); + expect(body.groupId).toMatch(/^[A-Za-z0-9_-]{21}$/); + expect(body.pageId).toMatch(/^[A-Za-z0-9_-]{21}$/); + expect(body.groupCreation.groupIsPublic).toBe(true); + }); +}); diff --git a/new-deepnotes/apps/web/src/features/auth/build-user-register.ts b/new-deepnotes/apps/web/src/features/auth/build-user-register.ts new file mode 100644 index 00000000..89e809a7 --- /dev/null +++ b/new-deepnotes/apps/web/src/features/auth/build-user-register.ts @@ -0,0 +1,24 @@ +import type { components } from "../../api/api-types.generated"; + +import { buildSessionDemoRequest } from "./build-demo-session"; +import { loginPreimageFromPassword, uint8ToBase64 } from "./bytes"; + +export type UserRegisterRequest = components["schemas"]["UserRegisterRequest"]; + +/** + * Random ciphertext-shaped fields (parity with demo) plus email and UTF-8 password + * preimage as `loginHash` — must match `loginWithPassword` / README sign-in contract. + */ +export async function buildUserRegisterRequest(input: { + email: string; + password: string; +}): Promise { + const base = await buildSessionDemoRequest(); + const email = input.email.trim().toLowerCase(); + const preimage = loginPreimageFromPassword(input.password); + return { + ...base, + email, + loginHash: uint8ToBase64(preimage), + }; +} diff --git a/new-deepnotes/apps/web/src/features/home/HomeView.vue b/new-deepnotes/apps/web/src/features/home/HomeView.vue index b14da144..c3db7bfb 100644 --- a/new-deepnotes/apps/web/src/features/home/HomeView.vue +++ b/new-deepnotes/apps/web/src/features/home/HomeView.vue @@ -1,4 +1,5 @@