diff --git a/.dockerignore b/.dockerignore index 23674ef..ed4c8e5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -27,6 +27,3 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* - -# SQLite -*.db diff --git a/.env.example b/.env.example index 128bd9f..f492443 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,12 @@ # Required environment variables +DATABASE_PASSWORD= SESSION_SECRET= # Optional environment variables -DATABASE_URL= +DATABASE_HOST= +DATABASE_PORT= +DATABASE_USER= +DATABASE_NAME= SESSION_EXPIRES= USER_CLIENT_CHALLENGE_EXPIRES= SESSION_UPGRADE_CHALLENGE_EXPIRES= diff --git a/.gitignore b/.gitignore index 1dbbe60..aac77c6 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,3 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* - -# SQLite -*.db diff --git a/.prettierignore b/.prettierignore index 0f54b15..0c65201 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,8 +3,5 @@ package-lock.json pnpm-lock.yaml yarn.lock -# Output -/drizzle - # Documents *.md diff --git a/Dockerfile b/Dockerfile index eec42f6..f809fbc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,10 @@ FROM node:22-alpine AS base WORKDIR /app +RUN apk add --no-cache bash curl && \ + curl -o /usr/local/bin/wait-for-it https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh && \ + chmod +x /usr/local/bin/wait-for-it + RUN npm install -g pnpm@9 COPY pnpm-lock.yaml . @@ -10,10 +14,9 @@ FROM base AS build RUN pnpm fetch COPY . . -RUN pnpm install --offline -RUN pnpm build - -RUN sed -i "s/http\.createServer()/http.createServer({ requestTimeout: 0 })/g" ./build/index.js +RUN pnpm install --offline && \ + pnpm build && \ + sed -i "s/http\.createServer()/http.createServer({ requestTimeout: 0 })/g" ./build/index.js # Deploy Stage FROM base @@ -23,9 +26,7 @@ COPY package.json . RUN pnpm install --offline --prod COPY --from=build /app/build ./build -COPY drizzle ./drizzle EXPOSE 3000 ENV BODY_SIZE_LIMIT=Infinity - -CMD ["node", "./build/index.js"] +CMD ["bash", "-c", "wait-for-it ${DATABASE_HOST:-localhost}:${DATABASE_PORT:-5432} -- node ./build/index.js"] diff --git a/README.md b/README.md index 45f5c5a..ec1918c 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ vim .env # 아래를 참고하여 환경 변수를 설정해 주세요. docker compose up --build -d ``` -모든 데이터는 `./data` 디렉터리에 저장될 거예요. +모든 데이터는 `./data` 디렉터리에 아래에 저장될 거예요. ### Environment Variables @@ -31,7 +31,8 @@ docker compose up --build -d |이름|필수|기본값|설명| |:-|:-:|:-:|:-| -|`SESSION_SECRET`|Y||Session ID의 서명을 위해 사용돼요. 안전한 값으로 설정해 주세요.| +|`DATABASE_PASSWORD`|Y||데이터베이스에 접근하기 위해 필요한 비밀번호예요. 안전한 값으로 설정해 주세요.| +|`SESSION_SECRET`|Y||Session ID의 서명에 사용되는 비밀번호예요. 안전한 값으로 설정해 주세요.| |`SESSION_EXPIRES`||`14d`|Session의 유효 시간이에요. Session은 마지막으로 사용된 후 설정된 유효 시간이 지나면 자동으로 삭제돼요.| |`USER_CLIENT_CHALLENGE_EXPIRES`||`5m`|암호 키를 서버에 처음 등록할 때 사용되는 챌린지의 유효 시간이에요.| |`SESSION_UPGRADE_CHALLENGE_EXPIRES`||`5m`|암호 키와 함께 로그인할 때 사용되는 챌린지의 유효 시간이에요.| diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 0000000..f6570a5 --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,15 @@ +services: + database: + image: postgres:17.2 + restart: on-failure + volumes: + - database:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${DATABASE_USER:-} + - POSTGRES_PASSWORD=${DATABASE_PASSWORD:?} # Required + - POSTGRES_DB=${DATABASE_NAME:-} + ports: + - ${DATABASE_PORT:-5432}:5432 + +volumes: + database: diff --git a/docker-compose.yaml b/docker-compose.yaml index aecd8c8..dc7f392 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,13 +1,17 @@ services: server: build: . - restart: unless-stopped + restart: on-failure + depends_on: + - database user: ${CONTAINER_UID:-0}:${CONTAINER_GID:-0} volumes: - - ./data:/app/data + - ./data/library:/app/data/library environment: # ArkVault - - DATABASE_URL=/app/data/database.sqlite + - DATABASE_HOST=database + - DATABASE_USER=arkvault + - DATABASE_PASSWORD=${DATABASE_PASSWORD:?} # Required - SESSION_SECRET=${SESSION_SECRET:?} # Required - SESSION_EXPIRES - USER_CLIENT_CHALLENGE_EXPIRES @@ -19,3 +23,13 @@ services: - NODE_ENV=${NODE_ENV:-production} ports: - ${PORT:-80}:3000 + + database: + image: postgres:17.2-alpine + restart: on-failure + user: ${CONTAINER_UID:-0}:${CONTAINER_GID:-0} + volumes: + - ./data/database:/var/lib/postgresql/data + environment: + - POSTGRES_USER=arkvault + - POSTGRES_PASSWORD=${DATABASE_PASSWORD:?} diff --git a/drizzle.config.ts b/drizzle.config.ts deleted file mode 100644 index c0b54d5..0000000 --- a/drizzle.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -export default defineConfig({ - schema: "./src/lib/server/db/schema", - - dbCredentials: { - url: process.env.DATABASE_URL || "local.db", - }, - - verbose: true, - strict: true, - dialect: "sqlite", -}); diff --git a/drizzle/0000_unknown_stark_industries.sql b/drizzle/0000_unknown_stark_industries.sql deleted file mode 100644 index 28a9787..0000000 --- a/drizzle/0000_unknown_stark_industries.sql +++ /dev/null @@ -1,175 +0,0 @@ -CREATE TABLE `client` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `encryption_public_key` text NOT NULL, - `signature_public_key` text NOT NULL -); ---> statement-breakpoint -CREATE TABLE `user_client` ( - `user_id` integer NOT NULL, - `client_id` integer NOT NULL, - `state` text DEFAULT 'challenging' NOT NULL, - PRIMARY KEY(`client_id`, `user_id`), - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `user_client_challenge` ( - `id` integer PRIMARY KEY NOT NULL, - `user_id` integer NOT NULL, - `client_id` integer NOT NULL, - `answer` text NOT NULL, - `allowed_ip` text NOT NULL, - `expires_at` integer NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`,`client_id`) REFERENCES `user_client`(`user_id`,`client_id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `directory` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `parent_id` integer, - `user_id` integer NOT NULL, - `master_encryption_key_version` integer NOT NULL, - `encrypted_data_encryption_key` text NOT NULL, - `data_encryption_key_version` integer NOT NULL, - `encrypted_name` text NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`parent_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `directory_log` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `directory_id` integer NOT NULL, - `timestamp` integer NOT NULL, - `action` text NOT NULL, - `new_name` text, - FOREIGN KEY (`directory_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `file` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `parent_id` integer, - `user_id` integer NOT NULL, - `path` text NOT NULL, - `master_encryption_key_version` integer NOT NULL, - `encrypted_data_encryption_key` text NOT NULL, - `data_encryption_key_version` integer NOT NULL, - `hmac_secret_key_version` integer, - `content_hmac` text, - `content_type` text NOT NULL, - `encrypted_content_iv` text NOT NULL, - `encrypted_name` text NOT NULL, - FOREIGN KEY (`parent_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`,`hmac_secret_key_version`) REFERENCES `hmac_secret_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `file_log` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `file_id` integer NOT NULL, - `timestamp` integer NOT NULL, - `action` text NOT NULL, - `new_name` text, - FOREIGN KEY (`file_id`) REFERENCES `file`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint -CREATE TABLE `hmac_secret_key` ( - `user_id` integer NOT NULL, - `version` integer NOT NULL, - `state` text NOT NULL, - `master_encryption_key_version` integer NOT NULL, - `encrypted_key` text NOT NULL, - PRIMARY KEY(`user_id`, `version`), - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `hmac_secret_key_log` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `user_id` integer NOT NULL, - `hmac_secret_key_version` integer NOT NULL, - `timestamp` integer NOT NULL, - `action` text NOT NULL, - `action_by` integer, - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`action_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`,`hmac_secret_key_version`) REFERENCES `hmac_secret_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `client_master_encryption_key` ( - `user_id` integer NOT NULL, - `client_id` integer NOT NULL, - `version` integer NOT NULL, - `encrypted_key` text NOT NULL, - `encrypted_key_signature` text NOT NULL, - PRIMARY KEY(`client_id`, `user_id`, `version`), - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`,`version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `master_encryption_key` ( - `user_id` integer NOT NULL, - `version` integer NOT NULL, - `state` text NOT NULL, - `retired_at` integer, - PRIMARY KEY(`user_id`, `version`), - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `master_encryption_key_log` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `user_id` integer NOT NULL, - `master_encryption_key_version` integer NOT NULL, - `timestamp` integer NOT NULL, - `action` text NOT NULL, - `action_by` integer, - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`action_by`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `session` ( - `id` text PRIMARY KEY NOT NULL, - `user_id` integer NOT NULL, - `client_id` integer, - `created_at` integer NOT NULL, - `last_used_at` integer NOT NULL, - `last_used_by_ip` text, - `last_used_by_user_agent` text, - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `session_upgrade_challenge` ( - `id` integer PRIMARY KEY NOT NULL, - `session_id` text NOT NULL, - `client_id` integer NOT NULL, - `answer` text NOT NULL, - `allowed_ip` text NOT NULL, - `expires_at` integer NOT NULL, - FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action -); ---> statement-breakpoint -CREATE TABLE `user` ( - `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `email` text NOT NULL, - `password` text NOT NULL, - `nickname` text NOT NULL -); ---> statement-breakpoint -CREATE UNIQUE INDEX `client_encryption_public_key_unique` ON `client` (`encryption_public_key`);--> statement-breakpoint -CREATE UNIQUE INDEX `client_signature_public_key_unique` ON `client` (`signature_public_key`);--> statement-breakpoint -CREATE UNIQUE INDEX `client_encryption_public_key_signature_public_key_unique` ON `client` (`encryption_public_key`,`signature_public_key`);--> statement-breakpoint -CREATE UNIQUE INDEX `user_client_challenge_answer_unique` ON `user_client_challenge` (`answer`);--> statement-breakpoint -CREATE UNIQUE INDEX `directory_encrypted_data_encryption_key_unique` ON `directory` (`encrypted_data_encryption_key`);--> statement-breakpoint -CREATE UNIQUE INDEX `file_path_unique` ON `file` (`path`);--> statement-breakpoint -CREATE UNIQUE INDEX `file_encrypted_data_encryption_key_unique` ON `file` (`encrypted_data_encryption_key`);--> statement-breakpoint -CREATE UNIQUE INDEX `hmac_secret_key_encrypted_key_unique` ON `hmac_secret_key` (`encrypted_key`);--> statement-breakpoint -CREATE UNIQUE INDEX `session_user_id_client_id_unique` ON `session` (`user_id`,`client_id`);--> statement-breakpoint -CREATE UNIQUE INDEX `session_upgrade_challenge_session_id_unique` ON `session_upgrade_challenge` (`session_id`);--> statement-breakpoint -CREATE UNIQUE INDEX `session_upgrade_challenge_answer_unique` ON `session_upgrade_challenge` (`answer`);--> statement-breakpoint -CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`); \ No newline at end of file diff --git a/drizzle/0001_blushing_alice.sql b/drizzle/0001_blushing_alice.sql deleted file mode 100644 index f68ba02..0000000 --- a/drizzle/0001_blushing_alice.sql +++ /dev/null @@ -1,2 +0,0 @@ -ALTER TABLE `file` ADD `encrypted_created_at` text;--> statement-breakpoint -ALTER TABLE `file` ADD `encrypted_last_modified_at` text NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json deleted file mode 100644 index 42641f1..0000000 --- a/drizzle/meta/0000_snapshot.json +++ /dev/null @@ -1,1287 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "928e5669-81cf-486c-9122-8ee64fc9f457", - "prevId": "00000000-0000-0000-0000-000000000000", - "tables": { - "client": { - "name": "client", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "encryption_public_key": { - "name": "encryption_public_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "signature_public_key": { - "name": "signature_public_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "client_encryption_public_key_unique": { - "name": "client_encryption_public_key_unique", - "columns": [ - "encryption_public_key" - ], - "isUnique": true - }, - "client_signature_public_key_unique": { - "name": "client_signature_public_key_unique", - "columns": [ - "signature_public_key" - ], - "isUnique": true - }, - "client_encryption_public_key_signature_public_key_unique": { - "name": "client_encryption_public_key_signature_public_key_unique", - "columns": [ - "encryption_public_key", - "signature_public_key" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "user_client": { - "name": "user_client", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "client_id": { - "name": "client_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "state": { - "name": "state", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'challenging'" - } - }, - "indexes": {}, - "foreignKeys": { - "user_client_user_id_user_id_fk": { - "name": "user_client_user_id_user_id_fk", - "tableFrom": "user_client", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "user_client_client_id_client_id_fk": { - "name": "user_client_client_id_client_id_fk", - "tableFrom": "user_client", - "tableTo": "client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "user_client_user_id_client_id_pk": { - "columns": [ - "client_id", - "user_id" - ], - "name": "user_client_user_id_client_id_pk" - } - }, - "uniqueConstraints": {} - }, - "user_client_challenge": { - "name": "user_client_challenge", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "client_id": { - "name": "client_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "answer": { - "name": "answer", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "allowed_ip": { - "name": "allowed_ip", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_client_challenge_answer_unique": { - "name": "user_client_challenge_answer_unique", - "columns": [ - "answer" - ], - "isUnique": true - } - }, - "foreignKeys": { - "user_client_challenge_user_id_user_id_fk": { - "name": "user_client_challenge_user_id_user_id_fk", - "tableFrom": "user_client_challenge", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "user_client_challenge_client_id_client_id_fk": { - "name": "user_client_challenge_client_id_client_id_fk", - "tableFrom": "user_client_challenge", - "tableTo": "client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "user_client_challenge_user_id_client_id_user_client_user_id_client_id_fk": { - "name": "user_client_challenge_user_id_client_id_user_client_user_id_client_id_fk", - "tableFrom": "user_client_challenge", - "tableTo": "user_client", - "columnsFrom": [ - "user_id", - "client_id" - ], - "columnsTo": [ - "user_id", - "client_id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "directory": { - "name": "directory", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "parent_id": { - "name": "parent_id", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "master_encryption_key_version": { - "name": "master_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_data_encryption_key": { - "name": "encrypted_data_encryption_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "data_encryption_key_version": { - "name": "data_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_name": { - "name": "encrypted_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "directory_encrypted_data_encryption_key_unique": { - "name": "directory_encrypted_data_encryption_key_unique", - "columns": [ - "encrypted_data_encryption_key" - ], - "isUnique": true - } - }, - "foreignKeys": { - "directory_user_id_user_id_fk": { - "name": "directory_user_id_user_id_fk", - "tableFrom": "directory", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "directory_parent_id_directory_id_fk": { - "name": "directory_parent_id_directory_id_fk", - "tableFrom": "directory", - "tableTo": "directory", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "directory_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { - "name": "directory_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", - "tableFrom": "directory", - "tableTo": "master_encryption_key", - "columnsFrom": [ - "user_id", - "master_encryption_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "directory_log": { - "name": "directory_log", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "directory_id": { - "name": "directory_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "timestamp": { - "name": "timestamp", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "new_name": { - "name": "new_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "directory_log_directory_id_directory_id_fk": { - "name": "directory_log_directory_id_directory_id_fk", - "tableFrom": "directory_log", - "tableTo": "directory", - "columnsFrom": [ - "directory_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "file": { - "name": "file", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "parent_id": { - "name": "parent_id", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "path": { - "name": "path", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "master_encryption_key_version": { - "name": "master_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_data_encryption_key": { - "name": "encrypted_data_encryption_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "data_encryption_key_version": { - "name": "data_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "hmac_secret_key_version": { - "name": "hmac_secret_key_version", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "content_hmac": { - "name": "content_hmac", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "content_type": { - "name": "content_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_content_iv": { - "name": "encrypted_content_iv", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_name": { - "name": "encrypted_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "file_path_unique": { - "name": "file_path_unique", - "columns": [ - "path" - ], - "isUnique": true - }, - "file_encrypted_data_encryption_key_unique": { - "name": "file_encrypted_data_encryption_key_unique", - "columns": [ - "encrypted_data_encryption_key" - ], - "isUnique": true - } - }, - "foreignKeys": { - "file_parent_id_directory_id_fk": { - "name": "file_parent_id_directory_id_fk", - "tableFrom": "file", - "tableTo": "directory", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "file_user_id_user_id_fk": { - "name": "file_user_id_user_id_fk", - "tableFrom": "file", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "file_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { - "name": "file_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", - "tableFrom": "file", - "tableTo": "master_encryption_key", - "columnsFrom": [ - "user_id", - "master_encryption_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "file_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk": { - "name": "file_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk", - "tableFrom": "file", - "tableTo": "hmac_secret_key", - "columnsFrom": [ - "user_id", - "hmac_secret_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "file_log": { - "name": "file_log", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "file_id": { - "name": "file_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "timestamp": { - "name": "timestamp", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "new_name": { - "name": "new_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "file_log_file_id_file_id_fk": { - "name": "file_log_file_id_file_id_fk", - "tableFrom": "file_log", - "tableTo": "file", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "hmac_secret_key": { - "name": "hmac_secret_key", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "state": { - "name": "state", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "master_encryption_key_version": { - "name": "master_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_key": { - "name": "encrypted_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "hmac_secret_key_encrypted_key_unique": { - "name": "hmac_secret_key_encrypted_key_unique", - "columns": [ - "encrypted_key" - ], - "isUnique": true - } - }, - "foreignKeys": { - "hmac_secret_key_user_id_user_id_fk": { - "name": "hmac_secret_key_user_id_user_id_fk", - "tableFrom": "hmac_secret_key", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "hmac_secret_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { - "name": "hmac_secret_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", - "tableFrom": "hmac_secret_key", - "tableTo": "master_encryption_key", - "columnsFrom": [ - "user_id", - "master_encryption_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "hmac_secret_key_user_id_version_pk": { - "columns": [ - "user_id", - "version" - ], - "name": "hmac_secret_key_user_id_version_pk" - } - }, - "uniqueConstraints": {} - }, - "hmac_secret_key_log": { - "name": "hmac_secret_key_log", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "hmac_secret_key_version": { - "name": "hmac_secret_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "timestamp": { - "name": "timestamp", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action_by": { - "name": "action_by", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "hmac_secret_key_log_user_id_user_id_fk": { - "name": "hmac_secret_key_log_user_id_user_id_fk", - "tableFrom": "hmac_secret_key_log", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "hmac_secret_key_log_action_by_user_id_fk": { - "name": "hmac_secret_key_log_action_by_user_id_fk", - "tableFrom": "hmac_secret_key_log", - "tableTo": "user", - "columnsFrom": [ - "action_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "hmac_secret_key_log_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk": { - "name": "hmac_secret_key_log_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk", - "tableFrom": "hmac_secret_key_log", - "tableTo": "hmac_secret_key", - "columnsFrom": [ - "user_id", - "hmac_secret_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "client_master_encryption_key": { - "name": "client_master_encryption_key", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "client_id": { - "name": "client_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_key": { - "name": "encrypted_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_key_signature": { - "name": "encrypted_key_signature", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "client_master_encryption_key_user_id_user_id_fk": { - "name": "client_master_encryption_key_user_id_user_id_fk", - "tableFrom": "client_master_encryption_key", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "client_master_encryption_key_client_id_client_id_fk": { - "name": "client_master_encryption_key_client_id_client_id_fk", - "tableFrom": "client_master_encryption_key", - "tableTo": "client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "client_master_encryption_key_user_id_version_master_encryption_key_user_id_version_fk": { - "name": "client_master_encryption_key_user_id_version_master_encryption_key_user_id_version_fk", - "tableFrom": "client_master_encryption_key", - "tableTo": "master_encryption_key", - "columnsFrom": [ - "user_id", - "version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "client_master_encryption_key_user_id_client_id_version_pk": { - "columns": [ - "client_id", - "user_id", - "version" - ], - "name": "client_master_encryption_key_user_id_client_id_version_pk" - } - }, - "uniqueConstraints": {} - }, - "master_encryption_key": { - "name": "master_encryption_key", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "state": { - "name": "state", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "retired_at": { - "name": "retired_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "master_encryption_key_user_id_user_id_fk": { - "name": "master_encryption_key_user_id_user_id_fk", - "tableFrom": "master_encryption_key", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "master_encryption_key_user_id_version_pk": { - "columns": [ - "user_id", - "version" - ], - "name": "master_encryption_key_user_id_version_pk" - } - }, - "uniqueConstraints": {} - }, - "master_encryption_key_log": { - "name": "master_encryption_key_log", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "master_encryption_key_version": { - "name": "master_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "timestamp": { - "name": "timestamp", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action_by": { - "name": "action_by", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "master_encryption_key_log_user_id_user_id_fk": { - "name": "master_encryption_key_log_user_id_user_id_fk", - "tableFrom": "master_encryption_key_log", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "master_encryption_key_log_action_by_client_id_fk": { - "name": "master_encryption_key_log_action_by_client_id_fk", - "tableFrom": "master_encryption_key_log", - "tableTo": "client", - "columnsFrom": [ - "action_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "master_encryption_key_log_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { - "name": "master_encryption_key_log_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", - "tableFrom": "master_encryption_key_log", - "tableTo": "master_encryption_key", - "columnsFrom": [ - "user_id", - "master_encryption_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "session": { - "name": "session", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "client_id": { - "name": "client_id", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "last_used_at": { - "name": "last_used_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "last_used_by_ip": { - "name": "last_used_by_ip", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_used_by_user_agent": { - "name": "last_used_by_user_agent", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "session_user_id_client_id_unique": { - "name": "session_user_id_client_id_unique", - "columns": [ - "user_id", - "client_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "session_user_id_user_id_fk": { - "name": "session_user_id_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "session_client_id_client_id_fk": { - "name": "session_client_id_client_id_fk", - "tableFrom": "session", - "tableTo": "client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "session_upgrade_challenge": { - "name": "session_upgrade_challenge", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "client_id": { - "name": "client_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "answer": { - "name": "answer", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "allowed_ip": { - "name": "allowed_ip", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "session_upgrade_challenge_session_id_unique": { - "name": "session_upgrade_challenge_session_id_unique", - "columns": [ - "session_id" - ], - "isUnique": true - }, - "session_upgrade_challenge_answer_unique": { - "name": "session_upgrade_challenge_answer_unique", - "columns": [ - "answer" - ], - "isUnique": true - } - }, - "foreignKeys": { - "session_upgrade_challenge_session_id_session_id_fk": { - "name": "session_upgrade_challenge_session_id_session_id_fk", - "tableFrom": "session_upgrade_challenge", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "session_upgrade_challenge_client_id_client_id_fk": { - "name": "session_upgrade_challenge_client_id_client_id_fk", - "tableFrom": "session_upgrade_challenge", - "tableTo": "client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "user": { - "name": "user", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "nickname": { - "name": "nickname", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_email_unique": { - "name": "user_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - } - }, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json deleted file mode 100644 index 3425d7b..0000000 --- a/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -1,1301 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "5e999e6f-1ec4-40b0-bb10-741ffc6da4af", - "prevId": "928e5669-81cf-486c-9122-8ee64fc9f457", - "tables": { - "client": { - "name": "client", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "encryption_public_key": { - "name": "encryption_public_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "signature_public_key": { - "name": "signature_public_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "client_encryption_public_key_unique": { - "name": "client_encryption_public_key_unique", - "columns": [ - "encryption_public_key" - ], - "isUnique": true - }, - "client_signature_public_key_unique": { - "name": "client_signature_public_key_unique", - "columns": [ - "signature_public_key" - ], - "isUnique": true - }, - "client_encryption_public_key_signature_public_key_unique": { - "name": "client_encryption_public_key_signature_public_key_unique", - "columns": [ - "encryption_public_key", - "signature_public_key" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "user_client": { - "name": "user_client", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "client_id": { - "name": "client_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "state": { - "name": "state", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'challenging'" - } - }, - "indexes": {}, - "foreignKeys": { - "user_client_user_id_user_id_fk": { - "name": "user_client_user_id_user_id_fk", - "tableFrom": "user_client", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "user_client_client_id_client_id_fk": { - "name": "user_client_client_id_client_id_fk", - "tableFrom": "user_client", - "tableTo": "client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "user_client_user_id_client_id_pk": { - "columns": [ - "client_id", - "user_id" - ], - "name": "user_client_user_id_client_id_pk" - } - }, - "uniqueConstraints": {} - }, - "user_client_challenge": { - "name": "user_client_challenge", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "client_id": { - "name": "client_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "answer": { - "name": "answer", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "allowed_ip": { - "name": "allowed_ip", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_client_challenge_answer_unique": { - "name": "user_client_challenge_answer_unique", - "columns": [ - "answer" - ], - "isUnique": true - } - }, - "foreignKeys": { - "user_client_challenge_user_id_user_id_fk": { - "name": "user_client_challenge_user_id_user_id_fk", - "tableFrom": "user_client_challenge", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "user_client_challenge_client_id_client_id_fk": { - "name": "user_client_challenge_client_id_client_id_fk", - "tableFrom": "user_client_challenge", - "tableTo": "client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "user_client_challenge_user_id_client_id_user_client_user_id_client_id_fk": { - "name": "user_client_challenge_user_id_client_id_user_client_user_id_client_id_fk", - "tableFrom": "user_client_challenge", - "tableTo": "user_client", - "columnsFrom": [ - "user_id", - "client_id" - ], - "columnsTo": [ - "user_id", - "client_id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "directory": { - "name": "directory", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "parent_id": { - "name": "parent_id", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "master_encryption_key_version": { - "name": "master_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_data_encryption_key": { - "name": "encrypted_data_encryption_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "data_encryption_key_version": { - "name": "data_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_name": { - "name": "encrypted_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "directory_encrypted_data_encryption_key_unique": { - "name": "directory_encrypted_data_encryption_key_unique", - "columns": [ - "encrypted_data_encryption_key" - ], - "isUnique": true - } - }, - "foreignKeys": { - "directory_user_id_user_id_fk": { - "name": "directory_user_id_user_id_fk", - "tableFrom": "directory", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "directory_parent_id_directory_id_fk": { - "name": "directory_parent_id_directory_id_fk", - "tableFrom": "directory", - "tableTo": "directory", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "directory_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { - "name": "directory_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", - "tableFrom": "directory", - "tableTo": "master_encryption_key", - "columnsFrom": [ - "user_id", - "master_encryption_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "directory_log": { - "name": "directory_log", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "directory_id": { - "name": "directory_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "timestamp": { - "name": "timestamp", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "new_name": { - "name": "new_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "directory_log_directory_id_directory_id_fk": { - "name": "directory_log_directory_id_directory_id_fk", - "tableFrom": "directory_log", - "tableTo": "directory", - "columnsFrom": [ - "directory_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "file": { - "name": "file", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "parent_id": { - "name": "parent_id", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "path": { - "name": "path", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "master_encryption_key_version": { - "name": "master_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_data_encryption_key": { - "name": "encrypted_data_encryption_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "data_encryption_key_version": { - "name": "data_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "hmac_secret_key_version": { - "name": "hmac_secret_key_version", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "content_hmac": { - "name": "content_hmac", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "content_type": { - "name": "content_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_content_iv": { - "name": "encrypted_content_iv", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_name": { - "name": "encrypted_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_created_at": { - "name": "encrypted_created_at", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "encrypted_last_modified_at": { - "name": "encrypted_last_modified_at", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "file_path_unique": { - "name": "file_path_unique", - "columns": [ - "path" - ], - "isUnique": true - }, - "file_encrypted_data_encryption_key_unique": { - "name": "file_encrypted_data_encryption_key_unique", - "columns": [ - "encrypted_data_encryption_key" - ], - "isUnique": true - } - }, - "foreignKeys": { - "file_parent_id_directory_id_fk": { - "name": "file_parent_id_directory_id_fk", - "tableFrom": "file", - "tableTo": "directory", - "columnsFrom": [ - "parent_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "file_user_id_user_id_fk": { - "name": "file_user_id_user_id_fk", - "tableFrom": "file", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "file_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { - "name": "file_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", - "tableFrom": "file", - "tableTo": "master_encryption_key", - "columnsFrom": [ - "user_id", - "master_encryption_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "file_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk": { - "name": "file_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk", - "tableFrom": "file", - "tableTo": "hmac_secret_key", - "columnsFrom": [ - "user_id", - "hmac_secret_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "file_log": { - "name": "file_log", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "file_id": { - "name": "file_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "timestamp": { - "name": "timestamp", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "new_name": { - "name": "new_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "file_log_file_id_file_id_fk": { - "name": "file_log_file_id_file_id_fk", - "tableFrom": "file_log", - "tableTo": "file", - "columnsFrom": [ - "file_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "hmac_secret_key": { - "name": "hmac_secret_key", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "state": { - "name": "state", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "master_encryption_key_version": { - "name": "master_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_key": { - "name": "encrypted_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "hmac_secret_key_encrypted_key_unique": { - "name": "hmac_secret_key_encrypted_key_unique", - "columns": [ - "encrypted_key" - ], - "isUnique": true - } - }, - "foreignKeys": { - "hmac_secret_key_user_id_user_id_fk": { - "name": "hmac_secret_key_user_id_user_id_fk", - "tableFrom": "hmac_secret_key", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "hmac_secret_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { - "name": "hmac_secret_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", - "tableFrom": "hmac_secret_key", - "tableTo": "master_encryption_key", - "columnsFrom": [ - "user_id", - "master_encryption_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "hmac_secret_key_user_id_version_pk": { - "columns": [ - "user_id", - "version" - ], - "name": "hmac_secret_key_user_id_version_pk" - } - }, - "uniqueConstraints": {} - }, - "hmac_secret_key_log": { - "name": "hmac_secret_key_log", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "hmac_secret_key_version": { - "name": "hmac_secret_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "timestamp": { - "name": "timestamp", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action_by": { - "name": "action_by", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "hmac_secret_key_log_user_id_user_id_fk": { - "name": "hmac_secret_key_log_user_id_user_id_fk", - "tableFrom": "hmac_secret_key_log", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "hmac_secret_key_log_action_by_user_id_fk": { - "name": "hmac_secret_key_log_action_by_user_id_fk", - "tableFrom": "hmac_secret_key_log", - "tableTo": "user", - "columnsFrom": [ - "action_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "hmac_secret_key_log_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk": { - "name": "hmac_secret_key_log_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk", - "tableFrom": "hmac_secret_key_log", - "tableTo": "hmac_secret_key", - "columnsFrom": [ - "user_id", - "hmac_secret_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "client_master_encryption_key": { - "name": "client_master_encryption_key", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "client_id": { - "name": "client_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_key": { - "name": "encrypted_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "encrypted_key_signature": { - "name": "encrypted_key_signature", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "client_master_encryption_key_user_id_user_id_fk": { - "name": "client_master_encryption_key_user_id_user_id_fk", - "tableFrom": "client_master_encryption_key", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "client_master_encryption_key_client_id_client_id_fk": { - "name": "client_master_encryption_key_client_id_client_id_fk", - "tableFrom": "client_master_encryption_key", - "tableTo": "client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "client_master_encryption_key_user_id_version_master_encryption_key_user_id_version_fk": { - "name": "client_master_encryption_key_user_id_version_master_encryption_key_user_id_version_fk", - "tableFrom": "client_master_encryption_key", - "tableTo": "master_encryption_key", - "columnsFrom": [ - "user_id", - "version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "client_master_encryption_key_user_id_client_id_version_pk": { - "columns": [ - "client_id", - "user_id", - "version" - ], - "name": "client_master_encryption_key_user_id_client_id_version_pk" - } - }, - "uniqueConstraints": {} - }, - "master_encryption_key": { - "name": "master_encryption_key", - "columns": { - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "version": { - "name": "version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "state": { - "name": "state", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "retired_at": { - "name": "retired_at", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "master_encryption_key_user_id_user_id_fk": { - "name": "master_encryption_key_user_id_user_id_fk", - "tableFrom": "master_encryption_key", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "master_encryption_key_user_id_version_pk": { - "columns": [ - "user_id", - "version" - ], - "name": "master_encryption_key_user_id_version_pk" - } - }, - "uniqueConstraints": {} - }, - "master_encryption_key_log": { - "name": "master_encryption_key_log", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "master_encryption_key_version": { - "name": "master_encryption_key_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "timestamp": { - "name": "timestamp", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action": { - "name": "action", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "action_by": { - "name": "action_by", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "master_encryption_key_log_user_id_user_id_fk": { - "name": "master_encryption_key_log_user_id_user_id_fk", - "tableFrom": "master_encryption_key_log", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "master_encryption_key_log_action_by_client_id_fk": { - "name": "master_encryption_key_log_action_by_client_id_fk", - "tableFrom": "master_encryption_key_log", - "tableTo": "client", - "columnsFrom": [ - "action_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "master_encryption_key_log_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { - "name": "master_encryption_key_log_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", - "tableFrom": "master_encryption_key_log", - "tableTo": "master_encryption_key", - "columnsFrom": [ - "user_id", - "master_encryption_key_version" - ], - "columnsTo": [ - "user_id", - "version" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "session": { - "name": "session", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "user_id": { - "name": "user_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "client_id": { - "name": "client_id", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "last_used_at": { - "name": "last_used_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "last_used_by_ip": { - "name": "last_used_by_ip", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "last_used_by_user_agent": { - "name": "last_used_by_user_agent", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "session_user_id_client_id_unique": { - "name": "session_user_id_client_id_unique", - "columns": [ - "user_id", - "client_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "session_user_id_user_id_fk": { - "name": "session_user_id_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": [ - "user_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "session_client_id_client_id_fk": { - "name": "session_client_id_client_id_fk", - "tableFrom": "session", - "tableTo": "client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "session_upgrade_challenge": { - "name": "session_upgrade_challenge", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "session_id": { - "name": "session_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "client_id": { - "name": "client_id", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "answer": { - "name": "answer", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "allowed_ip": { - "name": "allowed_ip", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "expires_at": { - "name": "expires_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "session_upgrade_challenge_session_id_unique": { - "name": "session_upgrade_challenge_session_id_unique", - "columns": [ - "session_id" - ], - "isUnique": true - }, - "session_upgrade_challenge_answer_unique": { - "name": "session_upgrade_challenge_answer_unique", - "columns": [ - "answer" - ], - "isUnique": true - } - }, - "foreignKeys": { - "session_upgrade_challenge_session_id_session_id_fk": { - "name": "session_upgrade_challenge_session_id_session_id_fk", - "tableFrom": "session_upgrade_challenge", - "tableTo": "session", - "columnsFrom": [ - "session_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - }, - "session_upgrade_challenge_client_id_client_id_fk": { - "name": "session_upgrade_challenge_client_id_client_id_fk", - "tableFrom": "session_upgrade_challenge", - "tableTo": "client", - "columnsFrom": [ - "client_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - }, - "user": { - "name": "user", - "columns": { - "id": { - "name": "id", - "type": "integer", - "primaryKey": true, - "notNull": true, - "autoincrement": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "nickname": { - "name": "nickname", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "user_email_unique": { - "name": "user_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {} - } - }, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json deleted file mode 100644 index 65be42a..0000000 --- a/drizzle/meta/_journal.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": "7", - "dialect": "sqlite", - "entries": [ - { - "idx": 0, - "version": "6", - "when": 1736704436996, - "tag": "0000_unknown_stark_industries", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1736720831242, - "tag": "0001_blushing_alice", - "breakpoints": true - } - ] -} \ No newline at end of file diff --git a/kysely.config.ts b/kysely.config.ts new file mode 100644 index 0000000..1fe5a76 --- /dev/null +++ b/kysely.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "kysely-ctl"; +import { Pool } from "pg"; + +export default defineConfig({ + dialect: "pg", + dialectConfig: { + pool: new Pool({ + host: process.env.DATABASE_HOST, + port: process.env.DATABASE_PORT ? parseInt(process.env.DATABASE_PORT) : undefined, + user: process.env.DATABASE_USER, + password: process.env.DATABASE_PASSWORD, + database: process.env.DATABASE_NAME, + }), + }, + migrations: { + migrationFolder: "./src/lib/server/db/migrations", + }, +}); diff --git a/package.json b/package.json index 3a4adf2..8d0ddba 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "arkvault", "private": true, - "version": "0.2.0", + "version": "0.4.0", "type": "module", "scripts": { "dev": "vite dev", @@ -11,10 +11,9 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write .", "lint": "prettier --check . && eslint .", - "db:push": "drizzle-kit push", - "db:generate": "drizzle-kit generate", - "db:migrate": "drizzle-kit migrate", - "db:studio": "drizzle-kit studio" + "db:up": "docker compose -f docker-compose.dev.yaml -p arkvault-dev up -d", + "db:down": "docker compose -f docker-compose.dev.yaml -p arkvault-dev down", + "db:migrate": "kysely migrate" }, "devDependencies": { "@eslint/compat": "^1.2.4", @@ -22,14 +21,13 @@ "@sveltejs/adapter-node": "^5.2.11", "@sveltejs/kit": "^2.15.2", "@sveltejs/vite-plugin-svelte": "^4.0.4", - "@types/better-sqlite3": "^7.6.12", "@types/file-saver": "^2.0.7", "@types/ms": "^0.7.34", "@types/node-schedule": "^2.1.7", + "@types/pg": "^8.11.10", "autoprefixer": "^10.4.20", "axios": "^1.7.9", "dexie": "^4.0.10", - "drizzle-kit": "^0.22.8", "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.46.1", @@ -38,12 +36,13 @@ "file-saver": "^2.0.5", "globals": "^15.14.0", "heic2any": "^0.0.4", + "kysely-ctl": "^0.10.1", "mime": "^4.0.6", "p-limit": "^6.2.0", "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.2", "prettier-plugin-tailwindcss": "^0.6.9", - "svelte": "^5.17.1", + "svelte": "^5.19.1", "svelte-check": "^4.1.3", "tailwindcss": "^3.4.17", "typescript": "^5.7.3", @@ -54,10 +53,10 @@ "dependencies": { "@fastify/busboy": "^3.1.1", "argon2": "^0.41.1", - "better-sqlite3": "^11.7.2", - "drizzle-orm": "^0.33.0", + "kysely": "^0.27.5", "ms": "^2.1.3", "node-schedule": "^2.1.1", + "pg": "^8.13.1", "uuid": "^11.0.4", "zod": "^3.24.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae948dc..9ed4442 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,18 +14,18 @@ importers: argon2: specifier: ^0.41.1 version: 0.41.1 - better-sqlite3: - specifier: ^11.7.2 - version: 11.7.2 - drizzle-orm: - specifier: ^0.33.0 - version: 0.33.0(@types/better-sqlite3@7.6.12)(better-sqlite3@11.7.2) + kysely: + specifier: ^0.27.5 + version: 0.27.5 ms: specifier: ^2.1.3 version: 2.1.3 node-schedule: specifier: ^2.1.1 version: 2.1.1 + pg: + specifier: ^8.13.1 + version: 8.13.1 uuid: specifier: ^11.0.4 version: 11.0.4 @@ -35,22 +35,19 @@ importers: devDependencies: '@eslint/compat': specifier: ^1.2.4 - version: 1.2.4(eslint@9.17.0(jiti@1.21.7)) + version: 1.2.4(eslint@9.17.0(jiti@2.4.2)) '@iconify-json/material-symbols': specifier: ^1.2.12 version: 1.2.12 '@sveltejs/adapter-node': specifier: ^5.2.11 - version: 5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))) + version: 5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))) '@sveltejs/kit': specifier: ^2.15.2 - version: 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) + version: 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) '@sveltejs/vite-plugin-svelte': specifier: ^4.0.4 - version: 4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) - '@types/better-sqlite3': - specifier: ^7.6.12 - version: 7.6.12 + version: 4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) '@types/file-saver': specifier: ^2.0.7 version: 2.0.7 @@ -60,6 +57,9 @@ importers: '@types/node-schedule': specifier: ^2.1.7 version: 2.1.7 + '@types/pg': + specifier: ^8.11.10 + version: 8.11.10 autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) @@ -69,18 +69,15 @@ importers: dexie: specifier: ^4.0.10 version: 4.0.10 - drizzle-kit: - specifier: ^0.22.8 - version: 0.22.8 eslint: specifier: ^9.17.0 - version: 9.17.0(jiti@1.21.7) + version: 9.17.0(jiti@2.4.2) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@9.17.0(jiti@1.21.7)) + version: 9.1.0(eslint@9.17.0(jiti@2.4.2)) eslint-plugin-svelte: specifier: ^2.46.1 - version: 2.46.1(eslint@9.17.0(jiti@1.21.7))(svelte@5.17.1) + version: 2.46.1(eslint@9.17.0(jiti@2.4.2))(svelte@5.19.1) eslint-plugin-tailwindcss: specifier: ^3.17.5 version: 3.17.5(tailwindcss@3.4.17) @@ -96,6 +93,9 @@ importers: heic2any: specifier: ^0.0.4 version: 0.0.4 + kysely-ctl: + specifier: ^0.10.1 + version: 0.10.1(kysely@0.27.5) mime: specifier: ^4.0.6 version: 4.0.6 @@ -107,16 +107,16 @@ importers: version: 3.4.2 prettier-plugin-svelte: specifier: ^3.3.2 - version: 3.3.2(prettier@3.4.2)(svelte@5.17.1) + version: 3.3.2(prettier@3.4.2)(svelte@5.19.1) prettier-plugin-tailwindcss: specifier: ^0.6.9 - version: 0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.17.1))(prettier@3.4.2) + version: 0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.19.1))(prettier@3.4.2) svelte: - specifier: ^5.17.1 - version: 5.17.1 + specifier: ^5.19.1 + version: 5.19.1 svelte-check: specifier: ^4.1.3 - version: 4.1.3(picomatch@4.0.2)(svelte@5.17.1)(typescript@5.7.3) + version: 4.1.3(picomatch@4.0.2)(svelte@5.19.1)(typescript@5.7.3) tailwindcss: specifier: ^3.4.17 version: 3.4.17 @@ -125,10 +125,10 @@ importers: version: 5.7.3 typescript-eslint: specifier: ^8.19.1 - version: 8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3) + version: 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) unplugin-icons: specifier: ^0.22.0 - version: 0.22.0(svelte@5.17.1) + version: 0.22.0(svelte@5.19.1) vite: specifier: ^5.4.11 version: 5.4.11(@types/node@22.10.5) @@ -152,37 +152,17 @@ packages: '@antfu/utils@0.7.10': resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} - '@esbuild-kit/core-utils@3.3.2': - resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild-kit/esm-loader@2.6.5': - resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild/aix-ppc64@0.19.12': - resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.18.20': - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.19.12': - resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] + '@esbuild/aix-ppc64@0.23.1': + resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} @@ -190,16 +170,10 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.18.20': - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.19.12': - resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} - engines: {node: '>=12'} - cpu: [arm] + '@esbuild/android-arm64@0.23.1': + resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} + engines: {node: '>=18'} + cpu: [arm64] os: [android] '@esbuild/android-arm@0.21.5': @@ -208,16 +182,10 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.18.20': - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.19.12': - resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/android-arm@0.23.1': + resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} + engines: {node: '>=18'} + cpu: [arm] os: [android] '@esbuild/android-x64@0.21.5': @@ -226,17 +194,11 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.18.20': - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.19.12': - resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] + '@esbuild/android-x64@0.23.1': + resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} @@ -244,16 +206,10 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.18.20': - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.19.12': - resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/darwin-arm64@0.23.1': + resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} + engines: {node: '>=18'} + cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.21.5': @@ -262,17 +218,11 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.18.20': - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.19.12': - resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] + '@esbuild/darwin-x64@0.23.1': + resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} @@ -280,16 +230,10 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.18.20': - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.19.12': - resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/freebsd-arm64@0.23.1': + resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} + engines: {node: '>=18'} + cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.21.5': @@ -298,17 +242,11 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.18.20': - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.19.12': - resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] + '@esbuild/freebsd-x64@0.23.1': + resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} @@ -316,16 +254,10 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.18.20': - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.19.12': - resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} - engines: {node: '>=12'} - cpu: [arm] + '@esbuild/linux-arm64@0.23.1': + resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} + engines: {node: '>=18'} + cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.21.5': @@ -334,16 +266,10 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.18.20': - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.19.12': - resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} - engines: {node: '>=12'} - cpu: [ia32] + '@esbuild/linux-arm@0.23.1': + resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} + engines: {node: '>=18'} + cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.21.5': @@ -352,16 +278,10 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.18.20': - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.19.12': - resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} - engines: {node: '>=12'} - cpu: [loong64] + '@esbuild/linux-ia32@0.23.1': + resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} + engines: {node: '>=18'} + cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.21.5': @@ -370,16 +290,10 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.18.20': - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.19.12': - resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} - engines: {node: '>=12'} - cpu: [mips64el] + '@esbuild/linux-loong64@0.23.1': + resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} + engines: {node: '>=18'} + cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.21.5': @@ -388,16 +302,10 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.18.20': - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.19.12': - resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} - engines: {node: '>=12'} - cpu: [ppc64] + '@esbuild/linux-mips64el@0.23.1': + resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} + engines: {node: '>=18'} + cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.21.5': @@ -406,16 +314,10 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.18.20': - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.19.12': - resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} - engines: {node: '>=12'} - cpu: [riscv64] + '@esbuild/linux-ppc64@0.23.1': + resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} + engines: {node: '>=18'} + cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.21.5': @@ -424,16 +326,10 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.18.20': - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.19.12': - resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} - engines: {node: '>=12'} - cpu: [s390x] + '@esbuild/linux-riscv64@0.23.1': + resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} + engines: {node: '>=18'} + cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.21.5': @@ -442,16 +338,10 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.18.20': - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.19.12': - resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/linux-s390x@0.23.1': + resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} + engines: {node: '>=18'} + cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.21.5': @@ -460,17 +350,11 @@ packages: cpu: [x64] os: [linux] - '@esbuild/netbsd-x64@0.18.20': - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} + '@esbuild/linux-x64@0.23.1': + resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} + engines: {node: '>=18'} cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.19.12': - resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] + os: [linux] '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} @@ -478,16 +362,16 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/openbsd-x64@0.18.20': - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} + '@esbuild/netbsd-x64@0.23.1': + resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} + engines: {node: '>=18'} cpu: [x64] - os: [openbsd] + os: [netbsd] - '@esbuild/openbsd-x64@0.19.12': - resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/openbsd-arm64@0.23.1': + resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} + engines: {node: '>=18'} + cpu: [arm64] os: [openbsd] '@esbuild/openbsd-x64@0.21.5': @@ -496,17 +380,11 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.18.20': - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} + '@esbuild/openbsd-x64@0.23.1': + resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} + engines: {node: '>=18'} cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.19.12': - resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] + os: [openbsd] '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} @@ -514,17 +392,11 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.18.20': - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.19.12': - resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] + '@esbuild/sunos-x64@0.23.1': + resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} @@ -532,16 +404,10 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.18.20': - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.19.12': - resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} - engines: {node: '>=12'} - cpu: [ia32] + '@esbuild/win32-arm64@0.23.1': + resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} + engines: {node: '>=18'} + cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.21.5': @@ -550,16 +416,10 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.18.20': - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.19.12': - resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} - engines: {node: '>=12'} - cpu: [x64] + '@esbuild/win32-ia32@0.23.1': + resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} + engines: {node: '>=18'} + cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.21.5': @@ -568,6 +428,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.23.1': + resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.4.1': resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -848,9 +714,6 @@ packages: svelte: ^5.0.0-next.96 || ^5.0.0 vite: ^5.0.0 - '@types/better-sqlite3@7.6.12': - resolution: {integrity: sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==} - '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -872,6 +735,9 @@ packages: '@types/node@22.10.5': resolution: {integrity: sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==} + '@types/pg@8.11.10': + resolution: {integrity: sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==} + '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -1001,22 +867,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - better-sqlite3@11.7.2: - resolution: {integrity: sha512-10a57cHVDmfNQS4jrZ9AH2t+2ekzYh5Rhbcnb4ytpmYweoLdogDmyTt5D+hLiY9b44Mx9foowb/4iXBTO2yP3Q==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1032,11 +886,13 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + c12@2.0.1: + resolution: {integrity: sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} @@ -1061,8 +917,12 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chownr@1.1.4: - resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} @@ -1092,6 +952,10 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + consola@3.4.0: + resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} + engines: {node: ^14.18.0 || >=16.10.0} + cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} @@ -1118,14 +982,6 @@ packages: supports-color: optional: true - decompress-response@6.0.0: - resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} - engines: {node: '>=10'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1133,13 +989,15 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} + destr@2.0.3: + resolution: {integrity: sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==} devalue@5.1.1: resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} @@ -1153,98 +1011,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - drizzle-kit@0.22.8: - resolution: {integrity: sha512-VjI4wsJjk3hSqHSa3TwBf+uvH6M6pRHyxyoVbt935GUzP9tUR/BRZ+MhEJNgryqbzN2Za1KP0eJMTgKEPsalYQ==} - hasBin: true - - drizzle-orm@0.33.0: - resolution: {integrity: sha512-SHy72R2Rdkz0LEq0PSG/IdvnT3nGiWuRk+2tXZQ90GVq/XQhpCzu/EFT3V2rox+w8MlkBQxifF8pCStNYnERfA==} - peerDependencies: - '@aws-sdk/client-rds-data': '>=3' - '@cloudflare/workers-types': '>=3' - '@electric-sql/pglite': '>=0.1.1' - '@libsql/client': '*' - '@neondatabase/serverless': '>=0.1' - '@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: '>=13.2.0' - knex: '*' - kysely: '*' - mysql2: '>=2' - pg: '>=8' - postgres: '>=3' - prisma: '*' - react: '>=18' - sql.js: '>=1' - sqlite3: '>=5' - peerDependenciesMeta: - '@aws-sdk/client-rds-data': - optional: true - '@cloudflare/workers-types': - optional: true - '@electric-sql/pglite': - optional: true - '@libsql/client': - optional: true - '@neondatabase/serverless': - optional: true - '@op-engineering/op-sqlite': - optional: true - '@opentelemetry/api': - optional: true - '@planetscale/database': - optional: true - '@prisma/client': - optional: true - '@tidbcloud/serverless': - optional: true - '@types/better-sqlite3': - optional: true - '@types/pg': - optional: true - '@types/react': - optional: true - '@types/sql.js': - optional: true - '@vercel/postgres': - optional: true - '@xata.io/client': - optional: true - better-sqlite3: - optional: true - bun-types: - optional: true - expo-sqlite: - optional: true - knex: - optional: true - kysely: - optional: true - mysql2: - optional: true - pg: - optional: true - postgres: - optional: true - prisma: - optional: true - react: - optional: true - sql.js: - optional: true - sqlite3: - optional: true + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -1258,29 +1027,16 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - - esbuild-register@3.6.0: - resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} - peerDependencies: - esbuild: '>=0.12 <1' - - esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} - hasBin: true - - esbuild@0.19.12: - resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} hasBin: true + esbuild@0.23.1: + resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1358,8 +1114,8 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - esrap@1.3.2: - resolution: {integrity: sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==} + esrap@1.4.3: + resolution: {integrity: sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -1376,13 +1132,13 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + exifreader@4.26.0: resolution: {integrity: sha512-nNN9B0oaXTOpArdnIdJBAro2Sa620m7wMjMA5Xy1rcua0EYHVjzGKM5syBOWDqIG2Qay6Pes/5FOdj65hvZ9Vw==} - expand-template@2.0.3: - resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} - engines: {node: '>=6'} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1414,9 +1170,6 @@ packages: file-saver@2.0.5: resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1452,8 +1205,9 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - fs-constants@1.0.0: - resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -1463,11 +1217,16 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} - github-from-package@0.0.0: - resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + giget@1.2.3: + resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==} + hasBin: true glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -1509,8 +1268,9 @@ packages: heic2any@0.0.4: resolution: {integrity: sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA==} - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} @@ -1527,12 +1287,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1566,6 +1320,10 @@ packages: is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -1576,6 +1334,10 @@ packages: resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} hasBin: true + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1602,6 +1364,21 @@ packages: kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + kysely-ctl@0.10.1: + resolution: {integrity: sha512-tB2mGV/MbfaQC6Lo582Rs2OdtfX23ueWDscCSDT42Iy8pYHYbhMy9ncXU35ee8LQz4BO2apQihyY8rDProP+9w==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + kysely: '>=0.18.1 <0.28.0' + kysely-postgres-js: ^2 + peerDependenciesMeta: + kysely-postgres-js: + optional: true + + kysely@0.27.5: + resolution: {integrity: sha512-s7hZHcQeSNKpzCkHRm8yA+0JPLjncSWnjb+2TIElwS2JAqYr+Kv3Ess+9KFfJS0C1xcQ1i9NkNHpWwCYpHMWsA==} + engines: {node: '>=14.0.0'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1644,6 +1421,9 @@ packages: magic-string@0.30.17: resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1665,9 +1445,9 @@ packages: engines: {node: '>=16'} hasBin: true - mimic-response@3.1.0: - resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} - engines: {node: '>=10'} + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1676,15 +1456,26 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + + minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - mkdirp-classic@0.5.3: - resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + + mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true mlly@1.7.3: resolution: {integrity: sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==} @@ -1708,20 +1499,16 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-build-utils@1.0.2: - resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - node-abi@3.71.0: - resolution: {integrity: sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==} - engines: {node: '>=10'} - node-addon-api@8.3.0: resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==} engines: {node: ^18 || ^20 || >= 21} + node-fetch-native@1.6.4: + resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -1741,6 +1528,20 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + nypm@0.3.12: + resolution: {integrity: sha512-D3pzNDWIvgA+7IORhD/IuWzEk4uXv6GsgOxiid4UU3h9oq5IqV1KtPDi63n4sZJ/xcWlr88c0QM2RgN5VbOhFA==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + + nypm@0.4.1: + resolution: {integrity: sha512-1b9mihliBh8UCcKtcGRu//G50iHpjxIQVUqkdhPT/SDVE7KdJKoHXLS0heuYTQCx95dFqiyUbXZB9r8ikn+93g==} + engines: {node: ^14.16.0 || >=16.10.0} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1749,8 +1550,18 @@ packages: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} + + ohash@1.1.4: + resolution: {integrity: sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g==} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} @@ -1786,6 +1597,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1796,6 +1611,51 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + + pg-connection-string@2.7.0: + resolution: {integrity: sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} + + pg-pool@3.7.0: + resolution: {integrity: sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.7.0: + resolution: {integrity: sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg-types@4.0.2: + resolution: {integrity: sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==} + engines: {node: '>=10'} + + pg@8.13.1: + resolution: {integrity: sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -1883,10 +1743,40 @@ packages: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} - prebuild-install@7.1.2: - resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} - engines: {node: '>=10'} - hasBin: true + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.2: + resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} + engines: {node: '>=12'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-date@2.1.0: + resolution: {integrity: sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==} + engines: {node: '>=12'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + + postgres-range@1.1.4: + resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -1961,9 +1851,6 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - pump@3.0.2: - resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -1971,17 +1858,12 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -2018,9 +1900,6 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - semver@7.6.3: resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} engines: {node: '>=10'} @@ -2041,12 +1920,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-concat@1.0.1: - resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} - - simple-get@4.0.1: - resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - sirv@3.0.0: resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} engines: {node: '>=18'} @@ -2058,12 +1931,12 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} + std-env@3.8.0: + resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -2073,9 +1946,6 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -2084,9 +1954,9 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} @@ -2122,8 +1992,8 @@ packages: svelte: optional: true - svelte@5.17.1: - resolution: {integrity: sha512-HitqD0XhU9OEytPuux/XYzxle4+7D8+fIb1tHbwMzOtBzDZZO+ESEuwMbahJ/3JoklfmRPB/Gzp74L87Qrxfpw==} + svelte@5.19.1: + resolution: {integrity: sha512-H/Vs2O51bwILZbaNUSdr4P1NbLpOGsxl4jJAjd88ELjzRgeRi1BHqexcVGannDr7D1pmTYWWajzHOM7bMbtB9Q==} engines: {node: '>=18'} tailwindcss@3.4.17: @@ -2131,12 +2001,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true - tar-fs@2.1.1: - resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} - - tar-stream@2.2.0: - resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} - engines: {node: '>=6'} + tar@6.2.1: + resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} + engines: {node: '>=10'} thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} @@ -2168,8 +2035,10 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - tunnel-agent@0.6.0: - resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} @@ -2295,8 +2164,12 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} yaml@1.10.2: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} @@ -2342,230 +2215,157 @@ snapshots: '@antfu/utils@0.7.10': {} - '@esbuild-kit/core-utils@3.3.2': - dependencies: - esbuild: 0.18.20 - source-map-support: 0.5.21 - - '@esbuild-kit/esm-loader@2.6.5': - dependencies: - '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.8.1 - - '@esbuild/aix-ppc64@0.19.12': - optional: true - '@esbuild/aix-ppc64@0.21.5': optional: true - '@esbuild/android-arm64@0.18.20': - optional: true - - '@esbuild/android-arm64@0.19.12': + '@esbuild/aix-ppc64@0.23.1': optional: true '@esbuild/android-arm64@0.21.5': optional: true - '@esbuild/android-arm@0.18.20': - optional: true - - '@esbuild/android-arm@0.19.12': + '@esbuild/android-arm64@0.23.1': optional: true '@esbuild/android-arm@0.21.5': optional: true - '@esbuild/android-x64@0.18.20': - optional: true - - '@esbuild/android-x64@0.19.12': + '@esbuild/android-arm@0.23.1': optional: true '@esbuild/android-x64@0.21.5': optional: true - '@esbuild/darwin-arm64@0.18.20': - optional: true - - '@esbuild/darwin-arm64@0.19.12': + '@esbuild/android-x64@0.23.1': optional: true '@esbuild/darwin-arm64@0.21.5': optional: true - '@esbuild/darwin-x64@0.18.20': - optional: true - - '@esbuild/darwin-x64@0.19.12': + '@esbuild/darwin-arm64@0.23.1': optional: true '@esbuild/darwin-x64@0.21.5': optional: true - '@esbuild/freebsd-arm64@0.18.20': - optional: true - - '@esbuild/freebsd-arm64@0.19.12': + '@esbuild/darwin-x64@0.23.1': optional: true '@esbuild/freebsd-arm64@0.21.5': optional: true - '@esbuild/freebsd-x64@0.18.20': - optional: true - - '@esbuild/freebsd-x64@0.19.12': + '@esbuild/freebsd-arm64@0.23.1': optional: true '@esbuild/freebsd-x64@0.21.5': optional: true - '@esbuild/linux-arm64@0.18.20': - optional: true - - '@esbuild/linux-arm64@0.19.12': + '@esbuild/freebsd-x64@0.23.1': optional: true '@esbuild/linux-arm64@0.21.5': optional: true - '@esbuild/linux-arm@0.18.20': - optional: true - - '@esbuild/linux-arm@0.19.12': + '@esbuild/linux-arm64@0.23.1': optional: true '@esbuild/linux-arm@0.21.5': optional: true - '@esbuild/linux-ia32@0.18.20': - optional: true - - '@esbuild/linux-ia32@0.19.12': + '@esbuild/linux-arm@0.23.1': optional: true '@esbuild/linux-ia32@0.21.5': optional: true - '@esbuild/linux-loong64@0.18.20': - optional: true - - '@esbuild/linux-loong64@0.19.12': + '@esbuild/linux-ia32@0.23.1': optional: true '@esbuild/linux-loong64@0.21.5': optional: true - '@esbuild/linux-mips64el@0.18.20': - optional: true - - '@esbuild/linux-mips64el@0.19.12': + '@esbuild/linux-loong64@0.23.1': optional: true '@esbuild/linux-mips64el@0.21.5': optional: true - '@esbuild/linux-ppc64@0.18.20': - optional: true - - '@esbuild/linux-ppc64@0.19.12': + '@esbuild/linux-mips64el@0.23.1': optional: true '@esbuild/linux-ppc64@0.21.5': optional: true - '@esbuild/linux-riscv64@0.18.20': - optional: true - - '@esbuild/linux-riscv64@0.19.12': + '@esbuild/linux-ppc64@0.23.1': optional: true '@esbuild/linux-riscv64@0.21.5': optional: true - '@esbuild/linux-s390x@0.18.20': - optional: true - - '@esbuild/linux-s390x@0.19.12': + '@esbuild/linux-riscv64@0.23.1': optional: true '@esbuild/linux-s390x@0.21.5': optional: true - '@esbuild/linux-x64@0.18.20': - optional: true - - '@esbuild/linux-x64@0.19.12': + '@esbuild/linux-s390x@0.23.1': optional: true '@esbuild/linux-x64@0.21.5': optional: true - '@esbuild/netbsd-x64@0.18.20': - optional: true - - '@esbuild/netbsd-x64@0.19.12': + '@esbuild/linux-x64@0.23.1': optional: true '@esbuild/netbsd-x64@0.21.5': optional: true - '@esbuild/openbsd-x64@0.18.20': + '@esbuild/netbsd-x64@0.23.1': optional: true - '@esbuild/openbsd-x64@0.19.12': + '@esbuild/openbsd-arm64@0.23.1': optional: true '@esbuild/openbsd-x64@0.21.5': optional: true - '@esbuild/sunos-x64@0.18.20': - optional: true - - '@esbuild/sunos-x64@0.19.12': + '@esbuild/openbsd-x64@0.23.1': optional: true '@esbuild/sunos-x64@0.21.5': optional: true - '@esbuild/win32-arm64@0.18.20': - optional: true - - '@esbuild/win32-arm64@0.19.12': + '@esbuild/sunos-x64@0.23.1': optional: true '@esbuild/win32-arm64@0.21.5': optional: true - '@esbuild/win32-ia32@0.18.20': - optional: true - - '@esbuild/win32-ia32@0.19.12': + '@esbuild/win32-arm64@0.23.1': optional: true '@esbuild/win32-ia32@0.21.5': optional: true - '@esbuild/win32-x64@0.18.20': - optional: true - - '@esbuild/win32-x64@0.19.12': + '@esbuild/win32-ia32@0.23.1': optional: true '@esbuild/win32-x64@0.21.5': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.17.0(jiti@1.21.7))': + '@esbuild/win32-x64@0.23.1': + optional: true + + '@eslint-community/eslint-utils@4.4.1(eslint@9.17.0(jiti@2.4.2))': dependencies: - eslint: 9.17.0(jiti@1.21.7) + eslint: 9.17.0(jiti@2.4.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/compat@1.2.4(eslint@9.17.0(jiti@1.21.7))': + '@eslint/compat@1.2.4(eslint@9.17.0(jiti@2.4.2))': optionalDependencies: - eslint: 9.17.0(jiti@1.21.7) + eslint: 9.17.0(jiti@2.4.2) '@eslint/config-array@0.19.1': dependencies: @@ -2773,17 +2573,17 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.30.1': optional: true - '@sveltejs/adapter-node@5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))': + '@sveltejs/adapter-node@5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))': dependencies: '@rollup/plugin-commonjs': 28.0.2(rollup@4.30.1) '@rollup/plugin-json': 6.1.0(rollup@4.30.1) '@rollup/plugin-node-resolve': 16.0.0(rollup@4.30.1) - '@sveltejs/kit': 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) + '@sveltejs/kit': 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) rollup: 4.30.1 - '@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))': + '@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) + '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.1.1 @@ -2795,36 +2595,32 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.1 sirv: 3.0.0 - svelte: 5.17.1 + svelte: 5.19.1 tiny-glob: 0.2.9 vite: 5.4.11(@types/node@22.10.5) - '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))': + '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) + '@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) debug: 4.4.0 - svelte: 5.17.1 + svelte: 5.19.1 vite: 5.4.11(@types/node@22.10.5) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))': + '@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)) + '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 - svelte: 5.17.1 + svelte: 5.19.1 vite: 5.4.11(@types/node@22.10.5) vitefu: 1.0.5(vite@5.4.11(@types/node@22.10.5)) transitivePeerDependencies: - supports-color - '@types/better-sqlite3@7.6.12': - dependencies: - '@types/node': 22.10.5 - '@types/cookie@0.6.0': {} '@types/estree@1.0.6': {} @@ -2843,17 +2639,23 @@ snapshots: dependencies: undici-types: 6.20.0 + '@types/pg@8.11.10': + dependencies: + '@types/node': 22.10.5 + pg-protocol: 1.7.0 + pg-types: 4.0.2 + '@types/resolve@1.20.2': {} - '@typescript-eslint/eslint-plugin@8.19.1(@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.19.1(@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/parser': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/scope-manager': 8.19.1 - '@typescript-eslint/type-utils': 8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3) - '@typescript-eslint/utils': 8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/type-utils': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.19.1 - eslint: 9.17.0(jiti@1.21.7) + eslint: 9.17.0(jiti@2.4.2) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -2862,14 +2664,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/scope-manager': 8.19.1 '@typescript-eslint/types': 8.19.1 '@typescript-eslint/typescript-estree': 8.19.1(typescript@5.7.3) '@typescript-eslint/visitor-keys': 8.19.1 debug: 4.4.0 - eslint: 9.17.0(jiti@1.21.7) + eslint: 9.17.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -2879,12 +2681,12 @@ snapshots: '@typescript-eslint/types': 8.19.1 '@typescript-eslint/visitor-keys': 8.19.1 - '@typescript-eslint/type-utils@8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: '@typescript-eslint/typescript-estree': 8.19.1(typescript@5.7.3) - '@typescript-eslint/utils': 8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) debug: 4.4.0 - eslint: 9.17.0(jiti@1.21.7) + eslint: 9.17.0(jiti@2.4.2) ts-api-utils: 2.0.0(typescript@5.7.3) typescript: 5.7.3 transitivePeerDependencies: @@ -2906,13 +2708,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3)': + '@typescript-eslint/utils@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2)) '@typescript-eslint/scope-manager': 8.19.1 '@typescript-eslint/types': 8.19.1 '@typescript-eslint/typescript-estree': 8.19.1(typescript@5.7.3) - eslint: 9.17.0(jiti@1.21.7) + eslint: 9.17.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -2995,25 +2797,8 @@ snapshots: balanced-match@1.0.2: {} - base64-js@1.5.1: {} - - better-sqlite3@11.7.2: - dependencies: - bindings: 1.5.0 - prebuild-install: 7.1.2 - binary-extensions@2.3.0: {} - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -3034,12 +2819,20 @@ snapshots: node-releases: 2.0.19 update-browserslist-db: 1.1.2(browserslist@4.24.4) - buffer-from@1.1.2: {} - - buffer@5.7.1: + c12@2.0.1: dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 + chokidar: 4.0.3 + confbox: 0.1.8 + defu: 6.1.4 + dotenv: 16.4.7 + giget: 1.2.3 + jiti: 2.4.2 + mlly: 1.7.3 + ohash: 1.1.4 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.3.0 + rc9: 2.1.2 callsites@3.1.0: {} @@ -3068,7 +2861,11 @@ snapshots: dependencies: readdirp: 4.0.2 - chownr@1.1.4: {} + chownr@2.0.0: {} + + citty@0.1.6: + dependencies: + consola: 3.4.0 clsx@2.1.1: {} @@ -3090,6 +2887,8 @@ snapshots: confbox@0.1.8: {} + consola@3.4.0: {} + cookie@0.6.0: {} cron-parser@4.9.0: @@ -3108,19 +2907,15 @@ snapshots: dependencies: ms: 2.1.3 - decompress-response@6.0.0: - dependencies: - mimic-response: 3.1.0 - - deep-extend@0.6.0: {} - deep-is@0.1.4: {} deepmerge@4.3.1: {} + defu@6.1.4: {} + delayed-stream@1.0.0: {} - detect-libc@2.0.3: {} + destr@2.0.3: {} devalue@5.1.1: {} @@ -3130,18 +2925,7 @@ snapshots: dlv@1.1.3: {} - drizzle-kit@0.22.8: - dependencies: - '@esbuild-kit/esm-loader': 2.6.5 - esbuild: 0.19.12 - esbuild-register: 3.6.0(esbuild@0.19.12) - transitivePeerDependencies: - - supports-color - - drizzle-orm@0.33.0(@types/better-sqlite3@7.6.12)(better-sqlite3@11.7.2): - optionalDependencies: - '@types/better-sqlite3': 7.6.12 - better-sqlite3: 11.7.2 + dotenv@16.4.7: {} eastasianwidth@0.2.0: {} @@ -3151,68 +2935,6 @@ snapshots: emoji-regex@9.2.2: {} - end-of-stream@1.4.4: - dependencies: - once: 1.4.0 - - esbuild-register@3.6.0(esbuild@0.19.12): - dependencies: - debug: 4.4.0 - esbuild: 0.19.12 - transitivePeerDependencies: - - supports-color - - esbuild@0.18.20: - optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 - - esbuild@0.19.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.19.12 - '@esbuild/android-arm': 0.19.12 - '@esbuild/android-arm64': 0.19.12 - '@esbuild/android-x64': 0.19.12 - '@esbuild/darwin-arm64': 0.19.12 - '@esbuild/darwin-x64': 0.19.12 - '@esbuild/freebsd-arm64': 0.19.12 - '@esbuild/freebsd-x64': 0.19.12 - '@esbuild/linux-arm': 0.19.12 - '@esbuild/linux-arm64': 0.19.12 - '@esbuild/linux-ia32': 0.19.12 - '@esbuild/linux-loong64': 0.19.12 - '@esbuild/linux-mips64el': 0.19.12 - '@esbuild/linux-ppc64': 0.19.12 - '@esbuild/linux-riscv64': 0.19.12 - '@esbuild/linux-s390x': 0.19.12 - '@esbuild/linux-x64': 0.19.12 - '@esbuild/netbsd-x64': 0.19.12 - '@esbuild/openbsd-x64': 0.19.12 - '@esbuild/sunos-x64': 0.19.12 - '@esbuild/win32-arm64': 0.19.12 - '@esbuild/win32-ia32': 0.19.12 - '@esbuild/win32-x64': 0.19.12 - esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -3239,25 +2961,52 @@ snapshots: '@esbuild/win32-ia32': 0.21.5 '@esbuild/win32-x64': 0.21.5 + esbuild@0.23.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.23.1 + '@esbuild/android-arm': 0.23.1 + '@esbuild/android-arm64': 0.23.1 + '@esbuild/android-x64': 0.23.1 + '@esbuild/darwin-arm64': 0.23.1 + '@esbuild/darwin-x64': 0.23.1 + '@esbuild/freebsd-arm64': 0.23.1 + '@esbuild/freebsd-x64': 0.23.1 + '@esbuild/linux-arm': 0.23.1 + '@esbuild/linux-arm64': 0.23.1 + '@esbuild/linux-ia32': 0.23.1 + '@esbuild/linux-loong64': 0.23.1 + '@esbuild/linux-mips64el': 0.23.1 + '@esbuild/linux-ppc64': 0.23.1 + '@esbuild/linux-riscv64': 0.23.1 + '@esbuild/linux-s390x': 0.23.1 + '@esbuild/linux-x64': 0.23.1 + '@esbuild/netbsd-x64': 0.23.1 + '@esbuild/openbsd-arm64': 0.23.1 + '@esbuild/openbsd-x64': 0.23.1 + '@esbuild/sunos-x64': 0.23.1 + '@esbuild/win32-arm64': 0.23.1 + '@esbuild/win32-ia32': 0.23.1 + '@esbuild/win32-x64': 0.23.1 + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} - eslint-compat-utils@0.5.1(eslint@9.17.0(jiti@1.21.7)): + eslint-compat-utils@0.5.1(eslint@9.17.0(jiti@2.4.2)): dependencies: - eslint: 9.17.0(jiti@1.21.7) + eslint: 9.17.0(jiti@2.4.2) semver: 7.6.3 - eslint-config-prettier@9.1.0(eslint@9.17.0(jiti@1.21.7)): + eslint-config-prettier@9.1.0(eslint@9.17.0(jiti@2.4.2)): dependencies: - eslint: 9.17.0(jiti@1.21.7) + eslint: 9.17.0(jiti@2.4.2) - eslint-plugin-svelte@2.46.1(eslint@9.17.0(jiti@1.21.7))(svelte@5.17.1): + eslint-plugin-svelte@2.46.1(eslint@9.17.0(jiti@2.4.2))(svelte@5.19.1): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2)) '@jridgewell/sourcemap-codec': 1.5.0 - eslint: 9.17.0(jiti@1.21.7) - eslint-compat-utils: 0.5.1(eslint@9.17.0(jiti@1.21.7)) + eslint: 9.17.0(jiti@2.4.2) + eslint-compat-utils: 0.5.1(eslint@9.17.0(jiti@2.4.2)) esutils: 2.0.3 known-css-properties: 0.35.0 postcss: 8.4.49 @@ -3265,9 +3014,9 @@ snapshots: postcss-safe-parser: 6.0.0(postcss@8.4.49) postcss-selector-parser: 6.1.2 semver: 7.6.3 - svelte-eslint-parser: 0.43.0(svelte@5.17.1) + svelte-eslint-parser: 0.43.0(svelte@5.19.1) optionalDependencies: - svelte: 5.17.1 + svelte: 5.19.1 transitivePeerDependencies: - ts-node @@ -3291,9 +3040,9 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.17.0(jiti@1.21.7): + eslint@9.17.0(jiti@2.4.2): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.1 '@eslint/core': 0.9.1 @@ -3328,7 +3077,7 @@ snapshots: natural-compare: 1.4.0 optionator: 0.9.4 optionalDependencies: - jiti: 1.21.7 + jiti: 2.4.2 transitivePeerDependencies: - supports-color @@ -3350,7 +3099,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@1.3.2: + esrap@1.4.3: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -3364,12 +3113,22 @@ snapshots: esutils@2.0.3: {} + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + exifreader@4.26.0: optionalDependencies: '@xmldom/xmldom': 0.9.6 - expand-template@2.0.3: {} - fast-deep-equal@3.1.3: {} fast-glob@3.3.3: @@ -3398,8 +3157,6 @@ snapshots: file-saver@2.0.5: {} - file-uri-to-path@1.0.0: {} - fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -3431,18 +3188,31 @@ snapshots: fraction.js@4.3.7: {} - fs-constants@1.0.0: {} + fs-minipass@2.1.0: + dependencies: + minipass: 3.3.6 fsevents@2.3.3: optional: true function-bind@1.1.2: {} + get-stream@8.0.1: {} + get-tsconfig@4.8.1: dependencies: resolve-pkg-maps: 1.0.0 - github-from-package@0.0.0: {} + giget@1.2.3: + dependencies: + citty: 0.1.6 + consola: 3.4.0 + defu: 6.1.4 + node-fetch-native: 1.6.4 + nypm: 0.3.12 + ohash: 1.1.4 + pathe: 1.1.2 + tar: 6.2.1 glob-parent@5.1.2: dependencies: @@ -3479,7 +3249,7 @@ snapshots: heic2any@0.0.4: {} - ieee754@1.2.1: {} + human-signals@5.0.0: {} ignore@5.3.2: {} @@ -3492,10 +3262,6 @@ snapshots: imurmurhash@0.1.4: {} - inherits@2.0.4: {} - - ini@1.3.8: {} - is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -3524,6 +3290,8 @@ snapshots: dependencies: '@types/estree': 1.0.6 + is-stream@3.0.0: {} + isexe@2.0.0: {} jackspeak@3.4.3: @@ -3534,6 +3302,8 @@ snapshots: jiti@1.21.7: {} + jiti@2.4.2: {} + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -3554,6 +3324,23 @@ snapshots: kolorist@1.8.0: {} + kysely-ctl@0.10.1(kysely@0.27.5): + dependencies: + c12: 2.0.1 + citty: 0.1.6 + consola: 3.4.0 + kysely: 0.27.5 + nypm: 0.4.1 + ofetch: 1.4.1 + pathe: 1.1.2 + pkg-types: 1.3.0 + std-env: 3.8.0 + tsx: 4.19.2 + transitivePeerDependencies: + - magicast + + kysely@0.27.5: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -3588,6 +3375,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + merge-stream@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -3603,7 +3392,7 @@ snapshots: mime@4.0.6: {} - mimic-response@3.1.0: {} + mimic-fn@4.0.0: {} minimatch@3.1.2: dependencies: @@ -3613,11 +3402,20 @@ snapshots: dependencies: brace-expansion: 2.0.1 - minimist@1.2.8: {} + minipass@3.3.6: + dependencies: + yallist: 4.0.0 + + minipass@5.0.0: {} minipass@7.1.2: {} - mkdirp-classic@0.5.3: {} + minizlib@2.1.2: + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + + mkdirp@1.0.4: {} mlly@1.7.3: dependencies: @@ -3640,16 +3438,12 @@ snapshots: nanoid@3.3.8: {} - napi-build-utils@1.0.2: {} - natural-compare@1.4.0: {} - node-abi@3.71.0: - dependencies: - semver: 7.6.3 - node-addon-api@8.3.0: {} + node-fetch-native@1.6.4: {} + node-gyp-build@4.8.4: {} node-releases@2.0.19: {} @@ -3664,13 +3458,45 @@ snapshots: normalize-range@0.1.2: {} + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + nypm@0.3.12: + dependencies: + citty: 0.1.6 + consola: 3.4.0 + execa: 8.0.1 + pathe: 1.1.2 + pkg-types: 1.3.0 + ufo: 1.5.4 + + nypm@0.4.1: + dependencies: + citty: 0.1.6 + consola: 3.4.0 + pathe: 1.1.2 + pkg-types: 1.3.0 + tinyexec: 0.3.2 + ufo: 1.5.4 + object-assign@4.1.1: {} object-hash@3.0.0: {} - once@1.4.0: + obuf@1.1.2: {} + + ofetch@1.4.1: dependencies: - wrappy: 1.0.2 + destr: 2.0.3 + node-fetch-native: 1.6.4 + ufo: 1.5.4 + + ohash@1.1.4: {} + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 optionator@0.9.4: dependencies: @@ -3705,6 +3531,8 @@ snapshots: path-key@3.1.1: {} + path-key@4.0.0: {} + path-parse@1.0.7: {} path-scurry@1.11.1: @@ -3714,6 +3542,55 @@ snapshots: pathe@1.1.2: {} + perfect-debounce@1.0.0: {} + + pg-cloudflare@1.1.1: + optional: true + + pg-connection-string@2.7.0: {} + + pg-int8@1.0.1: {} + + pg-numeric@1.0.2: {} + + pg-pool@3.7.0(pg@8.13.1): + dependencies: + pg: 8.13.1 + + pg-protocol@1.7.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg-types@4.0.2: + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.1.0 + postgres-interval: 3.0.0 + postgres-range: 1.1.4 + + pg@8.13.1: + dependencies: + pg-connection-string: 2.7.0 + pg-pool: 3.7.0(pg@8.13.1) + pg-protocol: 1.7.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -3782,64 +3659,58 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - prebuild-install@7.1.2: + postgres-array@2.0.0: {} + + postgres-array@3.0.2: {} + + postgres-bytea@1.0.0: {} + + postgres-bytea@3.0.0: dependencies: - detect-libc: 2.0.3 - expand-template: 2.0.3 - github-from-package: 0.0.0 - minimist: 1.2.8 - mkdirp-classic: 0.5.3 - napi-build-utils: 1.0.2 - node-abi: 3.71.0 - pump: 3.0.2 - rc: 1.2.8 - simple-get: 4.0.1 - tar-fs: 2.1.1 - tunnel-agent: 0.6.0 + obuf: 1.1.2 + + postgres-date@1.0.7: {} + + postgres-date@2.1.0: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres-interval@3.0.0: {} + + postgres-range@1.1.4: {} prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.17.1): + prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.19.1): dependencies: prettier: 3.4.2 - svelte: 5.17.1 + svelte: 5.19.1 - prettier-plugin-tailwindcss@0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.17.1))(prettier@3.4.2): + prettier-plugin-tailwindcss@0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.19.1))(prettier@3.4.2): dependencies: prettier: 3.4.2 optionalDependencies: - prettier-plugin-svelte: 3.3.2(prettier@3.4.2)(svelte@5.17.1) + prettier-plugin-svelte: 3.3.2(prettier@3.4.2)(svelte@5.19.1) prettier@3.4.2: {} proxy-from-env@1.1.0: {} - pump@3.0.2: - dependencies: - end-of-stream: 1.4.4 - once: 1.4.0 - punycode@2.3.1: {} queue-microtask@1.2.3: {} - rc@1.2.8: + rc9@2.1.2: dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 + defu: 6.1.4 + destr: 2.0.3 read-cache@1.0.0: dependencies: pify: 2.3.0 - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -3891,8 +3762,6 @@ snapshots: dependencies: mri: 1.2.0 - safe-buffer@5.2.1: {} - semver@7.6.3: {} set-cookie-parser@2.7.1: {} @@ -3905,14 +3774,6 @@ snapshots: signal-exit@4.1.0: {} - simple-concat@1.0.1: {} - - simple-get@4.0.1: - dependencies: - decompress-response: 6.0.0 - once: 1.4.0 - simple-concat: 1.0.1 - sirv@3.0.0: dependencies: '@polka/url': 1.0.0-next.28 @@ -3923,12 +3784,9 @@ snapshots: source-map-js@1.2.1: {} - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 + split2@4.2.0: {} - source-map@0.6.1: {} + std-env@3.8.0: {} string-width@4.2.3: dependencies: @@ -3942,10 +3800,6 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -3954,7 +3808,7 @@ snapshots: dependencies: ansi-regex: 6.1.0 - strip-json-comments@2.0.1: {} + strip-final-newline@3.0.0: {} strip-json-comments@3.1.1: {} @@ -3974,19 +3828,19 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.1.3(picomatch@4.0.2)(svelte@5.17.1)(typescript@5.7.3): + svelte-check@4.1.3(picomatch@4.0.2)(svelte@5.19.1)(typescript@5.7.3): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 4.0.3 fdir: 6.4.2(picomatch@4.0.2) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.17.1 + svelte: 5.19.1 typescript: 5.7.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@0.43.0(svelte@5.17.1): + svelte-eslint-parser@0.43.0(svelte@5.19.1): dependencies: eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 @@ -3994,9 +3848,9 @@ snapshots: postcss: 8.4.49 postcss-scss: 4.0.9(postcss@8.4.49) optionalDependencies: - svelte: 5.17.1 + svelte: 5.19.1 - svelte@5.17.1: + svelte@5.19.1: dependencies: '@ampproject/remapping': 2.3.0 '@jridgewell/sourcemap-codec': 1.5.0 @@ -4007,7 +3861,7 @@ snapshots: axobject-query: 4.1.0 clsx: 2.1.1 esm-env: 1.2.2 - esrap: 1.3.2 + esrap: 1.4.3 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.17 @@ -4040,20 +3894,14 @@ snapshots: transitivePeerDependencies: - ts-node - tar-fs@2.1.1: + tar@6.2.1: dependencies: - chownr: 1.1.4 - mkdirp-classic: 0.5.3 - pump: 3.0.2 - tar-stream: 2.2.0 - - tar-stream@2.2.0: - dependencies: - bl: 4.1.0 - end-of-stream: 1.4.4 - fs-constants: 1.0.0 - inherits: 2.0.4 - readable-stream: 3.6.2 + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 thenify-all@1.6.0: dependencies: @@ -4082,20 +3930,23 @@ snapshots: ts-interface-checker@0.1.13: {} - tunnel-agent@0.6.0: + tsx@4.19.2: dependencies: - safe-buffer: 5.2.1 + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3): + typescript-eslint@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.19.1(@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3))(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3) - '@typescript-eslint/parser': 8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3) - '@typescript-eslint/utils': 8.19.1(eslint@9.17.0(jiti@1.21.7))(typescript@5.7.3) - eslint: 9.17.0(jiti@1.21.7) + '@typescript-eslint/eslint-plugin': 8.19.1(@typescript-eslint/parser@8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3))(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/utils': 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3) + eslint: 9.17.0(jiti@2.4.2) typescript: 5.7.3 transitivePeerDependencies: - supports-color @@ -4106,7 +3957,7 @@ snapshots: undici-types@6.20.0: {} - unplugin-icons@0.22.0(svelte@5.17.1): + unplugin-icons@0.22.0(svelte@5.19.1): dependencies: '@antfu/install-pkg': 0.5.0 '@antfu/utils': 0.7.10 @@ -4116,7 +3967,7 @@ snapshots: local-pkg: 0.5.1 unplugin: 2.1.2 optionalDependencies: - svelte: 5.17.1 + svelte: 5.19.1 transitivePeerDependencies: - supports-color @@ -4172,7 +4023,9 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 - wrappy@1.0.2: {} + xtend@4.0.2: {} + + yallist@4.0.0: {} yaml@1.10.2: {} diff --git a/src/app.html b/src/app.html index 4471298..2e7fd3e 100644 --- a/src/app.html +++ b/src/app.html @@ -1,5 +1,5 @@ - + diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 9dac88c..6f94a7e 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -2,15 +2,15 @@ import type { ServerInit } from "@sveltejs/kit"; import { sequence } from "@sveltejs/kit/hooks"; import schedule from "node-schedule"; import { cleanupExpiredUserClientChallenges } from "$lib/server/db/client"; -import { migrateDB } from "$lib/server/db/drizzle"; +import { migrateDB } from "$lib/server/db/kysely"; import { cleanupExpiredSessions, cleanupExpiredSessionUpgradeChallenges, } from "$lib/server/db/session"; import { authenticate, setAgentInfo } from "$lib/server/middlewares"; -export const init: ServerInit = () => { - migrateDB(); +export const init: ServerInit = async () => { + await migrateDB(); schedule.scheduleJob("0 * * * *", () => { cleanupExpiredUserClientChallenges(); diff --git a/src/lib/components/BottomSheet.svelte b/src/lib/components/BottomSheet.svelte deleted file mode 100644 index 3d3fbd6..0000000 --- a/src/lib/components/BottomSheet.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - -{#if isOpen} - - -
-
-
- -
e.stopPropagation()} - class="flex max-h-[70vh] min-h-[30vh] rounded-t-2xl bg-white px-4" - transition:fly={{ y: 100, duration: 200 }} - > - {@render children?.()} -
-
-
-
-{/if} diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte deleted file mode 100644 index 5da23b2..0000000 --- a/src/lib/components/Modal.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - -{#if isOpen} - - -
- -
-
e.stopPropagation()} class="rounded-2xl bg-white p-4"> - {@render children?.()} -
-
-
-
-{/if} diff --git a/src/lib/components/TopBar.svelte b/src/lib/components/TopBar.svelte deleted file mode 100644 index 6691feb..0000000 --- a/src/lib/components/TopBar.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - -
- - {#if title} -

{title}

- {/if} -
- {#if children} - {@render children?.()} - {/if} -
-
diff --git a/src/lib/components/atoms/BottomSheet.svelte b/src/lib/components/atoms/BottomSheet.svelte new file mode 100644 index 0000000..6378322 --- /dev/null +++ b/src/lib/components/atoms/BottomSheet.svelte @@ -0,0 +1,40 @@ + + +{#if isOpen} + + +
(isOpen = false))} + class="fixed inset-0 z-10 flex items-end justify-center" + > +
+ +
e.stopPropagation()} + class="flex max-h-[70vh] min-h-[30vh] flex-col rounded-t-2xl bg-white" + transition:fly|global={{ y: 100, duration: 200 }} + > +
+ {@render children()} +
+
+
+
+{/if} diff --git a/src/lib/components/atoms/Modal.svelte b/src/lib/components/atoms/Modal.svelte new file mode 100644 index 0000000..679e756 --- /dev/null +++ b/src/lib/components/atoms/Modal.svelte @@ -0,0 +1,31 @@ + + +{#if isOpen} + + +
(isOpen = false))} + class="fixed inset-0 z-10 bg-black bg-opacity-50" + transition:fade|global={{ duration: 100 }} + > + +
e.stopPropagation()} class={["rounded-2xl bg-white p-4", className]}> + {@render children()} +
+
+
+{/if} diff --git a/src/lib/components/atoms/buttons/ActionEntryButton.svelte b/src/lib/components/atoms/buttons/ActionEntryButton.svelte new file mode 100644 index 0000000..c29fd7f --- /dev/null +++ b/src/lib/components/atoms/buttons/ActionEntryButton.svelte @@ -0,0 +1,59 @@ + + + + +
setTimeout(onclick, 100))} + class={["rounded-xl", className]} +> +
+
+ {@render children()} +
+ {#if ActionButtonIcon} + + {/if} +
+
+ + diff --git a/src/lib/components/atoms/buttons/Button.svelte b/src/lib/components/atoms/buttons/Button.svelte new file mode 100644 index 0000000..7b5fb1e --- /dev/null +++ b/src/lib/components/atoms/buttons/Button.svelte @@ -0,0 +1,38 @@ + + + diff --git a/src/lib/components/atoms/buttons/EntryButton.svelte b/src/lib/components/atoms/buttons/EntryButton.svelte new file mode 100644 index 0000000..dc2534e --- /dev/null +++ b/src/lib/components/atoms/buttons/EntryButton.svelte @@ -0,0 +1,26 @@ + + + diff --git a/src/lib/components/atoms/buttons/FloatingButton.svelte b/src/lib/components/atoms/buttons/FloatingButton.svelte new file mode 100644 index 0000000..10dd4f4 --- /dev/null +++ b/src/lib/components/atoms/buttons/FloatingButton.svelte @@ -0,0 +1,27 @@ + + +
+ + + +
diff --git a/src/lib/components/buttons/TextButton.svelte b/src/lib/components/atoms/buttons/TextButton.svelte similarity index 66% rename from src/lib/components/buttons/TextButton.svelte rename to src/lib/components/atoms/buttons/TextButton.svelte index c7688a5..84603e2 100644 --- a/src/lib/components/buttons/TextButton.svelte +++ b/src/lib/components/atoms/buttons/TextButton.svelte @@ -10,14 +10,10 @@ diff --git a/src/lib/components/buttons/index.ts b/src/lib/components/atoms/buttons/index.ts similarity index 76% rename from src/lib/components/buttons/index.ts rename to src/lib/components/atoms/buttons/index.ts index 1765400..fc5022f 100644 --- a/src/lib/components/buttons/index.ts +++ b/src/lib/components/atoms/buttons/index.ts @@ -1,3 +1,4 @@ +export { default as ActionEntryButton } from "./ActionEntryButton.svelte"; export { default as Button } from "./Button.svelte"; export { default as EntryButton } from "./EntryButton.svelte"; export { default as FloatingButton } from "./FloatingButton.svelte"; diff --git a/src/lib/components/atoms/divs/AdaptiveDiv.svelte b/src/lib/components/atoms/divs/AdaptiveDiv.svelte new file mode 100644 index 0000000..23fa00b --- /dev/null +++ b/src/lib/components/atoms/divs/AdaptiveDiv.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children()} +
diff --git a/src/lib/components/atoms/divs/BottomDiv.svelte b/src/lib/components/atoms/divs/BottomDiv.svelte new file mode 100644 index 0000000..e28765b --- /dev/null +++ b/src/lib/components/atoms/divs/BottomDiv.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children()} +
diff --git a/src/lib/components/atoms/divs/FullscreenDiv.svelte b/src/lib/components/atoms/divs/FullscreenDiv.svelte new file mode 100644 index 0000000..c90e02c --- /dev/null +++ b/src/lib/components/atoms/divs/FullscreenDiv.svelte @@ -0,0 +1,7 @@ + + +
+ {@render children()} +
diff --git a/src/lib/components/divs/index.ts b/src/lib/components/atoms/divs/index.ts similarity index 64% rename from src/lib/components/divs/index.ts rename to src/lib/components/atoms/divs/index.ts index 7bd6a34..561bbde 100644 --- a/src/lib/components/divs/index.ts +++ b/src/lib/components/atoms/divs/index.ts @@ -1,3 +1,3 @@ export { default as AdaptiveDiv } from "./AdaptiveDiv.svelte"; export { default as BottomDiv } from "./BottomDiv.svelte"; -export { default as TitleDiv } from "./TitleDiv.svelte"; +export { default as FullscreenDiv } from "./FullscreenDiv.svelte"; diff --git a/src/lib/components/index.ts b/src/lib/components/atoms/index.ts similarity index 59% rename from src/lib/components/index.ts rename to src/lib/components/atoms/index.ts index 536f01f..14b0849 100644 --- a/src/lib/components/index.ts +++ b/src/lib/components/atoms/index.ts @@ -1,3 +1,5 @@ export { default as BottomSheet } from "./BottomSheet.svelte"; +export * from "./buttons"; +export * from "./divs"; +export * from "./inputs"; export { default as Modal } from "./Modal.svelte"; -export { default as TopBar } from "./TopBar.svelte"; diff --git a/src/lib/components/atoms/inputs/CheckBox.svelte b/src/lib/components/atoms/inputs/CheckBox.svelte new file mode 100644 index 0000000..fe2899b --- /dev/null +++ b/src/lib/components/atoms/inputs/CheckBox.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/atoms/inputs/TextInput.svelte b/src/lib/components/atoms/inputs/TextInput.svelte new file mode 100644 index 0000000..613c85c --- /dev/null +++ b/src/lib/components/atoms/inputs/TextInput.svelte @@ -0,0 +1,40 @@ + + +
+
+ + + +
+
+ + diff --git a/src/lib/components/inputs/index.ts b/src/lib/components/atoms/inputs/index.ts similarity index 50% rename from src/lib/components/inputs/index.ts rename to src/lib/components/atoms/inputs/index.ts index c2c534d..47cb929 100644 --- a/src/lib/components/inputs/index.ts +++ b/src/lib/components/atoms/inputs/index.ts @@ -1 +1,2 @@ +export { default as CheckBox } from "./CheckBox.svelte"; export { default as TextInput } from "./TextInput.svelte"; diff --git a/src/lib/components/buttons/Button.svelte b/src/lib/components/buttons/Button.svelte deleted file mode 100644 index 65f80ab..0000000 --- a/src/lib/components/buttons/Button.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - - diff --git a/src/lib/components/buttons/EntryButton.svelte b/src/lib/components/buttons/EntryButton.svelte deleted file mode 100644 index ed455aa..0000000 --- a/src/lib/components/buttons/EntryButton.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/src/lib/components/buttons/FloatingButton.svelte b/src/lib/components/buttons/FloatingButton.svelte deleted file mode 100644 index 51870c7..0000000 --- a/src/lib/components/buttons/FloatingButton.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -
-
- -
-
- -
-
-
-
-
diff --git a/src/lib/components/divs/AdaptiveDiv.svelte b/src/lib/components/divs/AdaptiveDiv.svelte deleted file mode 100644 index ee845cc..0000000 --- a/src/lib/components/divs/AdaptiveDiv.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/src/lib/components/divs/BottomDiv.svelte b/src/lib/components/divs/BottomDiv.svelte deleted file mode 100644 index 8a10a99..0000000 --- a/src/lib/components/divs/BottomDiv.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -
- {@render children?.()} -
diff --git a/src/lib/components/divs/TitleDiv.svelte b/src/lib/components/divs/TitleDiv.svelte deleted file mode 100644 index d6c4414..0000000 --- a/src/lib/components/divs/TitleDiv.svelte +++ /dev/null @@ -1,21 +0,0 @@ - - -
-
- {#if Icon} - - {/if} -
- {@render children?.()} -
diff --git a/src/lib/components/inputs/TextInput.svelte b/src/lib/components/inputs/TextInput.svelte deleted file mode 100644 index 61f42ad..0000000 --- a/src/lib/components/inputs/TextInput.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - -
- - - -
- - diff --git a/src/lib/components/molecules/ActionModal.svelte b/src/lib/components/molecules/ActionModal.svelte new file mode 100644 index 0000000..a351403 --- /dev/null +++ b/src/lib/components/molecules/ActionModal.svelte @@ -0,0 +1,50 @@ + + + + + +
+

{title}

+ {@render children()} +
+
+ + +
+
diff --git a/src/lib/components/molecules/Categories/Categories.svelte b/src/lib/components/molecules/Categories/Categories.svelte new file mode 100644 index 0000000..a11313e --- /dev/null +++ b/src/lib/components/molecules/Categories/Categories.svelte @@ -0,0 +1,63 @@ + + +{#if categoriesWithName.length > 0} +
+ {#each categoriesWithName as { info }} + + {/each} +
+{/if} diff --git a/src/lib/components/molecules/Categories/Category.svelte b/src/lib/components/molecules/Categories/Category.svelte new file mode 100644 index 0000000..aab227c --- /dev/null +++ b/src/lib/components/molecules/Categories/Category.svelte @@ -0,0 +1,43 @@ + + +{#if $info} + + + +{/if} diff --git a/src/lib/components/molecules/Categories/index.ts b/src/lib/components/molecules/Categories/index.ts new file mode 100644 index 0000000..d8a70c2 --- /dev/null +++ b/src/lib/components/molecules/Categories/index.ts @@ -0,0 +1,2 @@ +export { default } from "./Categories.svelte"; +export * from "./service"; diff --git a/src/lib/components/molecules/Categories/service.ts b/src/lib/components/molecules/Categories/service.ts new file mode 100644 index 0000000..08c41db --- /dev/null +++ b/src/lib/components/molecules/Categories/service.ts @@ -0,0 +1,6 @@ +export interface SelectedCategory { + id: number; + dataKey: CryptoKey; + dataKeyVersion: Date; + name: string; +} diff --git a/src/lib/components/molecules/IconEntryButton.svelte b/src/lib/components/molecules/IconEntryButton.svelte new file mode 100644 index 0000000..4b6ad47 --- /dev/null +++ b/src/lib/components/molecules/IconEntryButton.svelte @@ -0,0 +1,30 @@ + + + + + {@render children()} + + diff --git a/src/lib/components/molecules/SubCategories.svelte b/src/lib/components/molecules/SubCategories.svelte new file mode 100644 index 0000000..9c84a89 --- /dev/null +++ b/src/lib/components/molecules/SubCategories.svelte @@ -0,0 +1,67 @@ + + +
+ {#snippet subCategoryCreate()} + + 카테고리 추가하기 + + {/snippet} + + {#if subCategoryCreatePosition === "top"} + {@render subCategoryCreate()} + {/if} + {#key info} + + {/key} + {#if subCategoryCreatePosition === "bottom"} + {@render subCategoryCreate()} + {/if} +
diff --git a/src/lib/components/molecules/TitledDiv.svelte b/src/lib/components/molecules/TitledDiv.svelte new file mode 100644 index 0000000..e7ddc25 --- /dev/null +++ b/src/lib/components/molecules/TitledDiv.svelte @@ -0,0 +1,43 @@ + + +
+
+ + {@render title()} + + {#if description} +

+ {@render description()} +

+ {/if} +
+ {#if children} +
+ {@render children()} +
+ {/if} +
diff --git a/src/lib/components/molecules/TopBar.svelte b/src/lib/components/molecules/TopBar.svelte new file mode 100644 index 0000000..cf7f8b8 --- /dev/null +++ b/src/lib/components/molecules/TopBar.svelte @@ -0,0 +1,37 @@ + + +
+ + {#if title} +

{title}

+ {/if} +
+ {#if children} + {@render children()} + {/if} +
+
diff --git a/src/lib/components/molecules/index.ts b/src/lib/components/molecules/index.ts new file mode 100644 index 0000000..8edc84a --- /dev/null +++ b/src/lib/components/molecules/index.ts @@ -0,0 +1,9 @@ +export * from "./ActionModal.svelte"; +export { default as ActionModal } from "./ActionModal.svelte"; +export * from "./Categories"; +export { default as Categories } from "./Categories"; +export { default as IconEntryButton } from "./IconEntryButton.svelte"; +export * from "./labels"; +export { default as SubCategories } from "./SubCategories.svelte"; +export { default as TitledDiv } from "./TitledDiv.svelte"; +export { default as TopBar } from "./TopBar.svelte"; diff --git a/src/lib/components/molecules/labels/CategoryLabel.svelte b/src/lib/components/molecules/labels/CategoryLabel.svelte new file mode 100644 index 0000000..cbafa54 --- /dev/null +++ b/src/lib/components/molecules/labels/CategoryLabel.svelte @@ -0,0 +1,28 @@ + + +{#snippet subtextSnippet()} + {subtext} +{/snippet} + + + {name} + diff --git a/src/lib/components/molecules/labels/DirectoryEntryLabel.svelte b/src/lib/components/molecules/labels/DirectoryEntryLabel.svelte new file mode 100644 index 0000000..5d4fb81 --- /dev/null +++ b/src/lib/components/molecules/labels/DirectoryEntryLabel.svelte @@ -0,0 +1,31 @@ + + +{#snippet subtextSnippet()} + {subtext} +{/snippet} + + + {name} + diff --git a/src/lib/components/molecules/labels/IconLabel.svelte b/src/lib/components/molecules/labels/IconLabel.svelte new file mode 100644 index 0000000..a173b68 --- /dev/null +++ b/src/lib/components/molecules/labels/IconLabel.svelte @@ -0,0 +1,38 @@ + + +
+
+ +
+
+

+ {@render children()} +

+ {#if subtext} +

+ {@render subtext()} +

+ {/if} +
+
diff --git a/src/lib/components/molecules/labels/TitleLabel.svelte b/src/lib/components/molecules/labels/TitleLabel.svelte new file mode 100644 index 0000000..ffb51c3 --- /dev/null +++ b/src/lib/components/molecules/labels/TitleLabel.svelte @@ -0,0 +1,24 @@ + + +
+
+ {#if Icon} + + {/if} +
+

+ {@render children()} +

+
diff --git a/src/lib/components/molecules/labels/index.ts b/src/lib/components/molecules/labels/index.ts new file mode 100644 index 0000000..af8f40b --- /dev/null +++ b/src/lib/components/molecules/labels/index.ts @@ -0,0 +1,4 @@ +export { default as CategoryLabel } from "./CategoryLabel.svelte"; +export { default as DirectoryEntryLabel } from "./DirectoryEntryLabel.svelte"; +export { default as IconLabel } from "./IconLabel.svelte"; +export { default as TitleLabel } from "./TitleLabel.svelte"; diff --git a/src/lib/components/organisms/Category/Category.svelte b/src/lib/components/organisms/Category/Category.svelte new file mode 100644 index 0000000..295bf99 --- /dev/null +++ b/src/lib/components/organisms/Category/Category.svelte @@ -0,0 +1,107 @@ + + +
+
+ {#if info.id !== "root"} +

하위 카테고리

+ {/if} + +
+ {#if info.id !== "root"} +
+
+

파일

+ +

하위 카테고리의 파일

+
+
+
+ {#key info} + {#each files as { info, isRecursive }} + + {:else} +

이 카테고리에 추가된 파일이 없어요.

+ {/each} + {/key} +
+
+ {/if} +
diff --git a/src/lib/components/organisms/Category/File.svelte b/src/lib/components/organisms/Category/File.svelte new file mode 100644 index 0000000..5263b95 --- /dev/null +++ b/src/lib/components/organisms/Category/File.svelte @@ -0,0 +1,42 @@ + + +{#if $info} + + + +{/if} diff --git a/src/lib/components/organisms/Category/index.ts b/src/lib/components/organisms/Category/index.ts new file mode 100644 index 0000000..51e0a58 --- /dev/null +++ b/src/lib/components/organisms/Category/index.ts @@ -0,0 +1,2 @@ +export { default } from "./Category.svelte"; +export * from "./service"; diff --git a/src/lib/components/organisms/Category/service.ts b/src/lib/components/organisms/Category/service.ts new file mode 100644 index 0000000..1d587b5 --- /dev/null +++ b/src/lib/components/organisms/Category/service.ts @@ -0,0 +1,6 @@ +export interface SelectedFile { + id: number; + dataKey: CryptoKey; + dataKeyVersion: Date; + name: string; +} diff --git a/src/lib/components/organisms/index.ts b/src/lib/components/organisms/index.ts new file mode 100644 index 0000000..64f9861 --- /dev/null +++ b/src/lib/components/organisms/index.ts @@ -0,0 +1,3 @@ +export * from "./Category"; +export { default as Category } from "./Category"; +export * from "./modals"; diff --git a/src/lib/components/organisms/modals/CategoryCreateModal.svelte b/src/lib/components/organisms/modals/CategoryCreateModal.svelte new file mode 100644 index 0000000..279c1a1 --- /dev/null +++ b/src/lib/components/organisms/modals/CategoryCreateModal.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/lib/components/organisms/modals/RenameModal.svelte b/src/lib/components/organisms/modals/RenameModal.svelte new file mode 100644 index 0000000..66ccfe0 --- /dev/null +++ b/src/lib/components/organisms/modals/RenameModal.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/organisms/modals/TextInputModal.svelte b/src/lib/components/organisms/modals/TextInputModal.svelte new file mode 100644 index 0000000..e1fd2c4 --- /dev/null +++ b/src/lib/components/organisms/modals/TextInputModal.svelte @@ -0,0 +1,42 @@ + + + onSubmitClick(value)} +> + + diff --git a/src/lib/components/organisms/modals/index.ts b/src/lib/components/organisms/modals/index.ts new file mode 100644 index 0000000..2fa9422 --- /dev/null +++ b/src/lib/components/organisms/modals/index.ts @@ -0,0 +1,3 @@ +export { default as CategoryCreateModal } from "./CategoryCreateModal.svelte"; +export { default as RenameModal } from "./RenameModal.svelte"; +export { default as TextInputModal } from "./TextInputModal.svelte"; diff --git a/src/lib/indexedDB/filesystem.ts b/src/lib/indexedDB/filesystem.ts index 5c9fc4d..293c16d 100644 --- a/src/lib/indexedDB/filesystem.ts +++ b/src/lib/indexedDB/filesystem.ts @@ -15,16 +15,28 @@ interface FileInfo { contentType: string; createdAt?: Date; lastModifiedAt: Date; + categoryIds: number[]; +} + +export type CategoryId = "root" | number; + +interface CategoryInfo { + id: number; + parentId: CategoryId; + name: string; + files: { id: number; isRecursive: boolean }[]; } const filesystem = new Dexie("filesystem") as Dexie & { directory: EntityTable; file: EntityTable; + category: EntityTable; }; -filesystem.version(1).stores({ +filesystem.version(2).stores({ directory: "id, parentId", file: "id, parentId", + category: "id, parentId", }); export const getDirectoryInfos = async (parentId: DirectoryId) => { @@ -59,13 +71,29 @@ export const deleteFileInfo = async (id: number) => { await filesystem.file.delete(id); }; +export const getCategoryInfos = async (parentId: CategoryId) => { + return await filesystem.category.where({ parentId }).toArray(); +}; + +export const getCategoryInfo = async (id: number) => { + return await filesystem.category.get(id); +}; + +export const storeCategoryInfo = async (categoryInfo: CategoryInfo) => { + await filesystem.category.put(categoryInfo); +}; + +export const deleteCategoryInfo = async (id: number) => { + await filesystem.category.delete(id); +}; + export const cleanupDanglingInfos = async () => { const validDirectoryIds: number[] = []; const validFileIds: number[] = []; - const queue: DirectoryId[] = ["root"]; + const directoryQueue: DirectoryId[] = ["root"]; while (true) { - const directoryId = queue.shift(); + const directoryId = directoryQueue.shift(); if (!directoryId) break; const [subDirectories, files] = await Promise.all([ @@ -74,13 +102,28 @@ export const cleanupDanglingInfos = async () => { ]); subDirectories.forEach(({ id }) => { validDirectoryIds.push(id); - queue.push(id); + directoryQueue.push(id); }); files.forEach(({ id }) => validFileIds.push(id)); } + const validCategoryIds: number[] = []; + const categoryQueue: CategoryId[] = ["root"]; + + while (true) { + const categoryId = categoryQueue.shift(); + if (!categoryId) break; + + const subCategories = await filesystem.category.where({ parentId: categoryId }).toArray(); + subCategories.forEach(({ id }) => { + validCategoryIds.push(id); + categoryQueue.push(id); + }); + } + await Promise.all([ filesystem.directory.where("id").noneOf(validDirectoryIds).delete(), filesystem.file.where("id").noneOf(validFileIds).delete(), + filesystem.category.where("id").noneOf(validCategoryIds).delete(), ]); }; diff --git a/src/lib/modules/file/upload.ts b/src/lib/modules/file/upload.ts index 2518c7f..71a38fb 100644 --- a/src/lib/modules/file/upload.ts +++ b/src/lib/modules/file/upload.ts @@ -8,12 +8,14 @@ import { wrapDataKey, encryptData, encryptString, + digestMessage, signMessageHmac, } from "$lib/modules/crypto"; import type { DuplicateFileScanRequest, DuplicateFileScanResponse, FileUploadRequest, + FileUploadResponse, } from "$lib/server/schemas"; import { fileUploadStatusStore, @@ -97,6 +99,8 @@ const encryptFile = limitFunction( const dataKeyWrapped = await wrapDataKey(dataKey, masterKey.key); const fileEncrypted = await encryptData(fileBuffer, dataKey); + const fileEncryptedHash = encodeToBase64(await digestMessage(fileEncrypted.ciphertext)); + const nameEncrypted = await encryptString(file.name, dataKey); const createdAtEncrypted = createdAt && (await encryptString(createdAt.getTime().toString(), dataKey)); @@ -110,8 +114,9 @@ const encryptFile = limitFunction( return { dataKeyWrapped, dataKeyVersion, - fileEncrypted, fileType, + fileEncrypted, + fileEncryptedHash, nameEncrypted, createdAtEncrypted, lastModifiedAtEncrypted, @@ -127,7 +132,7 @@ const requestFileUpload = limitFunction( return value; }); - await axios.post("/api/file/upload", form, { + const res = await axios.post("/api/file/upload", form, { onUploadProgress: ({ progress, rate, estimated }) => { status.update((value) => { value.progress = progress; @@ -137,11 +142,14 @@ const requestFileUpload = limitFunction( }); }, }); + const { file }: FileUploadResponse = res.data; status.update((value) => { value.status = "uploaded"; return value; }); + + return { fileId: file }; }, { concurrency: 1 }, ); @@ -152,7 +160,7 @@ export const uploadFile = async ( hmacSecret: HmacSecret, masterKey: MasterKey, onDuplicate: () => Promise, -) => { +): Promise<{ fileId: number; fileBuffer: ArrayBuffer } | undefined> => { const status = writable({ name: file.name, parentId, @@ -178,14 +186,15 @@ export const uploadFile = async ( value = value.filter((v) => v !== status); return value; }); - return false; + return undefined; } const { dataKeyWrapped, dataKeyVersion, - fileEncrypted, fileType, + fileEncrypted, + fileEncryptedHash, nameEncrypted, createdAtEncrypted, lastModifiedAtEncrypted, @@ -212,9 +221,10 @@ export const uploadFile = async ( } as FileUploadRequest), ); form.set("content", new Blob([fileEncrypted.ciphertext])); + form.set("checksum", fileEncryptedHash); - await requestFileUpload(status, form); - return true; + const { fileId } = await requestFileUpload(status, form); + return { fileId, fileBuffer }; } catch (e) { status.update((value) => { value.status = "error"; diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts index 7313cb5..eaf1d1a 100644 --- a/src/lib/modules/filesystem.ts +++ b/src/lib/modules/filesystem.ts @@ -9,10 +9,20 @@ import { getFileInfo as getFileInfoFromIndexedDB, storeFileInfo, deleteFileInfo, + getCategoryInfos as getCategoryInfosFromIndexedDB, + getCategoryInfo as getCategoryInfoFromIndexedDB, + storeCategoryInfo, + deleteCategoryInfo, type DirectoryId, + type CategoryId, } from "$lib/indexedDB"; import { unwrapDataKey, decryptString } from "$lib/modules/crypto"; -import type { DirectoryInfoResponse, FileInfoResponse } from "$lib/server/schemas"; +import type { + CategoryInfoResponse, + CategoryFileListResponse, + DirectoryInfoResponse, + FileInfoResponse, +} from "$lib/server/schemas"; export type DirectoryInfo = | { @@ -41,10 +51,30 @@ export interface FileInfo { name: string; createdAt?: Date; lastModifiedAt: Date; + categoryIds: number[]; } +export type CategoryInfo = + | { + id: "root"; + dataKey?: undefined; + dataKeyVersion?: undefined; + name?: undefined; + subCategoryIds: number[]; + files?: undefined; + } + | { + id: number; + dataKey?: CryptoKey; + dataKeyVersion?: Date; + name: string; + subCategoryIds: number[]; + files: { id: number; isRecursive: boolean }[]; + }; + const directoryInfoStore = new Map>(); const fileInfoStore = new Map>(); +const categoryInfoStore = new Map>(); const fetchDirectoryInfoFromIndexedDB = async ( id: DirectoryId, @@ -77,6 +107,7 @@ const fetchDirectoryInfoFromServer = async ( if (res.status === 404) { info.set(null); await deleteDirectoryInfo(id as number); + return; } else if (!res.ok) { throw new Error("Failed to fetch directory information"); } @@ -123,7 +154,7 @@ export const getDirectoryInfo = (id: DirectoryId, masterKey: CryptoKey) => { directoryInfoStore.set(id, info); } - fetchDirectoryInfo(id, info, masterKey); + fetchDirectoryInfo(id, info, masterKey); // Intended return info; }; @@ -149,6 +180,7 @@ const fetchFileInfoFromServer = async ( if (res.status === 404) { info.set(null); await deleteFileInfo(id); + return; } else if (!res.ok) { throw new Error("Failed to fetch file information"); } @@ -176,6 +208,7 @@ const fetchFileInfoFromServer = async ( name, createdAt, lastModifiedAt, + categoryIds: metadata.categories, }); await storeFileInfo({ id, @@ -184,6 +217,7 @@ const fetchFileInfoFromServer = async ( contentType: metadata.contentType, createdAt, lastModifiedAt, + categoryIds: metadata.categories, }); }; @@ -201,6 +235,95 @@ export const getFileInfo = (fileId: number, masterKey: CryptoKey) => { fileInfoStore.set(fileId, info); } - fetchFileInfo(fileId, info, masterKey); + fetchFileInfo(fileId, info, masterKey); // Intended + return info; +}; + +const fetchCategoryInfoFromIndexedDB = async ( + id: CategoryId, + info: Writable, +) => { + if (get(info)) return; + + const [category, subCategories] = await Promise.all([ + id !== "root" ? getCategoryInfoFromIndexedDB(id) : undefined, + getCategoryInfosFromIndexedDB(id), + ]); + const subCategoryIds = subCategories.map(({ id }) => id); + + if (id === "root") { + info.set({ id, subCategoryIds }); + } else { + if (!category) return; + info.set({ id, name: category.name, subCategoryIds, files: category.files }); + } +}; + +const fetchCategoryInfoFromServer = async ( + id: CategoryId, + info: Writable, + masterKey: CryptoKey, +) => { + let res = await callGetApi(`/api/category/${id}`); + if (res.status === 404) { + info.set(null); + await deleteCategoryInfo(id as number); + return; + } else if (!res.ok) { + throw new Error("Failed to fetch category information"); + } + + const { metadata, subCategories }: CategoryInfoResponse = await res.json(); + + if (id === "root") { + info.set({ id, subCategoryIds: subCategories }); + } else { + const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey); + const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey); + + res = await callGetApi(`/api/category/${id}/file/list?recurse=true`); + if (!res.ok) { + throw new Error("Failed to fetch category files"); + } + + const { files }: CategoryFileListResponse = await res.json(); + const filesMapped = files.map(({ file, isRecursive }) => ({ id: file, isRecursive })); + + info.set({ + id, + dataKey, + dataKeyVersion: new Date(metadata!.dekVersion), + name, + subCategoryIds: subCategories, + files: filesMapped, + }); + await storeCategoryInfo({ + id, + parentId: metadata!.parent, + name, + files: filesMapped, + }); + } +}; + +const fetchCategoryInfo = async ( + id: CategoryId, + info: Writable, + masterKey: CryptoKey, +) => { + await fetchCategoryInfoFromIndexedDB(id, info); + await fetchCategoryInfoFromServer(id, info, masterKey); +}; + +export const getCategoryInfo = (categoryId: CategoryId, masterKey: CryptoKey) => { + // TODO: MEK rotation + + let info = categoryInfoStore.get(categoryId); + if (!info) { + info = writable(null); + categoryInfoStore.set(categoryId, info); + } + + fetchCategoryInfo(categoryId, info, masterKey); // Intended return info; }; diff --git a/src/lib/modules/util.ts b/src/lib/modules/util.ts index 67e1b3b..3fff89d 100644 --- a/src/lib/modules/util.ts +++ b/src/lib/modules/util.ts @@ -27,3 +27,37 @@ export const formatNetworkSpeed = (speed: number) => { if (speed < 1000 * 1000 * 1000) return `${(speed / 1000 / 1000).toFixed(1)} Mbps`; return `${(speed / 1000 / 1000 / 1000).toFixed(1)} Gbps`; }; + +export const truncateString = (str: string, maxLength = 20) => { + if (str.length <= maxLength) return str; + return `${str.slice(0, maxLength)}...`; +}; + +export enum SortBy { + NAME_ASC, + NAME_DESC, +} + +type SortFunc = (a?: string, b?: string) => number; + +const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); + +const sortByNameAsc: SortFunc = (a, b) => { + if (a && b) return collator.compare(a, b); + if (a) return -1; + if (b) return 1; + return 0; +}; + +const sortByNameDesc: SortFunc = (a, b) => -sortByNameAsc(a, b); + +export const sortEntries = (entries: T[], sortBy: SortBy) => { + let sortFunc: SortFunc; + if (sortBy === SortBy.NAME_ASC) { + sortFunc = sortByNameAsc; + } else { + sortFunc = sortByNameDesc; + } + + entries.sort((a, b) => sortFunc(a.name, b.name)); +}; diff --git a/src/lib/server/db/category.ts b/src/lib/server/db/category.ts new file mode 100644 index 0000000..f5c22ff --- /dev/null +++ b/src/lib/server/db/category.ts @@ -0,0 +1,147 @@ +import { IntegrityError } from "./error"; +import db from "./kysely"; +import type { Ciphertext } from "./schema"; + +export type CategoryId = "root" | number; + +interface Category { + id: number; + parentId: CategoryId; + userId: number; + mekVersion: number; + encDek: string; + dekVersion: Date; + encName: Ciphertext; +} + +export type NewCategory = Omit; + +export const registerCategory = async (params: NewCategory) => { + await db.transaction().execute(async (trx) => { + const mek = await trx + .selectFrom("master_encryption_key") + .select("version") + .where("user_id", "=", params.userId) + .where("state", "=", "active") + .limit(1) + .forUpdate() + .executeTakeFirst(); + if (mek?.version !== params.mekVersion) { + throw new IntegrityError("Inactive MEK version"); + } + + const { categoryId } = await trx + .insertInto("category") + .values({ + parent_id: params.parentId !== "root" ? params.parentId : null, + user_id: params.userId, + master_encryption_key_version: params.mekVersion, + encrypted_data_encryption_key: params.encDek, + data_encryption_key_version: params.dekVersion, + encrypted_name: params.encName, + }) + .returning("id as categoryId") + .executeTakeFirstOrThrow(); + await trx + .insertInto("category_log") + .values({ + category_id: categoryId, + timestamp: new Date(), + action: "create", + new_name: params.encName, + }) + .execute(); + }); +}; + +export const getAllCategoriesByParent = async (userId: number, parentId: CategoryId) => { + let query = db.selectFrom("category").selectAll().where("user_id", "=", userId); + query = + parentId === "root" + ? query.where("parent_id", "is", null) + : query.where("parent_id", "=", parentId); + const categories = await query.execute(); + return categories.map( + (category) => + ({ + id: category.id, + parentId: category.parent_id ?? "root", + userId: category.user_id, + mekVersion: category.master_encryption_key_version, + encDek: category.encrypted_data_encryption_key, + dekVersion: category.data_encryption_key_version, + encName: category.encrypted_name, + }) satisfies Category, + ); +}; + +export const getCategory = async (userId: number, categoryId: number) => { + const category = await db + .selectFrom("category") + .selectAll() + .where("id", "=", categoryId) + .where("user_id", "=", userId) + .limit(1) + .executeTakeFirst(); + return category + ? ({ + id: category.id, + parentId: category.parent_id ?? "root", + userId: category.user_id, + mekVersion: category.master_encryption_key_version, + encDek: category.encrypted_data_encryption_key, + dekVersion: category.data_encryption_key_version, + encName: category.encrypted_name, + } satisfies Category) + : null; +}; + +export const setCategoryEncName = async ( + userId: number, + categoryId: number, + dekVersion: Date, + encName: Ciphertext, +) => { + await db.transaction().execute(async (trx) => { + const category = await trx + .selectFrom("category") + .select("data_encryption_key_version") + .where("id", "=", categoryId) + .where("user_id", "=", userId) + .limit(1) + .forUpdate() + .executeTakeFirst(); + if (!category) { + throw new IntegrityError("Category not found"); + } else if (category.data_encryption_key_version.getTime() !== dekVersion.getTime()) { + throw new IntegrityError("Invalid DEK version"); + } + + await trx + .updateTable("category") + .set({ encrypted_name: encName }) + .where("id", "=", categoryId) + .where("user_id", "=", userId) + .execute(); + await trx + .insertInto("category_log") + .values({ + category_id: categoryId, + timestamp: new Date(), + action: "rename", + new_name: encName, + }) + .execute(); + }); +}; + +export const unregisterCategory = async (userId: number, categoryId: number) => { + const res = await db + .deleteFrom("category") + .where("id", "=", categoryId) + .where("user_id", "=", userId) + .executeTakeFirst(); + if (res.numDeletedRows === 0n) { + throw new IntegrityError("Category not found"); + } +}; diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index 37a1054..cb873c7 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -1,53 +1,97 @@ -import { SqliteError } from "better-sqlite3"; -import { and, or, eq, gt, lte } from "drizzle-orm"; -import db from "./drizzle"; +import pg from "pg"; import { IntegrityError } from "./error"; -import { client, userClient, userClientChallenge } from "./schema"; +import db from "./kysely"; +import type { UserClientState } from "./schema"; + +interface Client { + id: number; + encPubKey: string; + sigPubKey: string; +} + +interface UserClient { + userId: number; + clientId: number; + state: UserClientState; +} + +interface UserClientWithDetails extends UserClient { + encPubKey: string; + sigPubKey: string; +} export const createClient = async (encPubKey: string, sigPubKey: string, userId: number) => { - return await db.transaction( - async (tx) => { - const clients = await tx - .select({ id: client.id }) - .from(client) - .where(or(eq(client.encPubKey, sigPubKey), eq(client.sigPubKey, encPubKey))) - .limit(1); - if (clients.length !== 0) { + return await db + .transaction() + .setIsolationLevel("serializable") + .execute(async (trx) => { + const client = await trx + .selectFrom("client") + .where((eb) => + eb.or([ + eb("encryption_public_key", "=", encPubKey), + eb("encryption_public_key", "=", sigPubKey), + eb("signature_public_key", "=", encPubKey), + eb("signature_public_key", "=", sigPubKey), + ]), + ) + .limit(1) + .executeTakeFirst(); + if (client) { throw new IntegrityError("Public key(s) already registered"); } - const newClients = await tx - .insert(client) - .values({ encPubKey, sigPubKey }) - .returning({ id: client.id }); - const { id: clientId } = newClients[0]!; - await tx.insert(userClient).values({ userId, clientId }); - - return clientId; - }, - { behavior: "exclusive" }, - ); + const { clientId } = await trx + .insertInto("client") + .values({ encryption_public_key: encPubKey, signature_public_key: sigPubKey }) + .returning("id as clientId") + .executeTakeFirstOrThrow(); + await trx + .insertInto("user_client") + .values({ user_id: userId, client_id: clientId }) + .execute(); + return { id: clientId }; + }); }; export const getClient = async (clientId: number) => { - const clients = await db.select().from(client).where(eq(client.id, clientId)).limit(1); - return clients[0] ?? null; + const client = await db + .selectFrom("client") + .selectAll() + .where("id", "=", clientId) + .limit(1) + .executeTakeFirst(); + return client + ? ({ + id: client.id, + encPubKey: client.encryption_public_key, + sigPubKey: client.signature_public_key, + } satisfies Client) + : null; }; export const getClientByPubKeys = async (encPubKey: string, sigPubKey: string) => { - const clients = await db - .select() - .from(client) - .where(and(eq(client.encPubKey, encPubKey), eq(client.sigPubKey, sigPubKey))) - .limit(1); - return clients[0] ?? null; + const client = await db + .selectFrom("client") + .selectAll() + .where("encryption_public_key", "=", encPubKey) + .where("signature_public_key", "=", sigPubKey) + .limit(1) + .executeTakeFirst(); + return client + ? ({ + id: client.id, + encPubKey: client.encryption_public_key, + sigPubKey: client.signature_public_key, + } satisfies Client) + : null; }; export const createUserClient = async (userId: number, clientId: number) => { try { - await db.insert(userClient).values({ userId, clientId }); + await db.insertInto("user_client").values({ user_id: userId, client_id: clientId }).execute(); } catch (e) { - if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") { + if (e instanceof pg.DatabaseError && e.code === "23505") { throw new IntegrityError("User client already exists"); } throw e; @@ -55,52 +99,76 @@ export const createUserClient = async (userId: number, clientId: number) => { }; export const getAllUserClients = async (userId: number) => { - return await db.select().from(userClient).where(eq(userClient.userId, userId)); + const userClients = await db + .selectFrom("user_client") + .selectAll() + .where("user_id", "=", userId) + .execute(); + return userClients.map( + ({ user_id, client_id, state }) => + ({ + userId: user_id, + clientId: client_id, + state, + }) satisfies UserClient, + ); }; export const getUserClient = async (userId: number, clientId: number) => { - const userClients = await db - .select() - .from(userClient) - .where(and(eq(userClient.userId, userId), eq(userClient.clientId, clientId))) - .limit(1); - return userClients[0] ?? null; + const userClient = await db + .selectFrom("user_client") + .selectAll() + .where("user_id", "=", userId) + .where("client_id", "=", clientId) + .limit(1) + .executeTakeFirst(); + return userClient + ? ({ + userId: userClient.user_id, + clientId: userClient.client_id, + state: userClient.state, + } satisfies UserClient) + : null; }; export const getUserClientWithDetails = async (userId: number, clientId: number) => { - const userClients = await db - .select() - .from(userClient) - .innerJoin(client, eq(userClient.clientId, client.id)) - .where(and(eq(userClient.userId, userId), eq(userClient.clientId, clientId))) - .limit(1); - return userClients[0] ?? null; + const userClient = await db + .selectFrom("user_client") + .innerJoin("client", "user_client.client_id", "client.id") + .selectAll() + .where("user_id", "=", userId) + .where("client_id", "=", clientId) + .limit(1) + .executeTakeFirst(); + return userClient + ? ({ + userId: userClient.user_id, + clientId: userClient.client_id, + state: userClient.state, + encPubKey: userClient.encryption_public_key, + sigPubKey: userClient.signature_public_key, + } satisfies UserClientWithDetails) + : null; }; export const setUserClientStateToPending = async (userId: number, clientId: number) => { await db - .update(userClient) + .updateTable("user_client") .set({ state: "pending" }) - .where( - and( - eq(userClient.userId, userId), - eq(userClient.clientId, clientId), - eq(userClient.state, "challenging"), - ), - ); + .where("user_id", "=", userId) + .where("client_id", "=", clientId) + .where("state", "=", "challenging") + .execute(); }; export const setUserClientStateToActive = async (userId: number, clientId: number) => { await db - .update(userClient) + .updateTable("user_client") .set({ state: "active" }) - .where( - and( - eq(userClient.userId, userId), - eq(userClient.clientId, clientId), - eq(userClient.state, "pending"), - ), - ); + .where("user_id", "=", userId) + .where("client_id", "=", clientId) + .where("state", "=", "pending") + .execute(); }; export const registerUserClientChallenge = async ( @@ -110,30 +178,30 @@ export const registerUserClientChallenge = async ( allowedIp: string, expiresAt: Date, ) => { - await db.insert(userClientChallenge).values({ - userId, - clientId, - answer, - allowedIp, - expiresAt, - }); + await db + .insertInto("user_client_challenge") + .values({ + user_id: userId, + client_id: clientId, + answer, + allowed_ip: allowedIp, + expires_at: expiresAt, + }) + .execute(); }; export const consumeUserClientChallenge = async (userId: number, answer: string, ip: string) => { - const challenges = await db - .delete(userClientChallenge) - .where( - and( - eq(userClientChallenge.userId, userId), - eq(userClientChallenge.answer, answer), - eq(userClientChallenge.allowedIp, ip), - gt(userClientChallenge.expiresAt, new Date()), - ), - ) - .returning({ clientId: userClientChallenge.clientId }); - return challenges[0] ?? null; + const challenge = await db + .deleteFrom("user_client_challenge") + .where("user_id", "=", userId) + .where("answer", "=", answer) + .where("allowed_ip", "=", ip) + .where("expires_at", ">", new Date()) + .returning("client_id") + .executeTakeFirst(); + return challenge ? { clientId: challenge.client_id } : null; }; export const cleanupExpiredUserClientChallenges = async () => { - await db.delete(userClientChallenge).where(lte(userClientChallenge.expiresAt, new Date())); + await db.deleteFrom("user_client_challenge").where("expires_at", "<=", new Date()).execute(); }; diff --git a/src/lib/server/db/drizzle.ts b/src/lib/server/db/drizzle.ts deleted file mode 100644 index 589c91e..0000000 --- a/src/lib/server/db/drizzle.ts +++ /dev/null @@ -1,15 +0,0 @@ -import Database from "better-sqlite3"; -import { drizzle } from "drizzle-orm/better-sqlite3"; -import { migrate } from "drizzle-orm/better-sqlite3/migrator"; -import env from "$lib/server/loadenv"; - -const client = new Database(env.databaseUrl); -const db = drizzle(client); - -export const migrateDB = () => { - if (process.env.NODE_ENV === "production") { - migrate(db, { migrationsFolder: "./drizzle" }); - } -}; - -export default db; diff --git a/src/lib/server/db/error.ts b/src/lib/server/db/error.ts index 547cc6c..a145f14 100644 --- a/src/lib/server/db/error.ts +++ b/src/lib/server/db/error.ts @@ -1,4 +1,6 @@ type IntegrityErrorMessages = + // Category + | "Category not found" // Challenge | "Challenge already registered" // Client @@ -7,6 +9,8 @@ type IntegrityErrorMessages = // File | "Directory not found" | "File not found" + | "File not found in category" + | "File already added to category" | "Invalid DEK version" // HSK | "HSK already registered" diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index a42235b..20343b2 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -1,21 +1,25 @@ -import { and, eq, isNull } from "drizzle-orm"; -import db from "./drizzle"; +import { sql, type NotNull } from "kysely"; +import pg from "pg"; import { IntegrityError } from "./error"; -import { directory, directoryLog, file, fileLog, hsk, mek } from "./schema"; +import db from "./kysely"; +import type { Ciphertext } from "./schema"; -type DirectoryId = "root" | number; +export type DirectoryId = "root" | number; -export interface NewDirectoryParams { +interface Directory { + id: number; parentId: DirectoryId; userId: number; mekVersion: number; encDek: string; dekVersion: Date; - encName: string; - encNameIv: string; + encName: Ciphertext; } -export interface NewFileParams { +export type NewDirectory = Omit; + +interface File { + id: number; parentId: DirectoryId; userId: number; path: string; @@ -26,216 +30,306 @@ export interface NewFileParams { contentHmac: string | null; contentType: string; encContentIv: string; - encName: string; - encNameIv: string; - encCreatedAt: string | null; - encCreatedAtIv: string | null; - encLastModifiedAt: string; - encLastModifiedAtIv: string; + encContentHash: string; + encName: Ciphertext; + encCreatedAt: Ciphertext | null; + encLastModifiedAt: Ciphertext; } -export const registerDirectory = async (params: NewDirectoryParams) => { - await db.transaction( - async (tx) => { - const meks = await tx - .select({ version: mek.version }) - .from(mek) - .where(and(eq(mek.userId, params.userId), eq(mek.state, "active"))) - .limit(1); - if (meks[0]?.version !== params.mekVersion) { - throw new IntegrityError("Inactive MEK version"); - } +export type NewFile = Omit; - const newDirectories = await tx - .insert(directory) - .values({ - parentId: params.parentId === "root" ? null : params.parentId, - userId: params.userId, - mekVersion: params.mekVersion, - encDek: params.encDek, - dekVersion: params.dekVersion, - encName: { ciphertext: params.encName, iv: params.encNameIv }, - }) - .returning({ id: directory.id }); - const { id: directoryId } = newDirectories[0]!; - await tx.insert(directoryLog).values({ - directoryId, +export const registerDirectory = async (params: NewDirectory) => { + await db.transaction().execute(async (trx) => { + const mek = await trx + .selectFrom("master_encryption_key") + .select("version") + .where("user_id", "=", params.userId) + .where("state", "=", "active") + .limit(1) + .forUpdate() + .executeTakeFirst(); + if (mek?.version !== params.mekVersion) { + throw new IntegrityError("Inactive MEK version"); + } + + const { directoryId } = await trx + .insertInto("directory") + .values({ + parent_id: params.parentId !== "root" ? params.parentId : null, + user_id: params.userId, + master_encryption_key_version: params.mekVersion, + encrypted_data_encryption_key: params.encDek, + data_encryption_key_version: params.dekVersion, + encrypted_name: params.encName, + }) + .returning("id as directoryId") + .executeTakeFirstOrThrow(); + await trx + .insertInto("directory_log") + .values({ + directory_id: directoryId, timestamp: new Date(), action: "create", - newName: { ciphertext: params.encName, iv: params.encNameIv }, - }); - }, - { behavior: "exclusive" }, - ); + new_name: params.encName, + }) + .execute(); + }); }; export const getAllDirectoriesByParent = async (userId: number, parentId: DirectoryId) => { - return await db - .select() - .from(directory) - .where( - and( - eq(directory.userId, userId), - parentId === "root" ? isNull(directory.parentId) : eq(directory.parentId, parentId), - ), - ); + let query = db.selectFrom("directory").selectAll().where("user_id", "=", userId); + query = + parentId === "root" + ? query.where("parent_id", "is", null) + : query.where("parent_id", "=", parentId); + const directories = await query.execute(); + return directories.map( + (directory) => + ({ + id: directory.id, + parentId: directory.parent_id ?? "root", + userId: directory.user_id, + mekVersion: directory.master_encryption_key_version, + encDek: directory.encrypted_data_encryption_key, + dekVersion: directory.data_encryption_key_version, + encName: directory.encrypted_name, + }) satisfies Directory, + ); }; export const getDirectory = async (userId: number, directoryId: number) => { - const res = await db - .select() - .from(directory) - .where(and(eq(directory.userId, userId), eq(directory.id, directoryId))) - .limit(1); - return res[0] ?? null; + const directory = await db + .selectFrom("directory") + .selectAll() + .where("id", "=", directoryId) + .where("user_id", "=", userId) + .limit(1) + .executeTakeFirst(); + return directory + ? ({ + id: directory.id, + parentId: directory.parent_id ?? "root", + userId: directory.user_id, + mekVersion: directory.master_encryption_key_version, + encDek: directory.encrypted_data_encryption_key, + dekVersion: directory.data_encryption_key_version, + encName: directory.encrypted_name, + } satisfies Directory) + : null; }; export const setDirectoryEncName = async ( userId: number, directoryId: number, dekVersion: Date, - encName: string, - encNameIv: string, + encName: Ciphertext, ) => { - await db.transaction( - async (tx) => { - const directories = await tx - .select({ version: directory.dekVersion }) - .from(directory) - .where(and(eq(directory.userId, userId), eq(directory.id, directoryId))) - .limit(1); - if (!directories[0]) { - throw new IntegrityError("Directory not found"); - } else if (directories[0].version.getTime() !== dekVersion.getTime()) { - throw new IntegrityError("Invalid DEK version"); - } + await db.transaction().execute(async (trx) => { + const directory = await trx + .selectFrom("directory") + .select("data_encryption_key_version") + .where("id", "=", directoryId) + .where("user_id", "=", userId) + .limit(1) + .forUpdate() + .executeTakeFirst(); + if (!directory) { + throw new IntegrityError("Directory not found"); + } else if (directory.data_encryption_key_version.getTime() !== dekVersion.getTime()) { + throw new IntegrityError("Invalid DEK version"); + } - await tx - .update(directory) - .set({ encName: { ciphertext: encName, iv: encNameIv } }) - .where(and(eq(directory.userId, userId), eq(directory.id, directoryId))); - await tx.insert(directoryLog).values({ - directoryId, + await trx + .updateTable("directory") + .set({ encrypted_name: encName }) + .where("id", "=", directoryId) + .where("user_id", "=", userId) + .execute(); + await trx + .insertInto("directory_log") + .values({ + directory_id: directoryId, timestamp: new Date(), action: "rename", - newName: { ciphertext: encName, iv: encNameIv }, - }); - }, - { behavior: "exclusive" }, - ); + new_name: encName, + }) + .execute(); + }); }; export const unregisterDirectory = async (userId: number, directoryId: number) => { - return await db.transaction( - async (tx) => { + return await db + .transaction() + .setIsolationLevel("repeatable read") // TODO: Sufficient? + .execute(async (trx) => { const unregisterFiles = async (parentId: number) => { - return await tx - .delete(file) - .where(and(eq(file.userId, userId), eq(file.parentId, parentId))) - .returning({ id: file.id, path: file.path }); + return await trx + .deleteFrom("file") + .where("parent_id", "=", parentId) + .where("user_id", "=", userId) + .returning(["id", "path"]) + .execute(); }; const unregisterDirectoryRecursively = async ( directoryId: number, ): Promise<{ id: number; path: string }[]> => { const files = await unregisterFiles(directoryId); - const subDirectories = await tx - .select({ id: directory.id }) - .from(directory) - .where(and(eq(directory.userId, userId), eq(directory.parentId, directoryId))); + const subDirectories = await trx + .selectFrom("directory") + .select("id") + .where("parent_id", "=", directoryId) + .where("user_id", "=", userId) + .execute(); const subDirectoryFilePaths = await Promise.all( subDirectories.map(async ({ id }) => await unregisterDirectoryRecursively(id)), ); - const deleteRes = await tx.delete(directory).where(eq(directory.id, directoryId)); - if (deleteRes.changes === 0) { + const deleteRes = await trx + .deleteFrom("directory") + .where("id", "=", directoryId) + .where("user_id", "=", userId) + .executeTakeFirst(); + if (deleteRes.numDeletedRows === 0n) { throw new IntegrityError("Directory not found"); } return files.concat(...subDirectoryFilePaths); }; return await unregisterDirectoryRecursively(directoryId); - }, - { behavior: "exclusive" }, - ); + }); }; -export const registerFile = async (params: NewFileParams) => { - if ( - (params.hskVersion && !params.contentHmac) || - (!params.hskVersion && params.contentHmac) || - (params.encCreatedAt && !params.encCreatedAtIv) || - (!params.encCreatedAt && params.encCreatedAtIv) - ) { +export const registerFile = async (params: NewFile) => { + if ((params.hskVersion && !params.contentHmac) || (!params.hskVersion && params.contentHmac)) { throw new Error("Invalid arguments"); } - await db.transaction( - async (tx) => { - const meks = await tx - .select({ version: mek.version }) - .from(mek) - .where(and(eq(mek.userId, params.userId), eq(mek.state, "active"))) - .limit(1); - if (meks[0]?.version !== params.mekVersion) { - throw new IntegrityError("Inactive MEK version"); - } + return await db.transaction().execute(async (trx) => { + const mek = await trx + .selectFrom("master_encryption_key") + .select("version") + .where("user_id", "=", params.userId) + .where("state", "=", "active") + .limit(1) + .forUpdate() + .executeTakeFirst(); + if (mek?.version !== params.mekVersion) { + throw new IntegrityError("Inactive MEK version"); + } - if (params.hskVersion) { - const hsks = await tx - .select({ version: hsk.version }) - .from(hsk) - .where(and(eq(hsk.userId, params.userId), eq(hsk.state, "active"))) - .limit(1); - if (hsks[0]?.version !== params.hskVersion) { - throw new IntegrityError("Inactive HSK version"); - } + if (params.hskVersion) { + const hsk = await trx + .selectFrom("hmac_secret_key") + .select("version") + .where("user_id", "=", params.userId) + .where("state", "=", "active") + .limit(1) + .forUpdate() + .executeTakeFirst(); + if (hsk?.version !== params.hskVersion) { + throw new IntegrityError("Inactive HSK version"); } + } - const newFiles = await tx - .insert(file) - .values({ - path: params.path, - parentId: params.parentId === "root" ? null : params.parentId, - userId: params.userId, - mekVersion: params.mekVersion, - hskVersion: params.hskVersion, - contentHmac: params.contentHmac, - contentType: params.contentType, - encDek: params.encDek, - dekVersion: params.dekVersion, - encContentIv: params.encContentIv, - encName: { ciphertext: params.encName, iv: params.encNameIv }, - encCreatedAt: - params.encCreatedAt && params.encCreatedAtIv - ? { ciphertext: params.encCreatedAt, iv: params.encCreatedAtIv } - : null, - encLastModifiedAt: { - ciphertext: params.encLastModifiedAt, - iv: params.encLastModifiedAtIv, - }, - }) - .returning({ id: file.id }); - const { id: fileId } = newFiles[0]!; - await tx.insert(fileLog).values({ - fileId, + const { fileId } = await trx + .insertInto("file") + .values({ + parent_id: params.parentId !== "root" ? params.parentId : null, + user_id: params.userId, + path: params.path, + master_encryption_key_version: params.mekVersion, + encrypted_data_encryption_key: params.encDek, + data_encryption_key_version: params.dekVersion, + hmac_secret_key_version: params.hskVersion, + content_hmac: params.contentHmac, + content_type: params.contentType, + encrypted_content_iv: params.encContentIv, + encrypted_content_hash: params.encContentHash, + encrypted_name: params.encName, + encrypted_created_at: params.encCreatedAt, + encrypted_last_modified_at: params.encLastModifiedAt, + }) + .returning("id as fileId") + .executeTakeFirstOrThrow(); + await trx + .insertInto("file_log") + .values({ + file_id: fileId, timestamp: new Date(), action: "create", - newName: { ciphertext: params.encName, iv: params.encNameIv }, - }); - }, - { behavior: "exclusive" }, - ); + new_name: params.encName, + }) + .execute(); + return { id: fileId }; + }); }; export const getAllFilesByParent = async (userId: number, parentId: DirectoryId) => { - return await db - .select() - .from(file) - .where( - and( - eq(file.userId, userId), - parentId === "root" ? isNull(file.parentId) : eq(file.parentId, parentId), - ), - ); + let query = db.selectFrom("file").selectAll().where("user_id", "=", userId); + query = + parentId === "root" + ? query.where("parent_id", "is", null) + : query.where("parent_id", "=", parentId); + const files = await query.execute(); + return files.map( + (file) => + ({ + id: file.id, + parentId: file.parent_id ?? "root", + userId: file.user_id, + path: file.path, + mekVersion: file.master_encryption_key_version, + encDek: file.encrypted_data_encryption_key, + dekVersion: file.data_encryption_key_version, + hskVersion: file.hmac_secret_key_version, + contentHmac: file.content_hmac, + contentType: file.content_type, + encContentIv: file.encrypted_content_iv, + encContentHash: file.encrypted_content_hash, + encName: file.encrypted_name, + encCreatedAt: file.encrypted_created_at, + encLastModifiedAt: file.encrypted_last_modified_at, + }) satisfies File, + ); +}; + +export const getAllFilesByCategory = async ( + userId: number, + categoryId: number, + recurse: boolean, +) => { + const files = await db + .withRecursive("cte", (db) => + db + .selectFrom("category") + .leftJoin("file_category", "category.id", "file_category.category_id") + .select(["id", "parent_id", "user_id", "file_category.file_id"]) + .select(sql`0`.as("depth")) + .where("id", "=", categoryId) + .$if(recurse, (qb) => + qb.unionAll((db) => + db + .selectFrom("category") + .leftJoin("file_category", "category.id", "file_category.category_id") + .innerJoin("cte", "category.parent_id", "cte.id") + .select([ + "category.id", + "category.parent_id", + "category.user_id", + "file_category.file_id", + ]) + .select(sql`cte.depth + 1`.as("depth")), + ), + ), + ) + .selectFrom("cte") + .select(["file_id", "depth"]) + .distinctOn("file_id") + .where("user_id", "=", userId) + .where("file_id", "is not", null) + .$narrowType<{ file_id: NotNull }>() + .orderBy(["file_id", "depth"]) + .execute(); + return files.map(({ file_id, depth }) => ({ id: file_id, isRecursive: depth > 0 })); }; export const getAllFileIdsByContentHmac = async ( @@ -243,69 +337,150 @@ export const getAllFileIdsByContentHmac = async ( hskVersion: number, contentHmac: string, ) => { - return await db - .select({ id: file.id }) - .from(file) - .where( - and( - eq(file.userId, userId), - eq(file.hskVersion, hskVersion), - eq(file.contentHmac, contentHmac), - ), - ); + const files = await db + .selectFrom("file") + .select("id") + .where("user_id", "=", userId) + .where("hmac_secret_key_version", "=", hskVersion) + .where("content_hmac", "=", contentHmac) + .execute(); + return files.map(({ id }) => ({ id })); }; export const getFile = async (userId: number, fileId: number) => { - const res = await db - .select() - .from(file) - .where(and(eq(file.userId, userId), eq(file.id, fileId))) - .limit(1); - return res[0] ?? null; + const file = await db + .selectFrom("file") + .selectAll() + .where("id", "=", fileId) + .where("user_id", "=", userId) + .limit(1) + .executeTakeFirst(); + return file + ? ({ + id: file.id, + parentId: file.parent_id ?? "root", + userId: file.user_id, + path: file.path, + mekVersion: file.master_encryption_key_version, + encDek: file.encrypted_data_encryption_key, + dekVersion: file.data_encryption_key_version, + hskVersion: file.hmac_secret_key_version, + contentHmac: file.content_hmac, + contentType: file.content_type, + encContentIv: file.encrypted_content_iv, + encContentHash: file.encrypted_content_hash, + encName: file.encrypted_name, + encCreatedAt: file.encrypted_created_at, + encLastModifiedAt: file.encrypted_last_modified_at, + } satisfies File) + : null; }; export const setFileEncName = async ( userId: number, fileId: number, dekVersion: Date, - encName: string, - encNameIv: string, + encName: Ciphertext, ) => { - await db.transaction( - async (tx) => { - const files = await tx - .select({ version: file.dekVersion }) - .from(file) - .where(and(eq(file.userId, userId), eq(file.id, fileId))) - .limit(1); - if (!files[0]) { - throw new IntegrityError("File not found"); - } else if (files[0].version.getTime() !== dekVersion.getTime()) { - throw new IntegrityError("Invalid DEK version"); - } + await db.transaction().execute(async (trx) => { + const file = await trx + .selectFrom("file") + .select("data_encryption_key_version") + .where("id", "=", fileId) + .where("user_id", "=", userId) + .limit(1) + .forUpdate() + .executeTakeFirst(); + if (!file) { + throw new IntegrityError("File not found"); + } else if (file.data_encryption_key_version.getTime() !== dekVersion.getTime()) { + throw new IntegrityError("Invalid DEK version"); + } - await tx - .update(file) - .set({ encName: { ciphertext: encName, iv: encNameIv } }) - .where(and(eq(file.userId, userId), eq(file.id, fileId))); - await tx.insert(fileLog).values({ - fileId, + await trx + .updateTable("file") + .set({ encrypted_name: encName }) + .where("id", "=", fileId) + .where("user_id", "=", userId) + .execute(); + await trx + .insertInto("file_log") + .values({ + file_id: fileId, timestamp: new Date(), action: "rename", - newName: { ciphertext: encName, iv: encNameIv }, - }); - }, - { behavior: "exclusive" }, - ); + new_name: encName, + }) + .execute(); + }); }; export const unregisterFile = async (userId: number, fileId: number) => { - const files = await db - .delete(file) - .where(and(eq(file.userId, userId), eq(file.id, fileId))) - .returning({ path: file.path }); - if (!files[0]) { + const file = await db + .deleteFrom("file") + .where("id", "=", fileId) + .where("user_id", "=", userId) + .returning("path") + .executeTakeFirst(); + if (!file) { throw new IntegrityError("File not found"); } - return files[0].path; + return { path: file.path }; +}; + +export const addFileToCategory = async (fileId: number, categoryId: number) => { + await db.transaction().execute(async (trx) => { + try { + await trx + .insertInto("file_category") + .values({ file_id: fileId, category_id: categoryId }) + .execute(); + await trx + .insertInto("file_log") + .values({ + file_id: fileId, + timestamp: new Date(), + action: "add-to-category", + category_id: categoryId, + }) + .execute(); + } catch (e) { + if (e instanceof pg.DatabaseError && e.code === "23505") { + throw new IntegrityError("File already added to category"); + } + throw e; + } + }); +}; + +export const getAllFileCategories = async (fileId: number) => { + const categories = await db + .selectFrom("file_category") + .select("category_id") + .where("file_id", "=", fileId) + .execute(); + return categories.map(({ category_id }) => ({ id: category_id })); +}; + +export const removeFileFromCategory = async (fileId: number, categoryId: number) => { + await db.transaction().execute(async (trx) => { + const res = await trx + .deleteFrom("file_category") + .where("file_id", "=", fileId) + .where("category_id", "=", categoryId) + .executeTakeFirst(); + if (res.numDeletedRows === 0n) { + throw new IntegrityError("File not found in category"); + } + + await trx + .insertInto("file_log") + .values({ + file_id: fileId, + timestamp: new Date(), + action: "remove-from-category", + category_id: categoryId, + }) + .execute(); + }); }; diff --git a/src/lib/server/db/hsk.ts b/src/lib/server/db/hsk.ts index faf7dc6..4673cae 100644 --- a/src/lib/server/db/hsk.ts +++ b/src/lib/server/db/hsk.ts @@ -1,8 +1,15 @@ -import { SqliteError } from "better-sqlite3"; -import { and, eq } from "drizzle-orm"; -import db from "./drizzle"; +import pg from "pg"; import { IntegrityError } from "./error"; -import { hsk, hskLog } from "./schema"; +import db from "./kysely"; +import type { HskState } from "./schema"; + +interface Hsk { + userId: number; + version: number; + state: HskState; + mekVersion: number; + encHsk: string; +} export const registerInitialHsk = async ( userId: number, @@ -10,37 +17,52 @@ export const registerInitialHsk = async ( mekVersion: number, encHsk: string, ) => { - await db.transaction( - async (tx) => { - try { - await tx.insert(hsk).values({ - userId, + await db.transaction().execute(async (trx) => { + try { + await trx + .insertInto("hmac_secret_key") + .values({ + user_id: userId, version: 1, state: "active", - mekVersion, - encHsk, - }); - await tx.insert(hskLog).values({ - userId, - hskVersion: 1, + master_encryption_key_version: mekVersion, + encrypted_key: encHsk, + }) + .execute(); + await trx + .insertInto("hmac_secret_key_log") + .values({ + user_id: userId, + hmac_secret_key_version: 1, timestamp: new Date(), action: "create", - actionBy: createdBy, - }); - } catch (e) { - if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") { - throw new IntegrityError("HSK already registered"); - } - throw e; + action_by: createdBy, + }) + .execute(); + } catch (e) { + if (e instanceof pg.DatabaseError && e.code === "23505") { + throw new IntegrityError("HSK already registered"); } - }, - { behavior: "exclusive" }, - ); + throw e; + } + }); }; export const getAllValidHsks = async (userId: number) => { - return await db - .select() - .from(hsk) - .where(and(eq(hsk.userId, userId), eq(hsk.state, "active"))); + const hsks = await db + .selectFrom("hmac_secret_key") + .selectAll() + .where("user_id", "=", userId) + .where("state", "=", "active") + .execute(); + return hsks.map( + ({ user_id, version, state, master_encryption_key_version, encrypted_key }) => + ({ + userId: user_id, + version, + state: state as "active", + mekVersion: master_encryption_key_version, + encHsk: encrypted_key, + }) satisfies Hsk, + ); }; diff --git a/src/lib/server/db/kysely.ts b/src/lib/server/db/kysely.ts new file mode 100644 index 0000000..302049e --- /dev/null +++ b/src/lib/server/db/kysely.ts @@ -0,0 +1,47 @@ +import { Kysely, PostgresDialect, Migrator } from "kysely"; +import pg from "pg"; +import env from "$lib/server/loadenv"; +import migrations from "./migrations"; +import type { Database } from "./schema"; + +const dialect = new PostgresDialect({ + pool: new pg.Pool({ + host: env.database.host, + port: env.database.port, + user: env.database.user, + password: env.database.password, + database: env.database.name, + }), +}); + +const db = new Kysely({ dialect }); + +export const migrateDB = async () => { + if (env.nodeEnv !== "production") return; + + const migrator = new Migrator({ + db, + provider: { + async getMigrations() { + return migrations; + }, + }, + }); + const { error, results } = await migrator.migrateToLatest(); + if (error) { + const migration = results?.find(({ status }) => status === "Error"); + if (migration) { + console.error(`Migration "${migration.migrationName}" failed.`); + } + console.error(error); + process.exit(1); + } + + if (results?.length === 0) { + console.log("Database is up-to-date."); + } else { + console.log("Database migration completed."); + } +}; + +export default db; diff --git a/src/lib/server/db/mek.ts b/src/lib/server/db/mek.ts index 944636e..d6eecb0 100644 --- a/src/lib/server/db/mek.ts +++ b/src/lib/server/db/mek.ts @@ -1,8 +1,19 @@ -import { SqliteError } from "better-sqlite3"; -import { and, or, eq } from "drizzle-orm"; -import db from "./drizzle"; +import pg from "pg"; import { IntegrityError } from "./error"; -import { mek, mekLog, clientMek } from "./schema"; +import db from "./kysely"; +import type { MekState } from "./schema"; + +interface Mek { + userId: number; + version: number; + state: MekState; +} + +interface ClientMekWithDetails extends Mek { + clientId: number; + encMek: string; + encMekSig: string; +} export const registerInitialMek = async ( userId: number, @@ -10,58 +21,80 @@ export const registerInitialMek = async ( encMek: string, encMekSig: string, ) => { - await db.transaction( - async (tx) => { - try { - await tx.insert(mek).values({ - userId, + await db.transaction().execute(async (trx) => { + try { + await trx + .insertInto("master_encryption_key") + .values({ + user_id: userId, version: 1, state: "active", - }); - await tx.insert(clientMek).values({ - userId, - clientId: createdBy, - mekVersion: 1, - encMek, - encMekSig, - }); - await tx.insert(mekLog).values({ - userId, - mekVersion: 1, + }) + .execute(); + await trx + .insertInto("client_master_encryption_key") + .values({ + user_id: userId, + client_id: createdBy, + version: 1, + encrypted_key: encMek, + encrypted_key_signature: encMekSig, + }) + .execute(); + await trx + .insertInto("master_encryption_key_log") + .values({ + user_id: userId, + master_encryption_key_version: 1, timestamp: new Date(), action: "create", - actionBy: createdBy, - }); - } catch (e) { - if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") { - throw new IntegrityError("MEK already registered"); - } - throw e; + action_by: createdBy, + }) + .execute(); + } catch (e) { + if (e instanceof pg.DatabaseError && e.code === "23505") { + throw new IntegrityError("MEK already registered"); } - }, - { behavior: "exclusive" }, - ); + throw e; + } + }); }; export const getInitialMek = async (userId: number) => { - const meks = await db - .select() - .from(mek) - .where(and(eq(mek.userId, userId), eq(mek.version, 1))) - .limit(1); - return meks[0] ?? null; + const mek = await db + .selectFrom("master_encryption_key") + .selectAll() + .where("user_id", "=", userId) + .where("version", "=", 1) + .limit(1) + .executeTakeFirst(); + return mek + ? ({ userId: mek.user_id, version: mek.version, state: mek.state } satisfies Mek) + : null; }; export const getAllValidClientMeks = async (userId: number, clientId: number) => { - return await db - .select() - .from(clientMek) - .innerJoin(mek, and(eq(clientMek.userId, mek.userId), eq(clientMek.mekVersion, mek.version))) - .where( - and( - eq(clientMek.userId, userId), - eq(clientMek.clientId, clientId), - or(eq(mek.state, "active"), eq(mek.state, "retired")), - ), - ); + const clientMeks = await db + .selectFrom("client_master_encryption_key") + .innerJoin("master_encryption_key", (join) => + join + .onRef("client_master_encryption_key.user_id", "=", "master_encryption_key.user_id") + .onRef("client_master_encryption_key.version", "=", "master_encryption_key.version"), + ) + .selectAll() + .where("client_master_encryption_key.user_id", "=", userId) + .where("client_master_encryption_key.client_id", "=", clientId) + .where((eb) => eb.or([eb("state", "=", "active"), eb("state", "=", "retired")])) + .execute(); + return clientMeks.map( + ({ user_id, client_id, version, state, encrypted_key, encrypted_key_signature }) => + ({ + userId: user_id, + version, + state: state as "active" | "retired", + clientId: client_id, + encMek: encrypted_key, + encMekSig: encrypted_key_signature, + }) satisfies ClientMekWithDetails, + ); }; diff --git a/src/lib/server/db/migrations/1737357000-Initial.ts b/src/lib/server/db/migrations/1737357000-Initial.ts new file mode 100644 index 0000000..5caf503 --- /dev/null +++ b/src/lib/server/db/migrations/1737357000-Initial.ts @@ -0,0 +1,224 @@ +import { Kysely } from "kysely"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const up = async (db: Kysely) => { + // user.ts + await db.schema + .createTable("user") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("email", "text", (col) => col.unique().notNull()) + .addColumn("nickname", "text", (col) => col.notNull()) + .addColumn("password", "text", (col) => col.notNull()) + .execute(); + + // client.ts + await db.schema + .createTable("client") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("encryption_public_key", "text", (col) => col.unique().notNull()) + .addColumn("signature_public_key", "text", (col) => col.unique().notNull()) + .addUniqueConstraint("client_ak01", ["encryption_public_key", "signature_public_key"]) + .execute(); + await db.schema + .createTable("user_client") + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("client_id", "integer", (col) => col.references("client.id").notNull()) + .addColumn("state", "text", (col) => col.notNull().defaultTo("challenging")) + .addPrimaryKeyConstraint("user_client_pk", ["user_id", "client_id"]) + .execute(); + await db.schema + .createTable("user_client_challenge") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("client_id", "integer", (col) => col.references("client.id").notNull()) + .addColumn("answer", "text", (col) => col.unique().notNull()) + .addColumn("allowed_ip", "text", (col) => col.notNull()) + .addColumn("expires_at", "timestamp(3)", (col) => col.notNull()) + .addForeignKeyConstraint( + "user_client_challenge_fk01", + ["user_id", "client_id"], + "user_client", + ["user_id", "client_id"], + ) + .execute(); + + // session.ts + await db.schema + .createTable("session") + .addColumn("id", "text", (col) => col.primaryKey()) + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("client_id", "integer", (col) => col.references("client.id")) + .addColumn("created_at", "timestamp(3)", (col) => col.notNull()) + .addColumn("last_used_at", "timestamp(3)", (col) => col.notNull()) + .addColumn("last_used_by_ip", "text") + .addColumn("last_used_by_agent", "text") + .addUniqueConstraint("session_ak01", ["user_id", "client_id"]) + .execute(); + await db.schema + .createTable("session_upgrade_challenge") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("session_id", "text", (col) => col.references("session.id").unique().notNull()) + .addColumn("client_id", "integer", (col) => col.references("client.id").notNull()) + .addColumn("answer", "text", (col) => col.unique().notNull()) + .addColumn("allowed_ip", "text", (col) => col.notNull()) + .addColumn("expires_at", "timestamp(3)", (col) => col.notNull()) + .execute(); + + // mek.ts + await db.schema + .createTable("master_encryption_key") + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("version", "integer", (col) => col.notNull()) + .addColumn("state", "text", (col) => col.notNull()) + .addPrimaryKeyConstraint("master_encryption_key_pk", ["user_id", "version"]) + .execute(); + await db.schema + .createTable("master_encryption_key_log") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("master_encryption_key_version", "integer", (col) => col.notNull()) + .addColumn("timestamp", "timestamp(3)", (col) => col.notNull()) + .addColumn("action", "text", (col) => col.notNull()) + .addColumn("action_by", "integer", (col) => col.references("client.id")) + .addForeignKeyConstraint( + "master_encryption_key_log_fk01", + ["user_id", "master_encryption_key_version"], + "master_encryption_key", + ["user_id", "version"], + ) + .execute(); + await db.schema + .createTable("client_master_encryption_key") + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("client_id", "integer", (col) => col.references("client.id").notNull()) + .addColumn("version", "integer", (col) => col.notNull()) + .addColumn("encrypted_key", "text", (col) => col.notNull()) + .addColumn("encrypted_key_signature", "text", (col) => col.notNull()) + .addPrimaryKeyConstraint("client_master_encryption_key_pk", ["user_id", "client_id", "version"]) + .addForeignKeyConstraint( + "client_master_encryption_key_fk01", + ["user_id", "version"], + "master_encryption_key", + ["user_id", "version"], + ) + .execute(); + + // hsk.ts + await db.schema + .createTable("hmac_secret_key") + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("version", "integer", (col) => col.notNull()) + .addColumn("state", "text", (col) => col.notNull()) + .addColumn("master_encryption_key_version", "integer", (col) => col.notNull()) + .addColumn("encrypted_key", "text", (col) => col.unique().notNull()) + .addPrimaryKeyConstraint("hmac_secret_key_pk", ["user_id", "version"]) + .addForeignKeyConstraint( + "hmac_secret_key_fk01", + ["user_id", "master_encryption_key_version"], + "master_encryption_key", + ["user_id", "version"], + ) + .execute(); + await db.schema + .createTable("hmac_secret_key_log") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("hmac_secret_key_version", "integer", (col) => col.notNull()) + .addColumn("timestamp", "timestamp(3)", (col) => col.notNull()) + .addColumn("action", "text", (col) => col.notNull()) + .addColumn("action_by", "integer", (col) => col.references("client.id")) + .addForeignKeyConstraint( + "hmac_secret_key_log_fk01", + ["user_id", "hmac_secret_key_version"], + "hmac_secret_key", + ["user_id", "version"], + ) + .execute(); + + // file.ts + await db.schema + .createTable("directory") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("parent_id", "integer", (col) => col.references("directory.id")) + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("master_encryption_key_version", "integer", (col) => col.notNull()) + .addColumn("encrypted_data_encryption_key", "text", (col) => col.unique().notNull()) + .addColumn("data_encryption_key_version", "timestamp(3)", (col) => col.notNull()) + .addColumn("encrypted_name", "json", (col) => col.notNull()) + .addForeignKeyConstraint( + "directory_fk01", + ["user_id", "master_encryption_key_version"], + "master_encryption_key", + ["user_id", "version"], + ) + .execute(); + await db.schema + .createTable("directory_log") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("directory_id", "integer", (col) => + col.references("directory.id").onDelete("cascade").notNull(), + ) + .addColumn("timestamp", "timestamp(3)", (col) => col.notNull()) + .addColumn("action", "text", (col) => col.notNull()) + .addColumn("new_name", "json") + .execute(); + await db.schema + .createTable("file") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("parent_id", "integer", (col) => col.references("directory.id")) + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("path", "text", (col) => col.unique().notNull()) + .addColumn("master_encryption_key_version", "integer", (col) => col.notNull()) + .addColumn("encrypted_data_encryption_key", "text", (col) => col.unique().notNull()) + .addColumn("data_encryption_key_version", "timestamp(3)", (col) => col.notNull()) + .addColumn("hmac_secret_key_version", "integer") + .addColumn("content_hmac", "text") + .addColumn("content_type", "text", (col) => col.notNull()) + .addColumn("encrypted_content_iv", "text", (col) => col.notNull()) + .addColumn("encrypted_content_hash", "text", (col) => col.notNull()) + .addColumn("encrypted_name", "json", (col) => col.notNull()) + .addColumn("encrypted_created_at", "json") + .addColumn("encrypted_last_modified_at", "json", (col) => col.notNull()) + .addForeignKeyConstraint( + "file_fk01", + ["user_id", "master_encryption_key_version"], + "master_encryption_key", + ["user_id", "version"], + ) + .addForeignKeyConstraint( + "file_fk02", + ["user_id", "hmac_secret_key_version"], + "hmac_secret_key", + ["user_id", "version"], + ) + .execute(); + await db.schema + .createTable("file_log") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("file_id", "integer", (col) => + col.references("file.id").onDelete("cascade").notNull(), + ) + .addColumn("timestamp", "timestamp(3)", (col) => col.notNull()) + .addColumn("action", "text", (col) => col.notNull()) + .addColumn("new_name", "json") + .execute(); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const down = async (db: Kysely) => { + await db.schema.dropTable("file_log").execute(); + await db.schema.dropTable("file").execute(); + await db.schema.dropTable("directory_log").execute(); + await db.schema.dropTable("directory").execute(); + await db.schema.dropTable("hmac_secret_key_log").execute(); + await db.schema.dropTable("hmac_secret_key").execute(); + await db.schema.dropTable("client_master_encryption_key").execute(); + await db.schema.dropTable("master_encryption_key_log").execute(); + await db.schema.dropTable("master_encryption_key").execute(); + await db.schema.dropTable("session_upgrade_challenge").execute(); + await db.schema.dropTable("session").execute(); + await db.schema.dropTable("user_client_challenge").execute(); + await db.schema.dropTable("user_client").execute(); + await db.schema.dropTable("client").execute(); + await db.schema.dropTable("user").execute(); +}; diff --git a/src/lib/server/db/migrations/1737422340-AddFileCategory.ts b/src/lib/server/db/migrations/1737422340-AddFileCategory.ts new file mode 100644 index 0000000..ff811e1 --- /dev/null +++ b/src/lib/server/db/migrations/1737422340-AddFileCategory.ts @@ -0,0 +1,65 @@ +import { Kysely } from "kysely"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const up = async (db: Kysely) => { + // category.ts + await db.schema + .createTable("category") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("parent_id", "integer", (col) => col.references("category.id").onDelete("cascade")) + .addColumn("user_id", "integer", (col) => col.references("user.id").notNull()) + .addColumn("master_encryption_key_version", "integer", (col) => col.notNull()) + .addColumn("encrypted_data_encryption_key", "text", (col) => col.unique().notNull()) + .addColumn("data_encryption_key_version", "timestamp(3)", (col) => col.notNull()) + .addColumn("encrypted_name", "json", (col) => col.notNull()) + .addForeignKeyConstraint( + "category_fk01", + ["user_id", "master_encryption_key_version"], + "master_encryption_key", + ["user_id", "version"], + ) + .execute(); + await db.schema + .createTable("category_log") + .addColumn("id", "integer", (col) => col.primaryKey().generatedAlwaysAsIdentity()) + .addColumn("category_id", "integer", (col) => + col.references("category.id").onDelete("cascade").notNull(), + ) + .addColumn("timestamp", "timestamp(3)", (col) => col.notNull()) + .addColumn("action", "text", (col) => col.notNull()) + .addColumn("new_name", "json") + .execute(); + + // file.ts + await db.schema + .alterTable("file_log") + .addColumn("category_id", "integer", (col) => + col.references("category.id").onDelete("set null"), + ) + .execute(); + await db.schema + .createTable("file_category") + .addColumn("file_id", "integer", (col) => + col.references("file.id").onDelete("cascade").notNull(), + ) + .addColumn("category_id", "integer", (col) => + col.references("category.id").onDelete("cascade").notNull(), + ) + .addPrimaryKeyConstraint("file_category_pk", ["file_id", "category_id"]) + .execute(); +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const down = async (db: Kysely) => { + await db + .deleteFrom("file_log") + .where((eb) => + eb.or([eb("action", "=", "add-to-category"), eb("action", "=", "remove-from-category")]), + ) + .execute(); + + await db.schema.dropTable("file_category").execute(); + await db.schema.alterTable("file_log").dropColumn("category_id").execute(); + await db.schema.dropTable("category_log").execute(); + await db.schema.dropTable("category").execute(); +}; diff --git a/src/lib/server/db/migrations/index.ts b/src/lib/server/db/migrations/index.ts new file mode 100644 index 0000000..aa6ee13 --- /dev/null +++ b/src/lib/server/db/migrations/index.ts @@ -0,0 +1,7 @@ +import * as Initial1737357000 from "./1737357000-Initial"; +import * as AddFileCategory1737422340 from "./1737422340-AddFileCategory"; + +export default { + "1737357000-Initial": Initial1737357000, + "1737422340-AddFileCategory": AddFileCategory1737422340, +}; diff --git a/src/lib/server/db/schema/category.ts b/src/lib/server/db/schema/category.ts new file mode 100644 index 0000000..2304264 --- /dev/null +++ b/src/lib/server/db/schema/category.ts @@ -0,0 +1,27 @@ +import type { Generated } from "kysely"; +import type { Ciphertext } from "./util"; + +interface CategoryTable { + id: Generated; + parent_id: number | null; + user_id: number; + master_encryption_key_version: number; + encrypted_data_encryption_key: string; // Base64 + data_encryption_key_version: Date; + encrypted_name: Ciphertext; +} + +interface CategoryLogTable { + id: Generated; + category_id: number; + timestamp: Date; + action: "create" | "rename"; + new_name: Ciphertext | null; +} + +declare module "./index" { + interface Database { + category: CategoryTable; + category_log: CategoryLogTable; + } +} diff --git a/src/lib/server/db/schema/client.ts b/src/lib/server/db/schema/client.ts index 1e9eb85..d66e42b 100644 --- a/src/lib/server/db/schema/client.ts +++ b/src/lib/server/db/schema/client.ts @@ -1,61 +1,32 @@ -import { - sqliteTable, - text, - integer, - primaryKey, - foreignKey, - unique, -} from "drizzle-orm/sqlite-core"; -import { user } from "./user"; +import type { ColumnType, Generated } from "kysely"; -export const client = sqliteTable( - "client", - { - id: integer("id").primaryKey({ autoIncrement: true }), - encPubKey: text("encryption_public_key").notNull().unique(), // Base64 - sigPubKey: text("signature_public_key").notNull().unique(), // Base64 - }, - (t) => ({ - unq: unique().on(t.encPubKey, t.sigPubKey), - }), -); +interface ClientTable { + id: Generated; + encryption_public_key: string; // Base64 + signature_public_key: string; // Base64 +} -export const userClient = sqliteTable( - "user_client", - { - userId: integer("user_id") - .notNull() - .references(() => user.id), - clientId: integer("client_id") - .notNull() - .references(() => client.id), - state: text("state", { enum: ["challenging", "pending", "active"] }) - .notNull() - .default("challenging"), - }, - (t) => ({ - pk: primaryKey({ columns: [t.userId, t.clientId] }), - }), -); +export type UserClientState = "challenging" | "pending" | "active"; -export const userClientChallenge = sqliteTable( - "user_client_challenge", - { - id: integer("id").primaryKey(), - userId: integer("user_id") - .notNull() - .references(() => user.id), - clientId: integer("client_id") - .notNull() - .references(() => client.id), - answer: text("answer").notNull().unique(), // Base64 - allowedIp: text("allowed_ip").notNull(), - expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), - }, - (t) => ({ - ref: foreignKey({ - columns: [t.userId, t.clientId], - foreignColumns: [userClient.userId, userClient.clientId], - }), - }), -); +interface UserClientTable { + user_id: number; + client_id: number; + state: ColumnType; +} + +interface UserClientChallengeTable { + id: Generated; + user_id: number; + client_id: number; + answer: string; // Base64 + allowed_ip: string; + expires_at: ColumnType; +} + +declare module "./index" { + interface Database { + client: ClientTable; + user_client: UserClientTable; + user_client_challenge: UserClientChallengeTable; + } +} diff --git a/src/lib/server/db/schema/file.ts b/src/lib/server/db/schema/file.ts index 65c5471..a1bf9bd 100644 --- a/src/lib/server/db/schema/file.ts +++ b/src/lib/server/db/schema/file.ts @@ -1,87 +1,62 @@ -import { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core"; -import { hsk } from "./hsk"; -import { mek } from "./mek"; -import { user } from "./user"; +import type { ColumnType, Generated } from "kysely"; +import type { Ciphertext } from "./util"; -const ciphertext = (name: string) => - text(name, { mode: "json" }).$type<{ - ciphertext: string; // Base64 - iv: string; // Base64 - }>(); +interface DirectoryTable { + id: Generated; + parent_id: number | null; + user_id: number; + master_encryption_key_version: number; + encrypted_data_encryption_key: string; // Base64 + data_encryption_key_version: Date; + encrypted_name: Ciphertext; +} -export const directory = sqliteTable( - "directory", - { - id: integer("id").primaryKey({ autoIncrement: true }), - parentId: integer("parent_id"), - userId: integer("user_id") - .notNull() - .references(() => user.id), - mekVersion: integer("master_encryption_key_version").notNull(), - encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64 - dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(), - encName: ciphertext("encrypted_name").notNull(), - }, - (t) => ({ - ref1: foreignKey({ - columns: [t.parentId], - foreignColumns: [t.id], - }), - ref2: foreignKey({ - columns: [t.userId, t.mekVersion], - foreignColumns: [mek.userId, mek.version], - }), - }), -); +interface DirectoryLogTable { + id: Generated; + directory_id: number; + timestamp: ColumnType; + action: "create" | "rename"; + new_name: Ciphertext | null; +} -export const directoryLog = sqliteTable("directory_log", { - id: integer("id").primaryKey({ autoIncrement: true }), - directoryId: integer("directory_id") - .notNull() - .references(() => directory.id, { onDelete: "cascade" }), - timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(), - action: text("action", { enum: ["create", "rename"] }).notNull(), - newName: ciphertext("new_name"), -}); +interface FileTable { + id: Generated; + parent_id: number | null; + user_id: number; + path: string; + master_encryption_key_version: number; + encrypted_data_encryption_key: string; // Base64 + data_encryption_key_version: Date; + hmac_secret_key_version: number | null; + content_hmac: string | null; // Base64 + content_type: string; + encrypted_content_iv: string; // Base64 + encrypted_content_hash: string; // Base64 + encrypted_name: Ciphertext; + encrypted_created_at: Ciphertext | null; + encrypted_last_modified_at: Ciphertext; +} -export const file = sqliteTable( - "file", - { - id: integer("id").primaryKey({ autoIncrement: true }), - parentId: integer("parent_id").references(() => directory.id), - userId: integer("user_id") - .notNull() - .references(() => user.id), - path: text("path").notNull().unique(), - mekVersion: integer("master_encryption_key_version").notNull(), - encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64 - dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(), - hskVersion: integer("hmac_secret_key_version"), - contentHmac: text("content_hmac"), // Base64 - contentType: text("content_type").notNull(), - encContentIv: text("encrypted_content_iv").notNull(), // Base64 - encName: ciphertext("encrypted_name").notNull(), - encCreatedAt: ciphertext("encrypted_created_at"), - encLastModifiedAt: ciphertext("encrypted_last_modified_at").notNull(), - }, - (t) => ({ - ref1: foreignKey({ - columns: [t.userId, t.mekVersion], - foreignColumns: [mek.userId, mek.version], - }), - ref2: foreignKey({ - columns: [t.userId, t.hskVersion], - foreignColumns: [hsk.userId, hsk.version], - }), - }), -); +interface FileLogTable { + id: Generated; + file_id: number; + timestamp: ColumnType; + action: "create" | "rename" | "add-to-category" | "remove-from-category"; + new_name: Ciphertext | null; + category_id: number | null; +} -export const fileLog = sqliteTable("file_log", { - id: integer("id").primaryKey({ autoIncrement: true }), - fileId: integer("file_id") - .notNull() - .references(() => file.id, { onDelete: "cascade" }), - timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(), - action: text("action", { enum: ["create", "rename"] }).notNull(), - newName: ciphertext("new_name"), -}); +interface FileCategoryTable { + file_id: number; + category_id: number; +} + +declare module "./index" { + interface Database { + directory: DirectoryTable; + directory_log: DirectoryLogTable; + file: FileTable; + file_log: FileLogTable; + file_category: FileCategoryTable; + } +} diff --git a/src/lib/server/db/schema/hsk.ts b/src/lib/server/db/schema/hsk.ts index b78c512..71457b0 100644 --- a/src/lib/server/db/schema/hsk.ts +++ b/src/lib/server/db/schema/hsk.ts @@ -1,43 +1,27 @@ -import { sqliteTable, text, integer, primaryKey, foreignKey } from "drizzle-orm/sqlite-core"; -import { mek } from "./mek"; -import { user } from "./user"; +import type { ColumnType, Generated } from "kysely"; -export const hsk = sqliteTable( - "hmac_secret_key", - { - userId: integer("user_id") - .notNull() - .references(() => user.id), - version: integer("version").notNull(), - state: text("state", { enum: ["active"] }).notNull(), - mekVersion: integer("master_encryption_key_version").notNull(), - encHsk: text("encrypted_key").notNull().unique(), // Base64 - }, - (t) => ({ - pk: primaryKey({ columns: [t.userId, t.version] }), - ref: foreignKey({ - columns: [t.userId, t.mekVersion], - foreignColumns: [mek.userId, mek.version], - }), - }), -); +export type HskState = "active"; -export const hskLog = sqliteTable( - "hmac_secret_key_log", - { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: integer("user_id") - .notNull() - .references(() => user.id), - hskVersion: integer("hmac_secret_key_version").notNull(), - timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(), - action: text("action", { enum: ["create"] }).notNull(), - actionBy: integer("action_by").references(() => user.id), - }, - (t) => ({ - ref: foreignKey({ - columns: [t.userId, t.hskVersion], - foreignColumns: [hsk.userId, hsk.version], - }), - }), -); +interface HskTable { + user_id: number; + version: number; + state: HskState; + master_encryption_key_version: number; + encrypted_key: string; // Base64 +} + +interface HskLogTable { + id: Generated; + user_id: number; + hmac_secret_key_version: number; + timestamp: ColumnType; + action: "create"; + action_by: number | null; +} + +declare module "./index" { + interface Database { + hmac_secret_key: HskTable; + hmac_secret_key_log: HskLogTable; + } +} diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 40cb9be..d3dd9b1 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -1,6 +1,11 @@ +export * from "./category"; export * from "./client"; export * from "./file"; export * from "./hsk"; export * from "./mek"; export * from "./session"; export * from "./user"; +export * from "./util"; + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface Database {} diff --git a/src/lib/server/db/schema/mek.ts b/src/lib/server/db/schema/mek.ts index e496d9e..d1b3c76 100644 --- a/src/lib/server/db/schema/mek.ts +++ b/src/lib/server/db/schema/mek.ts @@ -1,60 +1,34 @@ -import { sqliteTable, text, integer, primaryKey, foreignKey } from "drizzle-orm/sqlite-core"; -import { client } from "./client"; -import { user } from "./user"; +import type { ColumnType, Generated } from "kysely"; -export const mek = sqliteTable( - "master_encryption_key", - { - userId: integer("user_id") - .notNull() - .references(() => user.id), - version: integer("version").notNull(), - state: text("state", { enum: ["active", "retired", "dead"] }).notNull(), - retiredAt: integer("retired_at", { mode: "timestamp_ms" }), - }, - (t) => ({ - pk: primaryKey({ columns: [t.userId, t.version] }), - }), -); +export type MekState = "active" | "retired" | "dead"; -export const mekLog = sqliteTable( - "master_encryption_key_log", - { - id: integer("id").primaryKey({ autoIncrement: true }), - userId: integer("user_id") - .notNull() - .references(() => user.id), - mekVersion: integer("master_encryption_key_version").notNull(), - timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(), - action: text("action", { enum: ["create"] }).notNull(), - actionBy: integer("action_by").references(() => client.id), - }, - (t) => ({ - ref: foreignKey({ - columns: [t.userId, t.mekVersion], - foreignColumns: [mek.userId, mek.version], - }), - }), -); +interface MekTable { + user_id: number; + version: number; + state: MekState; +} -export const clientMek = sqliteTable( - "client_master_encryption_key", - { - userId: integer("user_id") - .notNull() - .references(() => user.id), - clientId: integer("client_id") - .notNull() - .references(() => client.id), - mekVersion: integer("version").notNull(), - encMek: text("encrypted_key").notNull(), // Base64 - encMekSig: text("encrypted_key_signature").notNull(), // Base64 - }, - (t) => ({ - pk: primaryKey({ columns: [t.userId, t.clientId, t.mekVersion] }), - ref: foreignKey({ - columns: [t.userId, t.mekVersion], - foreignColumns: [mek.userId, mek.version], - }), - }), -); +interface MekLogTable { + id: Generated; + user_id: number; + master_encryption_key_version: number; + timestamp: ColumnType; + action: "create"; + action_by: number | null; +} + +interface ClientMekTable { + user_id: number; + client_id: number; + version: number; + encrypted_key: string; // Base64 + encrypted_key_signature: string; // Base64 +} + +declare module "./index" { + interface Database { + master_encryption_key: MekTable; + master_encryption_key_log: MekLogTable; + client_master_encryption_key: ClientMekTable; + } +} diff --git a/src/lib/server/db/schema/session.ts b/src/lib/server/db/schema/session.ts index 5f2129d..301a879 100644 --- a/src/lib/server/db/schema/session.ts +++ b/src/lib/server/db/schema/session.ts @@ -1,35 +1,27 @@ -import { sqliteTable, text, integer, unique } from "drizzle-orm/sqlite-core"; -import { client } from "./client"; -import { user } from "./user"; +import type { ColumnType, Generated } from "kysely"; -export const session = sqliteTable( - "session", - { - id: text("id").notNull().primaryKey(), - userId: integer("user_id") - .notNull() - .references(() => user.id), - clientId: integer("client_id").references(() => client.id), - createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), - lastUsedAt: integer("last_used_at", { mode: "timestamp_ms" }).notNull(), - lastUsedByIp: text("last_used_by_ip"), - lastUsedByUserAgent: text("last_used_by_user_agent"), - }, - (t) => ({ - unq: unique().on(t.userId, t.clientId), - }), -); +interface SessionTable { + id: string; + user_id: number; + client_id: number | null; + created_at: ColumnType; + last_used_at: Date; + last_used_by_ip: string | null; + last_used_by_agent: string | null; +} -export const sessionUpgradeChallenge = sqliteTable("session_upgrade_challenge", { - id: integer("id").primaryKey(), - sessionId: text("session_id") - .notNull() - .references(() => session.id) - .unique(), - clientId: integer("client_id") - .notNull() - .references(() => client.id), - answer: text("answer").notNull().unique(), // Base64 - allowedIp: text("allowed_ip").notNull(), - expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), -}); +interface SessionUpgradeChallengeTable { + id: Generated; + session_id: string; + client_id: number; + answer: string; // Base64 + allowed_ip: string; + expires_at: ColumnType; +} + +declare module "./index" { + interface Database { + session: SessionTable; + session_upgrade_challenge: SessionUpgradeChallengeTable; + } +} diff --git a/src/lib/server/db/schema/user.ts b/src/lib/server/db/schema/user.ts index c98fa01..a5f111f 100644 --- a/src/lib/server/db/schema/user.ts +++ b/src/lib/server/db/schema/user.ts @@ -1,8 +1,14 @@ -import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; +import type { Generated } from "kysely"; -export const user = sqliteTable("user", { - id: integer("id").primaryKey({ autoIncrement: true }), - email: text("email").notNull().unique(), - password: text("password").notNull(), - nickname: text("nickname").notNull(), -}); +interface UserTable { + id: Generated; + email: string; + nickname: string; + password: string; +} + +declare module "./index" { + interface Database { + user: UserTable; + } +} diff --git a/src/lib/server/db/schema/util.ts b/src/lib/server/db/schema/util.ts new file mode 100644 index 0000000..74f92de --- /dev/null +++ b/src/lib/server/db/schema/util.ts @@ -0,0 +1,4 @@ +export interface Ciphertext { + ciphertext: string; // Base64 + iv: string; // Base64 +} diff --git a/src/lib/server/db/session.ts b/src/lib/server/db/session.ts index 819dd86..727f795 100644 --- a/src/lib/server/db/session.ts +++ b/src/lib/server/db/session.ts @@ -1,30 +1,31 @@ -import { SqliteError } from "better-sqlite3"; -import { and, eq, ne, gt, lte, isNull } from "drizzle-orm"; +import pg from "pg"; import env from "$lib/server/loadenv"; -import db from "./drizzle"; import { IntegrityError } from "./error"; -import { session, sessionUpgradeChallenge } from "./schema"; +import db from "./kysely"; export const createSession = async ( userId: number, clientId: number | null, sessionId: string, ip: string | null, - userAgent: string | null, + agent: string | null, ) => { try { const now = new Date(); - await db.insert(session).values({ - id: sessionId, - userId, - clientId, - createdAt: now, - lastUsedAt: now, - lastUsedByIp: ip || null, - lastUsedByUserAgent: userAgent || null, - }); + await db + .insertInto("session") + .values({ + id: sessionId, + user_id: userId, + client_id: clientId, + created_at: now, + last_used_at: now, + last_used_by_ip: ip || null, + last_used_by_agent: agent || null, + }) + .execute(); } catch (e) { - if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { + if (e instanceof pg.DatabaseError && e.code === "23505") { throw new IntegrityError("Session already exists"); } throw e; @@ -34,49 +35,55 @@ export const createSession = async ( export const refreshSession = async ( sessionId: string, ip: string | null, - userAgent: string | null, + agent: string | null, ) => { const now = new Date(); - const sessions = await db - .update(session) + const session = await db + .updateTable("session") .set({ - lastUsedAt: now, - lastUsedByIp: ip || undefined, - lastUsedByUserAgent: userAgent || undefined, + last_used_at: now, + last_used_by_ip: ip !== "" ? ip : undefined, // Don't update if empty + last_used_by_agent: agent !== "" ? agent : undefined, // Don't update if empty }) - .where( - and( - eq(session.id, sessionId), - gt(session.lastUsedAt, new Date(now.getTime() - env.session.exp)), - ), - ) - .returning({ userId: session.userId, clientId: session.clientId }); - if (!sessions[0]) { + .where("id", "=", sessionId) + .where("last_used_at", ">", new Date(now.getTime() - env.session.exp)) + .returning(["user_id", "client_id"]) + .executeTakeFirst(); + if (!session) { throw new IntegrityError("Session not found"); } - return sessions[0]; + return { userId: session.user_id, clientId: session.client_id }; }; export const upgradeSession = async (sessionId: string, clientId: number) => { const res = await db - .update(session) - .set({ clientId }) - .where(and(eq(session.id, sessionId), isNull(session.clientId))); - if (res.changes === 0) { + .updateTable("session") + .set({ client_id: clientId }) + .where("id", "=", sessionId) + .where("client_id", "is", null) + .executeTakeFirst(); + if (res.numUpdatedRows === 0n) { throw new IntegrityError("Session not found"); } }; export const deleteSession = async (sessionId: string) => { - await db.delete(session).where(eq(session.id, sessionId)); + await db.deleteFrom("session").where("id", "=", sessionId).execute(); }; export const deleteAllOtherSessions = async (userId: number, sessionId: string) => { - await db.delete(session).where(and(eq(session.userId, userId), ne(session.id, sessionId))); + await db + .deleteFrom("session") + .where("id", "!=", sessionId) + .where("user_id", "=", userId) + .execute(); }; export const cleanupExpiredSessions = async () => { - await db.delete(session).where(lte(session.lastUsedAt, new Date(Date.now() - env.session.exp))); + await db + .deleteFrom("session") + .where("last_used_at", "<=", new Date(Date.now() - env.session.exp)) + .execute(); }; export const registerSessionUpgradeChallenge = async ( @@ -87,15 +94,18 @@ export const registerSessionUpgradeChallenge = async ( expiresAt: Date, ) => { try { - await db.insert(sessionUpgradeChallenge).values({ - sessionId, - clientId, - answer, - allowedIp, - expiresAt, - }); + await db + .insertInto("session_upgrade_challenge") + .values({ + session_id: sessionId, + client_id: clientId, + answer, + allowed_ip: allowedIp, + expires_at: expiresAt, + }) + .execute(); } catch (e) { - if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") { + if (e instanceof pg.DatabaseError && e.code === "23505") { throw new IntegrityError("Challenge already registered"); } throw e; @@ -107,22 +117,17 @@ export const consumeSessionUpgradeChallenge = async ( answer: string, ip: string, ) => { - const challenges = await db - .delete(sessionUpgradeChallenge) - .where( - and( - eq(sessionUpgradeChallenge.sessionId, sessionId), - eq(sessionUpgradeChallenge.answer, answer), - eq(sessionUpgradeChallenge.allowedIp, ip), - gt(sessionUpgradeChallenge.expiresAt, new Date()), - ), - ) - .returning({ clientId: sessionUpgradeChallenge.clientId }); - return challenges[0] ?? null; + const challenge = await db + .deleteFrom("session_upgrade_challenge") + .where("session_id", "=", sessionId) + .where("answer", "=", answer) + .where("allowed_ip", "=", ip) + .where("expires_at", ">", new Date()) + .returning("client_id") + .executeTakeFirst(); + return challenge ? { clientId: challenge.client_id } : null; }; export const cleanupExpiredSessionUpgradeChallenges = async () => { - await db - .delete(sessionUpgradeChallenge) - .where(lte(sessionUpgradeChallenge.expiresAt, new Date())); + await db.deleteFrom("session_upgrade_challenge").where("expires_at", "<=", new Date()).execute(); }; diff --git a/src/lib/server/db/user.ts b/src/lib/server/db/user.ts index d970438..3964a94 100644 --- a/src/lib/server/db/user.ts +++ b/src/lib/server/db/user.ts @@ -1,21 +1,36 @@ -import { eq } from "drizzle-orm"; -import db from "./drizzle"; -import { user } from "./schema"; +import db from "./kysely"; + +interface User { + id: number; + email: string; + nickname: string; + password: string; +} export const getUser = async (userId: number) => { - const users = await db.select().from(user).where(eq(user.id, userId)).limit(1); - return users[0] ?? null; + const user = await db + .selectFrom("user") + .selectAll() + .where("id", "=", userId) + .limit(1) + .executeTakeFirst(); + return user ? (user satisfies User) : null; }; export const getUserByEmail = async (email: string) => { - const users = await db.select().from(user).where(eq(user.email, email)).limit(1); - return users[0] ?? null; -}; - -export const setUserPassword = async (userId: number, password: string) => { - await db.update(user).set({ password }).where(eq(user.id, userId)); + const user = await db + .selectFrom("user") + .selectAll() + .where("email", "=", email) + .limit(1) + .executeTakeFirst(); + return user ? (user satisfies User) : null; }; export const setUserNickname = async (userId: number, nickname: string) => { - await db.update(user).set({ nickname }).where(eq(user.id, userId)); + await db.updateTable("user").set({ nickname }).where("id", "=", userId).execute(); +}; + +export const setUserPassword = async (userId: number, password: string) => { + await db.updateTable("user").set({ password }).where("id", "=", userId).execute(); }; diff --git a/src/lib/server/loadenv.ts b/src/lib/server/loadenv.ts index 01e442a..d6f4675 100644 --- a/src/lib/server/loadenv.ts +++ b/src/lib/server/loadenv.ts @@ -3,11 +3,19 @@ import { building } from "$app/environment"; import { env } from "$env/dynamic/private"; if (!building) { + if (!env.DATABASE_PASSWORD) throw new Error("DATABASE_PASSWORD not set"); if (!env.SESSION_SECRET) throw new Error("SESSION_SECRET not set"); } export default { - databaseUrl: env.DATABASE_URL || "local.db", + nodeEnv: env.NODE_ENV || "development", + database: { + host: env.DATABASE_HOST, + port: env.DATABASE_PORT ? parseInt(env.DATABASE_PORT, 10) : undefined, + user: env.DATABASE_USER, + password: env.DATABASE_PASSWORD!, + name: env.DATABASE_NAME, + }, session: { secret: env.SESSION_SECRET!, exp: ms(env.SESSION_EXPIRES || "14d"), diff --git a/src/lib/server/modules/mek.ts b/src/lib/server/modules/mek.ts index d65ef0a..1605d75 100644 --- a/src/lib/server/modules/mek.ts +++ b/src/lib/server/modules/mek.ts @@ -21,5 +21,5 @@ export const verifyClientEncMekSig = async ( } const data = JSON.stringify({ version, key: encMek }); - return verifySignature(Buffer.from(data), encMekSig, userClient.client.sigPubKey); + return verifySignature(Buffer.from(data), encMekSig, userClient.sigPubKey); }; diff --git a/src/lib/server/schemas/auth.ts b/src/lib/server/schemas/auth.ts index eae349f..e3d6264 100644 --- a/src/lib/server/schemas/auth.ts +++ b/src/lib/server/schemas/auth.ts @@ -7,7 +7,7 @@ export const passwordChangeRequest = z.object({ export type PasswordChangeRequest = z.infer; export const loginRequest = z.object({ - email: z.string().email().nonempty(), + email: z.string().email(), password: z.string().trim().nonempty(), }); export type LoginRequest = z.infer; diff --git a/src/lib/server/schemas/category.ts b/src/lib/server/schemas/category.ts new file mode 100644 index 0000000..d02fa4f --- /dev/null +++ b/src/lib/server/schemas/category.ts @@ -0,0 +1,55 @@ +import { z } from "zod"; + +export const categoryIdSchema = z.union([z.literal("root"), z.number().int().positive()]); + +export const categoryInfoResponse = z.object({ + metadata: z + .object({ + parent: categoryIdSchema, + mekVersion: z.number().int().positive(), + dek: z.string().base64().nonempty(), + dekVersion: z.string().datetime(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }) + .optional(), + subCategories: z.number().int().positive().array(), +}); +export type CategoryInfoResponse = z.infer; + +export const categoryFileAddRequest = z.object({ + file: z.number().int().positive(), +}); +export type CategoryFileAddRequest = z.infer; + +export const categoryFileListResponse = z.object({ + files: z.array( + z.object({ + file: z.number().int().positive(), + isRecursive: z.boolean(), + }), + ), +}); +export type CategoryFileListResponse = z.infer; + +export const categoryFileRemoveRequest = z.object({ + file: z.number().int().positive(), +}); +export type CategoryFileRemoveRequest = z.infer; + +export const categoryRenameRequest = z.object({ + dekVersion: z.string().datetime(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), +}); +export type CategoryRenameRequest = z.infer; + +export const categoryCreateRequest = z.object({ + parent: categoryIdSchema, + mekVersion: z.number().int().positive(), + dek: z.string().base64().nonempty(), + dekVersion: z.string().datetime(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), +}); +export type CategoryCreateRequest = z.infer; diff --git a/src/lib/server/schemas/directory.ts b/src/lib/server/schemas/directory.ts index 15a5886..016cfe8 100644 --- a/src/lib/server/schemas/directory.ts +++ b/src/lib/server/schemas/directory.ts @@ -1,9 +1,11 @@ import { z } from "zod"; +export const directoryIdSchema = z.union([z.literal("root"), z.number().int().positive()]); + export const directoryInfoResponse = z.object({ metadata: z .object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), @@ -29,7 +31,7 @@ export const directoryRenameRequest = z.object({ export type DirectoryRenameRequest = z.infer; export const directoryCreateRequest = z.object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index 781baf2..b6aa648 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -1,13 +1,15 @@ import mime from "mime"; import { z } from "zod"; +import { directoryIdSchema } from "./directory"; export const fileInfoResponse = z.object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), contentType: z .string() + .trim() .nonempty() .refine((value) => mime.getExtension(value) !== null), // MIME type contentIv: z.string().base64().nonempty(), @@ -17,6 +19,7 @@ export const fileInfoResponse = z.object({ createdAtIv: z.string().base64().nonempty().optional(), lastModifiedAt: z.string().base64().nonempty(), lastModifiedAtIv: z.string().base64().nonempty(), + categories: z.number().int().positive().array(), }); export type FileInfoResponse = z.infer; @@ -39,7 +42,7 @@ export const duplicateFileScanResponse = z.object({ export type DuplicateFileScanResponse = z.infer; export const fileUploadRequest = z.object({ - parent: z.union([z.enum(["root"]), z.number().int().positive()]), + parent: directoryIdSchema, mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), @@ -47,6 +50,7 @@ export const fileUploadRequest = z.object({ contentHmac: z.string().base64().nonempty(), contentType: z .string() + .trim() .nonempty() .refine((value) => mime.getExtension(value) !== null), // MIME type contentIv: z.string().base64().nonempty(), @@ -58,3 +62,8 @@ export const fileUploadRequest = z.object({ lastModifiedAtIv: z.string().base64().nonempty(), }); export type FileUploadRequest = z.infer; + +export const fileUploadResponse = z.object({ + file: z.number().int().positive(), +}); +export type FileUploadResponse = z.infer; diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts index 6f8270b..1fed0d0 100644 --- a/src/lib/server/schemas/index.ts +++ b/src/lib/server/schemas/index.ts @@ -1,4 +1,5 @@ export * from "./auth"; +export * from "./category"; export * from "./client"; export * from "./directory"; export * from "./file"; diff --git a/src/lib/server/schemas/user.ts b/src/lib/server/schemas/user.ts index b359467..e5cc8c1 100644 --- a/src/lib/server/schemas/user.ts +++ b/src/lib/server/schemas/user.ts @@ -1,12 +1,12 @@ import { z } from "zod"; export const userInfoResponse = z.object({ - email: z.string().email().nonempty(), + email: z.string().email(), nickname: z.string().nonempty(), }); export type UserInfoResponse = z.infer; export const nicknameChangeRequest = z.object({ - newNickname: z.string().min(2).max(8), + newNickname: z.string().trim().min(2).max(8), }); export type NicknameChangeRequest = z.infer; diff --git a/src/lib/server/services/category.ts b/src/lib/server/services/category.ts new file mode 100644 index 0000000..cb3db7a --- /dev/null +++ b/src/lib/server/services/category.ts @@ -0,0 +1,133 @@ +import { error } from "@sveltejs/kit"; +import { + registerCategory, + getAllCategoriesByParent, + getCategory, + setCategoryEncName, + unregisterCategory, + type CategoryId, + type NewCategory, +} from "$lib/server/db/category"; +import { IntegrityError } from "$lib/server/db/error"; +import { + getAllFilesByCategory, + getFile, + addFileToCategory, + removeFileFromCategory, +} from "$lib/server/db/file"; +import type { Ciphertext } from "$lib/server/db/schema"; + +export const getCategoryInformation = async (userId: number, categoryId: CategoryId) => { + const category = categoryId !== "root" ? await getCategory(userId, categoryId) : undefined; + if (category === null) { + error(404, "Invalid category id"); + } + + const categories = await getAllCategoriesByParent(userId, categoryId); + return { + metadata: category && { + parentId: category.parentId ?? ("root" as const), + mekVersion: category.mekVersion, + encDek: category.encDek, + dekVersion: category.dekVersion, + encName: category.encName, + }, + categories: categories.map(({ id }) => id), + }; +}; + +export const deleteCategory = async (userId: number, categoryId: number) => { + try { + await unregisterCategory(userId, categoryId); + } catch (e) { + if (e instanceof IntegrityError && e.message === "Category not found") { + error(404, "Invalid category id"); + } + throw e; + } +}; + +export const addCategoryFile = async (userId: number, categoryId: number, fileId: number) => { + const category = await getCategory(userId, categoryId); + const file = await getFile(userId, fileId); + if (!category) { + error(404, "Invalid category id"); + } else if (!file) { + error(404, "Invalid file id"); + } + + try { + await addFileToCategory(fileId, categoryId); + } catch (e) { + if (e instanceof IntegrityError && e.message === "File already added to category") { + error(400, "File already added"); + } + throw e; + } +}; + +export const getCategoryFiles = async (userId: number, categoryId: number, recurse: boolean) => { + const category = await getCategory(userId, categoryId); + if (!category) { + error(404, "Invalid category id"); + } + + const files = await getAllFilesByCategory(userId, categoryId, recurse); + return { files }; +}; + +export const removeCategoryFile = async (userId: number, categoryId: number, fileId: number) => { + const category = await getCategory(userId, categoryId); + const file = await getFile(userId, fileId); + if (!category) { + error(404, "Invalid category id"); + } else if (!file) { + error(404, "Invalid file id"); + } + + try { + await removeFileFromCategory(fileId, categoryId); + } catch (e) { + if (e instanceof IntegrityError && e.message === "File not found in category") { + error(400, "File not added"); + } + throw e; + } +}; + +export const renameCategory = async ( + userId: number, + categoryId: number, + dekVersion: Date, + newEncName: Ciphertext, +) => { + try { + await setCategoryEncName(userId, categoryId, dekVersion, newEncName); + } catch (e) { + if (e instanceof IntegrityError) { + if (e.message === "Category not found") { + error(404, "Invalid category id"); + } else if (e.message === "Invalid DEK version") { + error(400, "Invalid DEK version"); + } + } + throw e; + } +}; + +export const createCategory = async (params: NewCategory) => { + const oneMinuteAgo = new Date(Date.now() - 60 * 1000); + const oneMinuteLater = new Date(Date.now() + 60 * 1000); + if (params.dekVersion <= oneMinuteAgo || params.dekVersion >= oneMinuteLater) { + error(400, "Invalid DEK version"); + } + + try { + await registerCategory(params); + } catch (e) { + if (e instanceof IntegrityError && e.message === "Inactive MEK version") { + error(400, "Inactive MEK version"); + } + throw e; + } +}; diff --git a/src/lib/server/services/client.ts b/src/lib/server/services/client.ts index b5b0209..ee1b5b3 100644 --- a/src/lib/server/services/client.ts +++ b/src/lib/server/services/client.ts @@ -63,7 +63,7 @@ export const registerUserClient = async ( } try { - const clientId = await createClient(encPubKey, sigPubKey, userId); + const { id: clientId } = await createClient(encPubKey, sigPubKey, userId); return { challenge: await createUserClientChallenge(ip, userId, clientId, encPubKey) }; } catch (e) { if (e instanceof IntegrityError && e.message === "Public key(s) already registered") { diff --git a/src/lib/server/services/directory.ts b/src/lib/server/services/directory.ts index 4dc14ce..2525069 100644 --- a/src/lib/server/services/directory.ts +++ b/src/lib/server/services/directory.ts @@ -8,10 +8,12 @@ import { setDirectoryEncName, unregisterDirectory, getAllFilesByParent, - type NewDirectoryParams, + type DirectoryId, + type NewDirectory, } from "$lib/server/db/file"; +import type { Ciphertext } from "$lib/server/db/schema"; -export const getDirectoryInformation = async (userId: number, directoryId: "root" | number) => { +export const getDirectoryInformation = async (userId: number, directoryId: DirectoryId) => { const directory = directoryId !== "root" ? await getDirectory(userId, directoryId) : undefined; if (directory === null) { error(404, "Invalid directory id"); @@ -53,11 +55,10 @@ export const renameDirectory = async ( userId: number, directoryId: number, dekVersion: Date, - newEncName: string, - newEncNameIv: string, + newEncName: Ciphertext, ) => { try { - await setDirectoryEncName(userId, directoryId, dekVersion, newEncName, newEncNameIv); + await setDirectoryEncName(userId, directoryId, dekVersion, newEncName); } catch (e) { if (e instanceof IntegrityError) { if (e.message === "Directory not found") { @@ -70,7 +71,7 @@ export const renameDirectory = async ( } }; -export const createDirectory = async (params: NewDirectoryParams) => { +export const createDirectory = async (params: NewDirectory) => { const oneMinuteAgo = new Date(Date.now() - 60 * 1000); const oneMinuteLater = new Date(Date.now() + 60 * 1000); if (params.dekVersion <= oneMinuteAgo || params.dekVersion >= oneMinuteLater) { diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index 3589bed..d0b35ef 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -1,4 +1,5 @@ import { error } from "@sveltejs/kit"; +import { createHash } from "crypto"; import { createReadStream, createWriteStream } from "fs"; import { mkdir, stat, unlink } from "fs/promises"; import { dirname } from "path"; @@ -12,8 +13,10 @@ import { getFile, setFileEncName, unregisterFile, - type NewFileParams, + getAllFileCategories, + type NewFile, } from "$lib/server/db/file"; +import type { Ciphertext } from "$lib/server/db/schema"; import env from "$lib/server/loadenv"; export const getFileInformation = async (userId: number, fileId: number) => { @@ -22,6 +25,7 @@ export const getFileInformation = async (userId: number, fileId: number) => { error(404, "Invalid file id"); } + const categories = await getAllFileCategories(fileId); return { parentId: file.parentId ?? ("root" as const), mekVersion: file.mekVersion, @@ -32,13 +36,14 @@ export const getFileInformation = async (userId: number, fileId: number) => { encName: file.encName, encCreatedAt: file.encCreatedAt, encLastModifiedAt: file.encLastModifiedAt, + categories: categories.map(({ id }) => id), }; }; export const deleteFile = async (userId: number, fileId: number) => { try { - const filePath = await unregisterFile(userId, fileId); - unlink(filePath); // Intended + const { path } = await unregisterFile(userId, fileId); + unlink(path); // Intended } catch (e) { if (e instanceof IntegrityError && e.message === "File not found") { error(404, "Invalid file id"); @@ -64,11 +69,10 @@ export const renameFile = async ( userId: number, fileId: number, dekVersion: Date, - newEncName: string, - newEncNameIv: string, + newEncName: Ciphertext, ) => { try { - await setFileEncName(userId, fileId, dekVersion, newEncName, newEncNameIv); + await setFileEncName(userId, fileId, dekVersion, newEncName); } catch (e) { if (e instanceof IntegrityError) { if (e.message === "File not found") { @@ -95,8 +99,9 @@ const safeUnlink = async (path: string) => { }; export const uploadFile = async ( - params: Omit, + params: Omit, encContentStream: Readable, + encContentHash: Promise, ) => { const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); const oneMinuteLater = new Date(Date.now() + 60 * 1000); @@ -108,16 +113,40 @@ export const uploadFile = async ( await mkdir(dirname(path), { recursive: true }); try { - await pipeline(encContentStream, createWriteStream(path, { flags: "wx", mode: 0o600 })); - await registerFile({ + const hashStream = createHash("sha256"); + const [, hash] = await Promise.all([ + pipeline( + encContentStream, + async function* (source) { + for await (const chunk of source) { + hashStream.update(chunk); + yield chunk; + } + }, + createWriteStream(path, { flags: "wx", mode: 0o600 }), + ), + encContentHash, + ]); + if (hashStream.digest("base64") !== hash) { + throw new Error("Invalid checksum"); + } + + const { id: fileId } = await registerFile({ ...params, path, + encContentHash: hash, }); + return { fileId }; } catch (e) { await safeUnlink(path); if (e instanceof IntegrityError && e.message === "Inactive MEK version") { error(400, "Invalid MEK version"); + } else if ( + e instanceof Error && + (e.message === "Invalid request body" || e.message === "Invalid checksum") + ) { + error(400, "Invalid request body"); } throw e; } diff --git a/src/lib/server/services/mek.ts b/src/lib/server/services/mek.ts index e0deeb0..097906a 100644 --- a/src/lib/server/services/mek.ts +++ b/src/lib/server/services/mek.ts @@ -7,11 +7,11 @@ import { verifyClientEncMekSig } from "$lib/server/modules/mek"; export const getClientMekList = async (userId: number, clientId: number) => { const clientMeks = await getAllValidClientMeks(userId, clientId); return { - encMeks: clientMeks.map((clientMek) => ({ - version: clientMek.master_encryption_key.version, - state: clientMek.master_encryption_key.state as "active" | "retired", - encMek: clientMek.client_master_encryption_key.encMek, - encMekSig: clientMek.client_master_encryption_key.encMekSig, + encMeks: clientMeks.map(({ version, state, encMek, encMekSig }) => ({ + version, + state, + encMek, + encMekSig, })), }; }; diff --git a/src/lib/services/category.ts b/src/lib/services/category.ts new file mode 100644 index 0000000..587df3f --- /dev/null +++ b/src/lib/services/category.ts @@ -0,0 +1,31 @@ +import { callPostApi } from "$lib/hooks"; +import { generateDataKey, wrapDataKey, encryptString } from "$lib/modules/crypto"; +import type { CategoryCreateRequest, CategoryFileRemoveRequest } from "$lib/server/schemas"; +import type { MasterKey } from "$lib/stores"; + +export const requestCategoryCreation = async ( + name: string, + parentId: "root" | number, + masterKey: MasterKey, +) => { + const { dataKey, dataKeyVersion } = await generateDataKey(); + const nameEncrypted = await encryptString(name, dataKey); + + const res = await callPostApi("/api/category/create", { + parent: parentId, + mekVersion: masterKey.version, + dek: await wrapDataKey(dataKey, masterKey.key), + dekVersion: dataKeyVersion.toISOString(), + name: nameEncrypted.ciphertext, + nameIv: nameEncrypted.iv, + }); + return res.ok; +}; + +export const requestFileRemovalFromCategory = async (fileId: number, categoryId: number) => { + const res = await callPostApi( + `/api/category/${categoryId}/file/remove`, + { file: fileId }, + ); + return res.ok; +}; diff --git a/src/routes/(fullscreen)/+layout.svelte b/src/routes/(fullscreen)/+layout.svelte index 017f507..9426a87 100644 --- a/src/routes/(fullscreen)/+layout.svelte +++ b/src/routes/(fullscreen)/+layout.svelte @@ -1,11 +1,9 @@ - -
- {@render children()} -
+ + {@render children()} diff --git a/src/routes/(fullscreen)/auth/changePassword/+page.svelte b/src/routes/(fullscreen)/auth/changePassword/+page.svelte index 2b4875f..25ded91 100644 --- a/src/routes/(fullscreen)/auth/changePassword/+page.svelte +++ b/src/routes/(fullscreen)/auth/changePassword/+page.svelte @@ -1,9 +1,7 @@ + +{#if $category} + + + + (category = getCategoryInfo(id, $masterKeyStore?.get(1)?.key!))} + onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)} + subCategoryCreatePosition="top" + /> + {#if $category.id !== "root"} + + + + {/if} + + +{/if} + + { + if (await requestCategoryCreation(name, $category!.id, $masterKeyStore?.get(1)!)) { + category = getCategoryInfo($category!.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} +/> diff --git a/src/routes/(fullscreen)/file/[id]/service.ts b/src/routes/(fullscreen)/file/[id]/service.ts index fcc5ce7..43f0134 100644 --- a/src/routes/(fullscreen)/file/[id]/service.ts +++ b/src/routes/(fullscreen)/file/[id]/service.ts @@ -1,4 +1,8 @@ +import { callPostApi } from "$lib/hooks"; import { getFileCache, storeFileCache, downloadFile } from "$lib/modules/file"; +import type { CategoryFileAddRequest } from "$lib/server/schemas"; + +export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category"; export const requestFileDownload = async ( fileId: number, @@ -12,3 +16,10 @@ export const requestFileDownload = async ( storeFileCache(fileId, fileBuffer); // Intended return fileBuffer; }; + +export const requestFileAdditionToCategory = async (fileId: number, categoryId: number) => { + const res = await callPostApi(`/api/category/${categoryId}/file/add`, { + file: fileId, + }); + return res.ok; +}; diff --git a/src/routes/(fullscreen)/file/downloads/+page.svelte b/src/routes/(fullscreen)/file/downloads/+page.svelte index a29b147..e1bad0e 100644 --- a/src/routes/(fullscreen)/file/downloads/+page.svelte +++ b/src/routes/(fullscreen)/file/downloads/+page.svelte @@ -1,10 +1,11 @@ - -
-
+ + +

암호 키 파일을 저장하셨나요?

암호 키 파일은 유출 방지를 위해 이 화면에서만 저장할 수 있어요. 파일이 잘 저장되었는지 다시 한 번 확인해 주세요.

- -
- - -
+ + + -
+ diff --git a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte index 7b9d98d..2f00b32 100644 --- a/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte +++ b/src/routes/(fullscreen)/key/export/BeforeContinueModal.svelte @@ -1,31 +1,20 @@ - -
-
-

내보내지 않고 계속할까요?

-

암호 키 파일은 유출 방지를 위해 이 화면에서만 저장할 수 있어요.

-
-
- - -
-
-
+ +

암호 키 파일은 유출 방지를 위해 이 화면에서만 저장할 수 있어요.

+
diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index 330a39e..03d6c6d 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -1,8 +1,8 @@
-
- - {@render children()} - -
+ + {@render children()} +
diff --git a/src/routes/(main)/BottomBar.svelte b/src/routes/(main)/BottomBar.svelte index db42bd2..60c2b24 100644 --- a/src/routes/(main)/BottomBar.svelte +++ b/src/routes/(main)/BottomBar.svelte @@ -1,7 +1,7 @@
- -
- {#each pages as { path, label, icon: Icon }} - {@const textColor = !page.url.pathname.startsWith(path) ? "text-gray-600" : ""} - - {/each} -
+ + {#each pages as { path, label, icon: Icon }} + + {/each}
diff --git a/src/routes/(main)/category/+page.svelte b/src/routes/(main)/category/+page.svelte deleted file mode 100644 index 73d68b7..0000000 --- a/src/routes/(main)/category/+page.svelte +++ /dev/null @@ -1,3 +0,0 @@ -
-

아직 개발 중이에요.

-
diff --git a/src/routes/(main)/category/[[id]]/+page.svelte b/src/routes/(main)/category/[[id]]/+page.svelte new file mode 100644 index 0000000..9bfa2a4 --- /dev/null +++ b/src/routes/(main)/category/[[id]]/+page.svelte @@ -0,0 +1,104 @@ + + + + 카테고리 + + +{#if data.id !== "root"} + +{/if} +
+ {#if $info} + goto(`/file/${id}`)} + onFileRemoveClick={async ({ id }) => { + await requestFileRemovalFromCategory(id, data.id as number); + info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + }} + onSubCategoryClick={({ id }) => goto(`/category/${id}`)} + onSubCategoryCreateClick={() => (isCategoryCreateModalOpen = true)} + onSubCategoryMenuClick={(subCategory) => { + context.selectedCategory = subCategory; + isCategoryMenuBottomSheetOpen = true; + }} + /> + {/if} +
+ + { + if (await requestCategoryCreation(name, data.id, $masterKeyStore?.get(1)!)) { + info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} +/> + + { + isCategoryMenuBottomSheetOpen = false; + isCategoryRenameModalOpen = true; + }} + onDeleteClick={() => { + isCategoryMenuBottomSheetOpen = false; + isCategoryDeleteModalOpen = true; + }} +/> + { + if (await requestCategoryRename(context.selectedCategory!, newName)) { + info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} +/> + { + if (await requestCategoryDeletion(context.selectedCategory!)) { + info = getCategoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} +/> diff --git a/src/routes/(main)/category/[[id]]/+page.ts b/src/routes/(main)/category/[[id]]/+page.ts new file mode 100644 index 0000000..cfa37f8 --- /dev/null +++ b/src/routes/(main)/category/[[id]]/+page.ts @@ -0,0 +1,17 @@ +import { error } from "@sveltejs/kit"; +import { z } from "zod"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ params }) => { + const zodRes = z + .object({ + id: z.coerce.number().int().positive().optional(), + }) + .safeParse(params); + if (!zodRes.success) error(404, "Not found"); + const { id } = zodRes.data; + + return { + id: id ? id : ("root" as const), + }; +}; diff --git a/src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte b/src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte new file mode 100644 index 0000000..5e792d1 --- /dev/null +++ b/src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte @@ -0,0 +1,29 @@ + + +{#if context.selectedCategory} + {@const { name } = context.selectedCategory} + +

+ 모든 하위 카테고리도 함께 삭제돼요.
+ 하지만 카테고리에 추가된 파일들은 삭제되지 않아요. +

+
+{/if} diff --git a/src/routes/(main)/category/[[id]]/CategoryMenuBottomSheet.svelte b/src/routes/(main)/category/[[id]]/CategoryMenuBottomSheet.svelte new file mode 100644 index 0000000..73f91ce --- /dev/null +++ b/src/routes/(main)/category/[[id]]/CategoryMenuBottomSheet.svelte @@ -0,0 +1,31 @@ + + +{#if context.selectedCategory} + {@const { name } = context.selectedCategory} + + +
+ + 이름 바꾸기 + + + 삭제하기 + +
+{/if} diff --git a/src/routes/(main)/category/[[id]]/CategoryRenameModal.svelte b/src/routes/(main)/category/[[id]]/CategoryRenameModal.svelte new file mode 100644 index 0000000..a3ce045 --- /dev/null +++ b/src/routes/(main)/category/[[id]]/CategoryRenameModal.svelte @@ -0,0 +1,17 @@ + + +{#if context.selectedCategory} + {@const { name } = context.selectedCategory} + +{/if} diff --git a/src/routes/(main)/category/[[id]]/service.svelte.ts b/src/routes/(main)/category/[[id]]/service.svelte.ts new file mode 100644 index 0000000..b573041 --- /dev/null +++ b/src/routes/(main)/category/[[id]]/service.svelte.ts @@ -0,0 +1,34 @@ +import { getContext, setContext } from "svelte"; +import { callPostApi } from "$lib/hooks"; +import { encryptString } from "$lib/modules/crypto"; +import type { SelectedCategory } from "$lib/components/molecules"; +import type { CategoryRenameRequest } from "$lib/server/schemas"; + +export { requestCategoryCreation, requestFileRemovalFromCategory } from "$lib/services/category"; + +export const createContext = () => { + const context = $state({ + selectedCategory: undefined as SelectedCategory | undefined, + }); + return setContext("context", context); +}; + +export const useContext = () => { + return getContext>("context"); +}; + +export const requestCategoryRename = async (category: SelectedCategory, newName: string) => { + const newNameEncrypted = await encryptString(newName, category.dataKey); + + const res = await callPostApi(`/api/category/${category.id}/rename`, { + dekVersion: category.dataKeyVersion.toISOString(), + name: newNameEncrypted.ciphertext, + nameIv: newNameEncrypted.iv, + }); + return res.ok; +}; + +export const requestCategoryDeletion = async (category: SelectedCategory) => { + const res = await callPostApi(`/api/category/${category.id}/delete`); + return res.ok; +}; diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index f8bd0c9..98572b3 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -2,51 +2,45 @@ import { onMount } from "svelte"; import type { Writable } from "svelte/store"; import { goto } from "$app/navigation"; - import { TopBar } from "$lib/components"; - import { FloatingButton } from "$lib/components/buttons"; + import { FloatingButton } from "$lib/components/atoms"; + import { TopBar } from "$lib/components/molecules"; import { getDirectoryInfo, type DirectoryInfo } from "$lib/modules/filesystem"; import { masterKeyStore, hmacSecretStore } from "$lib/stores"; - import CreateBottomSheet from "./CreateBottomSheet.svelte"; - import CreateDirectoryModal from "./CreateDirectoryModal.svelte"; - import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte"; + import DirectoryCreateModal from "./DirectoryCreateModal.svelte"; import DirectoryEntries from "./DirectoryEntries"; - import DirectoryEntryMenuBottomSheet from "./DirectoryEntryMenuBottomSheet.svelte"; import DownloadStatusCard from "./DownloadStatusCard.svelte"; import DuplicateFileModal from "./DuplicateFileModal.svelte"; - import RenameDirectoryEntryModal from "./RenameDirectoryEntryModal.svelte"; + import EntryCreateBottomSheet from "./EntryCreateBottomSheet.svelte"; + import EntryDeleteModal from "./EntryDeleteModal.svelte"; + import EntryMenuBottomSheet from "./EntryMenuBottomSheet.svelte"; + import EntryRenameModal from "./EntryRenameModal.svelte"; import UploadStatusCard from "./UploadStatusCard.svelte"; import { + createContext, requestHmacSecretDownload, requestDirectoryCreation, requestFileUpload, - requestDirectoryEntryRename, - requestDirectoryEntryDeletion, - type SelectedDirectoryEntry, - } from "./service"; + requestEntryRename, + requestEntryDeletion, + } from "./service.svelte"; import IconAdd from "~icons/material-symbols/add"; let { data } = $props(); + let context = createContext(); let info: Writable | undefined = $state(); let fileInput: HTMLInputElement | undefined = $state(); - let resolveForDuplicateFileModal: ((res: boolean) => void) | undefined = $state(); let duplicatedFile: File | undefined = $state(); - let selectedEntry: SelectedDirectoryEntry | undefined = $state(); + let resolveForDuplicateFileModal: ((res: boolean) => void) | undefined = $state(); - let isCreateBottomSheetOpen = $state(false); - let isCreateDirectoryModalOpen = $state(false); + let isEntryCreateBottomSheetOpen = $state(false); + let isDirectoryCreateModalOpen = $state(false); let isDuplicateFileModalOpen = $state(false); - let isDirectoryEntryMenuBottomSheetOpen = $state(false); - let isRenameDirectoryEntryModalOpen = $state(false); - let isDeleteDirectoryEntryModalOpen = $state(false); - - const createDirectory = async (name: string) => { - await requestDirectoryCreation(name, data.id, $masterKeyStore?.get(1)!); - isCreateDirectoryModalOpen = false; - info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME - }; + let isEntryMenuBottomSheetOpen = $state(false); + let isEntryRenameModalOpen = $state(false); + let isEntryDeleteModalOpen = $state(false); const uploadFile = () => { const files = fileInput?.files; @@ -55,22 +49,19 @@ for (const file of files) { requestFileUpload(file, data.id, $hmacSecretStore?.get(1)!, $masterKeyStore?.get(1)!, () => { return new Promise((resolve) => { - resolveForDuplicateFileModal = resolve; duplicatedFile = file; + resolveForDuplicateFileModal = resolve; isDuplicateFileModalOpen = true; }); }) .then((res) => { if (!res) return; - // TODO: FIXME info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); - window.alert(`'${file.name}' 파일이 업로드되었어요.`); }) .catch((e: Error) => { // TODO: FIXME console.error(e); - window.alert(`'${file.name}' 파일 업로드에 실패했어요.\n${e.message}`); }); } @@ -94,13 +85,12 @@ -
+
{#if data.id !== "root"} - + {/if} {#if $info} - {@const topMargin = data.id === "root" ? "mt-4" : ""} -
+
goto("/file/uploads")} /> goto("/file/downloads")} /> @@ -110,8 +100,8 @@ info={$info} onEntryClick={({ type, id }) => goto(`/${type}/${id}`)} onEntryMenuClick={(entry) => { - selectedEntry = entry; - isDirectoryEntryMenuBottomSheetOpen = true; + context.selectedEntry = entry; + isEntryMenuBottomSheetOpen = true; }} /> {/key} @@ -122,65 +112,72 @@ { - isCreateBottomSheetOpen = true; + isEntryCreateBottomSheetOpen = true; }} + class="bottom-24 right-4" /> - { - isCreateBottomSheetOpen = false; - isCreateDirectoryModalOpen = true; + isEntryCreateBottomSheetOpen = false; + isDirectoryCreateModalOpen = true; }} onFileUploadClick={() => { - isCreateBottomSheetOpen = false; + isEntryCreateBottomSheetOpen = false; fileInput?.click(); }} /> - + { + if (await requestDirectoryCreation(name, data.id, $masterKeyStore?.get(1)!)) { + info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; + }} +/> { + onbeforeclose={() => { resolveForDuplicateFileModal?.(false); - resolveForDuplicateFileModal = undefined; - duplicatedFile = undefined; isDuplicateFileModalOpen = false; }} - onDuplicateClick={() => { + onUploadClick={() => { resolveForDuplicateFileModal?.(true); - resolveForDuplicateFileModal = undefined; - duplicatedFile = undefined; isDuplicateFileModalOpen = false; }} /> - { - isDirectoryEntryMenuBottomSheetOpen = false; - isRenameDirectoryEntryModalOpen = true; + isEntryMenuBottomSheetOpen = false; + isEntryRenameModalOpen = true; }} onDeleteClick={() => { - isDirectoryEntryMenuBottomSheetOpen = false; - isDeleteDirectoryEntryModalOpen = true; + isEntryMenuBottomSheetOpen = false; + isEntryDeleteModalOpen = true; }} /> - { - await requestDirectoryEntryRename(selectedEntry!, newName); - info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME - return true; + { + if (await requestEntryRename(context.selectedEntry!, newName)) { + info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; }} /> - { - await requestDirectoryEntryDeletion(selectedEntry!); - info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME - return true; + if (await requestEntryDeletion(context.selectedEntry!)) { + info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME + return true; + } + return false; }} /> diff --git a/src/routes/(main)/directory/[[id]]/CreateBottomSheet.svelte b/src/routes/(main)/directory/[[id]]/CreateBottomSheet.svelte deleted file mode 100644 index c306c8f..0000000 --- a/src/routes/(main)/directory/[[id]]/CreateBottomSheet.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - -
- -
- -

폴더 만들기

-
-
- -
- -

파일 업로드

-
-
-
-
diff --git a/src/routes/(main)/directory/[[id]]/CreateDirectoryModal.svelte b/src/routes/(main)/directory/[[id]]/CreateDirectoryModal.svelte deleted file mode 100644 index 52265fe..0000000 --- a/src/routes/(main)/directory/[[id]]/CreateDirectoryModal.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - - -

새 폴더

-
- -
-
- - -
-
diff --git a/src/routes/(main)/directory/[[id]]/DeleteDirectoryEntryModal.svelte b/src/routes/(main)/directory/[[id]]/DeleteDirectoryEntryModal.svelte deleted file mode 100644 index 07fb6dd..0000000 --- a/src/routes/(main)/directory/[[id]]/DeleteDirectoryEntryModal.svelte +++ /dev/null @@ -1,63 +0,0 @@ - - - - {#if selectedEntry} - {@const { type, name } = selectedEntry} - {@const nameShort = name.length > 20 ? `${name.slice(0, 20)}...` : name} -
-
-

- {#if type === "directory"} - '{nameShort}' 폴더를 삭제할까요? - {:else} - '{nameShort}' 파일을 삭제할까요? - {/if} -

-

- {#if type === "directory"} - 삭제한 폴더는 복구할 수 없어요.
- 폴더 안의 모든 파일과 폴더도 함께 삭제돼요. - {:else} - 삭제한 파일은 복구할 수 없어요. - {/if} -

-
-
- - -
-
- {/if} -
diff --git a/src/routes/(main)/directory/[[id]]/DirectoryCreateModal.svelte b/src/routes/(main)/directory/[[id]]/DirectoryCreateModal.svelte new file mode 100644 index 0000000..e43b569 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/DirectoryCreateModal.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte index e2a187c..7ad3af7 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/DirectoryEntries.svelte @@ -7,6 +7,7 @@ type DirectoryInfo, type FileInfo, } from "$lib/modules/filesystem"; + import { SortBy, sortEntries } from "$lib/modules/util"; import { fileUploadStatusStore, isFileUploading, @@ -15,14 +16,13 @@ } from "$lib/stores"; import File from "./File.svelte"; import SubDirectory from "./SubDirectory.svelte"; - import { SortBy, sortEntries } from "./service"; import UploadingFile from "./UploadingFile.svelte"; - import type { SelectedDirectoryEntry } from "../service"; + import type { SelectedEntry } from "../service.svelte"; interface Props { info: DirectoryInfo; - onEntryClick: (entry: SelectedDirectoryEntry) => void; - onEntryMenuClick: (entry: SelectedDirectoryEntry) => void; + onEntryClick: (entry: SelectedEntry) => void; + onEntryMenuClick: (entry: SelectedEntry) => void; sortBy?: SortBy; } @@ -68,7 +68,7 @@ $fileUploadStatusStore .filter((statusStore) => { const { parentId, status } = get(statusStore); - return parentId === info.id && !isFileUploading(status); + return parentId === info.id && isFileUploading(status); }) .map((status) => ({ type: "uploading-file", diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte index 0dad51b..fd59d03 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/File.svelte @@ -1,16 +1,17 @@ {#if $info} - - -
-
-
- -
-
-

- {$info.name} -

-

- {formatDateTime($info.createdAt ?? $info.lastModifiedAt)} -

-
- -
-
+ + + {/if} - - diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte index 11a788a..5454695 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/SubDirectory.svelte @@ -1,17 +1,18 @@ {#if $info} - - -
-
-
- -
-

- {$info.name} -

- -
-
+ + + {/if} - - diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts b/src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts index 72ab278..075644e 100644 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntries/index.ts @@ -1,2 +1 @@ export { default } from "./DirectoryEntries.svelte"; -export * from "./service"; diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts b/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts deleted file mode 100644 index e1fc716..0000000 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntries/service.ts +++ /dev/null @@ -1,29 +0,0 @@ -export enum SortBy { - NAME_ASC, - NAME_DESC, -} - -type SortFunc = (a?: string, b?: string) => number; - -const sortByNameAsc: SortFunc = (a, b) => { - if (a && b) return a.localeCompare(b); - if (a) return -1; - if (b) return 1; - return 0; -}; - -const sortByNameDesc: SortFunc = (a, b) => -sortByNameAsc(a, b); - -export const sortEntries = ( - entries: T[], - sortBy: SortBy = SortBy.NAME_ASC, -) => { - let sortFunc: SortFunc; - if (sortBy === SortBy.NAME_ASC) { - sortFunc = sortByNameAsc; - } else { - sortFunc = sortByNameDesc; - } - - entries.sort((a, b) => sortFunc(a.name, b.name)); -}; diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntryMenuBottomSheet.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntryMenuBottomSheet.svelte deleted file mode 100644 index 231acc5..0000000 --- a/src/routes/(main)/directory/[[id]]/DirectoryEntryMenuBottomSheet.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - - -
- {#if selectedEntry} - {@const { type, name } = selectedEntry} -
-
- {#if type === "directory"} - - {:else} - - {/if} -
-

- {name} -

-
-
- {/if} - -
- -

이름 바꾸기

-
-
- -
- -

삭제하기

-
-
-
-
diff --git a/src/routes/(main)/directory/[[id]]/DuplicateFileModal.svelte b/src/routes/(main)/directory/[[id]]/DuplicateFileModal.svelte index 6c9d9af..28b8d6b 100644 --- a/src/routes/(main)/directory/[[id]]/DuplicateFileModal.svelte +++ b/src/routes/(main)/directory/[[id]]/DuplicateFileModal.svelte @@ -1,30 +1,27 @@ - - {#if file} - {@const { name } = file} - {@const nameShort = name.length > 20 ? `${name.slice(0, 20)}...` : name} -
-
-

'{nameShort}' 파일이 있어요.

-

예전에 이미 업로드된 파일이에요. 그래도 업로드할까요?

-
-
- - -
-
- {/if} -
+{#if file} + {@const { name } = file} + +

예전에 이미 업로드된 파일이에요. 그래도 업로드할까요?

+
+{/if} diff --git a/src/routes/(main)/directory/[[id]]/EntryCreateBottomSheet.svelte b/src/routes/(main)/directory/[[id]]/EntryCreateBottomSheet.svelte new file mode 100644 index 0000000..4eda017 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/EntryCreateBottomSheet.svelte @@ -0,0 +1,34 @@ + + + + + 폴더 만들기 + + + 파일 업로드 + + diff --git a/src/routes/(main)/directory/[[id]]/EntryDeleteModal.svelte b/src/routes/(main)/directory/[[id]]/EntryDeleteModal.svelte new file mode 100644 index 0000000..ad9ca2c --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/EntryDeleteModal.svelte @@ -0,0 +1,33 @@ + + +{#if context.selectedEntry} + {@const { name, type } = context.selectedEntry} + +

+ {#if type === "directory"} + 삭제한 폴더는 복구할 수 없어요.
+ 폴더 안의 모든 파일과 폴더도 함께 삭제돼요. + {:else} + 삭제한 파일은 복구할 수 없어요. + {/if} +

+
+{/if} diff --git a/src/routes/(main)/directory/[[id]]/EntryMenuBottomSheet.svelte b/src/routes/(main)/directory/[[id]]/EntryMenuBottomSheet.svelte new file mode 100644 index 0000000..95e675a --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/EntryMenuBottomSheet.svelte @@ -0,0 +1,31 @@ + + +{#if context.selectedEntry} + {@const { name, type } = context.selectedEntry} + + +
+ + 이름 바꾸기 + + + 삭제하기 + +
+{/if} diff --git a/src/routes/(main)/directory/[[id]]/EntryRenameModal.svelte b/src/routes/(main)/directory/[[id]]/EntryRenameModal.svelte new file mode 100644 index 0000000..225884d --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/EntryRenameModal.svelte @@ -0,0 +1,17 @@ + + +{#if context.selectedEntry} + {@const { name } = context.selectedEntry} + +{/if} diff --git a/src/routes/(main)/directory/[[id]]/RenameDirectoryEntryModal.svelte b/src/routes/(main)/directory/[[id]]/RenameDirectoryEntryModal.svelte deleted file mode 100644 index 015d157..0000000 --- a/src/routes/(main)/directory/[[id]]/RenameDirectoryEntryModal.svelte +++ /dev/null @@ -1,47 +0,0 @@ - - - -

이름 바꾸기

-
- -
-
- - -
-
diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.svelte.ts similarity index 71% rename from src/routes/(main)/directory/[[id]]/service.ts rename to src/routes/(main)/directory/[[id]]/service.svelte.ts index c9a62b2..3c5f689 100644 --- a/src/routes/(main)/directory/[[id]]/service.ts +++ b/src/routes/(main)/directory/[[id]]/service.svelte.ts @@ -1,7 +1,8 @@ +import { getContext, setContext } from "svelte"; import { callGetApi, callPostApi } from "$lib/hooks"; import { storeHmacSecrets } from "$lib/indexedDB"; import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$lib/modules/crypto"; -import { deleteFileCache, uploadFile } from "$lib/modules/file"; +import { storeFileCache, deleteFileCache, uploadFile } from "$lib/modules/file"; import type { DirectoryRenameRequest, DirectoryCreateRequest, @@ -11,7 +12,7 @@ import type { } from "$lib/server/schemas"; import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores"; -export interface SelectedDirectoryEntry { +export interface SelectedEntry { type: "directory" | "file"; id: number; dataKey: CryptoKey; @@ -19,6 +20,17 @@ export interface SelectedDirectoryEntry { name: string; } +export const createContext = () => { + const context = $state({ + selectedEntry: undefined as SelectedEntry | undefined, + }); + return setContext("context", context); +}; + +export const useContext = () => { + return getContext>("context"); +}; + export const requestHmacSecretDownload = async (masterKey: CryptoKey) => { // TODO: MEK rotation @@ -46,7 +58,8 @@ export const requestDirectoryCreation = async ( ) => { const { dataKey, dataKeyVersion } = await generateDataKey(); const nameEncrypted = await encryptString(name, dataKey); - await callPostApi("/api/directory/create", { + + const res = await callPostApi("/api/directory/create", { parent: parentId, mekVersion: masterKey.version, dek: await wrapDataKey(dataKey, masterKey.key), @@ -54,6 +67,7 @@ export const requestDirectoryCreation = async ( name: nameEncrypted.ciphertext, nameIv: nameEncrypted.iv, }); + return res.ok; }; export const requestFileUpload = async ( @@ -63,31 +77,34 @@ export const requestFileUpload = async ( masterKey: MasterKey, onDuplicate: () => Promise, ) => { - return await uploadFile(file, parentId, hmacSecret, masterKey, onDuplicate); + const res = await uploadFile(file, parentId, hmacSecret, masterKey, onDuplicate); + if (!res) return false; + + storeFileCache(res.fileId, res.fileBuffer); // Intended + return true; }; -export const requestDirectoryEntryRename = async ( - entry: SelectedDirectoryEntry, - newName: string, -) => { +export const requestEntryRename = async (entry: SelectedEntry, newName: string) => { const newNameEncrypted = await encryptString(newName, entry.dataKey); + let res; if (entry.type === "directory") { - await callPostApi(`/api/directory/${entry.id}/rename`, { + res = await callPostApi(`/api/directory/${entry.id}/rename`, { dekVersion: entry.dataKeyVersion.toISOString(), name: newNameEncrypted.ciphertext, nameIv: newNameEncrypted.iv, }); } else { - await callPostApi(`/api/file/${entry.id}/rename`, { + res = await callPostApi(`/api/file/${entry.id}/rename`, { dekVersion: entry.dataKeyVersion.toISOString(), name: newNameEncrypted.ciphertext, nameIv: newNameEncrypted.iv, }); } + return res.ok; }; -export const requestDirectoryEntryDeletion = async (entry: SelectedDirectoryEntry) => { +export const requestEntryDeletion = async (entry: SelectedEntry) => { const res = await callPostApi(`/api/${entry.type}/${entry.id}/delete`); if (!res.ok) return false; diff --git a/src/routes/(main)/menu/MenuEntryButton.svelte b/src/routes/(main)/menu/MenuEntryButton.svelte index eb236c9..84341e6 100644 --- a/src/routes/(main)/menu/MenuEntryButton.svelte +++ b/src/routes/(main)/menu/MenuEntryButton.svelte @@ -1,25 +1,23 @@ - -
-
- -
-

- {@render children?.()} -

-
-
+ + {@render children()} + diff --git a/src/routes/api/category/[id]/+server.ts b/src/routes/api/category/[id]/+server.ts new file mode 100644 index 0000000..4a486fa --- /dev/null +++ b/src/routes/api/category/[id]/+server.ts @@ -0,0 +1,33 @@ +import { error, json } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryInfoResponse, type CategoryInfoResponse } from "$lib/server/schemas"; +import { getCategoryInformation } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = async ({ locals, params }) => { + const { userId } = await authorize(locals, "activeClient"); + + const zodRes = z + .object({ + id: z.union([z.enum(["root"]), z.coerce.number().int().positive()]), + }) + .safeParse(params); + if (!zodRes.success) error(400, "Invalid path parameters"); + const { id } = zodRes.data; + + const { metadata, categories } = await getCategoryInformation(userId, id); + return json( + categoryInfoResponse.parse({ + metadata: metadata && { + parent: metadata.parentId, + mekVersion: metadata.mekVersion, + dek: metadata.encDek, + dekVersion: metadata.dekVersion.toISOString(), + name: metadata.encName.ciphertext, + nameIv: metadata.encName.iv, + }, + subCategories: categories, + } satisfies CategoryInfoResponse), + ); +}; diff --git a/src/routes/api/category/[id]/delete/+server.ts b/src/routes/api/category/[id]/delete/+server.ts new file mode 100644 index 0000000..cbbe356 --- /dev/null +++ b/src/routes/api/category/[id]/delete/+server.ts @@ -0,0 +1,20 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { deleteCategory } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, params }) => { + const { userId } = await authorize(locals, "activeClient"); + + const zodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!zodRes.success) error(400, "Invalid path parameters"); + const { id } = zodRes.data; + + await deleteCategory(userId, id); + return text("Category deleted", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/category/[id]/file/add/+server.ts b/src/routes/api/category/[id]/file/add/+server.ts new file mode 100644 index 0000000..2eaf2f2 --- /dev/null +++ b/src/routes/api/category/[id]/file/add/+server.ts @@ -0,0 +1,25 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryFileAddRequest } from "$lib/server/schemas"; +import { addCategoryFile } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, params, request }) => { + const { userId } = await authorize(locals, "activeClient"); + + const paramsZodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!paramsZodRes.success) error(400, "Invalid path parameters"); + const { id } = paramsZodRes.data; + + const bodyZodRes = categoryFileAddRequest.safeParse(await request.json()); + if (!bodyZodRes.success) error(400, "Invalid request body"); + const { file } = bodyZodRes.data; + + await addCategoryFile(userId, id, file); + return text("File added", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/category/[id]/file/list/+server.ts b/src/routes/api/category/[id]/file/list/+server.ts new file mode 100644 index 0000000..e354d8b --- /dev/null +++ b/src/routes/api/category/[id]/file/list/+server.ts @@ -0,0 +1,36 @@ +import { error, json } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryFileListResponse, type CategoryFileListResponse } from "$lib/server/schemas"; +import { getCategoryFiles } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = async ({ locals, url, params }) => { + const { userId } = await authorize(locals, "activeClient"); + + const paramsZodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!paramsZodRes.success) error(400, "Invalid path parameters"); + const { id } = paramsZodRes.data; + + const queryZodRes = z + .object({ + recurse: z + .enum(["true", "false"]) + .transform((value) => value === "true") + .nullable(), + }) + .safeParse({ recurse: url.searchParams.get("recurse") }); + if (!queryZodRes.success) error(400, "Invalid query parameters"); + const { recurse } = queryZodRes.data; + + const { files } = await getCategoryFiles(userId, id, recurse ?? false); + return json( + categoryFileListResponse.parse({ + files: files.map(({ id, isRecursive }) => ({ file: id, isRecursive })), + } satisfies CategoryFileListResponse), + ); +}; diff --git a/src/routes/api/category/[id]/file/remove/+server.ts b/src/routes/api/category/[id]/file/remove/+server.ts new file mode 100644 index 0000000..6fdcccf --- /dev/null +++ b/src/routes/api/category/[id]/file/remove/+server.ts @@ -0,0 +1,25 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryFileRemoveRequest } from "$lib/server/schemas"; +import { removeCategoryFile } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, params, request }) => { + const { userId } = await authorize(locals, "activeClient"); + + const paramsZodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!paramsZodRes.success) error(400, "Invalid path parameters"); + const { id } = paramsZodRes.data; + + const bodyZodRes = categoryFileRemoveRequest.safeParse(await request.json()); + if (!bodyZodRes.success) error(400, "Invalid request body"); + const { file } = bodyZodRes.data; + + await removeCategoryFile(userId, id, file); + return text("File removed", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/category/[id]/rename/+server.ts b/src/routes/api/category/[id]/rename/+server.ts new file mode 100644 index 0000000..5351544 --- /dev/null +++ b/src/routes/api/category/[id]/rename/+server.ts @@ -0,0 +1,25 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryRenameRequest } from "$lib/server/schemas"; +import { renameCategory } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, params, request }) => { + const { userId } = await authorize(locals, "activeClient"); + + const paramsZodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!paramsZodRes.success) error(400, "Invalid path parameters"); + const { id } = paramsZodRes.data; + + const bodyZodRes = categoryRenameRequest.safeParse(await request.json()); + if (!bodyZodRes.success) error(400, "Invalid request body"); + const { dekVersion, name, nameIv } = bodyZodRes.data; + + await renameCategory(userId, id, new Date(dekVersion), { ciphertext: name, iv: nameIv }); + return text("Category renamed", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/category/create/+server.ts b/src/routes/api/category/create/+server.ts new file mode 100644 index 0000000..216d850 --- /dev/null +++ b/src/routes/api/category/create/+server.ts @@ -0,0 +1,23 @@ +import { error, text } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { categoryCreateRequest } from "$lib/server/schemas"; +import { createCategory } from "$lib/server/services/category"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, request }) => { + const { userId } = await authorize(locals, "activeClient"); + + const zodRes = categoryCreateRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { parent, mekVersion, dek, dekVersion, name, nameIv } = zodRes.data; + + await createCategory({ + userId, + parentId: parent, + mekVersion, + encDek: dek, + dekVersion: new Date(dekVersion), + encName: { ciphertext: name, iv: nameIv }, + }); + return text("Category created", { headers: { "Content-Type": "text/plain" } }); +}; diff --git a/src/routes/api/directory/[id]/rename/+server.ts b/src/routes/api/directory/[id]/rename/+server.ts index 0d95e13..cc50b2f 100644 --- a/src/routes/api/directory/[id]/rename/+server.ts +++ b/src/routes/api/directory/[id]/rename/+server.ts @@ -20,6 +20,6 @@ export const POST: RequestHandler = async ({ locals, params, request }) => { if (!bodyZodRes.success) error(400, "Invalid request body"); const { dekVersion, name, nameIv } = bodyZodRes.data; - await renameDirectory(userId, id, new Date(dekVersion), name, nameIv); + await renameDirectory(userId, id, new Date(dekVersion), { ciphertext: name, iv: nameIv }); return text("Directory renamed", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/directory/create/+server.ts b/src/routes/api/directory/create/+server.ts index 07711fc..7c65436 100644 --- a/src/routes/api/directory/create/+server.ts +++ b/src/routes/api/directory/create/+server.ts @@ -17,8 +17,7 @@ export const POST: RequestHandler = async ({ locals, request }) => { mekVersion, encDek: dek, dekVersion: new Date(dekVersion), - encName: name, - encNameIv: nameIv, + encName: { ciphertext: name, iv: nameIv }, }); return text("Directory created", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/file/[id]/+server.ts b/src/routes/api/file/[id]/+server.ts index 892f62b..23e9385 100644 --- a/src/routes/api/file/[id]/+server.ts +++ b/src/routes/api/file/[id]/+server.ts @@ -26,6 +26,7 @@ export const GET: RequestHandler = async ({ locals, params }) => { encName, encCreatedAt, encLastModifiedAt, + categories, } = await getFileInformation(userId, id); return json( fileInfoResponse.parse({ @@ -41,6 +42,7 @@ export const GET: RequestHandler = async ({ locals, params }) => { createdAtIv: encCreatedAt?.iv, lastModifiedAt: encLastModifiedAt.ciphertext, lastModifiedAtIv: encLastModifiedAt.iv, + categories, } satisfies FileInfoResponse), ); }; diff --git a/src/routes/api/file/[id]/rename/+server.ts b/src/routes/api/file/[id]/rename/+server.ts index c6748a0..343f146 100644 --- a/src/routes/api/file/[id]/rename/+server.ts +++ b/src/routes/api/file/[id]/rename/+server.ts @@ -20,6 +20,6 @@ export const POST: RequestHandler = async ({ locals, params, request }) => { if (!bodyZodRes.success) error(400, "Invalid request body"); const { dekVersion, name, nameIv } = bodyZodRes.data; - await renameFile(userId, id, new Date(dekVersion), name, nameIv); + await renameFile(userId, id, new Date(dekVersion), { ciphertext: name, iv: nameIv }); return text("File renamed", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/file/upload/+server.ts b/src/routes/api/file/upload/+server.ts index 0e8c082..f9cbd53 100644 --- a/src/routes/api/file/upload/+server.ts +++ b/src/routes/api/file/upload/+server.ts @@ -1,8 +1,12 @@ import Busboy from "@fastify/busboy"; -import { error, text } from "@sveltejs/kit"; +import { error, json } from "@sveltejs/kit"; import { Readable, Writable } from "stream"; import { authorize } from "$lib/server/modules/auth"; -import { fileUploadRequest } from "$lib/server/schemas"; +import { + fileUploadRequest, + fileUploadResponse, + type FileUploadResponse, +} from "$lib/server/schemas"; import { uploadFile } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; @@ -40,12 +44,9 @@ const parseFileMetadata = (userId: number, json: string) => { contentHmac, contentType, encContentIv: contentIv, - encName: name, - encNameIv: nameIv, - encCreatedAt: createdAt ?? null, - encCreatedAtIv: createdAtIv ?? null, - encLastModifiedAt: lastModifiedAt, - encLastModifiedAtIv: lastModifiedAtIv, + encName: { ciphertext: name, iv: nameIv }, + encCreatedAt: createdAt && createdAtIv ? { ciphertext: createdAt, iv: createdAtIv } : null, + encLastModifiedAt: { ciphertext: lastModifiedAt, iv: lastModifiedAtIv }, } satisfies FileMetadata; }; @@ -67,27 +68,40 @@ export const POST: RequestHandler = async ({ locals, request }) => { let metadata: FileMetadata | null = null; let content: Readable | null = null; + const checksum = new Promise((resolveChecksum, rejectChecksum) => { + bb.on( + "field", + handler(async (fieldname, val) => { + if (fieldname === "metadata") { + // Ignore subsequent metadata fields + if (!metadata) { + metadata = parseFileMetadata(userId, val); + } + } else if (fieldname === "checksum") { + // Ignore subsequent checksum fields + resolveChecksum(val); + } else { + error(400, "Invalid request body"); + } + }), + ); + bb.on( + "file", + handler(async (fieldname, file) => { + if (fieldname !== "content") error(400, "Invalid request body"); + if (!metadata || content) error(400, "Invalid request body"); + content = file; - bb.on( - "field", - handler(async (fieldname, val) => { - if (fieldname !== "metadata") error(400, "Invalid request body"); - if (metadata || content) error(400, "Invalid request body"); - metadata = parseFileMetadata(userId, val); - }), - ); - bb.on( - "file", - handler(async (fieldname, file) => { - if (fieldname !== "content") error(400, "Invalid request body"); - if (!metadata || content) error(400, "Invalid request body"); - content = file; - - await uploadFile(metadata, content); - resolve(text("File uploaded", { headers: { "Content-Type": "text/plain" } })); - }), - ); - bb.on("error", (e) => content?.emit("error", e) ?? reject(e)); + const { fileId } = await uploadFile(metadata, content, checksum); + resolve(json(fileUploadResponse.parse({ file: fileId } satisfies FileUploadResponse))); + }), + ); + bb.on("finish", () => rejectChecksum(new Error("Invalid request body"))); + bb.on("error", (e) => { + content?.emit("error", e) ?? reject(e); + rejectChecksum(e); + }); + }); request.body!.pipeTo(Writable.toWeb(bb)).catch(() => {}); // busboy will handle the error });