Compare commits

...

8 Commits

Author SHA1 Message Date
bb80776776
ui: auth: improve auth form and invite page
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
2025-05-04 18:43:15 -07:00
230fcf79df
auth: refactor common oauth provider logic, add options to disable providers and require invites
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
2025-05-02 16:41:13 -07:00
4300729638
ci: add woodpecker ci for container image builds
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
2025-04-23 18:43:54 -07:00
73ef39770e
fix docker builds 2025-04-23 18:42:47 -07:00
12a3001aec
add editorconfig and zed settings 2025-04-10 19:02:55 -07:00
06a7f1bd51
ui: update shadcn components 2025-04-08 21:56:27 -07:00
d430f1db17
db: drop opnsense_id from devices schema 2025-03-30 19:55:05 -07:00
0e23c8e21c
refactor: add interface for wg provider with opnsense implementation 2025-03-30 19:54:10 -07:00
60 changed files with 1127 additions and 740 deletions

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
[*]
charset = utf-8
insert_final_newline = true
end_of_line = lf
indent_style = tab
indent_size = 2
max_line_length = 100
quote_type = single
trim_trailing_whitespace = true

View File

@ -1,12 +1,17 @@
DATABASE_URL=file:local.db
AUTH_DOMAIN=auth.lab.cazzzer.com
AUTH_CLIENT_ID=
AUTH_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
PUBLIC_AUTH_AUTHENTIK_ENABLE=1
AUTH_AUTHENTIK_REQUIRE_INVITE=0
AUTH_AUTHENTIK_DOMAIN=auth.lab.cazzzer.com
AUTH_AUTHENTIK_CLIENT_ID=
AUTH_AUTHENTIK_CLIENT_SECRET=
INVITE_TOKEN=GUjdsz9aREFTEBYDrA3AajUE8oVys2xW
PUBLIC_AUTH_GOOGLE_ENABLE=1
AUTH_GOOGLE_REQUIRE_INVITE=1
AUTH_GOOGLE_CLIENT_ID=
AUTH_GOOGLE_CLIENT_SECRET=
AUTH_INVITE_TOKEN=GUjdsz9aREFTEBYDrA3AajUE8oVys2xW
OPNSENSE_API_URL=https://opnsense.cazzzer.com
OPNSENSE_API_KEY=

View File

