From 3921d9c00d7b67d7d71a87cd8604a2c88a53ec78 Mon Sep 17 00:00:00 2001 From: saltbo Date: Sun, 26 Jul 2026 13:51:02 -0400 Subject: [PATCH] perf(webdav): reduce hot-path database work --- docs/clean-arch-migration.md | 2 +- migrations/0072_optimize-webdav-paths.sql | 2 + migrations/meta/0072_snapshot.json | 4553 +++++++++++++++++ migrations/meta/_journal.json | 7 + .../api-keys-rate-limit.integration.test.ts | 31 - server/adapters/repos/api-keys.ts | 55 +- server/adapters/repos/quota.ts | 81 +- server/adapters/repos/user-admin.ts | 8 +- server/adapters/repos/webdav-state.cf-test.ts | 26 +- server/adapters/repos/webdav-state.ts | 13 +- server/app.ts | 1 + server/db/schema.ts | 14 +- server/http/webdav.integration.test.ts | 50 + server/http/webdav.ts | 206 +- server/middleware/platform.ts | 2 + server/scheduled-worker.test.ts | 4 + server/usecases/ports/user.ts | 5 +- server/usecases/ports/webdav-state.ts | 7 +- server/usecases/store/traffic-metering.ts | 5 +- server/usecases/user.test.ts | 2 +- server/usecases/webdav.test.ts | 7 +- server/usecases/webdav.ts | 4 +- shared/api-key-templates.ts | 1 - workers/scheduled.ts | 2 +- wrangler.toml | 4 + 25 files changed, 4846 insertions(+), 246 deletions(-) create mode 100644 migrations/0072_optimize-webdav-paths.sql create mode 100644 migrations/meta/0072_snapshot.json diff --git a/docs/clean-arch-migration.md b/docs/clean-arch-migration.md index e66e0135..a2a6fce3 100644 --- a/docs/clean-arch-migration.md +++ b/docs/clean-arch-migration.md @@ -82,7 +82,7 @@ rewired callers to `c.get('deps')`), the orchestrator wired the 3 barrels and ra `server/services/` is empty and removed. Every drizzle access lives in `adapters/repos/`; every route gets its dependencies from `c.get('deps')`. **The ratchet is empty and removed** — including the final two files: `http/webdav.ts` (listDescendants/proppatch/PUT-overwrite/Basic-Auth → MatterRepo.{listActiveDescendants, -trashByIds,restoreActiveByIds,touch,applyUpload} + UserAdminRepo.{isBanned,matchesUsername}) and +trashByIds,restoreActiveByIds,touch,applyUpload} + UserAdminRepo.{isBanned,matchesActiveUsername}) and `middleware/auth.ts` (disabled-user check → UserAdminRepo.isBanned). Gates green: `typecheck`, `lint:arch` (267 modules, **no-circular + drizzle-only-in-repos fully enforced, zero exemptions**), `lint:spec`, `lint`, `test` (3810), `test:cf` (57). diff --git a/migrations/0072_optimize-webdav-paths.sql b/migrations/0072_optimize-webdav-paths.sql new file mode 100644 index 00000000..c7196ac1 --- /dev/null +++ b/migrations/0072_optimize-webdav-paths.sql @@ -0,0 +1,2 @@ +CREATE INDEX `matters_webdav_path_idx` ON `matters` (`org_id`,`parent`,`name`,`status`,`trashed_at`,`purged_at`);--> statement-breakpoint +CREATE INDEX `matters_webdav_children_idx` ON `matters` (`org_id`,`parent`,`status`,`trashed_at`,`purged_at`,"dirtype" desc,`name`); \ No newline at end of file diff --git a/migrations/meta/0072_snapshot.json b/migrations/meta/0072_snapshot.json new file mode 100644 index 00000000..0b43fd15 --- /dev/null +++ b/migrations/meta/0072_snapshot.json @@ -0,0 +1,4553 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "4671aee9-76b7-4dcb-8830-4d82419d1353", + "prevId": "50fce4fa-dd18-4b70-85fa-fd8b5baab701", + "tables": { + "announcements": { + "name": "announcements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "announcements_status_priority_idx": { + "name": "announcements_status_priority_idx", + "columns": [ + "status", + "priority" + ], + "isUnique": false + }, + "announcements_published_idx": { + "name": "announcements_published_idx", + "columns": [ + "published_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "audit_events": { + "name": "audit_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_name": { + "name": "target_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_ref": { + "name": "actor_ref", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "audit_events_org_created_idx": { + "name": "audit_events_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_user_created_idx": { + "name": "audit_events_user_created_idx", + "columns": [ + "user_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_action_created_idx": { + "name": "audit_events_action_created_idx", + "columns": [ + "action", + "created_at" + ], + "isUnique": false + }, + "audit_events_target_created_idx": { + "name": "audit_events_target_created_idx", + "columns": [ + "target_type", + "target_id", + "created_at" + ], + "isUnique": false + }, + "audit_events_created_idx": { + "name": "audit_events_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "background_jobs": { + "name": "background_jobs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_folder": { + "name": "target_folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_path": { + "name": "target_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input_bytes": { + "name": "input_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_bytes": { + "name": "output_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "processed_bytes": { + "name": "processed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "current_filename": { + "name": "current_filename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_metadata": { + "name": "result_metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retryable": { + "name": "retryable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "cancelable": { + "name": "cancelable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "retried_from_job_id": { + "name": "retried_from_job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "background_jobs_org_created_idx": { + "name": "background_jobs_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "background_jobs_org_status_idx": { + "name": "background_jobs_org_status_idx", + "columns": [ + "org_id", + "status" + ], + "isUnique": false + }, + "background_jobs_org_type_idx": { + "name": "background_jobs_org_type_idx", + "columns": [ + "org_id", + "type" + ], + "isUnique": false + }, + "background_jobs_created_idx": { + "name": "background_jobs_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cloud_traffic_reports": { + "name": "cloud_traffic_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_bytes": { + "name": "unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credits_per_unit": { + "name": "credits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "next_retry_at": { + "name": "next_retry_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "issued_at": { + "name": "issued_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cloud_traffic_reports_event_uniq": { + "name": "cloud_traffic_reports_event_uniq", + "columns": [ + "event_id" + ], + "isUnique": true + }, + "cloud_traffic_reports_org_period_idx": { + "name": "cloud_traffic_reports_org_period_idx", + "columns": [ + "org_id", + "period" + ], + "isUnique": false + }, + "cloud_traffic_reports_status_idx": { + "name": "cloud_traffic_reports_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "cloud_traffic_reports_retry_idx": { + "name": "cloud_traffic_reports_retry_idx", + "columns": [ + "status", + "next_retry_at", + "created_at" + ], + "isUnique": false + }, + "cloud_traffic_reports_issued_idx": { + "name": "cloud_traffic_reports_issued_idx", + "columns": [ + "issued_at" + ], + "isUnique": false + }, + "cloud_traffic_reports_updated_idx": { + "name": "cloud_traffic_reports_updated_idx", + "columns": [ + "updated_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "download_tasks": { + "name": "download_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_uri": { + "name": "source_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_folder": { + "name": "target_folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "assigned_downloader_id": { + "name": "assigned_downloader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "billing_authorized_bytes": { + "name": "billing_authorized_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_charged_bytes": { + "name": "billing_charged_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_charged_credits": { + "name": "billing_charged_credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_status": { + "name": "billing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_object_id": { + "name": "result_object_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "events": { + "name": "events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "resolve_started_at": { + "name": "resolve_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolve_completed_at": { + "name": "resolve_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "download_completed_at": { + "name": "download_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ingest_started_at": { + "name": "ingest_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ingest_completed_at": { + "name": "ingest_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seeding_started_at": { + "name": "seeding_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seeding_stopped_at": { + "name": "seeding_stopped_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "download_tasks_org_created_idx": { + "name": "download_tasks_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "download_tasks_org_status_idx": { + "name": "download_tasks_org_status_idx", + "columns": [ + "org_id", + "status" + ], + "isUnique": false + }, + "download_tasks_org_category_idx": { + "name": "download_tasks_org_category_idx", + "columns": [ + "org_id", + "category" + ], + "isUnique": false + }, + "download_tasks_org_tags_idx": { + "name": "download_tasks_org_tags_idx", + "columns": [ + "org_id", + "tags" + ], + "isUnique": false + }, + "download_tasks_downloader_idx": { + "name": "download_tasks_downloader_idx", + "columns": [ + "assigned_downloader_id", + "status" + ], + "isUnique": false + }, + "download_tasks_created_idx": { + "name": "download_tasks_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "download_tasks_finished_idx": { + "name": "download_tasks_finished_idx", + "columns": [ + "finished_at" + ], + "isUnique": false + }, + "download_tasks_org_deleted_created_idx": { + "name": "download_tasks_org_deleted_created_idx", + "columns": [ + "org_id", + "deleted_at", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "downloaders": { + "name": "downloaders", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_jti": { + "name": "token_jti", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'offline'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "arch": { + "name": "arch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "engine": { + "name": "engine", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'http'" + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "max_concurrent_tasks": { + "name": "max_concurrent_tasks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "current_tasks": { + "name": "current_tasks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "download_bps": { + "name": "download_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "upload_bps": { + "name": "upload_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "free_disk_bytes": { + "name": "free_disk_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remote_download_credit_billing_enabled": { + "name": "remote_download_credit_billing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_download_credit_unit_bytes": { + "name": "remote_download_credit_unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 104857600 + }, + "remote_download_credit_per_unit": { + "name": "remote_download_credit_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "downloaders_token_jti_unique": { + "name": "downloaders_token_jti_unique", + "columns": [ + "token_jti" + ], + "isUnique": true + }, + "downloaders_status_idx": { + "name": "downloaders_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "downloaders_enabled_idx": { + "name": "downloaders_enabled_idx", + "columns": [ + "enabled" + ], + "isUnique": false + }, + "downloaders_created_idx": { + "name": "downloaders_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "image_hosting_configs": { + "name": "image_hosting_configs", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cf_hostname_id": { + "name": "cf_hostname_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_verified_at": { + "name": "domain_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referer_allowlist": { + "name": "referer_allowlist", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "image_hosting_configs_custom_domain_unique": { + "name": "image_hosting_configs_custom_domain_unique", + "columns": [ + "custom_domain" + ], + "isUnique": true + } + }, + "foreignKeys": { + "image_hosting_configs_org_id_organization_id_fk": { + "name": "image_hosting_configs_org_id_organization_id_fk", + "tableFrom": "image_hosting_configs", + "tableTo": "organization", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "image_hostings": { + "name": "image_hostings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime": { + "name": "mime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "purged_at": { + "name": "purged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "image_hostings_token_unique": { + "name": "image_hostings_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "image_hostings_org_path_uniq": { + "name": "image_hostings_org_path_uniq", + "columns": [ + "org_id", + "path" + ], + "isUnique": true, + "where": "\"image_hostings\".\"purged_at\" IS NULL" + }, + "image_hostings_org_created_idx": { + "name": "image_hostings_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "image_hostings_token_idx": { + "name": "image_hostings_token_idx", + "columns": [ + "token" + ], + "isUnique": false + } + }, + "foreignKeys": { + "image_hostings_org_id_organization_id_fk": { + "name": "image_hostings_org_id_organization_id_fk", + "tableFrom": "image_hostings", + "tableTo": "organization", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "image_hostings_storage_id_storages_id_fk": { + "name": "image_hostings_storage_id_storages_id_fk", + "tableFrom": "image_hostings", + "tableTo": "storages", + "columnsFrom": [ + "storage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite_codes": { + "name": "invite_codes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_by": { + "name": "used_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_codes_code_unique": { + "name": "invite_codes_code_unique", + "columns": [ + "code" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "license_bindings": { + "name": "license_bindings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "cloud_binding_id": { + "name": "cloud_binding_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_store_id": { + "name": "cloud_store_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_account_id": { + "name": "cloud_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_account_email": { + "name": "cloud_account_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cached_certificate": { + "name": "cached_certificate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cached_certificate_expires_at": { + "name": "cached_certificate_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_at": { + "name": "bound_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refresh_at": { + "name": "last_refresh_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refresh_error": { + "name": "last_refresh_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "license_bindings_active_uniq": { + "name": "license_bindings_active_uniq", + "columns": [ + "status" + ], + "isUnique": true, + "where": "status = 'active'" + }, + "license_bindings_cloud_binding_idx": { + "name": "license_bindings_cloud_binding_idx", + "columns": [ + "cloud_binding_id" + ], + "isUnique": false + }, + "license_bindings_instance_idx": { + "name": "license_bindings_instance_idx", + "columns": [ + "instance_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "matters": { + "name": "matters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "dirtype": { + "name": "dirtype", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "parent": { + "name": "parent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "object": { + "name": "object", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "trashed_at": { + "name": "trashed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "purged_at": { + "name": "purged_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "matters_alias_unique": { + "name": "matters_alias_unique", + "columns": [ + "alias" + ], + "isUnique": true + }, + "matters_status_dir_created_idx": { + "name": "matters_status_dir_created_idx", + "columns": [ + "status", + "dirtype", + "created_at" + ], + "isUnique": false + }, + "matters_webdav_path_idx": { + "name": "matters_webdav_path_idx", + "columns": [ + "org_id", + "parent", + "name", + "status", + "trashed_at", + "purged_at" + ], + "isUnique": false + }, + "matters_webdav_children_idx": { + "name": "matters_webdav_children_idx", + "columns": [ + "org_id", + "parent", + "status", + "trashed_at", + "purged_at", + "\"dirtype\" desc", + "name" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notifications": { + "name": "notifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "ref_type": { + "name": "ref_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ref_id": { + "name": "ref_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read_at": { + "name": "read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notifications_user_created_idx": { + "name": "notifications_user_created_idx", + "columns": [ + "user_id", + "created_at" + ], + "isUnique": false + }, + "notifications_user_read_idx": { + "name": "notifications_user_read_idx", + "columns": [ + "user_id", + "read_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "object_upload_sessions": { + "name": "object_upload_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_id": { + "name": "object_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upload_id": { + "name": "upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "on_conflict": { + "name": "on_conflict", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fail'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "object_upload_sessions_object_idx": { + "name": "object_upload_sessions_object_idx", + "columns": [ + "org_id", + "object_id" + ], + "isUnique": false + }, + "object_upload_sessions_expires_idx": { + "name": "object_upload_sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "org_quota_entitlements": { + "name": "org_quota_entitlements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entitlement_type": { + "name": "entitlement_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'grant'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "starts_at": { + "name": "starts_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "org_quota_entitlements_org_resource_idx": { + "name": "org_quota_entitlements_org_resource_idx", + "columns": [ + "org_id", + "resource_type", + "status" + ], + "isUnique": false + }, + "org_quota_entitlements_org_type_idx": { + "name": "org_quota_entitlements_org_type_idx", + "columns": [ + "org_id", + "resource_type", + "entitlement_type", + "status" + ], + "isUnique": false + }, + "org_quota_entitlements_active_plan_uniq": { + "name": "org_quota_entitlements_active_plan_uniq", + "columns": [ + "org_id", + "resource_type", + "entitlement_type" + ], + "isUnique": true, + "where": "status = 'active' AND entitlement_type = 'plan' AND source <> 'free_plan'" + }, + "org_quota_entitlements_source_resource_uniq": { + "name": "org_quota_entitlements_source_resource_uniq", + "columns": [ + "source", + "source_id", + "resource_type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "org_quotas": { + "name": "org_quotas", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quota": { + "name": "quota", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_quota": { + "name": "traffic_quota", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_used": { + "name": "traffic_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_period": { + "name": "traffic_period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'1970-01'" + } + }, + "indexes": { + "org_quotas_org_uniq": { + "name": "org_quotas_org_uniq", + "columns": [ + "org_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "remote_download_usage_reports": { + "name": "remote_download_usage_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "downloader_id": { + "name": "downloader_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_index": { + "name": "unit_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_bytes": { + "name": "unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credits_per_unit": { + "name": "credits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "remote_download_usage_reports_event_id_unique": { + "name": "remote_download_usage_reports_event_id_unique", + "columns": [ + "event_id" + ], + "isUnique": true + }, + "remote_download_usage_task_unit_uniq": { + "name": "remote_download_usage_task_unit_uniq", + "columns": [ + "task_id", + "unit_index" + ], + "isUnique": true + }, + "remote_download_usage_org_idx": { + "name": "remote_download_usage_org_idx", + "columns": [ + "org_id" + ], + "isUnique": false + }, + "remote_download_usage_status_idx": { + "name": "remote_download_usage_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "remote_download_usage_created_idx": { + "name": "remote_download_usage_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "share_recipients": { + "name": "share_recipients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "share_recipients_share_id_idx": { + "name": "share_recipients_share_id_idx", + "columns": [ + "share_id" + ], + "isUnique": false + }, + "share_recipients_user_id_idx": { + "name": "share_recipients_user_id_idx", + "columns": [ + "recipient_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shares": { + "name": "shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "matter_id": { + "name": "matter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "download_limit": { + "name": "download_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "downloads": { + "name": "downloads", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "private": { + "name": "private", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "shares_token_unique": { + "name": "shares_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "shares_creator_status_created_idx": { + "name": "shares_creator_status_created_idx", + "columns": [ + "creator_id", + "status", + "created_at" + ], + "isUnique": false + }, + "shares_creator_private_created_idx": { + "name": "shares_creator_private_created_idx", + "columns": [ + "creator_id", + "private", + "created_at" + ], + "isUnique": false + }, + "shares_created_idx": { + "name": "shares_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "site_invitations": { + "name": "site_invitations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "site_invitations_token_unique": { + "name": "site_invitations_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "site_invitations_email_idx": { + "name": "site_invitations_email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "site_invitations_created_idx": { + "name": "site_invitations_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "site_invitations_expires_idx": { + "name": "site_invitations_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "stats_rollups_hourly": { + "name": "stats_rollups_hourly", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bucket_start": { + "name": "bucket_start", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "metric_key": { + "name": "metric_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dimension_key": { + "name": "dimension_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "dimension_value": { + "name": "dimension_value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "unique_count": { + "name": "unique_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "stats_rollups_hourly_bucket_metric_dim_uniq": { + "name": "stats_rollups_hourly_bucket_metric_dim_uniq", + "columns": [ + "bucket_start", + "org_id", + "metric_key", + "dimension_key", + "dimension_value" + ], + "isUnique": true + }, + "stats_rollups_hourly_metric_bucket_idx": { + "name": "stats_rollups_hourly_metric_bucket_idx", + "columns": [ + "metric_key", + "bucket_start" + ], + "isUnique": false + }, + "stats_rollups_hourly_dimension_bucket_idx": { + "name": "stats_rollups_hourly_dimension_bucket_idx", + "columns": [ + "metric_key", + "dimension_key", + "bucket_start" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "storage_usage_breakdowns": { + "name": "storage_usage_breakdowns", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "storage_usage_breakdowns_org_category_uniq": { + "name": "storage_usage_breakdowns_org_category_uniq", + "columns": [ + "org_id", + "category" + ], + "isUnique": true + }, + "storage_usage_breakdowns_org_idx": { + "name": "storage_usage_breakdowns_org_idx", + "columns": [ + "org_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "storage_usage_ledger": { + "name": "storage_usage_ledger", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "delta_bytes": { + "name": "delta_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "storage_usage_ledger_event_key_unique": { + "name": "storage_usage_ledger_event_key_unique", + "columns": [ + "event_key" + ], + "isUnique": true + }, + "storage_usage_ledger_occurred_idx": { + "name": "storage_usage_ledger_occurred_idx", + "columns": [ + "occurred_at" + ], + "isUnique": false + }, + "storage_usage_ledger_org_occurred_idx": { + "name": "storage_usage_ledger_org_occurred_idx", + "columns": [ + "org_id", + "occurred_at" + ], + "isUnique": false + }, + "storage_usage_ledger_storage_occurred_idx": { + "name": "storage_usage_ledger_storage_occurred_idx", + "columns": [ + "storage_id", + "occurred_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "storages": { + "name": "storages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "access_key": { + "name": "access_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secret_key": { + "name": "secret_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "custom_host": { + "name": "custom_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "egress_credit_billing_enabled": { + "name": "egress_credit_billing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "egress_credit_unit_bytes": { + "name": "egress_credit_unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 104857600 + }, + "egress_credit_per_unit": { + "name": "egress_credit_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "force_path_style": { + "name": "force_path_style", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "status_reason": { + "name": "status_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status_checked_at": { + "name": "status_checked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "system_options": { + "name": "system_options", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team_invite_links": { + "name": "team_invite_links", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "team_invite_links_token_unique": { + "name": "team_invite_links_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webdav_dead_properties": { + "name": "webdav_dead_properties", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_path": { + "name": "resource_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webdav_dead_properties_resource_prop_uniq": { + "name": "webdav_dead_properties_resource_prop_uniq", + "columns": [ + "org_id", + "resource_path", + "namespace", + "name" + ], + "isUnique": true + }, + "webdav_dead_properties_resource_idx": { + "name": "webdav_dead_properties_resource_idx", + "columns": [ + "org_id", + "resource_path" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webdav_locks": { + "name": "webdav_locks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_path": { + "name": "resource_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "depth": { + "name": "depth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'infinity'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webdav_locks_token_unique": { + "name": "webdav_locks_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "webdav_locks_resource_idx": { + "name": "webdav_locks_resource_idx", + "columns": [ + "org_id", + "resource_path" + ], + "isUnique": false + }, + "webdav_locks_expires_idx": { + "name": "webdav_locks_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webhook_events": { + "name": "webhook_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cloud'" + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'order.quota_changed'" + }, + "payload_hash": { + "name": "payload_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_payload": { + "name": "raw_payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "processed_at": { + "name": "processed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webhook_events_source_event_uniq": { + "name": "webhook_events_source_event_uniq", + "columns": [ + "source", + "event_id" + ], + "isUnique": true + }, + "webhook_events_source_created_idx": { + "name": "webhook_events_source_created_idx", + "columns": [ + "source", + "created_at" + ], + "isUnique": false + }, + "webhook_events_status_idx": { + "name": "webhook_events_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "webhook_events_processed_idx": { + "name": "webhook_events_processed_idx", + "columns": [ + "processed_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apikey": { + "name": "apikey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_request": { + "name": "last_request", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + "reference_id" + ], + "isUnique": false + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + "key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deviceCode": { + "name": "deviceCode", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "deviceCode_device_code_idx": { + "name": "deviceCode_device_code_idx", + "columns": [ + "device_code" + ], + "isUnique": false + }, + "deviceCode_user_code_idx": { + "name": "deviceCode_user_code_idx", + "columns": [ + "user_code" + ], + "isUnique": false + }, + "deviceCode_status_idx": { + "name": "deviceCode_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "session_created_idx": { + "name": "session_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned": { + "name": "banned", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_username_unique": { + "name": "user_username_unique", + "columns": [ + "username" + ], + "isUnique": true + }, + "user_created_idx": { + "name": "user_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "user_lastActiveAt_idx": { + "name": "user_lastActiveAt_idx", + "columns": [ + "last_active_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": { + "matters_webdav_children_idx": { + "columns": { + "\"dirtype\" desc": { + "isExpression": true + } + } + } + } + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index abd6c485..16572f4e 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -498,6 +498,13 @@ "when": 1784869319265, "tag": "0071_share-privacy", "breakpoints": true + }, + { + "idx": 72, + "version": "6", + "when": 1785081860795, + "tag": "0072_optimize-webdav-paths", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/adapters/repos/api-keys-rate-limit.integration.test.ts b/server/adapters/repos/api-keys-rate-limit.integration.test.ts index 1784d13a..a7d898fd 100644 --- a/server/adapters/repos/api-keys-rate-limit.integration.test.ts +++ b/server/adapters/repos/api-keys-rate-limit.integration.test.ts @@ -1,7 +1,6 @@ import { sql } from 'drizzle-orm' import { describe, expect, it } from 'vitest' import { - WEBDAV_API_KEY_LEGACY_RATE_LIMIT_MAX_REQUESTS, WEBDAV_API_KEY_RATE_LIMIT_MAX_REQUESTS, WEBDAV_API_KEY_RATE_LIMIT_WINDOW_MS, } from '../../../shared/api-key-templates.js' @@ -252,36 +251,6 @@ describe('API keys', () => { expect(body.error.status).toBe('RESOURCE_EXHAUSTED') }) - it('upgrades legacy WebDAV default rate limits before verification', async () => { - const { app, db, auth } = await createTestApp() - await authedHeaders(app) - const { userId } = await getUserAndOrg(db) - // biome-ignore lint/suspicious/noExplicitAny: better-auth plugin API is not fully typed - const apiKey = (await (auth.api as any).createApiKey({ - body: { - configId: 'webdav', - userId, - permissions: { webdav: ['read'] }, - }, - })) as { key: string; id: string } - - await db.run(sql` - UPDATE apikey - SET rate_limit_max = ${WEBDAV_API_KEY_LEGACY_RATE_LIMIT_MAX_REQUESTS}, - request_count = ${WEBDAV_API_KEY_LEGACY_RATE_LIMIT_MAX_REQUESTS}, - last_request = ${Date.now()} - WHERE id = ${apiKey.id} - `) - - await expect( - apiKeys.verifyApiKeyForPermission(auth, db, apiKey.key, 'webdav', 'read', 'webdav'), - ).resolves.toMatchObject({ id: apiKey.id }) - expect(await getApiKeyRow(db, apiKey.id)).toMatchObject({ - rate_limit_max: WEBDAV_API_KEY_RATE_LIMIT_MAX_REQUESTS, - request_count: WEBDAV_API_KEY_LEGACY_RATE_LIMIT_MAX_REQUESTS + 1, - }) - }) - it('WebDAV surfaces a custom rate-limited API key as too many requests', async () => { const { app, db, auth } = await createTestApp() await authedHeaders(app) diff --git a/server/adapters/repos/api-keys.ts b/server/adapters/repos/api-keys.ts index 0f9ed0b5..ea610338 100644 --- a/server/adapters/repos/api-keys.ts +++ b/server/adapters/repos/api-keys.ts @@ -1,12 +1,5 @@ import { defaultKeyHasher } from '@better-auth/api-key' -import { - API_KEY_TEMPLATES, - type ApiKeyPermissions, - ApiKeyTemplate, - WEBDAV_API_KEY_LEGACY_RATE_LIMIT_MAX_REQUESTS, - WEBDAV_API_KEY_RATE_LIMIT_MAX_REQUESTS, - WEBDAV_API_KEY_RATE_LIMIT_WINDOW_MS, -} from '@shared/api-key-templates' +import { API_KEY_TEMPLATES, type ApiKeyPermissions, type ApiKeyTemplate } from '@shared/api-key-templates' import { eq } from 'drizzle-orm' import { apikey } from '../../db/auth-schema' import type { Database } from '../../platform/interface' @@ -28,9 +21,8 @@ type VerifyApiKeyResult = { export function createApiKeyGateway(): ApiKeyGateway { return { async verifyApiKey(auth, db, key, configId) { - const resolvedConfigId = await resolveApiKeyConfigId(db, key) + const resolvedConfigId = configId ?? (await resolveApiKeyConfigId(db, key)) if (!resolvedConfigId) return null - if (configId && resolvedConfigId !== configId) return null const result = await verify(auth, { configId: resolvedConfigId, key }) throwIfRateLimited(result) if (result?.valid && result.key) return normalizeVerifiedApiKey(db, result.key) @@ -38,9 +30,8 @@ export function createApiKeyGateway(): ApiKeyGateway { }, async verifyApiKeyForPermission(auth, db, key, resource, action, configId) { - const resolvedConfigId = await resolveApiKeyConfigId(db, key) + const resolvedConfigId = configId ?? (await resolveApiKeyConfigId(db, key)) if (!resolvedConfigId) return null - if (configId && resolvedConfigId !== configId) return null const result = await verify(auth, { configId: resolvedConfigId, key, @@ -86,41 +77,9 @@ function throwIfRateLimited(result: VerifyApiKeyResult | null) { throw new ApiKeyRateLimitError(result.error.message, result.error.details?.tryAgainIn) } -async function resolveApiKeyConfigId(db: Database, rawKey: string): Promise { +async function resolveApiKeyConfigId(db: Database, rawKey: string): Promise { const hashedKey = await defaultKeyHasher(rawKey) - const rows = await db - .select({ - id: apikey.id, - configId: apikey.configId, - rateLimitEnabled: apikey.rateLimitEnabled, - rateLimitTimeWindow: apikey.rateLimitTimeWindow, - rateLimitMax: apikey.rateLimitMax, - }) - .from(apikey) - .where(eq(apikey.key, hashedKey)) - .limit(1) - const row = rows[0] - const configId = row?.configId - if (!configId || !API_KEY_TEMPLATES.includes(configId as ApiKeyTemplate)) return null - if (configId === ApiKeyTemplate.WEBDAV) await upgradeLegacyWebDavRateLimit(db, row) - return configId -} - -async function upgradeLegacyWebDavRateLimit( - db: Database, - row: { - id: string - rateLimitEnabled: boolean - rateLimitTimeWindow: number | null - rateLimitMax: number | null - }, -) { - if (!row.rateLimitEnabled) return - if (row.rateLimitTimeWindow !== WEBDAV_API_KEY_RATE_LIMIT_WINDOW_MS) return - if (row.rateLimitMax !== WEBDAV_API_KEY_LEGACY_RATE_LIMIT_MAX_REQUESTS) return - - await db - .update(apikey) - .set({ rateLimitMax: WEBDAV_API_KEY_RATE_LIMIT_MAX_REQUESTS, updatedAt: new Date() }) - .where(eq(apikey.id, row.id)) + const rows = await db.select({ configId: apikey.configId }).from(apikey).where(eq(apikey.key, hashedKey)).limit(1) + const configId = rows[0]?.configId + return configId && API_KEY_TEMPLATES.includes(configId as ApiKeyTemplate) ? (configId as ApiKeyTemplate) : null } diff --git a/server/adapters/repos/quota.ts b/server/adapters/repos/quota.ts index e2f5c716..d42872ed 100644 --- a/server/adapters/repos/quota.ts +++ b/server/adapters/repos/quota.ts @@ -271,57 +271,33 @@ async function consumeTrafficIfQuotaAllows( ): Promise { if (bytes <= 0) return true const period = currentTrafficPeriod(now) - const quotaRows = await db - .select({ id: orgQuotas.id, trafficPeriod: orgQuotas.trafficPeriod, trafficQuota: orgQuotas.trafficQuota }) - .from(orgQuotas) - .where(eq(orgQuotas.orgId, orgId)) - .limit(1) - if (quotaRows.length === 0) return false - - const trafficOverageAllowed = await hasActiveTrafficOverage(db, orgId, now) - const overageAllowedSql = trafficOverageAllowed ? sql`1 = 1` : sql`1 = 0` - - if (quotaRows[0].trafficPeriod !== period) { - const limitBytes = activeEntitlementBytesSql(orgId, 'traffic', now) - const hasPlan = activePlanEntitlementExistsSql(orgId, 'traffic', now) - const updated = await db + const limitBytes = activeEntitlementBytesSql(orgId, 'traffic', now) + const hasPlan = activePlanEntitlementExistsSql(orgId, 'traffic', now) + const overageAllowed = activeTrafficOverageAllowedSql(orgId, now) + const nextUsed = sql`CASE + WHEN ${orgQuotas.trafficPeriod} = ${period} THEN ${orgQuotas.trafficUsed} + ${bytes} + ELSE ${bytes} + END` + const consume = () => + db .update(orgQuotas) - .set({ trafficUsed: bytes, trafficPeriod: period }) + .set({ trafficUsed: nextUsed, trafficPeriod: period }) .where( sql`${orgQuotas.orgId} = ${orgId} - AND ${orgQuotas.trafficPeriod} != ${period} AND ( (${limitBytes} = 0 AND ${hasPlan}) - OR ${overageAllowedSql} - OR ${bytes} <= ${limitBytes} + OR ${overageAllowed} + OR ${nextUsed} <= ${limitBytes} )`, ) .returning({ id: orgQuotas.id }) - if (updated.length > 0) return true - } - const limitBytes = activeEntitlementBytesSql(orgId, 'traffic', now) - const hasPlan = activePlanEntitlementExistsSql(orgId, 'traffic', now) - const updated = await db - .update(orgQuotas) - .set({ trafficUsed: sql`${orgQuotas.trafficUsed} + ${bytes}` }) - .where( - sql`${orgQuotas.orgId} = ${orgId} - AND ${orgQuotas.trafficPeriod} = ${period} - AND ( - (${limitBytes} = 0 AND ${hasPlan}) - OR ${overageAllowedSql} - OR ${orgQuotas.trafficUsed} + ${bytes} <= ${limitBytes} - )`, - ) - .returning({ id: orgQuotas.id }) + const updated = await consume() + if (updated.length > 0) return true - return updated.length > 0 -} - -async function hasActiveTrafficOverage(db: Database, orgId: string, now: Date) { - const trafficPlan = await activePlanEntitlement(db, orgId, 'traffic', now) - return (trafficPlan?.trafficOveragePriceCents ?? 0) > 0 + // A concurrent rollover may have changed the row while the first statement + // was executing. Retry once against the new current-period value. + return (await consume()).length > 0 } async function refundTraffic(db: Database, orgId: string, bytes: number, now = new Date()): Promise { @@ -465,6 +441,29 @@ function activePlanEntitlementExistsSql(orgId: string, resourceType: 'storage' | )` } +function activeTrafficOverageAllowedSql(orgId: string, now: Date) { + const timestamp = now.getTime() + return sql`COALESCE(( + SELECT CASE + WHEN json_valid(${orgQuotaEntitlements.metadata}) = 1 + THEN CAST(json_extract(${orgQuotaEntitlements.metadata}, '$.trafficOveragePriceCents') AS INTEGER) + ELSE 0 + END + FROM ${orgQuotaEntitlements} + WHERE ${orgQuotaEntitlements.orgId} = ${orgId} + AND ${orgQuotaEntitlements.resourceType} = 'traffic' + AND ${orgQuotaEntitlements.entitlementType} = 'plan' + AND ${orgQuotaEntitlements.status} = 'active' + AND ${orgQuotaEntitlements.startsAt} <= ${timestamp} + AND (${orgQuotaEntitlements.expiresAt} IS NULL OR ${orgQuotaEntitlements.expiresAt} > ${timestamp}) + ORDER BY + CASE WHEN ${orgQuotaEntitlements.source} = 'free_plan' THEN 1 ELSE 0 END, + ${orgQuotaEntitlements.bytes} DESC, + ${orgQuotaEntitlements.startsAt} DESC + LIMIT 1 + ), 0) > 0` +} + interface PlanEntitlement { sourceId: string bytes: number diff --git a/server/adapters/repos/user-admin.ts b/server/adapters/repos/user-admin.ts index 93a95e4d..a131182b 100644 --- a/server/adapters/repos/user-admin.ts +++ b/server/adapters/repos/user-admin.ts @@ -17,14 +17,14 @@ async function isBanned(db: Database, userId: string): Promise { return Boolean(rows[0]?.banned) } -async function matchesUsername(db: Database, userId: string, username: string): Promise { +async function matchesActiveUsername(db: Database, userId: string, username: string): Promise { const rows = await db - .select({ email: user.email, username: user.username }) + .select({ banned: user.banned, email: user.email, username: user.username }) .from(user) .where(eq(user.id, userId)) .limit(1) const account = rows[0] - if (!account) return false + if (!account || account.banned) return false return account.email.toLowerCase() === username.toLowerCase() || account.username === username } @@ -220,7 +220,7 @@ function mergeGrantMetadata(existing: string | null, patch: Record isBanned(db, userId), - matchesUsername: (userId, username) => matchesUsername(db, userId, username), + matchesActiveUsername: (userId, username) => matchesActiveUsername(db, userId, username), listUserPersonalEntitlements: (userId) => listUserPersonalEntitlements(db, userId), grantUserPersonalEntitlement: (input) => grantUserPersonalEntitlement(db, input), updateUserPersonalEntitlement: (input) => updateUserPersonalEntitlement(db, input), diff --git a/server/adapters/repos/webdav-state.cf-test.ts b/server/adapters/repos/webdav-state.cf-test.ts index 8fad3c07..f39a0d00 100644 --- a/server/adapters/repos/webdav-state.cf-test.ts +++ b/server/adapters/repos/webdav-state.cf-test.ts @@ -1,16 +1,19 @@ import { env } from 'cloudflare:workers' +import { eq } from 'drizzle-orm' import { nanoid } from 'nanoid' import { describe, expect, it } from 'vitest' +import { webdavLocks } from '../../db/schema' import { createCloudflarePlatform } from '../../platform/cloudflare' import { createWebDavStateRepo } from './webdav-state' function buildRepo() { - return createWebDavStateRepo(createCloudflarePlatform(env).db) + const db = createCloudflarePlatform(env).db + return { db, repo: createWebDavStateRepo(db) } } describe('[CF] WebDAV locks on D1', () => { it('matches depth-infinity lock scopes without D1 dynamic LIKE expressions', async () => { - const repo = buildRepo() + const { repo } = buildRepo() const orgId = `org-${nanoid(8)}` const parent = await repo.createLock({ orgId, @@ -33,4 +36,23 @@ describe('[CF] WebDAV locks on D1', () => { expect(await repo.removeLock(orgId, 'Folder/child.txt', parent.token)).toBe(true) expect(await repo.activeLocks(orgId, 'Folder/child.txt')).toEqual([]) }) + + it('filters expired locks on reads and removes them during scheduled cleanup', async () => { + const { db, repo } = buildRepo() + const orgId = `org-${nanoid(8)}` + const expired = await repo.createLock({ + orgId, + resourcePath: 'expired.txt', + owner: 'tester', + depth: '0', + timeoutSeconds: -1, + }) + + expect(await repo.activeLocks(orgId, 'expired.txt')).toEqual([]) + expect(await db.select().from(webdavLocks).where(eq(webdavLocks.id, expired.id))).toHaveLength(1) + + await repo.purgeExpiredLocks() + + expect(await db.select().from(webdavLocks).where(eq(webdavLocks.id, expired.id))).toHaveLength(0) + }) }) diff --git a/server/adapters/repos/webdav-state.ts b/server/adapters/repos/webdav-state.ts index 696af1ab..0a0a41ca 100644 --- a/server/adapters/repos/webdav-state.ts +++ b/server/adapters/repos/webdav-state.ts @@ -171,7 +171,6 @@ export function createWebDavStateRepo(db: Database): WebDavStateRepo { }, async activeLocks(orgId, resourcePath) { - await purgeExpiredLocks(db) const now = Date.now() const rows = await db .select() @@ -185,7 +184,6 @@ export function createWebDavStateRepo(db: Database): WebDavStateRepo { const result = new Map(uniquePaths.map((path) => [path, [] as DavLock[]])) if (uniquePaths.length === 0) return result - await purgeExpiredLocks(db) const now = Date.now() const rows = await db .select() @@ -202,7 +200,6 @@ export function createWebDavStateRepo(db: Database): WebDavStateRepo { }, async conflictingLocks(orgId, resourcePath) { - await purgeExpiredLocks(db) const now = Date.now() const rows = await db .select() @@ -229,7 +226,6 @@ export function createWebDavStateRepo(db: Database): WebDavStateRepo { }, async refreshLock(orgId, resourcePath, token, timeoutSeconds) { - await purgeExpiredLocks(db) const now = new Date() const expiresAt = new Date(now.getTime() + timeoutSeconds * 1000) const rows = await db @@ -249,7 +245,6 @@ export function createWebDavStateRepo(db: Database): WebDavStateRepo { }, async removeLock(orgId, resourcePath, token) { - await purgeExpiredLocks(db) const rows = await db .select() .from(webdavLocks) @@ -265,11 +260,11 @@ export function createWebDavStateRepo(db: Database): WebDavStateRepo { await db.delete(webdavLocks).where(eq(webdavLocks.id, lock.id)) return true }, - } -} -async function purgeExpiredLocks(db: Database): Promise { - await db.delete(webdavLocks).where(sql`${webdavLocks.expiresAt} <= ${Date.now()}`) + async purgeExpiredLocks(now = Date.now()) { + await db.delete(webdavLocks).where(sql`${webdavLocks.expiresAt} <= ${now}`) + }, + } } function lockAppliesToResource(lock: DavLock, resourcePath: string): boolean { diff --git a/server/app.ts b/server/app.ts index fc081867..20d1df6c 100644 --- a/server/app.ts +++ b/server/app.ts @@ -69,6 +69,7 @@ export function createApp(platform: Platform, auth: Auth, deps: Deps = createDep getSitePublicOrigin(deps), getSiteWebDavRuntimeConfig(deps), ]) + c.set('webDavEnabled', webDavConfig.enabled) c.set('webDavDomain', webDavConfig.domain) const routingOrigin = sitePublicOrigin ?? (webDavConfig.domain ? new URL(c.req.url).origin : null) if (webDavConfig.enabled && isWebDavPublicRequest(c.req.url, routingOrigin, webDavConfig.domain)) { diff --git a/server/db/schema.ts b/server/db/schema.ts index b31a0527..1cc241c5 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -21,7 +21,19 @@ export const matters = sqliteTable( createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), }, - (t) => [index('matters_status_dir_created_idx').on(t.status, t.dirtype, t.createdAt)], + (t) => [ + index('matters_status_dir_created_idx').on(t.status, t.dirtype, t.createdAt), + index('matters_webdav_path_idx').on(t.orgId, t.parent, t.name, t.status, t.trashedAt, t.purgedAt), + index('matters_webdav_children_idx').on( + t.orgId, + t.parent, + t.status, + t.trashedAt, + t.purgedAt, + sql`${t.dirtype} desc`, + t.name, + ), + ], ) export const webdavDeadProperties = sqliteTable( diff --git a/server/http/webdav.integration.test.ts b/server/http/webdav.integration.test.ts index 621e5b42..2e3ff407 100644 --- a/server/http/webdav.integration.test.ts +++ b/server/http/webdav.integration.test.ts @@ -599,6 +599,56 @@ describe('WebDAV API', () => { }) }) + it('returns a streamed GET before Workers background bookkeeping completes', async () => { + const { app, db, auth, deps } = await createTestApp() + await authedHeaders(app) + await seedStorage(db) + const workspace = await org(db) + const account = await userAccount(db) + const key = await apiKey(auth, account.id, { webdav: ['read'] }) + await file(db, workspace.id, { id: 'background-read', name: 'background.txt', size: 12 }) + + const originalRecord = deps.audit.record + let releaseAudit: (() => void) | undefined + const auditGate = new Promise((resolve) => { + releaseAudit = resolve + }) + vi.spyOn(deps.audit, 'record').mockImplementation(async (event) => { + if (event.action === 'webdav_download') await auditGate + await originalRecord(event) + }) + + const backgroundTasks: Promise[] = [] + const executionCtx = { + waitUntil(task: Promise) { + backgroundTasks.push(task) + }, + passThroughOnException() {}, + props: undefined, + } + const response = await app.fetch( + new Request(`http://localhost/dav/${workspace.slug}/background.txt`, { + method: 'GET', + headers: basicHeaders(account.email, key), + }), + undefined, + executionCtx, + ) + + expect(response.status).toBe(200) + expect(backgroundTasks.length).toBeGreaterThan(0) + expect(await response.text()).toBe('hello webdav') + + releaseAudit?.() + await Promise.all(backgroundTasks) + expect( + await db.all(sql` + SELECT action FROM audit_events + WHERE target_id = 'background-read' AND action = 'webdav_download' + `), + ).toEqual([{ action: 'webdav_download' }]) + }) + it('GET consumes WebDAV traffic while HEAD does not [spec: webdav/get-traffic]', async () => { const { app, db, auth } = await createTestApp() await authedHeaders(app) diff --git a/server/http/webdav.ts b/server/http/webdav.ts index 617b1a59..a4996d5b 100644 --- a/server/http/webdav.ts +++ b/server/http/webdav.ts @@ -30,7 +30,6 @@ import { WebDavPathError, type WebDavTarget, } from '../usecases/ports' -import { getSiteWebDavRuntimeConfig } from '../usecases/site/settings' import { recordAuditEvent, recordDownloadFailure, @@ -414,7 +413,10 @@ function fixedLengthResponseBody(body: BodyInit, contentLength: number): BodyIni if (!ctor || !isReadableBodyStream(body)) return body const { readable, writable } = new ctor(contentLength) - void bridgeFixedLengthStream(body, writable) + void body.pipeTo(writable).catch((error) => { + if (isExpectedStreamCancellation(error)) return + console.error(JSON.stringify({ message: 'webdav.response_stream.error', error: errorMessage(error) })) + }) return readable } @@ -422,35 +424,6 @@ function isReadableBodyStream(body: BodyInit): body is ReadableStream).getReader === 'function' } -async function bridgeFixedLengthStream( - body: ReadableStream, - writable: WritableStream, -): Promise { - const reader = body.getReader() - const writer = writable.getWriter() - try { - for (;;) { - const { done, value } = await reader.read() - if (done) break - await writer.write(value) - } - await writer.close() - } catch (error) { - await Promise.allSettled([cancelStreamReader(reader, error), writer.abort(error)]) - } finally { - releaseStreamLock(reader) - releaseStreamLock(writer) - } -} - -async function cancelStreamReader(reader: ReadableStreamDefaultReader, reason: unknown): Promise { - try { - await reader.cancel(reason) - } catch { - return - } -} - function releaseStreamLock(stream: { releaseLock: () => void }): void { try { stream.releaseLock() @@ -598,23 +571,24 @@ async function davEntries(c: DavContext, targets: WebDavTarget[]): Promise { + const paths = workspaceTargets.map(resourcePath) + const [deadPropertiesByPath, locksByPath] = await Promise.all([ + listDeadPropertiesForResources(c.get('deps'), { orgId: workspace.id, resourcePaths: paths }), + activeLocksForResources(c.get('deps'), { orgId: workspace.id, resourcePaths: paths }), + ]) + return workspaceTargets.map((target) => { + const path = resourcePath(target) + const deadProperties = deadPropertiesByPath.get(path) ?? [] + const locks = locksByPath.get(path) ?? [] + return target.matter ? matterEntry(workspace, target.matter, deadProperties, locks, mountPath) - : workspaceEntry(workspace, deadProperties, locks, mountPath), - ) - } - } + : workspaceEntry(workspace, deadProperties, locks, mountPath) + }) + }), + ) + for (const resolved of workspaceEntries) entries.push(...resolved) return entries } @@ -622,8 +596,7 @@ async function davEntries(c: DavContext, targets: WebDavTarget[]): Promise() app.use('*', async (c, next) => { - const config = await getSiteWebDavRuntimeConfig(c.get('deps')) - if (!config.enabled) return c.notFound() + if (!c.get('webDavEnabled')) return c.notFound() await next() }) @@ -639,7 +612,15 @@ app.use('*', async (c, next) => { const preparedPut = c.req.method === 'PUT' ? await prepareWebDavPutAudit(c, userId) : null const preparedAction = await prepareWebDavActionAudit(c, userId) await next() + await waitUntilOrAwait(c, 'audit', processWebDavAudit(c, userId, preparedPut, preparedAction)) +}) +async function processWebDavAudit( + c: DavContext, + userId: string, + preparedPut: TransferAuditTarget | null, + preparedAction: WebDavActionAuditContext | null, +): Promise { const actor = transferAuditActor(c.get('principal')) if (c.req.method === 'GET' && isDownloadFailureStatus(c.res.status)) { const target = await webDavDownloadAuditTarget(c, userId) @@ -666,7 +647,7 @@ app.use('*', async (c, next) => { const bytes = exactRequestContentLength(c) if (bytes === null) return await recordUploadResult(c.get('deps'), actor, { ...preparedPut, bytes }, transferFailureReason(c)) -}) +} type WebDavActionAuditContext = { method: 'MKCOL' | 'DELETE' | 'MOVE' | 'COPY' @@ -985,15 +966,19 @@ async function readFile(c: DavContext, auth: DavAuth): Promise { if (reservation.error) return reservation.error try { const body = await getWebDavObjectBody(c.get('deps'), { storage, object: matter.object }) - await finishWebDavDownload(c, { - orgId: workspace.id, - userId: auth.userId, - matterId: matter.id, - matterName: matter.name, - storageId: storage.id, - bytes: size, - trafficEventId: reservation.trafficEventId, - }) + await waitUntilOrAwait( + c, + 'download_finish', + finishWebDavDownload(c, { + orgId: workspace.id, + userId: auth.userId, + matterId: matter.id, + matterName: matter.name, + storageId: storage.id, + bytes: size, + trafficEventId: reservation.trafficEventId, + }), + ) return new Response(fixedLengthResponseBody(body, size), { headers }) } catch (e) { await refundWebDavTraffic(c.get('deps'), { @@ -1018,15 +1003,19 @@ async function readFile(c: DavContext, auth: DavAuth): Promise { headers.set('Content-Length', String(contentLength)) headers.delete('Content-Range') try { - await finishWebDavDownload(c, { - orgId: workspace.id, - userId: auth.userId, - matterId: matter.id, - matterName: matter.name, - storageId: storage.id, - bytes: trafficBytes, - trafficEventId: reservation.trafficEventId, - }) + await waitUntilOrAwait( + c, + 'download_finish', + finishWebDavDownload(c, { + orgId: workspace.id, + userId: auth.userId, + matterId: matter.id, + matterName: matter.name, + storageId: storage.id, + bytes: trafficBytes, + trafficEventId: reservation.trafficEventId, + }), + ) } catch (error) { await refundWebDavTraffic(c.get('deps'), { orgId: workspace.id, @@ -1058,15 +1047,19 @@ async function readFile(c: DavContext, auth: DavAuth): Promise { headers.set('Content-Length', String(contentLength)) headers.set('Content-Range', `bytes ${range.start}-${range.end}/${size}`) try { - await finishWebDavDownload(c, { - orgId: workspace.id, - userId: auth.userId, - matterId: matter.id, - matterName: matter.name, - storageId: storage.id, - bytes: contentLength, - trafficEventId: reservation.trafficEventId, - }) + await waitUntilOrAwait( + c, + 'download_finish', + finishWebDavDownload(c, { + orgId: workspace.id, + userId: auth.userId, + matterId: matter.id, + matterName: matter.name, + storageId: storage.id, + bytes: contentLength, + trafficEventId: reservation.trafficEventId, + }), + ) } catch (error) { await refundWebDavTraffic(c.get('deps'), { orgId: workspace.id, @@ -1093,25 +1086,48 @@ async function finishWebDavDownload( trafficEventId: string }, ): Promise { - await recordWebDavDownloadIssued(c.get('deps'), params) - await recordDownloadIssued( - c.get('deps'), - transferAuditActor(c.get('principal')), - 'webdav_download', - { - orgId: params.orgId, - targetType: 'file', - targetId: params.matterId, - targetName: params.matterName, - bytes: params.bytes, - source: 'webdav_download', - metadata: { - matterId: params.matterId, - storageId: params.storageId, + await Promise.all([ + recordWebDavDownloadIssued(c.get('deps'), params), + recordDownloadIssued( + c.get('deps'), + transferAuditActor(c.get('principal')), + 'webdav_download', + { + orgId: params.orgId, + targetType: 'file', + targetId: params.matterId, + targetName: params.matterName, + bytes: params.bytes, + source: 'webdav_download', + metadata: { + matterId: params.matterId, + storageId: params.storageId, + }, }, - }, - params.trafficEventId, - ) + params.trafficEventId, + ), + ]) +} + +async function waitUntilOrAwait(c: DavContext, operation: string, task: Promise): Promise { + const loggedTask = task.catch((error) => { + console.error(JSON.stringify({ message: 'webdav.background.error', operation, error: errorMessage(error) })) + throw error + }) + try { + c.executionCtx.waitUntil(loggedTask) + } catch { + await loggedTask + } +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error) +} + +function isExpectedStreamCancellation(error: unknown): boolean { + const message = errorMessage(error) + return message === 'Network connection lost.' || message.includes('destination stream aborted') } // Meters a WebDAV download (consume traffic quota → report egress) and renders diff --git a/server/middleware/platform.ts b/server/middleware/platform.ts index 665c1c10..8afec8e5 100644 --- a/server/middleware/platform.ts +++ b/server/middleware/platform.ts @@ -14,6 +14,7 @@ export type Env = { userRole: string | null orgId: string | null sitePublicOrigin: string | null + webDavEnabled: boolean webDavDomain: string webDavMountPath: WebDavMountPath // Structured detail for the access log on a failed request. Set by `jsonError` @@ -64,6 +65,7 @@ export const platformMiddleware = (platform: Platform, auth: Auth) => c.set('principal', null) c.set('errorLog', null) c.set('sitePublicOrigin', null) + c.set('webDavEnabled', false) c.set('webDavDomain', '') c.set('webDavMountPath', '/dav') await next() diff --git a/server/scheduled-worker.test.ts b/server/scheduled-worker.test.ts index f763d0f8..7894309c 100644 --- a/server/scheduled-worker.test.ts +++ b/server/scheduled-worker.test.ts @@ -12,11 +12,13 @@ vi.mock('../server/platform/cloudflare', () => ({ })) const refreshHourlyRollups = vi.fn() +const purgeExpiredLocks = vi.fn() const reconcileFreePlanBaselines = vi.fn() const fakeDeps = { instance: 'instance', systemOptions: 'system-options', adminStats: { refreshHourlyRollups }, + webdavState: { purgeExpiredLocks }, quota: { reconcileFreePlanBaselines }, } vi.mock('../server/composition', () => ({ @@ -60,6 +62,7 @@ describe('handleScheduled', () => { vi.mocked(runLicensingRefresh).mockReset() mockResetExpiredTrafficQuotas.mockReset() refreshHourlyRollups.mockReset() + purgeExpiredLocks.mockReset() reconcileFreePlanBaselines.mockReset() }) @@ -80,6 +83,7 @@ describe('handleScheduled', () => { await handleScheduled({ cron: '10 * * * *' }, { DB: {} as D1Database }) expect(refreshHourlyRollups).toHaveBeenCalledOnce() + expect(purgeExpiredLocks).toHaveBeenCalledOnce() expect(reconcileFreePlanBaselines).not.toHaveBeenCalled() expect(syncPendingCloudTrafficReports).not.toHaveBeenCalled() expect(syncPendingRemoteDownloadUsageReports).not.toHaveBeenCalled() diff --git a/server/usecases/ports/user.ts b/server/usecases/ports/user.ts index c13fa59a..8b0347af 100644 --- a/server/usecases/ports/user.ts +++ b/server/usecases/ports/user.ts @@ -51,8 +51,9 @@ export interface UserAdminRepo { // Whether the user is banned/disabled — checked by the auth middleware on every // authenticated request to reject sessions of users disabled mid-session. isBanned(userId: string): Promise - // Whether `username` matches the user's email or username (WebDAV Basic Auth check). - matchesUsername(userId: string, username: string): Promise + // Whether the user is active and `username` matches their email or username + // (WebDAV Basic Auth check). + matchesActiveUsername(userId: string, username: string): Promise listUserPersonalEntitlements( userId: string, diff --git a/server/usecases/ports/webdav-state.ts b/server/usecases/ports/webdav-state.ts index edeca25e..07cfbcfc 100644 --- a/server/usecases/ports/webdav-state.ts +++ b/server/usecases/ports/webdav-state.ts @@ -14,9 +14,9 @@ export interface CreateLockInput { timeoutSeconds: number } -// WebDAV lock + dead-property state. Locks expire on a wall-clock TTL, so reads -// purge expired rows before returning; depth-infinity scoping is resolved in the -// repo (D1 cannot express the dynamic LIKE the scope check needs). +// WebDAV lock + dead-property state. Reads exclude expired locks; scheduled +// cleanup removes their rows. Depth-infinity scoping is resolved in the repo +// (D1 cannot express the dynamic LIKE the scope check needs). export interface WebDavStateRepo { listDeadPropertiesForResources(orgId: string, resourcePaths: string[]): Promise> applyDeadPropertyUpdate(orgId: string, resourcePath: string, operations: DeadPropertyUpdate[]): Promise @@ -29,4 +29,5 @@ export interface WebDavStateRepo { createLock(input: CreateLockInput): Promise refreshLock(orgId: string, resourcePath: string, token: string, timeoutSeconds: number): Promise removeLock(orgId: string, resourcePath: string, token: string): Promise + purgeExpiredLocks(now?: number): Promise } diff --git a/server/usecases/store/traffic-metering.ts b/server/usecases/store/traffic-metering.ts index a261edf7..e9f93e76 100644 --- a/server/usecases/store/traffic-metering.ts +++ b/server/usecases/store/traffic-metering.ts @@ -50,6 +50,7 @@ export async function reportTrafficEgress( source: TrafficReportSource sourceId: string eventId?: string + eventIdIsNew?: boolean now?: Date }, ): Promise<{ status: CloudTrafficReportStatus; eventId: string; duplicate: boolean }> { @@ -63,7 +64,7 @@ export async function reportTrafficEgress( const eventId = params.eventId ?? `traffic_${nanoid()}` await deps.cloudTrafficReports.ensureLedgerOpening(now) - const existing = await deps.cloudTrafficReports.findByEventId(eventId) + const existing = params.eventIdIsNew ? undefined : await deps.cloudTrafficReports.findByEventId(eventId) const period = existing?.period ?? currentTrafficPeriod(now) if (existing) { assertSameReport(existing, { @@ -278,6 +279,7 @@ export type DownloadTrafficParams = { source: TrafficReportSource sourceId: string eventId?: string + eventIdIsNew?: boolean // Compensating action run when traffic is rejected (quota or egress). onRejected?: () => Promise } @@ -319,6 +321,7 @@ export async function reportDownloadEgress( source: params.source, sourceId: params.sourceId, eventId: params.eventId, + eventIdIsNew: params.eventIdIsNew ?? params.eventId === undefined, }) return { ok: true } } catch (error) { diff --git a/server/usecases/user.test.ts b/server/usecases/user.test.ts index 8974a229..23061d7b 100644 --- a/server/usecases/user.test.ts +++ b/server/usecases/user.test.ts @@ -38,7 +38,7 @@ const failure: UserOperationFailure = { error: 'User not found: missing', status function makeDeps(userAdmin: Partial = {}) { const repo: UserAdminRepo = { isBanned: async () => false, - matchesUsername: async () => false, + matchesActiveUsername: async () => false, listUserPersonalEntitlements: async () => ({ orgId: 'org-1', items: [] }), grantUserPersonalEntitlement: async () => sampleResult, updateUserPersonalEntitlement: async () => sampleResult, diff --git a/server/usecases/webdav.test.ts b/server/usecases/webdav.test.ts index 229da9f9..f5e33dc3 100644 --- a/server/usecases/webdav.test.ts +++ b/server/usecases/webdav.test.ts @@ -127,7 +127,7 @@ function makeDeps( } as unknown as ApiKeyGateway, userAdmin: { isBanned: async () => false, - matchesUsername: async () => true, + matchesActiveUsername: async () => true, ...overrides.userAdmin, } as unknown as UserAdminRepo, matter: { @@ -192,6 +192,7 @@ function makeDeps( createLock: async () => lock, refreshLock: async () => lock, removeLock: async () => true, + purgeExpiredLocks: async () => {}, ...overrides.webdavState, } as unknown as WebDavStateRepo, downloadTasks: { @@ -236,12 +237,12 @@ describe('webdav usecase', () => { }) it('is unauthorized when the key owner is disabled', async () => { - const deps = makeDeps({ userAdmin: { isBanned: async () => true } }) + const deps = makeDeps({ userAdmin: { matchesActiveUsername: async () => false } }) expect(await resolveWebDavAuth(deps, authParams)).toEqual({ ok: false, reason: 'unauthorized' }) }) it('is unauthorized when the username does not match the key owner', async () => { - const deps = makeDeps({ userAdmin: { matchesUsername: async () => false } }) + const deps = makeDeps({ userAdmin: { matchesActiveUsername: async () => false } }) expect(await resolveWebDavAuth(deps, authParams)).toEqual({ ok: false, reason: 'unauthorized' }) }) diff --git a/server/usecases/webdav.ts b/server/usecases/webdav.ts index 0ee6a4e8..d9bd66ad 100644 --- a/server/usecases/webdav.ts +++ b/server/usecases/webdav.ts @@ -70,8 +70,7 @@ export async function resolveWebDavAuth( params.configId, ) if (!key) return { ok: false, reason: 'unauthorized' } - if (await deps.userAdmin.isBanned(key.referenceId)) return { ok: false, reason: 'unauthorized' } - if (!(await deps.userAdmin.matchesUsername(key.referenceId, params.username))) { + if (!(await deps.userAdmin.matchesActiveUsername(key.referenceId, params.username))) { return { ok: false, reason: 'unauthorized' } } return { @@ -217,6 +216,7 @@ export async function meterWebDavDownload( source: WEBDAV_DOWNLOAD_SOURCE, sourceId: params.matterId, eventId: params.trafficEventId, + eventIdIsNew: true, }) return outcome } diff --git a/shared/api-key-templates.ts b/shared/api-key-templates.ts index 3784ee76..6d7c235a 100644 --- a/shared/api-key-templates.ts +++ b/shared/api-key-templates.ts @@ -36,7 +36,6 @@ export function parseApiKeyScope(metadata: unknown): ApiKeyScope | null { } export const WEBDAV_API_KEY_RATE_LIMIT_WINDOW_MS = 60_000 -export const WEBDAV_API_KEY_LEGACY_RATE_LIMIT_MAX_REQUESTS = 120 export const WEBDAV_API_KEY_RATE_LIMIT_MAX_REQUESTS = 3600 export const IHOST_API_KEY_PERMISSIONS = { ihost: ['upload'] } satisfies ApiKeyPermissions diff --git a/workers/scheduled.ts b/workers/scheduled.ts index 1d9f27ec..45cfca9e 100644 --- a/workers/scheduled.ts +++ b/workers/scheduled.ts @@ -44,7 +44,7 @@ export async function handleScheduled(event: ScheduledTrigger, env: ScheduledEnv } if (event.cron === STATS_ROLLUP_CRON) { - await deps.adminStats.refreshHourlyRollups(new Date()) + await Promise.all([deps.adminStats.refreshHourlyRollups(new Date()), deps.webdavState.purgeExpiredLocks()]) return } diff --git a/wrangler.toml b/wrangler.toml index 5963064a..16c30709 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -37,6 +37,10 @@ bucket_name = "zpan-public-images" [observability] enabled = true +[observability.traces] +enabled = true +head_sampling_rate = 0.1 + [triggers] crons = ["*/10 * * * *", "10 * * * *", "0 */6 * * *", "0 */12 * * *", "0 0 1 * *", "0 4 * * *"]