@ -0,0 +1,15 @@
when:
- event: [push]
steps:
- name: build
image: woodpeckerci/plugin-kaniko
settings:
registry: gitea.cazzzer.com
repo: ${CI_REPO,,}
# replace '/' in branch name
tags: ${CI_COMMIT_BRANCH/\//-}
cache: true
username:
from_secret: registry-username
password:
from_secret: registry-password

7
.zed/settings.json Normal file
View File

@ -0,0 +1,7 @@
// Folder-specific settings
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{
"formatter": "prettier"
}

View File

@ -2,7 +2,7 @@
# see all versions at https://hub.docker.com/r/oven/bun/tags
FROM oven/bun:1-alpine AS base
WORKDIR /app
COPY package.json bun.lockb /app/
COPY package.json bun.lock /app/
# install dependencies into temp directory
# this will cache them and speed up future builds
@ -14,7 +14,7 @@ RUN cd /temp/dev && bun install --frozen-lockfile
# install with --production (exclude devDependencies)
RUN mkdir -p /temp/prod
COPY package.json bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production
RUN cd /temp/prod && bun install --frozen-lockfile --production --ignore-scripts
# copy node_modules from temp directory
# then copy all (non-ignored) project files into the image

196
bun.lock
View File

@ -5,48 +5,48 @@
"name": "vpgen",
"dependencies": {
"@libsql/client": "^0.14.0",
"drizzle-kit": "^0.30.5",
"drizzle-kit": "^0.30.6",
"drizzle-orm": "^0.38.4",
},
"devDependencies": {
"@lucide/svelte": "^0.487.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@ryoppippi/unplugin-typia": "npm:@jsr/ryoppippi__unplugin-typia",
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.19.1",
"@sveltejs/kit": "^2.20.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@types/better-sqlite3": "^7.6.12",
"@types/better-sqlite3": "^7.6.13",
"@types/eslint": "^9.6.1",
"@types/qrcode-svg": "^1.1.5",
"arctic": "^2.3.4",
"autoprefixer": "^10.4.21",
"bits-ui": "^0.22.0",
"bits-ui": "^1.3.19",
"clsx": "^2.1.1",
"eslint": "^9.22.0",
"eslint": "^9.25.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0",
"ip-address": "^10.0.1",
"lucide-svelte": "^0.469.0",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"qrcode-svg": "^1.1.0",
"svelte": "^5.23.0",
"svelte-check": "^4.1.5",
"svelte": "^5.28.1",
"svelte-check": "^4.1.6",
"tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"ts-patch": "^3.3.0",
"typescript": "~5.8.2",
"typescript-eslint": "^8.26.1",
"typia": "^8.0.2",
"vite": "^6.2.2",
"typescript": "~5.8.3",
"typescript-eslint": "^8.31.0",
"typia": "^8.2.0",
"vite": "^6.3.2",
},
},
},
@ -115,19 +115,19 @@
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
"@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="],
"@eslint/config-array": ["@eslint/config-array@0.20.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.1.0", "", {}, "sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA=="],
"@eslint/config-helpers": ["@eslint/config-helpers@0.2.1", "", {}, "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw=="],
"@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="],
"@eslint/core": ["@eslint/core@0.13.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ=="],
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
"@eslint/js": ["@eslint/js@9.22.0", "", {}, "sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ=="],
"@eslint/js": ["@eslint/js@9.25.1", "", {}, "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg=="],
"@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="],
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.8", "", { "dependencies": { "@eslint/core": "^0.13.0", "levn": "^0.4.1" } }, "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA=="],
"@floating-ui/core": ["@floating-ui/core@1.6.8", "", { "dependencies": { "@floating-ui/utils": "^0.2.8" } }, "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA=="],
@ -181,7 +181,7 @@
"@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.4.7", "", { "os": "win32", "cpu": "x64" }, "sha512-7pJzOWzPm6oJUxml+PCDRzYQ4A1hTMHAciTAHfFK4fkbDZX33nWPVG7Y3vqdKtslcwAzwmrNDc6sXy2nwWnbiw=="],
"@melt-ui/svelte": ["@melt-ui/svelte@0.76.2", "", { "dependencies": { "@floating-ui/core": "^1.3.1", "@floating-ui/dom": "^1.4.5", "@internationalized/date": "^3.5.0", "dequal": "^2.0.3", "focus-trap": "^7.5.2", "nanoid": "^5.0.4" }, "peerDependencies": { "svelte": ">=3 <5" } }, "sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA=="],
"@lucide/svelte": ["@lucide/svelte@0.487.0", "", { "peerDependencies": { "svelte": "^5" } }, "sha512-27b/wUzWrqDJu97+1iSV2X8L2JGRWH/mAWAjHgazWxhGxVu/kS0p3SbNu6w3skNmQNEku33EKU1v44IVwULzbw=="],
"@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="],
@ -241,6 +241,8 @@
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.29.1", "", { "os": "linux", "cpu": "none" }, "sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.29.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.29.1", "", { "os": "linux", "cpu": "x64" }, "sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ=="],
@ -255,7 +257,7 @@
"@ryoppippi/unplugin-typia": ["@jsr/ryoppippi__unplugin-typia@1.2.0", "https://npm.jsr.io/~/11/@jsr/ryoppippi__unplugin-typia/1.2.0.tgz", { "dependencies": { "@rollup/pluginutils": "^5.1.3", "consola": "^3.2.3", "defu": "^6.1.4", "diff-match-patch": "^1.0.5", "find-cache-dir": "^5.0.0", "magic-string": "^0.30.14", "pathe": "^1.1.2", "pkg-types": "^1.2.1", "type-fest": "^4.30.0", "typescript": "~5.6.3", "typia": "^7.6.0", "unplugin": "^1.16.0", "vite": "^6.0.2" } }, "sha512-WslO8hg5nbTRn1LD+fYpHub1CnwOg1y3+ezjrRH6gdAXdamr9n4Vr3COe5ejE8EjM6Ai+5WgTlOT0NJMu7w3MQ=="],
"@samchon/openapi": ["@samchon/openapi@3.0.0", "", {}, "sha512-eVQlyKRYv1/C2Mikc1xZr7c0jMjg1vjPkeY/gheKB4c5WOOWyTNZ1uvnXR+ETpPHwaQ54I9NrQZhoNk6BEGuuw=="],
"@samchon/openapi": ["@samchon/openapi@3.3.0", "", {}, "sha512-f8HMS0PVAKTGR0TwgiBX4PKlZRPfdtH5fJbtpUth3fMUZmikjZ7CBey8gX7HZgY4n/aYHyNQKcf/W/E2267oYw=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="],
@ -263,7 +265,7 @@
"@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.2.12", "", { "dependencies": { "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "rollup": "^4.9.5" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ=="],
"@sveltejs/kit": ["@sveltejs/kit@2.19.1", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0" }, "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-1u8FGWtT+V0eBSQpDtsjINhQd6RTjadvEgVApdNVxbUeOXmQTDENTeRiZw2i1lNUx1/5Wa65CHzxtMh3P3yviw=="],
"@sveltejs/kit": ["@sveltejs/kit@2.20.7", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3 || ^6.0.0" }, "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-dVbLMubpJJSLI4OYB+yWYNHGAhgc2bVevWuBjDj8jFUXIJOAnLwYP3vsmtcgoxNGUXoq0rHS5f7MFCsryb6nzg=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@5.0.3", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", "debug": "^4.4.0", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.15", "vitefu": "^1.0.4" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.0.0" } }, "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw=="],
@ -277,7 +279,7 @@
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.16", "", { "dependencies": { "lodash.castarray": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA=="],
"@types/better-sqlite3": ["@types/better-sqlite3@7.6.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg=="],
"@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
@ -295,21 +297,21 @@
"@types/ws": ["@types/ws@8.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.26.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.26.1", "@typescript-eslint/type-utils": "8.26.1", "@typescript-eslint/utils": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.31.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.31.0", "@typescript-eslint/type-utils": "8.31.0", "@typescript-eslint/utils": "8.31.0", "@typescript-eslint/visitor-keys": "8.31.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.26.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.26.1", "@typescript-eslint/types": "8.26.1", "@typescript-eslint/typescript-estree": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.31.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.31.0", "@typescript-eslint/types": "8.31.0", "@typescript-eslint/typescript-estree": "8.31.0", "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.26.1", "", { "dependencies": { "@typescript-eslint/types": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1" } }, "sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.31.0", "", { "dependencies": { "@typescript-eslint/types": "8.31.0", "@typescript-eslint/visitor-keys": "8.31.0" } }, "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.26.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.26.1", "@typescript-eslint/utils": "8.26.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.31.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.31.0", "@typescript-eslint/utils": "8.31.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.26.1", "", {}, "sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.31.0", "", {}, "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.26.1", "", { "dependencies": { "@typescript-eslint/types": "8.26.1", "@typescript-eslint/visitor-keys": "8.26.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.31.0", "", { "dependencies": { "@typescript-eslint/types": "8.31.0", "@typescript-eslint/visitor-keys": "8.31.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.26.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.26.1", "@typescript-eslint/types": "8.26.1", "@typescript-eslint/typescript-estree": "8.26.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.31.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.31.0", "@typescript-eslint/types": "8.31.0", "@typescript-eslint/typescript-estree": "8.31.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.26.1", "", { "dependencies": { "@typescript-eslint/types": "8.26.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.31.0", "", { "dependencies": { "@typescript-eslint/types": "8.31.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ=="],
"acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
@ -347,7 +349,7 @@
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"bits-ui": ["bits-ui@0.22.0", "", { "dependencies": { "@internationalized/date": "^3.5.1", "@melt-ui/svelte": "0.76.2", "nanoid": "^5.0.5" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0" } }, "sha512-r7Fw1HNgA4YxZBRcozl7oP0bheQ8EHh+kfMBZJgyFISix8t4p/nqDcHLmBgIiJ3T5XjYnJRorYDjIWaCfhb5fw=="],
"bits-ui": ["bits-ui@1.3.19", "", { "dependencies": { "@floating-ui/core": "^1.6.4", "@floating-ui/dom": "^1.6.7", "@internationalized/date": "^3.5.6", "esm-env": "^1.1.2", "runed": "^0.23.2", "svelte-toolbelt": "^0.7.1", "tabbable": "^6.2.0" }, "peerDependencies": { "svelte": "^5.11.0" } }, "sha512-2blb6dkgedHUsDXqCjvmtUi4Advgd9MhaJDT8r7bEWDzHI8HGsOoYsLeh8CxpEWWEYPrlGN+7k+kpxRhIDdFrQ=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
@ -421,8 +423,6 @@
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="],
"devalue": ["devalue@5.1.1", "", {}, "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="],
@ -435,7 +435,7 @@
"drange": ["drange@1.1.1", "", {}, "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA=="],
"drizzle-kit": ["drizzle-kit@0.30.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-l6dMSE100u7sDaTbLczibrQZjA35jLsHNqIV+jmhNVO3O8jzM6kywMOmV9uOz9ZVSCMPQhAZEFjL/qDPVrqpUA=="],
"drizzle-kit": ["drizzle-kit@0.30.6", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0", "gel": "^2.0.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g=="],
"drizzle-orm": ["drizzle-orm@0.38.4", "", { "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/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "react", "sql.js", "sqlite3"] }, "sha512-s7/5BpLKO+WJRHspvpqTydxFob8i1vo2rEx4pY6TGY7QSMuUfWUuzaY0DIpXCkgHOo37BaFC+SJQb99dDUXT3Q=="],
@ -455,7 +455,7 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@9.22.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.2", "@eslint/config-helpers": "^0.1.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.0", "@eslint/js": "9.22.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ=="],
"eslint": ["eslint@9.25.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.1", "@eslint/core": "^0.13.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.25.1", "@eslint/plugin-kit": "^0.2.8", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.3.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ=="],
"eslint-compat-utils": ["eslint-compat-utils@0.5.1", "", { "dependencies": { "semver": "^7.5.4" }, "peerDependencies": { "eslint": ">=6.0.0" } }, "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q=="],
@ -475,7 +475,7 @@
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
"esrap": ["esrap@1.4.5", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g=="],
"esrap": ["esrap@1.4.6", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw=="],
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
@ -515,8 +515,6 @@
"flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="],
"focus-trap": ["focus-trap@7.6.2", "", { "dependencies": { "tabbable": "^6.2.0" } }, "sha512-9FhUxK1hVju2+AiQIDJ5Dd//9R2n2RAfJ0qfhF4IHGHgcoEUTMpbTeG/zbEuwaiYXfuAH6XE0/aCyxDdRM+W5w=="],
"foreground-child": ["foreground-child@3.3.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" } }, "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
@ -563,6 +561,8 @@
"ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="],
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
"inquirer": ["inquirer@8.2.6", "", { "dependencies": { "ansi-escapes": "^4.2.1", "chalk": "^4.1.1", "cli-cursor": "^3.1.0", "cli-width": "^3.0.0", "external-editor": "^3.0.3", "figures": "^3.0.0", "lodash": "^4.17.21", "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", "wrap-ansi": "^6.0.1" } }, "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg=="],
"ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="],
@ -635,8 +635,6 @@
"lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"lucide-svelte": ["lucide-svelte@0.469.0", "", { "peerDependencies": { "svelte": "^3 || ^4 || ^5.0.0-next.42" } }, "sha512-PMIJ8jrFqVUsXJz4d1yfAQplaGhNOahwwkzbunha8DhpiD73xqX24n8dE1dPpUk3vcrdWVsHc1y/liHHotOnGQ=="],
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
@ -665,7 +663,7 @@
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"nanoid": ["nanoid@5.0.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q=="],
"nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
@ -787,6 +785,8 @@
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
@ -827,18 +827,22 @@
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"style-to-object": ["style-to-object@1.0.8", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g=="],
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"svelte": ["svelte@5.23.0", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.3", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-v0lL3NuKontiCxholEiAXCB+BYbndlKbwlDMK0DS86WgGELMJSpyqCSbJeMEMBDwOglnS7Ar2Rq0wwa/z2L8Vg=="],
"svelte": ["svelte@5.28.1", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "esm-env": "^1.2.1", "esrap": "^1.4.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-iOa9WmfNG95lSOSJdMhdjJ4Afok7IRAQYXpbnxhd5EINnXseG0GVa9j6WPght4eX78XfFez45Fi+uRglGKPV/Q=="],
"svelte-check": ["svelte-check@4.1.5", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg=="],
"svelte-check": ["svelte-check@4.1.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": ">=5.0.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-P7w/6tdSfk3zEVvfsgrp3h3DFC75jCdZjTQvgGJtjPORs1n7/v2VMPIoty3PWv7jnfEm3x0G/p9wH4pecTb0Wg=="],
"svelte-eslint-parser": ["svelte-eslint-parser@0.43.0", "", { "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "postcss": "^8.4.39", "postcss-scss": "^4.0.9" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA=="],
"svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="],
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
"tailwind-merge": ["tailwind-merge@2.6.0", "", {}, "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA=="],
@ -855,6 +859,8 @@
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
"tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
"tmp": ["tmp@0.0.33", "", { "dependencies": { "os-tmpdir": "~1.0.2" } }, "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
@ -873,11 +879,11 @@
"type-fest": ["type-fest@4.37.0", "", {}, "sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg=="],
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"typescript-eslint": ["typescript-eslint@8.26.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.26.1", "@typescript-eslint/parser": "8.26.1", "@typescript-eslint/utils": "8.26.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg=="],
"typescript-eslint": ["typescript-eslint@8.31.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.31.0", "@typescript-eslint/parser": "8.31.0", "@typescript-eslint/utils": "8.31.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ=="],
"typia": ["typia@8.0.2", "", { "dependencies": { "@samchon/openapi": "^3.0.0", "commander": "^10.0.0", "comment-json": "^4.2.3", "inquirer": "^8.2.5", "package-manager-detector": "^0.2.0", "randexp": "^0.5.3" }, "peerDependencies": { "typescript": ">=4.8.0 <5.9.0" }, "bin": { "typia": "lib/executable/typia.js" } }, "sha512-xDacELBticuXx17TtPWhSd1thyE2MF74LZhhDoCFkpCKhiElnB40X/Jb3zMLRx86WUkqCH60rwm3ghaJdBs7hA=="],
"typia": ["typia@8.2.0", "", { "dependencies": { "@samchon/openapi": "^3.3.0", "commander": "^10.0.0", "comment-json": "^4.2.3", "inquirer": "^8.2.5", "package-manager-detector": "^0.2.0", "randexp": "^0.5.3" }, "peerDependencies": { "typescript": ">=4.8.0 <5.9.0" }, "bin": { "typia": "lib/executable/typia.js" } }, "sha512-DAQl9CLItDwMd4k4HnaExxv6wjBFLW04WqGBQEnOpylq2YmZsM6HTj4Bfxxp3wtgAsMXZTugAdQRPsFOxMiRrQ=="],
"ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
@ -891,7 +897,7 @@
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vite": ["vite@6.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ=="],
"vite": ["vite@6.3.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.3", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.12" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg=="],
"vitefu": ["vitefu@1.0.4", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-y6zEE3PQf6uu/Mt6DTJ9ih+kyJLr4XcSgHR2zUkM8SWDhuixEJxfJ6CZGMHh1Ec3vPLoEA0IHU5oWzVqw8ulow=="],
@ -939,6 +945,8 @@
"@ryoppippi/unplugin-typia/typia": ["typia@7.6.4", "", { "dependencies": { "@samchon/openapi": "^2.4.2", "commander": "^10.0.0", "comment-json": "^4.2.3", "inquirer": "^8.2.5", "package-manager-detector": "^0.2.0", "randexp": "^0.5.3" }, "peerDependencies": { "typescript": ">=4.8.0 <5.8.0" }, "bin": { "typia": "lib/executable/typia.js" } }, "sha512-Z3AcvGBjS7Dc7+iAG2VhsT0NIv3HMoXVTJ3F7Pgth8z7rhQu1JnyS8GGqqWdKk5ROvOgALEVEWmWv3Lym2eBIg=="],
"@ryoppippi/unplugin-typia/vite": ["vite@6.2.2", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ=="],
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
@ -969,8 +977,6 @@
"pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"postcss-load-config/lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="],
"postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
@ -997,7 +1003,11 @@
"tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="],
"vite/rollup": ["rollup@4.34.8", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.8", "@rollup/rollup-android-arm64": "4.34.8", "@rollup/rollup-darwin-arm64": "4.34.8", "@rollup/rollup-darwin-x64": "4.34.8", "@rollup/rollup-freebsd-arm64": "4.34.8", "@rollup/rollup-freebsd-x64": "4.34.8", "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", "@rollup/rollup-linux-arm-musleabihf": "4.34.8", "@rollup/rollup-linux-arm64-gnu": "4.34.8", "@rollup/rollup-linux-arm64-musl": "4.34.8", "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", "@rollup/rollup-linux-riscv64-gnu": "4.34.8", "@rollup/rollup-linux-s390x-gnu": "4.34.8", "@rollup/rollup-linux-x64-gnu": "4.34.8", "@rollup/rollup-linux-x64-musl": "4.34.8", "@rollup/rollup-win32-arm64-msvc": "4.34.8", "@rollup/rollup-win32-ia32-msvc": "4.34.8", "@rollup/rollup-win32-x64-msvc": "4.34.8", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ=="],
"tinyglobby/fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
"vite/fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
"vite/rollup": ["rollup@4.40.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.0", "@rollup/rollup-android-arm64": "4.40.0", "@rollup/rollup-darwin-arm64": "4.40.0", "@rollup/rollup-darwin-x64": "4.40.0", "@rollup/rollup-freebsd-arm64": "4.40.0", "@rollup/rollup-freebsd-x64": "4.40.0", "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", "@rollup/rollup-linux-arm-musleabihf": "4.40.0", "@rollup/rollup-linux-arm64-gnu": "4.40.0", "@rollup/rollup-linux-arm64-musl": "4.40.0", "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-musl": "4.40.0", "@rollup/rollup-linux-s390x-gnu": "4.40.0", "@rollup/rollup-linux-x64-gnu": "4.40.0", "@rollup/rollup-linux-x64-musl": "4.40.0", "@rollup/rollup-win32-arm64-msvc": "4.40.0", "@rollup/rollup-win32-ia32-msvc": "4.40.0", "@rollup/rollup-win32-x64-msvc": "4.40.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
@ -1051,6 +1061,8 @@
"@ryoppippi/unplugin-typia/typia/@samchon/openapi": ["@samchon/openapi@2.5.3", "", {}, "sha512-gLcGqPGsr/V43yNrhC+5E8ZR8y1JdLZDgkC6ULg+B03uvmdS2C7qIXpIWUe19MUPvXt5SCpjhvOykomEspinaw=="],
"@ryoppippi/unplugin-typia/vite/rollup": ["rollup@4.34.8", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.8", "@rollup/rollup-android-arm64": "4.34.8", "@rollup/rollup-darwin-arm64": "4.34.8", "@rollup/rollup-darwin-x64": "4.34.8", "@rollup/rollup-freebsd-arm64": "4.34.8", "@rollup/rollup-freebsd-x64": "4.34.8", "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", "@rollup/rollup-linux-arm-musleabihf": "4.34.8", "@rollup/rollup-linux-arm64-gnu": "4.34.8", "@rollup/rollup-linux-arm64-musl": "4.34.8", "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", "@rollup/rollup-linux-riscv64-gnu": "4.34.8", "@rollup/rollup-linux-s390x-gnu": "4.34.8", "@rollup/rollup-linux-x64-gnu": "4.34.8", "@rollup/rollup-linux-x64-musl": "4.34.8", "@rollup/rollup-win32-arm64-msvc": "4.34.8", "@rollup/rollup-win32-ia32-msvc": "4.34.8", "@rollup/rollup-win32-x64-msvc": "4.34.8", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ=="],
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="],
@ -1099,8 +1111,6 @@
"drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="],
"eslint-plugin-svelte/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"gel/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="],
"glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
@ -1111,53 +1121,89 @@
"pkg-dir/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
"svelte-eslint-parser/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"tailwindcss/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"tailwindcss/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"tailwindcss/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
"tailwindcss/postcss-load-config/yaml": ["yaml@2.6.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg=="],
"vite/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.8", "", { "os": "android", "cpu": "arm" }, "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw=="],
"vite/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.0", "", { "os": "android", "cpu": "arm" }, "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="],
"vite/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q=="],
"vite/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="],
"vite/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q=="],
"vite/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="],
"vite/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw=="],
"vite/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="],
"vite/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA=="],
"vite/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="],
"vite/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q=="],
"vite/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="],
"vite/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g=="],
"vite/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="],
"vite/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA=="],
"vite/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="],
"vite/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A=="],
"vite/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="],
"vite/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q=="],
"vite/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="],
"vite/rollup/@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ=="],
"vite/rollup/@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="],
"vite/rollup/@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw=="],
"vite/rollup/@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="],
"vite/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw=="],
"vite/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="],
"vite/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA=="],
"vite/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="],
"vite/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA=="],
"vite/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="],
"vite/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ=="],
"vite/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="],
"vite/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ=="],
"vite/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="],
"vite/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w=="],
"vite/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="],
"vite/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.8", "", { "os": "win32", "cpu": "x64" }, "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g=="],
"vite/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="],
"vite/rollup/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.8", "", { "os": "android", "cpu": "arm" }, "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w=="],
"@ryoppippi/unplugin-typia/vite/rollup/@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.8", "", { "os": "win32", "cpu": "x64" }, "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g=="],
"pkg-dir/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="],

View File

@ -0,0 +1 @@
ALTER TABLE `devices` DROP COLUMN `opnsense_id`;

View File

@ -0,0 +1,222 @@
{
"version": "6",
"dialect": "sqlite",
"id": "0b364191-58d0-46a3-8372-4a30b0b88d85",
"prevId": "cc1fa973-1e9c-4bd6-b082-d7cf36f7342c",
"tables": {
"devices": {
"name": "devices",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"public_key": {
"name": "public_key",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"private_key": {
"name": "private_key",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"pre_shared_key": {
"name": "pre_shared_key",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"devices_public_key_unique": {
"name": "devices_public_key_unique",
"columns": [
"public_key"
],
"isUnique": true
}
},
"foreignKeys": {
"devices_user_id_users_id_fk": {
"name": "devices_user_id_users_id_fk",
"tableFrom": "devices",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"ip_allocations": {
"name": "ip_allocations",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"device_id": {
"name": "device_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"ip_allocations_device_id_unique": {
"name": "ip_allocations_device_id_unique",
"columns": [
"device_id"
],
"isUnique": true
}
},
"foreignKeys": {
"ip_allocations_device_id_devices_id_fk": {
"name": "ip_allocations_device_id_devices_id_fk",
"tableFrom": "ip_allocations",
"tableTo": "devices",
"columnsFrom": [
"device_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"sessions": {
"name": "sessions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"sessions_user_id_users_id_fk": {
"name": "sessions_user_id_users_id_fk",
"tableFrom": "sessions",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"auth_source": {
"name": "auth_source",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'authentik'"
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -15,6 +15,13 @@
"when": 1741936760967,
"tag": "0001_equal_unicorn",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1743389268079,
"tag": "0002_minor_black_panther",
"breakpoints": true
}
]
}

View File

@ -1,6 +1,7 @@
{
"name": "vpgen",
"version": "0.0.1",
"license": "AGPL-3.0-or-later",
"type": "module",
"scripts": {
"dev": "vite dev",
@ -18,48 +19,48 @@
"prepare": "ts-patch install"
},
"devDependencies": {
"@lucide/svelte": "^0.487.0",
"@oslojs/crypto": "^1.0.1",
"@oslojs/encoding": "^1.1.0",
"@ryoppippi/unplugin-typia": "npm:@jsr/ryoppippi__unplugin-typia",
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.19.1",
"@sveltejs/kit": "^2.20.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@types/better-sqlite3": "^7.6.12",
"@types/better-sqlite3": "^7.6.13",
"@types/eslint": "^9.6.1",
"@types/qrcode-svg": "^1.1.5",
"arctic": "^2.3.4",
"autoprefixer": "^10.4.21",
"bits-ui": "^0.22.0",
"bits-ui": "^1.3.19",
"clsx": "^2.1.1",
"eslint": "^9.22.0",
"eslint": "^9.25.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0",
"ip-address": "^10.0.1",
"lucide-svelte": "^0.469.0",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"qrcode-svg": "^1.1.0",
"svelte": "^5.23.0",
"svelte-check": "^4.1.5",
"svelte": "^5.28.1",
"svelte-check": "^4.1.6",
"tailwind-merge": "^2.6.0",
"tailwind-variants": "^0.3.1",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"ts-patch": "^3.3.0",
"typescript": "~5.8.2",
"typescript-eslint": "^8.26.1",
"typia": "^8.0.2",
"vite": "^6.2.2"
"typescript": "~5.8.3",
"typescript-eslint": "^8.31.0",
"typia": "^8.2.0",
"vite": "^6.3.2"
},
"dependencies": {
"@libsql/client": "^0.14.0",
"drizzle-kit": "^0.30.5",
"drizzle-kit": "^0.30.6",
"drizzle-orm": "^0.38.4"
}
}

View File

@ -2,10 +2,9 @@ import { type Handle, redirect } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import { dev } from '$app/environment';
import * as auth from '$lib/server/auth';
import { fetchOpnsenseServer } from '$lib/server/opnsense';
import wgProvider from '$lib/server/wg-provider';
// fetch opnsense server info on startup
await fetchOpnsenseServer();
await wgProvider.init();
const handleAuth: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get(auth.sessionCookieName);

9
src/lib/auth.ts Normal file
View File

@ -0,0 +1,9 @@
import { envToBool } from '$lib/utils';
import { env } from '$env/dynamic/public';
export type AuthProvider = 'authentik' | 'google';
export const enabledAuthProviders: Record<AuthProvider, boolean> = {
authentik: envToBool(env.PUBLIC_AUTH_AUTHENTIK_ENABLE),
google: envToBool(env.PUBLIC_AUTH_GOOGLE_ENABLE),
};

View File

@ -0,0 +1,28 @@
<script lang="ts">
import { LucideLoaderCircle } from '@lucide/svelte';
import { Button } from '$lib/components/ui/button';
interface Props {
providerName: string;
displayName: string;
iconSrc: string;
inviteToken?: string;
}
let { providerName, displayName, inviteToken, iconSrc }: Props = $props();
let submitted = $state(false);
</script>
<form method="get" onsubmit={() => (submitted = true)} action="/auth/{providerName}">
{#if inviteToken}
<input type="hidden" value={inviteToken} name="invite" />
{/if}
<Button type="submit" disabled={submitted}>
{#if submitted}
<LucideLoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<img class="mr-2 h-4 w-4" alt="{displayName} Logo" src={iconSrc} />
{/if}
Sign {inviteToken ? 'up' : 'in'} with {displayName}
</Button>
</form>

View File

@ -1,53 +1,26 @@
<script lang="ts">
import { LucideLoaderCircle } from 'lucide-svelte';
import { Button } from '$lib/components/ui/button';
import { cn } from '$lib/utils.js';
import googleIcon from '$lib/assets/google.svg';
import { enabledAuthProviders } from '$lib/auth';
import AuthButton from './auth-button.svelte';
let { inviteToken, class: className, ...rest }: { inviteToken?: string; class?: string; rest?: { [p: string]: unknown } } = $props();
let isLoading = $state(false);
interface Props {
inviteToken?: string;
class?: string;
}
let { inviteToken, class: className }: Props = $props();
</script>
<div class={cn('flex gap-6', className)} {...rest}>
<form method="get" action="/auth/authentik{inviteToken ? `?invite=${inviteToken}` : ''}">
<input type="hidden" value={inviteToken} name="invite" />
<Button
type="submit"
onclick={() => {
isLoading = true;
}}
>
{#if isLoading}
<LucideLoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<img
class="mr-2 h-4 w-4"
alt="Authentik Logo"
src="https://auth.cazzzer.com/static/dist/assets/icons/icon.svg"
/>
{/if}
Sign in with Authentik
</Button>
</form>
<form method="get" action="/auth/google">
<input type="hidden" value={inviteToken} name="invite" />
<Button
type="submit"
onclick={() => {
isLoading = true;
}}
>
{#if isLoading}
<LucideLoaderCircle class="mr-2 h-4 w-4 animate-spin" />
{:else}
<img
class="mr-2 h-4 w-4"
alt="Google Logo"
src={googleIcon}
/>
{/if}
Sign in with Google
</Button>
</form>
<div class={cn('flex gap-6', className)}>
{#if enabledAuthProviders.authentik}
<AuthButton
providerName="authentik"
displayName="Authentik"
iconSrc="https://auth.cazzzer.com/static/dist/assets/icons/icon.svg"
{inviteToken}
/>
{/if}
{#if enabledAuthProviders.google}
<AuthButton providerName="google" displayName="Google" iconSrc={googleIcon} {inviteToken} />
{/if}
</div>

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import { LucideClipboardCopy, LucideDownload } from 'lucide-svelte';
import { LucideClipboardCopy, LucideDownload } from '@lucide/svelte';
const {
data,

View File

@ -43,7 +43,7 @@
this={href ? "a" : "span"}
bind:this={ref}
{href}
class={cn(badgeVariants({ variant, className }))}
class={cn(badgeVariants({ variant }), className)}
{...restProps}
>
{@render children?.()}

View File

@ -56,7 +56,7 @@
{#if href}
<a
bind:this={ref}
class={cn(buttonVariants({ variant, size, className }))}
class={cn(buttonVariants({ variant, size }), className)}
{href}
{...restProps}
>
@ -65,7 +65,7 @@
{:else}
<button
bind:this={ref}
class={cn(buttonVariants({ variant, size, className }))}
class={cn(buttonVariants({ variant, size }), className)}
{type}
{...restProps}
>

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { Checkbox as CheckboxPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import Check from "lucide-svelte/icons/check";
import Minus from "lucide-svelte/icons/minus";
import Check from "@lucide/svelte/icons/check";
import Minus from "@lucide/svelte/icons/minus";
import { cn } from "$lib/utils.js";
let {

View File

@ -1,6 +1,6 @@
<script lang="ts">
import { Dialog as DialogPrimitive, type WithoutChildrenOrChild } from "bits-ui";
import X from "lucide-svelte/icons/x";
import X from "@lucide/svelte/icons/x";
import type { Snippet } from "svelte";
import * as Dialog from "./index.js";
import { cn } from "$lib/utils.js";
@ -17,7 +17,7 @@
} = $props();
</script>
<Dialog.Portal class="absolute" {...portalProps}>
<Dialog.Portal {...portalProps}>
<Dialog.Overlay />
<DialogPrimitive.Content
bind:ref

View File

@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: DialogPrimitive.DescriptionProps = $props();
</script>
@ -14,6 +13,4 @@
bind:ref
class={cn("text-muted-foreground text-sm", className)}
{...restProps}
>
{@render children?.()}
</DialogPrimitive.Description>
/>

View File

@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: DialogPrimitive.OverlayProps = $props();
</script>
@ -17,6 +16,4 @@
className
)}
{...restProps}
>
{@render children?.()}
</DialogPrimitive.Overlay>
/>

View File

@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: DialogPrimitive.TitleProps = $props();
</script>
@ -14,6 +13,4 @@
bind:ref
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</DialogPrimitive.Title>
/>

View File

@ -1,22 +1,46 @@
<script lang="ts">
import type { HTMLInputAttributes } from "svelte/elements";
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
import type { WithElementRef } from "bits-ui";
import { cn } from "$lib/utils.js";
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
type Props = WithElementRef<
Omit<HTMLInputAttributes, "type"> &
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
>;
let {
ref = $bindable(null),
value = $bindable(),
type,
files = $bindable(),
class: className,
...restProps
}: WithElementRef<HTMLInputAttributes> = $props();
}: Props = $props();
</script>
<input
bind:this={ref}
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
bind:value
{...restProps}
/>
{#if type === "file"}
<input
bind:this={ref}
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
type="file"
bind:files
bind:value
{...restProps}
/>
{:else}
<input
bind:this={ref}
class={cn(
"border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-base file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{type}
bind:value
{...restProps}
/>
{/if}

View File

@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: LabelPrimitive.RootProps = $props();
</script>
@ -17,6 +16,4 @@
className
)}
{...restProps}
>
{@render children?.()}
</LabelPrimitive.Root>
/>

View File

@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: TabsPrimitive.ContentProps = $props();
</script>
@ -17,6 +16,4 @@
className
)}
{...restProps}
>
{@render children?.()}
</TabsPrimitive.Content>
/>

View File

@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: TabsPrimitive.ListProps = $props();
</script>
@ -17,6 +16,4 @@
className
)}
{...restProps}
>
{@render children?.()}
</TabsPrimitive.List>
/>

View File

@ -5,7 +5,6 @@
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: TabsPrimitive.TriggerProps = $props();
</script>
@ -17,6 +16,4 @@
className
)}
{...restProps}
>
{@render children?.()}
</TabsPrimitive.Trigger>
/>

View File

@ -1,3 +0,0 @@
export function opnsenseSanitezedUsername(username: string) {
return username.slice(0, 63).replace(/[^a-zA-Z0-9_-]/g, '_');
}

View File

@ -3,7 +3,7 @@ import { sha256 } from '@oslojs/crypto/sha2';
import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from '@oslojs/encoding';
import { db } from '$lib/server/db';
import * as table from '$lib/server/db/schema';
import type { RequestEvent } from '@sveltejs/kit';
import type { Cookies } from '@sveltejs/kit';
import { dev } from '$app/environment';
import { env } from '$env/dynamic/private';
@ -22,14 +22,14 @@ export async function createSession(userId: string): Promise<table.Session> {
const session: table.Session = {
id: sessionId,
userId,
expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
expiresAt: new Date(Date.now() + DAY_IN_MS * 30),
};
await db.insert(table.sessions).values(session);
return session;
}
export function setSessionTokenCookie(event: RequestEvent, sessionId: string, expiresAt: Date) {
event.cookies.set(sessionCookieName, sessionId, {
export function setSessionTokenCookie(cookies: Cookies, sessionId: string, expiresAt: Date) {
cookies.set(sessionCookieName, sessionId, {
path: '/',
sameSite: 'lax',
httpOnly: true,
@ -42,16 +42,21 @@ export async function invalidateSession(sessionId: string): Promise<void> {
await db.delete(table.sessions).where(eq(table.sessions.id, sessionId));
}
export function deleteSessionTokenCookie(event: RequestEvent) {
event.cookies.delete(sessionCookieName, { path: '/' });
export function deleteSessionTokenCookie(cookies: Cookies) {
cookies.delete(sessionCookieName, { path: '/' });
}
export async function validateSession(sessionId: string) {
const [result] = await db
.select({
// Adjust user table here to tweak returned data
user: { id: table.users.id, username: table.users.username, name: table.users.name },
session: table.sessions
user: {
id: table.users.id,
authSource: table.users.authSource,
username: table.users.username,
name: table.users.name,
},
session: table.sessions,
})
.from(table.sessions)
.innerJoin(table.users, eq(table.sessions.userId, table.users.id))
@ -81,7 +86,7 @@ export async function validateSession(sessionId: string) {
}
export function isValidInviteToken(inviteToken: string) {
return inviteToken === env.INVITE_TOKEN;
return inviteToken === env.AUTH_INVITE_TOKEN;
}
export type SessionValidationResult = Awaited<ReturnType<typeof validateSession>>;

View File

@ -36,8 +36,6 @@ export const devices = sqliteTable('devices', {
.notNull()
.references(() => users.id),
name: text('name').notNull(),
// questioning whether this should be nullable
opnsenseId: text('opnsense_id'),
publicKey: text('public_key').notNull().unique(),
// nullable for the possibility of a user supplying their own private key
privateKey: text('private_key'),

View File

@ -3,9 +3,8 @@ import { err, ok, type Result } from '$lib/types';
import { db } from '$lib/server/db';
import { count, eq, isNull } from 'drizzle-orm';
import { env } from '$env/dynamic/private';
import { opnsenseAuth, opnsenseUrl, serverUuid } from '$lib/server/opnsense';
import { opnsenseSanitezedUsername } from '$lib/opnsense';
import { getIpsFromIndex } from './utils';
import wgProvider from '$lib/server/wg-provider';
export async function createDevice(params: {
name: string;
@ -19,17 +18,15 @@ export async function createDevice(params: {
if (deviceCount >= parseInt(env.MAX_CLIENTS_PER_USER))
return err([400, 'Maximum number of devices reached'] as [400, string]);
// this is going to be quite long
// 1. fetch params for new device from opnsense api
// 1. fetch params for new device from provider
// 2.1 get an allocation for the device
// 2.2. insert new device into db
// 2.3. update the allocation with the device id
// 3. create the client in opnsense
// 4. reconfigure opnsense to enable the new client
// 3. create the client in provider
return await db.transaction(async (tx) => {
const [keys, availableAllocation, lastAllocation] = await Promise.all([
// fetch params for new device from opnsense api
getKeys(),
const [keysResult, availableAllocation, lastAllocation] = await Promise.all([
// fetch params for new device from provider
wgProvider.generateKeys(),
// find first unallocated IP
await tx.query.ipAllocations.findFirst({
columns: {
@ -46,9 +43,12 @@ export async function createDevice(params: {
}),
]);
if (keysResult?._tag === 'err') return err([500, 'Failed to get keys']);
const keys = keysResult.value;
// check for existing allocation or if we have any IPs left
if (!availableAllocation && lastAllocation && lastAllocation.id >= parseInt(env.IP_MAX_INDEX)) {
return err([500, 'No more IP addresses available'] as [500, string]);
return err([500, 'No more IP addresses available']);
}
// use existing allocation or create a new one
@ -65,9 +65,9 @@ export async function createDevice(params: {
.values({
userId: params.user.id,
name: params.name,
publicKey: keys.pubkey,
privateKey: keys.privkey,
preSharedKey: keys.psk,
publicKey: keys.publicKey,
privateKey: keys.privateKey,
preSharedKey: keys.preSharedKey,
})
.returning({ id: devices.id });
@ -77,80 +77,18 @@ export async function createDevice(params: {
.set({ deviceId: newDevice.id })
.where(eq(ipAllocations.id, ipAllocationId));
// create client in opnsense
const opnsenseRes = await opnsenseCreateClient({
username: params.user.username,
pubkey: keys.pubkey,
psk: keys.psk,
// create client in provider
const providerRes = await wgProvider.createClient({
user: params.user,
publicKey: keys.publicKey,
preSharedKey: keys.preSharedKey,
allowedIps: getIpsFromIndex(ipAllocationId).join(','),
});
const opnsenseResJson = await opnsenseRes.json();
if (opnsenseResJson['result'] !== 'saved') {
if (providerRes._tag === 'err') {
tx2.rollback();
console.error(`Error creating client in OPNsense: \n${opnsenseResJson}`);
return err([500, 'Error creating client in OPNsense'] as [500, string]);
return err([500, 'Failed to create client in provider']);
}
// reconfigure opnsense
await opnsenseReconfigure();
return ok(newDevice.id);
});
});
}
async function getKeys() {
// fetch key pair from opnsense
const options: RequestInit = {
method: 'GET',
headers: {
Authorization: opnsenseAuth,
Accept: 'application/json',
},
};
const resKeyPair = await fetch(`${opnsenseUrl}/api/wireguard/server/key_pair`, options);
const resPsk = await fetch(`${opnsenseUrl}/api/wireguard/client/psk`, options);
const keyPair = await resKeyPair.json();
const psk = await resPsk.json();
return {
pubkey: keyPair['pubkey'] as string,
privkey: keyPair['privkey'] as string,
psk: psk['psk'] as string,
};
}
async function opnsenseCreateClient(params: {
username: string;
pubkey: string;
psk: string;
allowedIps: string;
}) {
return fetch(`${opnsenseUrl}/api/wireguard/client/addClientBuilder`, {
method: 'POST',
headers: {
Authorization: opnsenseAuth,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
configbuilder: {
enabled: '1',
name: `vpgen-${opnsenseSanitezedUsername(params.username)}`,
pubkey: params.pubkey,
psk: params.psk,
tunneladdress: params.allowedIps,
server: serverUuid,
endpoint: env.VPN_ENDPOINT,
},
}),
});
}
async function opnsenseReconfigure() {
return fetch(`${opnsenseUrl}/api/wireguard/service/reconfigure`, {
method: 'POST',
headers: {
Authorization: opnsenseAuth,
Accept: 'application/json',
},
});
}

View File

@ -2,7 +2,7 @@ import { and, eq } from 'drizzle-orm';
import { db } from '$lib/server/db';
import { devices } from '$lib/server/db/schema';
import { err, ok, type Result } from '$lib/types';
import { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense';
import wgProvider from '$lib/server/wg-provider';
export async function deleteDevice(
userId: string,
@ -15,59 +15,13 @@ export async function deleteDevice(
where: and(eq(devices.userId, userId), eq(devices.id, deviceId)),
});
if (!device) return err([400, 'Device not found']);
const opnsenseClientUuid = (await opnsenseFindClient(device.publicKey))?.['uuid'];
if (typeof opnsenseClientUuid !== 'string') {
console.error(
'failed to get OPNsense client for deletion for device',
deviceId,
device.publicKey,
);
return err([500, 'Error getting client from OPNsense']);
}
const opnsenseDeletionResult = await opnsenseDeleteClient(opnsenseClientUuid);
if (opnsenseDeletionResult?.['result'] !== 'deleted') {
console.error(
'failed to delete OPNsense client for device',
deviceId,
device.publicKey,
'\n',
'OPNsense client uuid:',
opnsenseClientUuid,
);
return err([500, 'Error deleting client in OPNsense']);
const providerDeletionResult = await wgProvider.deleteClient(device.publicKey);
if (providerDeletionResult._tag === 'err') {
console.error('failed to delete provider client for device', deviceId, device.publicKey);
return err([500, 'Error deleting client in provider']);
}
await db.delete(devices).where(eq(devices.id, deviceId));
return ok(null);
}
async function opnsenseFindClient(pubkey: string) {
const res = await fetch(`${opnsenseUrl}/api/wireguard/client/searchClient`, {
method: 'POST',
headers: {
Authorization: opnsenseAuth,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
current: 1,
// "rowCount": 7,
sort: {},
searchPhrase: pubkey,
type: ['peer'],
}),
});
return (await res.json())?.rows?.[0] ?? null;
}
async function opnsenseDeleteClient(clientUuid: string) {
const res = await fetch(`${opnsenseUrl}/api/wireguard/client/delClient/${clientUuid}`, {
method: 'POST',
headers: {
Authorization: opnsenseAuth,
Accept: 'application/json',
},
});
return res.json();
}

View File

@ -2,9 +2,9 @@ import { db } from '$lib/server/db';
import { and, eq } from 'drizzle-orm';
import { devices } from '$lib/server/db/schema';
import type { DeviceDetails } from '$lib/devices';
import { serverPublicKey } from '$lib/server/opnsense';
import { env } from '$env/dynamic/private';
import { getIpsFromIndex } from '$lib/server/devices/index';
import wgProvider from '$lib/server/wg-provider';
export async function findDevices(userId: string) {
return db.query.devices.findMany({
@ -49,7 +49,7 @@ export function mapDeviceToDetails(
privateKey: device.privateKey,
preSharedKey: device.preSharedKey,
ips,
vpnPublicKey: serverPublicKey,
vpnPublicKey: wgProvider.getServerPublicKey(),
vpnEndpoint: env.VPN_ENDPOINT,
vpnDns: env.VPN_DNS,
};

View File

@ -0,0 +1,34 @@
import { envToBool } from '$lib/utils';
import { env } from '$env/dynamic/private';
import { Authentik, decodeIdToken } from 'arctic';
import { assertGuard } from 'typia';
import type { IOAuthProvider } from '$lib/server/oauth';
const authentikProvider = new Authentik(
env.AUTH_AUTHENTIK_DOMAIN,
env.AUTH_AUTHENTIK_CLIENT_ID,
env.AUTH_AUTHENTIK_CLIENT_SECRET,
`${env.ORIGIN}/auth/authentik/callback`,
);
export const authentik: IOAuthProvider = {
requireInvite: envToBool(env.AUTH_AUTHENTIK_REQUIRE_INVITE, true),
createAuthorizationURL: (state: string, codeVerifier: string) => {
const scopes = ['openid', 'profile'];
return authentikProvider.createAuthorizationURL(state, codeVerifier, scopes);
},
validateAuthorizationCode: async (code: string, codeVerifier: string) => {
const tokens = await authentikProvider.validateAuthorizationCode(code, codeVerifier);
const claims = decodeIdToken(tokens.idToken());
assertGuard<{
sub: string;
name: string;
preferred_username: string;
}>(claims);
return {
sub: claims.sub,
name: claims.name,
username: claims.preferred_username,
};
},
};

View File

@ -0,0 +1,33 @@
import { decodeIdToken, Google } from 'arctic';
import { env } from '$env/dynamic/private';
import { envToBool } from '$lib/utils';
import { assertGuard } from 'typia';
import type { IOAuthProvider } from '$lib/server/oauth';
const googleProvider = new Google(
env.AUTH_GOOGLE_CLIENT_ID,
env.AUTH_GOOGLE_CLIENT_SECRET,
`${env.ORIGIN}/auth/google/callback`,
);
export const google: IOAuthProvider = {
requireInvite: envToBool(env.AUTH_GOOGLE_REQUIRE_INVITE, true),
createAuthorizationURL: (state: string, codeVerifier: string) => {
const scopes = ['openid', 'profile', 'email'];
return googleProvider.createAuthorizationURL(state, codeVerifier, scopes);
},
validateAuthorizationCode: async (code: string, codeVerifier: string) => {
const tokens = await googleProvider.validateAuthorizationCode(code, codeVerifier);
const claims = decodeIdToken(tokens.idToken());
assertGuard<{
sub: string;
email: string;
name: string;
}>(claims);
return {
sub: claims.sub,
name: claims.name,
username: claims.email,
};
},
};

View File

@ -0,0 +1,2 @@
export { authentik } from './authentik';
export { google } from './google';

View File

@ -1,15 +1,19 @@
import { Authentik, Google } from 'arctic';
import { env } from '$env/dynamic/private';
import type { AuthProvider } from '$lib/auth';
import { authentik, google } from '$lib/server/oauth-providers';
export const authentik = new Authentik(
env.AUTH_DOMAIN,
env.AUTH_CLIENT_ID,
env.AUTH_CLIENT_SECRET,
`${env.ORIGIN}/auth/authentik/callback`,
);
export interface IOAuthClaims {
sub: string;
name: string;
username: string;
}
export const google = new Google(
env.GOOGLE_CLIENT_ID,
env.GOOGLE_CLIENT_SECRET,
`${env.ORIGIN}/auth/google/callback`,
);
export interface IOAuthProvider {
readonly requireInvite: boolean;
createAuthorizationURL(state: string, codeVerifier: string): URL;
validateAuthorizationCode(code: string, codeVerifier: string): Promise<IOAuthClaims>;
}
export const oauthProviders: Record<AuthProvider, IOAuthProvider> = {
authentik,
google,
};

View File

@ -1,49 +0,0 @@
import { env } from '$env/dynamic/private';
import assert from 'node:assert';
import { encodeBasicCredentials } from 'arctic/dist/request';
import { dev } from '$app/environment';
import type { OpnsenseWgServers } from '$lib/opnsense/wg';
export const opnsenseUrl = env.OPNSENSE_API_URL;
export const opnsenseAuth =
'Basic ' + encodeBasicCredentials(env.OPNSENSE_API_KEY, env.OPNSENSE_API_SECRET);
export const opnsenseIfname = env.OPNSENSE_WG_IFNAME;
// unset secret for security
if (!dev) env.OPNSENSE_API_SECRET = '';
export let serverUuid: string, serverPublicKey: string;
export async function fetchOpnsenseServer() {
// this might be pretty bad if the server is down and in a bunch of other cases
// TODO: write a retry loop later
const resServers = await fetch(`${opnsenseUrl}/api/wireguard/client/list_servers`, {
method: 'GET',
headers: {
Authorization: opnsenseAuth,
Accept: 'application/json',
},
});
assert(resServers.ok, 'Failed to fetch OPNsense WireGuard servers');
const servers = (await resServers.json()) as OpnsenseWgServers;
assert.equal(servers.status, 'ok', 'Failed to fetch OPNsense WireGuard servers');
const uuid = servers.rows.find((server) => server.name === opnsenseIfname)?.uuid;
assert(uuid, 'Failed to find server UUID for OPNsense WireGuard server');
serverUuid = uuid;
console.log('OPNsense WireGuard server UUID:', serverUuid);
const resServerInfo = await fetch(
`${opnsenseUrl}/api/wireguard/client/get_server_info/${serverUuid}`,
{
method: 'GET',
headers: {
Authorization: opnsenseAuth,
Accept: 'application/json',
},
},
);
assert(resServerInfo.ok, 'Failed to fetch OPNsense WireGuard server info');
const serverInfo = await resServerInfo.json();
assert.equal(serverInfo.status, 'ok', 'Failed to fetch OPNsense WireGuard server info');
serverPublicKey = serverInfo['pubkey'];
}

View File

@ -0,0 +1,38 @@
import type { Result } from '$lib/types';
import type { User } from '$lib/server/db/schema';
export interface IWgProvider {
init(): Promise<Result<null, Error>>;
getServerPublicKey(): string;
generateKeys(): Promise<Result<WgKeys, Error>>;
createClient(params: CreateClientParams): Promise<Result<null, Error>>;
findConnections(user: User): Promise<Result<ClientConnection[], Error>>;
deleteClient(publicKey: string): Promise<Result<null, Error>>;
}
export type WgKeys = {
publicKey: string;
privateKey: string;
preSharedKey: string;
};
export type CreateClientParams = {
user: User;
publicKey: string;
preSharedKey: string;
allowedIps: string;
}
export type ClientConnection = {
publicKey: string;
endpoint: string;
allowedIps: string;
transferRx: number;
transferTx: number;
latestHandshake: number;
}

View File

@ -0,0 +1,12 @@
import { WgProviderOpnsense } from '$lib/server/wg-providers/opnsense';
import { env } from '$env/dynamic/private';
import type { IWgProvider } from '$lib/server/types';
const wgProvider: IWgProvider = new WgProviderOpnsense({
opnsenseUrl: env.OPNSENSE_API_URL,
opnsenseApiKey: env.OPNSENSE_API_KEY,
opnsenseApiSecret: env.OPNSENSE_API_SECRET,
opnsenseWgIfname: env.OPNSENSE_WG_IFNAME,
});
export default wgProvider;

View File

@ -0,0 +1,244 @@
import type { ClientConnection, CreateClientParams, IWgProvider, WgKeys } from '$lib/server/types';
import { encodeBasicCredentials } from 'arctic/dist/request';
import { is } from 'typia';
import type { OpnsenseWgPeers, OpnsenseWgServers } from '$lib/server/wg-providers/opnsense/types';
import { err, ok, type Result } from '$lib/types';
import assert from 'node:assert';
import type { User } from '$lib/server/db/schema';
export class WgProviderOpnsense implements IWgProvider {
private opnsenseUrl: string;
private opnsenseAuth: string;
private opnsenseIfname: string;
private opnsenseWgServerUuid: string | undefined;
private opnsenseWgServerPublicKey: string | undefined;
public constructor(params: OpnsenseParams) {
this.opnsenseUrl = params.opnsenseUrl;
this.opnsenseAuth =
'Basic ' + encodeBasicCredentials(params.opnsenseApiKey, params.opnsenseApiSecret);
this.opnsenseIfname = params.opnsenseWgIfname;
}
public async init(): Promise<Result<null, Error>> {
const resServers = await fetch(`${this.opnsenseUrl}/api/wireguard/client/list_servers`, {
method: 'GET',
headers: {
Authorization: this.opnsenseAuth,
Accept: 'application/json',
},
});
const servers = await resServers.json();
if (!is<OpnsenseWgServers>(servers)) {
console.error('Unexpected response for OPNsense WireGuard servers', servers);
return err(new Error('Failed to fetch OPNsense WireGuard servers'));
}
const uuid = servers.rows.find((server) => server.name === this.opnsenseIfname)?.uuid;
if (!uuid) {
console.error('OPNsense WireGuard servers', servers);
return err(new Error('Failed to find server UUID for OPNsense WireGuard server'));
}
const resServerInfo = await fetch(
`${this.opnsenseUrl}/api/wireguard/client/get_server_info/${uuid}`,
{
method: 'GET',
headers: {
Authorization: this.opnsenseAuth,
Accept: 'application/json',
},
},
);
const serverInfo = await resServerInfo.json();
const serverPublicKey = serverInfo['pubkey'];
if (serverInfo['status'] !== 'ok' || typeof serverPublicKey !== 'string') {
console.error('Failed to fetch OPNsense WireGuard server info', serverInfo);
return err(new Error('Failed to fetch OPNsense WireGuard server info'));
}
console.debug('OPNsense WireGuard server UUID:', uuid);
console.debug('OPNsense WireGuard server public key:', serverPublicKey);
this.opnsenseWgServerUuid = uuid;
this.opnsenseWgServerPublicKey = serverPublicKey;
return ok(null);
}
getServerPublicKey(): string {
assert(
this.opnsenseWgServerPublicKey,
'OPNsense server public key not set, init() must be called first',
);
return this.opnsenseWgServerPublicKey;
}
public async generateKeys(): Promise<Result<WgKeys, Error>> {
const options: RequestInit = {
method: 'GET',
headers: {
Authorization: this.opnsenseAuth,
Accept: 'application/json',
},
};
const resKeyPair = await fetch(`${this.opnsenseUrl}/api/wireguard/server/key_pair`, options);
const resPsk = await fetch(`${this.opnsenseUrl}/api/wireguard/client/psk`, options);
const keyPair = await resKeyPair.json();
const psk = await resPsk.json();
if (!is<{ pubkey: string; privkey: string }>(keyPair)) {
console.error('Unexpected response for OPNsense key pair', keyPair);
return err(new Error('Failed to fetch OPNsense key pair'));
}
if (!is<{ psk: string }>(psk)) {
console.error('Unexpected response for OPNsense PSK', psk);
return err(new Error('Failed to fetch OPNsense PSK'));
}
return ok({
publicKey: keyPair.pubkey,
privateKey: keyPair.privkey,
preSharedKey: psk.psk,
});
}
async createClient(params: CreateClientParams): Promise<Result<null, Error>> {
assert(this.opnsenseWgServerUuid, 'OPNsense server UUID not set, init() must be called first');
const createClientRes = await fetch(
`${this.opnsenseUrl}/api/wireguard/client/addClientBuilder`,
{
method: 'POST',
headers: {
Authorization: this.opnsenseAuth,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
configbuilder: {
enabled: '1',
name: `vpgen-${opnsenseSanitezedUsername(params.user.username)}`,
pubkey: params.publicKey,
psk: params.preSharedKey,
tunneladdress: params.allowedIps,
server: this.opnsenseWgServerUuid,
endpoint: '',
},
}),
},
);
const createClientResJson = await createClientRes.json();
if (createClientResJson['result'] !== 'saved') {
console.error('Error creating client in OPNsense', createClientResJson);
return err(new Error('Failed to create client in OPNsense'));
}
const reconfigureRes = await fetch(`${this.opnsenseUrl}/api/wireguard/service/reconfigure`, {
method: 'POST',
headers: {
Authorization: this.opnsenseAuth,
Accept: 'application/json',
},
});
if (reconfigureRes.status !== 200) {
console.error('Error reconfiguring OPNsense', reconfigureRes);
return err(new Error('Failed to reconfigure OPNsense'));
}
return ok(null);
}
async findConnections(user: User): Promise<Result<ClientConnection[], Error>> {
const res = await fetch(`${this.opnsenseUrl}/api/wireguard/service/show`, {
method: 'POST',
headers: {
Authorization: this.opnsenseAuth,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
current: 1,
// "rowCount": 7,
sort: {},
// TODO: use a more unique search phrase
// unfortunately 64 character limit,
// but it should be fine if users can't change their own username
searchPhrase: `vpgen-${opnsenseSanitezedUsername(user.username)}`,
type: ['peer'],
}),
});
const peers = await res.json();
if (!is<OpnsenseWgPeers>(peers)) {
console.error('Unexpected response for OPNsense WireGuard peers', peers);
return err(new Error('Failed to fetch OPNsense WireGuard peers'));
}
return ok(
peers.rows.map((peer) => {
return {
publicKey: peer['public-key'],
endpoint: peer['endpoint'],
allowedIps: peer['allowed-ips'],
transferRx: peer['transfer-rx'],
transferTx: peer['transfer-tx'],
latestHandshake: peer['latest-handshake'] * 1000,
};
}),
);
}
async deleteClient(publicKey: string): Promise<Result<null, Error>> {
const client = await this.findOpnsenseClient(publicKey);
const clientUuid = client?.uuid;
if (typeof clientUuid !== 'string') {
console.error('Failed to get OPNsense client UUID for deletion', client);
return err(new Error('Failed to get OPNsense client UUID for deletion'));
}
const res = await fetch(`${this.opnsenseUrl}/api/wireguard/client/delClient/${clientUuid}`, {
method: 'POST',
headers: {
Authorization: this.opnsenseAuth,
Accept: 'application/json',
},
});
const resJson = await res.json();
if (resJson['result'] !== 'deleted') {
console.error('Failed to delete OPNsense client', resJson);
return err(new Error('Failed to delete OPNsense client'));
}
return ok(null);
}
private async findOpnsenseClient(publicKey: string) {
const res = await fetch(`${this.opnsenseUrl}/api/wireguard/client/searchClient`, {
method: 'POST',
headers: {
Authorization: this.opnsenseAuth,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
current: 1,
sort: {},
searchPhrase: publicKey,
type: ['peer'],
}),
});
return (await res.json())?.rows?.[0] ?? null;
}
}
function opnsenseSanitezedUsername(username: string) {
return username.slice(0, 63).replace(/[^a-zA-Z0-9_-]/g, '_');
}
export type OpnsenseParams = {
opnsenseUrl: string;
opnsenseApiKey: string;
opnsenseApiSecret: string;
opnsenseWgIfname: string;
};

View File

@ -1,27 +1,2 @@
class Ok<T> {
readonly _tag = 'ok';
value: T;
constructor(value: T) {
this.value = value;
}
}
class Err<E> {
readonly _tag = 'err';
error: E;
constructor(error: E) {
this.error = error;
}
}
export type Result<T, E> = Ok<T> | Err<E>;
export function err<E>(e: E): Err<E> {
return new Err(e);
}
export function ok<T>(t: T): Ok<T> {
return new Ok(t);
}
export type { Result } from './result';
export { ok, err } from './result';

27
src/lib/types/result.ts Normal file
View File

@ -0,0 +1,27 @@
class Ok<T> {
readonly _tag = 'ok';
value: T;
constructor(value: T) {
this.value = value;
}
}
class Err<E> {
readonly _tag = 'err';
error: E;
constructor(error: E) {
this.error = error;
}
}
export type Result<T, E> = Ok<T> | Err<E>;
export function err<E>(e: E): Err<E> {
return new Err(e);
}
export function ok<T>(t: T): Ok<T> {
return new Ok(t);
}

View File

@ -4,3 +4,11 @@ import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function envToBool(value: string | undefined, defaultValue = false): boolean {
if (typeof value === "undefined") {
return defaultValue;
}
return ['true', '1', 'yes'].includes(value.toLowerCase());
}

View File

@ -1,44 +1,43 @@
import { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { opnsenseAuth, opnsenseUrl } from '$lib/server/opnsense';
import type { OpnsenseWgPeers } from '$lib/opnsense/wg';
import { findDevices } from '$lib/server/devices';
import type { ConnectionDetails } from '$lib/connections';
import { opnsenseSanitezedUsername } from '$lib/opnsense';
import type { Result } from '$lib/types';
import type { ClientConnection } from '$lib/server/types';
import wgProvider from '$lib/server/wg-provider';
export const GET: RequestHandler = async (event) => {
if (!event.locals.user) {
return error(401, 'Unauthorized');
}
console.debug('/api/connections');
const peers = await fetchOpnsensePeers(event.locals.user.username);
console.debug('/api/connections: fetched opnsense peers', peers.rowCount);
const peersResult: Result<ClientConnection[], Error> = await wgProvider.findConnections(event.locals.user);
if (peersResult._tag === 'err') return error(500, peersResult.error.message);
const devices = await findDevices(event.locals.user.id);
console.debug('/api/connections: fetched db devices');
if (!peers) {
return error(500, 'Error getting info from OPNsense API');
}
// TODO: this is all garbage performance
// filter devices with no recent handshakes
peers.rows = peers.rows.filter((peer) => peer['latest-handshake']);
const peers = peersResult.value.filter((peer) => peer.latestHandshake);
// start from devices, to treat db as the source of truth
const connections: ConnectionDetails[] = [];
for (const device of devices) {
const peerData = peers.rows.find((peer) => peer['public-key'] === device.publicKey);
const peerData = peers.find((peer) => peer.publicKey === device.publicKey);
if (!peerData) continue;
connections.push({
deviceId: device.id,
deviceName: device.name,
devicePublicKey: device.publicKey,
deviceIps: peerData['allowed-ips'].split(','),
endpoint: peerData['endpoint'],
deviceIps: peerData.allowedIps.split(','),
endpoint: peerData.endpoint,
// swap rx and tx, since the opnsense values are from the server perspective
transferRx: peerData['transfer-tx'],
transferTx: peerData['transfer-rx'],
latestHandshake: peerData['latest-handshake'] * 1000,
transferRx: peerData.transferTx,
transferTx: peerData.transferRx,
latestHandshake: peerData.latestHandshake,
});
}
@ -49,25 +48,3 @@ export const GET: RequestHandler = async (event) => {
},
});
};
async function fetchOpnsensePeers(username: string) {
const res = await fetch(`${opnsenseUrl}/api/wireguard/service/show`, {
method: 'POST',
headers: {
Authorization: opnsenseAuth,
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
current: 1,
// "rowCount": 7,
sort: {},
// TODO: use a more unique search phrase
// unfortunately 64 character limit,
// but it should be fine if users can't change their own username
searchPhrase: `vpgen-${opnsenseSanitezedUsername(username)}`,
type: ['peer'],
}),
});
return (await res.json()) as OpnsenseWgPeers;
}

View File

@ -1,14 +1,14 @@
import { fail, redirect } from "@sveltejs/kit";
import { invalidateSession, deleteSessionTokenCookie } from "$lib/server/auth";
import type { Actions } from "./$types";
import { fail, redirect } from '@sveltejs/kit';
import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/auth';
import type { Actions } from './$types';
export const actions: Actions = {
logout: async (event) => {
if (event.locals.session === null) {
logout: async ({ locals, cookies }) => {
if (locals.session === null) {
return fail(401);
}
await invalidateSession(event.locals.session.id);
deleteSessionTokenCookie(event);
return redirect(302, "/");
}
await invalidateSession(locals.session.id);
deleteSessionTokenCookie(cookies);
redirect(302, '/');
},
};

View File

@ -0,0 +1,36 @@
import { generateCodeVerifier, generateState } from 'arctic';
import { oauthProviders } from '$lib/server/oauth';
import { is } from 'typia';
import { type AuthProvider, enabledAuthProviders } from '$lib/auth';
export async function GET({ params: { provider }, url, cookies }) {
if (!is<AuthProvider>(provider) || !enabledAuthProviders[provider]) {
return new Response(null, { status: 404 });
}
const oauthProvider = oauthProviders[provider];
const inviteToken = url.searchParams.get('invite') ?? '';
const state = generateState();
const codeVerifier = generateCodeVerifier();
const authUrl = oauthProvider.createAuthorizationURL(state + inviteToken, codeVerifier);
cookies.set(`${provider}_oauth_state`, state, {
path: '/',
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
});
cookies.set(`${provider}_code_verifier`, codeVerifier, {
path: '/',
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
});
return new Response(null, {
status: 302,
headers: {
Location: authUrl.toString(),
},
});
}

View File

@ -0,0 +1,76 @@
import { is } from 'typia';
import type { PageServerLoad } from './$types';
import { error, redirect } from '@sveltejs/kit';
import { type AuthProvider, enabledAuthProviders } from '$lib/auth';
import { oauthProviders } from '$lib/server/oauth';
import { ArcticFetchError, OAuth2RequestError } from 'arctic';
import { db } from '$lib/server/db';
import * as table from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
import { createSession, isValidInviteToken, setSessionTokenCookie } from '$lib/server/auth';
export const load: PageServerLoad = async ({ params: { provider }, url, cookies }) => {
if (!is<AuthProvider>(provider) || !enabledAuthProviders[provider]) {
error(404);
}
const oauthProvider = oauthProviders[provider];
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const storedState = cookies.get(`${provider}_oauth_state`) ?? null;
const codeVerifier = cookies.get(`${provider}_code_verifier`) ?? null;
if (code === null || state === null || storedState === null || codeVerifier === null) {
error(400, 'Missing url parameters');
}
const stateGeneratedToken = state.slice(0, storedState.length);
const stateInviteToken = state.slice(storedState.length);
if (stateGeneratedToken !== storedState) {
error(400, 'Invalid state in url');
}
let claims;
try {
claims = await oauthProvider.validateAuthorizationCode(code, codeVerifier);
} catch (e) {
if (e instanceof OAuth2RequestError) {
console.debug('Arctic: OAuth: invalid authorization code, credentials, or redirect URI', e);
error(400, 'Invalid authorization code');
}
if (e instanceof ArcticFetchError) {
console.debug('Arctic: failed to call `fetch()`', e);
error(400, 'Failed to validate authorization code');
}
console.error('Unexpected error validating authorization code', code, e);
error(500);
}
const existingUser = await db.query.users.findFirst({ where: eq(table.users.id, claims.sub) });
if (existingUser) {
const session = await createSession(existingUser.id);
setSessionTokenCookie(cookies, session.id, session.expiresAt);
redirect(302, '/');
}
if (oauthProvider.requireInvite && !isValidInviteToken(stateInviteToken)) {
const message =
stateInviteToken.length === 0 ? 'sign up with an invite link first' : 'invalid invite link';
error(403, 'Not Authorized: ' + message);
}
const user: table.User = {
id: claims.sub,
authSource: provider,
username: claims.username,
name: claims.name,
};
await db.insert(table.users).values(user);
console.log('created user', user, 'using provider', provider, 'with invite token', stateInviteToken);
const session = await createSession(user.id);
setSessionTokenCookie(cookies, session.id, session.expiresAt);
redirect(302, '/');
};

View File

@ -1,30 +0,0 @@
import { generateState, generateCodeVerifier } from "arctic";
import { authentik } from "$lib/server/oauth";
import type { RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent): Promise<Response> {
const state = generateState();
const codeVerifier = generateCodeVerifier();
const url = authentik.createAuthorizationURL(state, codeVerifier, ["openid", "profile"]);
event.cookies.set("authentik_oauth_state", state, {
path: "/",
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: "lax"
});
event.cookies.set("authentik_code_verifier", codeVerifier, {
path: "/",
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: "lax"
});
return new Response(null, {
status: 302,
headers: {
Location: url.toString()
}
});
}

View File

@ -1,84 +0,0 @@
import { createSession, setSessionTokenCookie } from '$lib/server/auth';
import { authentik } from '$lib/server/oauth';
import { decodeIdToken } from 'arctic';
import type { RequestEvent } from '@sveltejs/kit';
import type { OAuth2Tokens } from 'arctic';
import { db } from '$lib/server/db';
import { eq } from 'drizzle-orm';
import * as table from '$lib/server/db/schema';
export async function GET(event: RequestEvent): Promise<Response> {
const code = event.url.searchParams.get('code');
const state = event.url.searchParams.get('state');
const storedState = event.cookies.get('authentik_oauth_state') ?? null;
const codeVerifier = event.cookies.get('authentik_code_verifier') ?? null;
if (code === null || state === null || storedState === null || codeVerifier === null) {
return new Response(null, {
status: 400,
});
}
if (state !== storedState) {
return new Response(null, {
status: 400,
});
}
let tokens: OAuth2Tokens;
try {
tokens = await authentik.validateAuthorizationCode(code, codeVerifier);
} catch (e) {
// Invalid code or client credentials
return new Response(null, {
status: 400,
});
}
const claims = decodeIdToken(tokens.idToken()) as {
sub: string;
preferred_username: string;
name: string;
};
console.log('claims', claims);
const userId: string = claims.sub;
const username: string = claims.preferred_username;
const existingUser = await db.query.users.findFirst({ where: eq(table.users.id, userId) });
if (existingUser) {
const session = await createSession(existingUser.id);
setSessionTokenCookie(event, session.id, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: '/',
},
});
}
const user: table.User = {
id: userId,
authSource: 'authentik',
username,
name: claims.name as string,
};
try {
await db.insert(table.users).values(user);
const session = await createSession(user.id);
setSessionTokenCookie(event, session.id, session.expiresAt);
} catch (e) {
console.error('failed to create user', e);
return new Response(null, {
status: 500,
});
}
return new Response(null, {
status: 302,
headers: {
Location: '/',
},
});
}

View File

@ -1,38 +0,0 @@
import type { RequestHandler } from './$types';
import { generateCodeVerifier, generateState } from 'arctic';
import { google } from '$lib/server/oauth';
export const GET: RequestHandler = ({ url, cookies }) => {
const inviteToken = url.searchParams.get('invite');
const state = generateState();
const codeVerifier = generateCodeVerifier();
const scopes = ['openid', 'profile', 'email'];
const authUrl = google.createAuthorizationURL(state, codeVerifier, scopes);
cookies.set('google_oauth_state', state, {
path: '/',
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
});
cookies.set('google_code_verifier', codeVerifier, {
path: '/',
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
});
if (inviteToken !== null) cookies.set('invite_token', inviteToken, {
path: '/',
httpOnly: true,
maxAge: 60 * 10, // 10 minutes
sameSite: 'lax',
});
return new Response(null, {
status: 302,
headers: {
Location: authUrl.toString(),
},
});
};

View File

@ -1,100 +0,0 @@
import type { RequestHandler } from './$types';
import * as arctic from 'arctic';
import { google } from '$lib/server/oauth';
import { db } from '$lib/server/db';
import { eq } from 'drizzle-orm';
import * as table from '$lib/server/db/schema';
import { createSession, isValidInviteToken, setSessionTokenCookie } from '$lib/server/auth';
import type { OAuth2Tokens } from 'arctic';
import { assertGuard } from 'typia';
export const GET: RequestHandler = async (event) => {
const { url, cookies } = event;
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const storedState = cookies.get('google_oauth_state') ?? null;
const codeVerifier = cookies.get('google_code_verifier', ) ?? null;
const inviteToken = cookies.get('invite_token') ?? null;
if (code === null || state === null || storedState === null || codeVerifier === null) {
return new Response(null, {
status: 400,
});
}
let tokens: OAuth2Tokens;
try {
tokens = await google.validateAuthorizationCode(code, codeVerifier);
} catch (e) {
if (e instanceof arctic.OAuth2RequestError) {
console.debug('Arctic: OAuth: invalid authorization code, credentials, or redirect URI', e);
return new Response(null, {
status: 400,
});
}
if (e instanceof arctic.ArcticFetchError) {
console.debug('Arctic: failed to call `fetch()`', e);
return new Response(null, {
status: 400,
});
}
return new Response(null, {
status: 500,
});
}
const idToken = tokens.idToken();
const claims = arctic.decodeIdToken(idToken);
console.log('claims', claims);
assertGuard<{
sub: string;
email: string;
name: string;
}>(claims);
const userId = claims.sub;
const existingUser = await db.query.users.findFirst({ where: eq(table.users.id, userId) });
if (existingUser) {
const session = await createSession(existingUser.id);
setSessionTokenCookie(event, session.id, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: '/',
},
});
}
// TODO: proper error page
if (inviteToken === null || !isValidInviteToken(inviteToken)) {
return new Response(null, {
status: 400,
});
}
const user: table.User = {
id: userId,
authSource: 'google',
username: claims.email,
name: claims.name,
};
// TODO: proper error handling, delete cookies
await db.insert(table.users).values(user);
console.log('created user', user, 'with invite token', inviteToken);
const session = await createSession(user.id);
setSessionTokenCookie(event, session.id, session.expiresAt);
return new Response(null, {
status: 302,
headers: {
Location: '/',
},
});
};

View File

@ -1,6 +1,7 @@
import type { Actions } from './$types';
import { createDevice } from '$lib/server/devices';
import { error, fail, redirect } from '@sveltejs/kit';
import wgProvider from '$lib/server/wg-provider';
export const actions = {
create: async (event) => {

View File

@ -4,7 +4,7 @@
import { Badge } from '$lib/components/ui/badge';
import { Button, buttonVariants } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { LucideLoaderCircle, LucidePlus } from 'lucide-svelte';
import { LucideLoaderCircle, LucidePlus } from '@lucide/svelte';
import type { PageData } from './$types';
import { Label } from '$lib/components/ui/label';
import { page } from '$app/state';

View File

@ -1,7 +1,7 @@
<script>
import { Button, buttonVariants } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { LucideLoaderCircle, LucideTrash } from 'lucide-svelte';
import { LucideLoaderCircle, LucideTrash } from '@lucide/svelte';
const { device } = $props();
let submitted = $state(false);
@ -26,10 +26,12 @@
<Button type="submit" variant="destructive" disabled={submitted}>
Delete
</Button>
<Dialog.Close asChild let:builder>
<button class={buttonVariants()} disabled={submitted} use:builder.action {...builder}>
Cancel
</button>
<Dialog.Close>
{#snippet child({ props })}
<Button {...props} disabled={submitted}>
Cancel
</Button>
{/snippet}
</Dialog.Close>
</Dialog.Footer>
</form>

View File

@ -2,6 +2,7 @@ import type { LayoutServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { isValidInviteToken } from '$lib/server/auth';
export const load: LayoutServerLoad = ({ params }) => {
if (!isValidInviteToken(params.id)) redirect(307, '/')
export const load: LayoutServerLoad = ({ params, locals }) => {
if (!isValidInviteToken(params.id)) redirect(302, '/');
if (locals.user !== null) redirect(302, '/');
};

View File

@ -10,7 +10,7 @@
</svelte:head>
<h1 class="mb-2 scroll-m-20 text-center text-3xl font-extrabold tracking-tight lg:text-4xl">
Welcome to VPGen
You are invited to VPGen
</h1>
<AuthForm inviteToken={inviteToken} />
<AuthForm {inviteToken} />

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { invalidate, invalidateAll } from '$app/navigation';
import { Button } from '$lib/components/ui/button';
import { LucideLoaderCircle, LucideLogOut, LucideRefreshCw } from 'lucide-svelte';
import { LucideLoaderCircle, LucideLogOut, LucideRefreshCw } from '@lucide/svelte';
import { CodeSnippet } from '$lib/components/app/code-snippet/index.js';
let { data } = $props();