From 30a282eaed7a7b4339fd186e99836d617fbfd108 Mon Sep 17 00:00:00 2001 From: Mateo Roldos Date: Fri, 18 Sep 2026 16:58:21 -0300 Subject: [PATCH 01/19] feat(auth): enable Better Auth organizations and generated schema --- .../migration.sql | 38 + .../snapshot.json | 1300 +++++++++++++++++ adapters/database-postgres/src/auth/schema.ts | 115 +- apps/web/src/lib/features/auth/client.ts | 3 +- .../web/src/lib/server/authentication.test.ts | 70 +- .../web/src/lib/server/better-auth-options.ts | 2 + 6 files changed, 1524 insertions(+), 4 deletions(-) create mode 100644 adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/migration.sql create mode 100644 adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/snapshot.json diff --git a/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/migration.sql b/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/migration.sql new file mode 100644 index 0000000..0f91675 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/migration.sql @@ -0,0 +1,38 @@ +CREATE TABLE "invitation" ( + "id" text PRIMARY KEY, + "organization_id" text NOT NULL, + "email" text NOT NULL, + "role" text, + "status" text DEFAULT 'pending' NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "inviter_id" text NOT NULL +); +--> statement-breakpoint +CREATE TABLE "member" ( + "id" text PRIMARY KEY, + "organization_id" text NOT NULL, + "user_id" text NOT NULL, + "role" text DEFAULT 'member' NOT NULL, + "created_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "organization" ( + "id" text PRIMARY KEY, + "name" text NOT NULL, + "slug" text NOT NULL UNIQUE, + "logo" text, + "created_at" timestamp NOT NULL, + "metadata" text +); +--> statement-breakpoint +ALTER TABLE "session" ADD COLUMN "active_organization_id" text;--> statement-breakpoint +CREATE INDEX "invitation_organizationId_idx" ON "invitation" ("organization_id");--> statement-breakpoint +CREATE INDEX "invitation_email_idx" ON "invitation" ("email");--> statement-breakpoint +CREATE INDEX "member_organizationId_idx" ON "member" ("organization_id");--> statement-breakpoint +CREATE INDEX "member_userId_idx" ON "member" ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "organization_slug_uidx" ON "organization" ("slug");--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fkey" FOREIGN KEY ("inviter_id") REFERENCES "user"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE CASCADE;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE; \ No newline at end of file diff --git a/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/snapshot.json b/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/snapshot.json new file mode 100644 index 0000000..c404145 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918195841_friendly_ken_ellis/snapshot.json @@ -0,0 +1,1300 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "2a9c34eb-9235-4966-b3f6-cd293645b3c9", + "prevIds": ["f3b5b1c1-720f-4d11-9a56-780da28be2d0"], + "ddl": [ + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "member", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "rate_limit", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "workspace_members", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "workspaces", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "count", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_organization_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspace_id", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "workspaces" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "workspaces" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_slug_uidx", + "entityType": "indexes", + "schema": "public", + "table": "organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "identifier", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "verification_identifier_idx", + "entityType": "indexes", + "schema": "public", + "table": "verification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "workspace_members_user_id_index", + "entityType": "indexes", + "schema": "public", + "table": "workspace_members" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["inviter_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_inviter_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": ["workspace_id"], + "schemaTo": "public", + "tableTo": "workspaces", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "workspace_members_workspace_id_workspaces_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "workspace_members" + }, + { + "columns": ["workspace_id", "user_id"], + "nameExplicit": false, + "name": "workspace_members_pkey", + "entityType": "pks", + "schema": "public", + "table": "workspace_members" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "public", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "member_pkey", + "schema": "public", + "table": "member", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "rate_limit_pkey", + "schema": "public", + "table": "rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "workspaces_pkey", + "schema": "public", + "table": "workspaces", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": ["name"], + "nullsNotDistinct": false, + "name": "workspaces_name_unique", + "entityType": "uniques", + "schema": "public", + "table": "workspaces" + }, + { + "nameExplicit": false, + "columns": ["slug"], + "nullsNotDistinct": false, + "name": "organization_slug_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["key"], + "nullsNotDistinct": false, + "name": "rate_limit_key_key", + "schema": "public", + "table": "rate_limit", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["token"], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + }, + { + "value": "\"role\" in ('owner', 'admin', 'member')", + "name": "workspace_members_role_check", + "entityType": "checks", + "schema": "public", + "table": "workspace_members" + } + ], + "renames": [] +} diff --git a/adapters/database-postgres/src/auth/schema.ts b/adapters/database-postgres/src/auth/schema.ts index f7806fc..9f0fddf 100644 --- a/adapters/database-postgres/src/auth/schema.ts +++ b/adapters/database-postgres/src/auth/schema.ts @@ -1,6 +1,15 @@ /** @effect-diagnostics globalDate:skip-file */ import { defineRelationsPart } from "drizzle-orm"; -import { pgTable, text, bigint, timestamp, boolean, integer, index } from "drizzle-orm/pg-core"; +import { + pgTable, + text, + bigint, + timestamp, + boolean, + integer, + index, + uniqueIndex, +} from "drizzle-orm/pg-core"; export const user = pgTable("user", { id: text("id").primaryKey(), @@ -30,6 +39,7 @@ export const session = pgTable( userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), + activeOrganizationId: text("active_organization_id"), }, (table) => [index("session_userId_idx").on(table.userId)], ); @@ -74,6 +84,60 @@ export const verification = pgTable( (table) => [index("verification_identifier_idx").on(table.identifier)], ); +export const organization = pgTable( + "organization", + { + id: text("id").primaryKey(), + name: text("name").notNull(), + slug: text("slug").notNull().unique(), + logo: text("logo"), + createdAt: timestamp("created_at").notNull(), + metadata: text("metadata"), + }, + (table) => [uniqueIndex("organization_slug_uidx").on(table.slug)], +); + +export const member = pgTable( + "member", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + role: text("role").default("member").notNull(), + createdAt: timestamp("created_at").notNull(), + }, + (table) => [ + index("member_organizationId_idx").on(table.organizationId), + index("member_userId_idx").on(table.userId), + ], +); + +export const invitation = pgTable( + "invitation", + { + id: text("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + email: text("email").notNull(), + role: text("role"), + status: text("status").default("pending").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + inviterId: text("inviter_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + }, + (table) => [ + index("invitation_organizationId_idx").on(table.organizationId), + index("invitation_email_idx").on(table.email), + ], +); + export const rateLimit = pgTable("rate_limit", { id: text("id").primaryKey(), key: text("key").notNull().unique(), @@ -82,7 +146,16 @@ export const rateLimit = pgTable("rate_limit", { }); export const authRelations = defineRelationsPart( - { user, session, account, verification, rateLimit }, + { + user, + session, + account, + verification, + organization, + member, + invitation, + rateLimit, + }, (r) => ({ user: { sessions: r.many.session({ @@ -93,6 +166,14 @@ export const authRelations = defineRelationsPart( from: r.user.id, to: r.account.userId, }), + members: r.many.member({ + from: r.user.id, + to: r.member.userId, + }), + invitations: r.many.invitation({ + from: r.user.id, + to: r.invitation.inviterId, + }), }, session: { user: r.one.user({ @@ -106,5 +187,35 @@ export const authRelations = defineRelationsPart( to: r.user.id, }), }, + organization: { + members: r.many.member({ + from: r.organization.id, + to: r.member.organizationId, + }), + invitations: r.many.invitation({ + from: r.organization.id, + to: r.invitation.organizationId, + }), + }, + member: { + organization: r.one.organization({ + from: r.member.organizationId, + to: r.organization.id, + }), + user: r.one.user({ + from: r.member.userId, + to: r.user.id, + }), + }, + invitation: { + organization: r.one.organization({ + from: r.invitation.organizationId, + to: r.organization.id, + }), + user: r.one.user({ + from: r.invitation.inviterId, + to: r.user.id, + }), + }, }), ); diff --git a/apps/web/src/lib/features/auth/client.ts b/apps/web/src/lib/features/auth/client.ts index 3792cf4..38b7cd3 100644 --- a/apps/web/src/lib/features/auth/client.ts +++ b/apps/web/src/lib/features/auth/client.ts @@ -1,4 +1,5 @@ import { createAuthClient } from "better-auth/svelte"; +import { organizationClient } from "better-auth/client/plugins"; /** Better Auth's shared browser session and protocol client. */ -export const authClient = createAuthClient(); +export const authClient = createAuthClient({ plugins: [organizationClient()] }); diff --git a/apps/web/src/lib/server/authentication.test.ts b/apps/web/src/lib/server/authentication.test.ts index cd54457..e233ce0 100644 --- a/apps/web/src/lib/server/authentication.test.ts +++ b/apps/web/src/lib/server/authentication.test.ts @@ -2,13 +2,16 @@ import { assert, describe, it } from "@effect/vitest"; import * as authSchema from "@effect-forge/database-postgres/auth-schema"; import { PgliteDatabase } from "@effect-forge/database-postgres/test/pglite-database"; import { eq } from "drizzle-orm"; -import { Effect, Fiber, Redacted, Result } from "effect"; +import { Effect, Fiber, Redacted, Result, Schema } from "effect"; import { Authentication } from "./authentication.ts"; const baseURL = new URL("http://localhost:5173"); const secret = Redacted.make("test-secret-value-with-at-least-32-characters"); const signUpBody = '{"name":"Ada Lovelace","email":"ada@example.com","password":"correct-horse-battery-staple"}'; +const encodeBody = Schema.encodeSync( + Schema.fromJsonString(Schema.Record(Schema.String, Schema.String)), +); const request = (path: string, init?: RequestInit & { duplex?: "half" }) => new Request(new URL(path, baseURL), init); @@ -145,4 +148,69 @@ describe("Authentication", () => { } }), ); + + it.effect( + "creates organizations with owner membership and rejects another user's selection", + () => + Effect.gen(function* () { + const auth = yield* fixture; + const cookie = yield* auth.signUp(); + const call = Effect.fnUntraced(function* ( + path: string, + body: Readonly>, + session = cookie, + ) { + const service = yield* auth.authentication( + request(`/api/auth/organization/${path}`, { + method: "POST", + headers: { + "content-type": "application/json", + origin: baseURL.origin, + cookie: session, + }, + body: encodeBody(body), + }), + ); + return yield* service.handle; + }); + const created = yield* call("create", { + name: "Analytical Engine", + slug: "analytical-engine", + }); + assert.strictEqual(created.status, 200); + const organization = yield* Effect.promise(() => created.json()).pipe( + Effect.flatMap(Schema.decodeUnknownEffect(Schema.Struct({ id: Schema.String }))), + ); + const members = yield* Effect.promise(() => auth.database.select().from(authSchema.member)); + assert.lengthOf(members, 1); + assert.strictEqual(members[0]?.role, "owner"); + assert.strictEqual(members[0]?.organizationId, organization.id); + + assert.strictEqual( + (yield* call("set-active", { organizationId: organization.id })).status, + 200, + ); + const other = yield* auth.authentication( + request("/api/auth/sign-up/email", { + method: "POST", + headers: { "content-type": "application/json", origin: baseURL.origin }, + body: encodeBody({ + name: "Grace", + email: "grace@example.com", + password: "correct-horse-battery-staple", + }), + }), + ); + const response = yield* other.handle; + assert.strictEqual(response.status, 200); + const otherCookie = response.headers + .getSetCookie() + .map((value) => value.split(";")[0]) + .join("; "); + assert.strictEqual( + (yield* call("set-active", { organizationId: organization.id }, otherCookie)).status, + 403, + ); + }), + ); }); diff --git a/apps/web/src/lib/server/better-auth-options.ts b/apps/web/src/lib/server/better-auth-options.ts index 5ad0399..f2a7429 100644 --- a/apps/web/src/lib/server/better-auth-options.ts +++ b/apps/web/src/lib/server/better-auth-options.ts @@ -1,7 +1,9 @@ import type { BetterAuthOptions } from "better-auth"; +import { organization } from "better-auth/plugins/organization"; /** Provider behavior shared by the Web runtime and schema generator. */ export const betterAuthOptions = { + plugins: [organization()], emailAndPassword: { enabled: true }, logger: { disabled: true }, telemetry: { enabled: false }, From 225e5c96ec275321daf08198f0c32fdfa07007ed Mon Sep 17 00:00:00 2001 From: Mateo Roldos Date: Fri, 18 Sep 2026 17:01:27 -0300 Subject: [PATCH 02/19] feat(core): authorize organization-owned todos by capability --- adapters/database-postgres/drizzle.config.ts | 2 +- .../migration.sql | 10 + .../snapshot.json | 1413 +++++++++++++++++ .../src/persistence-postgres.ts | 5 +- .../src/test/persistence-pglite.ts | 5 +- adapters/database-postgres/src/todo/schema.ts | 16 + .../src/todo/todo-store-postgres.test.ts | 120 ++ .../src/todo/todo-store-postgres.ts | 62 + .../web/src/lib/server/authentication.test.ts | 101 +- apps/web/src/lib/server/authentication.ts | 53 +- apps/web/src/lib/server/runtime.ts | 3 +- packages/core/package.json | 3 + packages/core/src/application.ts | 3 +- .../organization-access.test.ts | 103 ++ .../organization-access.ts | 43 + .../organization-membership.ts | 24 + .../src/organization-access/permission.ts | 27 + packages/core/src/todo/todo-directory.test.ts | 172 ++ packages/core/src/todo/todo-directory.ts | 85 + .../core/src/todo/todo-store-memory.test.ts | 46 + packages/core/src/todo/todo-store-memory.ts | 45 + packages/core/src/todo/todo-store.ts | 33 + packages/domain/package.json | 2 + .../domain/src/organization/organization.ts | 19 + packages/domain/src/todo/todo.test.ts | 14 + packages/domain/src/todo/todo.ts | 19 + 26 files changed, 2416 insertions(+), 12 deletions(-) create mode 100644 adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/migration.sql create mode 100644 adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/snapshot.json create mode 100644 adapters/database-postgres/src/todo/schema.ts create mode 100644 adapters/database-postgres/src/todo/todo-store-postgres.test.ts create mode 100644 adapters/database-postgres/src/todo/todo-store-postgres.ts create mode 100644 packages/core/src/organization-access/organization-access.test.ts create mode 100644 packages/core/src/organization-access/organization-access.ts create mode 100644 packages/core/src/organization-access/organization-membership.ts create mode 100644 packages/core/src/organization-access/permission.ts create mode 100644 packages/core/src/todo/todo-directory.test.ts create mode 100644 packages/core/src/todo/todo-directory.ts create mode 100644 packages/core/src/todo/todo-store-memory.test.ts create mode 100644 packages/core/src/todo/todo-store-memory.ts create mode 100644 packages/core/src/todo/todo-store.ts create mode 100644 packages/domain/src/organization/organization.ts create mode 100644 packages/domain/src/todo/todo.test.ts create mode 100644 packages/domain/src/todo/todo.ts diff --git a/adapters/database-postgres/drizzle.config.ts b/adapters/database-postgres/drizzle.config.ts index c20f290..c6ff0e0 100644 --- a/adapters/database-postgres/drizzle.config.ts +++ b/adapters/database-postgres/drizzle.config.ts @@ -3,5 +3,5 @@ import { defineConfig } from "drizzle-kit"; export default defineConfig({ dialect: "postgresql", out: "./drizzle", - schema: ["./src/auth/schema.ts", "./src/workspace/schema.ts"], + schema: ["./src/auth/schema.ts", "./src/workspace/schema.ts", "./src/todo/schema.ts"], }); diff --git a/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/migration.sql b/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/migration.sql new file mode 100644 index 0000000..04b33a1 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/migration.sql @@ -0,0 +1,10 @@ +CREATE TABLE "todos" ( + "id" uuid PRIMARY KEY, + "organization_id" text NOT NULL, + "title" varchar(200) NOT NULL, + "completed" boolean DEFAULT false NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE INDEX "todos_organization_id_index" ON "todos" ("organization_id");--> statement-breakpoint +ALTER TABLE "todos" ADD CONSTRAINT "todos_organization_id_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organization"("id") ON DELETE CASCADE; \ No newline at end of file diff --git a/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/snapshot.json b/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/snapshot.json new file mode 100644 index 0000000..29cbf30 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918200404_pale_iron_fist/snapshot.json @@ -0,0 +1,1413 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "b2323111-6852-49eb-ac65-d97a9b51df79", + "prevIds": ["2a9c34eb-9235-4966-b3f6-cd293645b3c9"], + "ddl": [ + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "member", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "rate_limit", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "workspace_members", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "workspaces", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "todos", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "count", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_organization_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "workspace_id", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "varchar(255)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "varchar(20)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "workspace_members" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "workspaces" + }, + { + "type": "varchar(100)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "workspaces" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "completed", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_slug_uidx", + "entityType": "indexes", + "schema": "public", + "table": "organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "identifier", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "verification_identifier_idx", + "entityType": "indexes", + "schema": "public", + "table": "verification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "workspace_members_user_id_index", + "entityType": "indexes", + "schema": "public", + "table": "workspace_members" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "todos_organization_id_index", + "entityType": "indexes", + "schema": "public", + "table": "todos" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["inviter_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_inviter_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": ["workspace_id"], + "schemaTo": "public", + "tableTo": "workspaces", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "workspace_members_workspace_id_workspaces_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "workspace_members" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "todos_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "todos" + }, + { + "columns": ["workspace_id", "user_id"], + "nameExplicit": false, + "name": "workspace_members_pkey", + "entityType": "pks", + "schema": "public", + "table": "workspace_members" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "public", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "member_pkey", + "schema": "public", + "table": "member", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "rate_limit_pkey", + "schema": "public", + "table": "rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "workspaces_pkey", + "schema": "public", + "table": "workspaces", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "todos_pkey", + "schema": "public", + "table": "todos", + "entityType": "pks" + }, + { + "nameExplicit": true, + "columns": ["name"], + "nullsNotDistinct": false, + "name": "workspaces_name_unique", + "entityType": "uniques", + "schema": "public", + "table": "workspaces" + }, + { + "nameExplicit": false, + "columns": ["slug"], + "nullsNotDistinct": false, + "name": "organization_slug_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["key"], + "nullsNotDistinct": false, + "name": "rate_limit_key_key", + "schema": "public", + "table": "rate_limit", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["token"], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + }, + { + "value": "\"role\" in ('owner', 'admin', 'member')", + "name": "workspace_members_role_check", + "entityType": "checks", + "schema": "public", + "table": "workspace_members" + } + ], + "renames": [] +} diff --git a/adapters/database-postgres/src/persistence-postgres.ts b/adapters/database-postgres/src/persistence-postgres.ts index 070e569..0e55f86 100644 --- a/adapters/database-postgres/src/persistence-postgres.ts +++ b/adapters/database-postgres/src/persistence-postgres.ts @@ -2,6 +2,7 @@ import { Layer } from "effect"; import { type CustomTypesConfig, types } from "pg"; import { Database } from "./internal/database.ts"; import { WorkspaceStorePostgres } from "./workspace/workspace-store-postgres.ts"; +import { TodoStorePostgres } from "./todo/todo-store-postgres.ts"; const drizzleRawStringOids = new Set([1082, 1114, 1184, 1186, 1231, 1115, 1185, 1187, 1182]); @@ -12,6 +13,8 @@ export const typeParsers: CustomTypesConfig = { }; /** Provides every PostgreSQL-backed application persistence port. */ -export const layer = WorkspaceStorePostgres.layer.pipe(Layer.provide(Database.layer)); +export const layer = Layer.merge(WorkspaceStorePostgres.layer, TodoStorePostgres.layer).pipe( + Layer.provide(Database.layer), +); export * as PersistencePostgres from "./persistence-postgres.ts"; diff --git a/adapters/database-postgres/src/test/persistence-pglite.ts b/adapters/database-postgres/src/test/persistence-pglite.ts index ae4e706..0042a5b 100644 --- a/adapters/database-postgres/src/test/persistence-pglite.ts +++ b/adapters/database-postgres/src/test/persistence-pglite.ts @@ -5,6 +5,7 @@ import { Effect, Layer } from "effect"; import { Database } from "../internal/database.ts"; import { migrationConfig } from "../migrations.ts"; import { WorkspaceStorePostgres } from "../workspace/workspace-store-postgres.ts"; +import { TodoStorePostgres } from "../todo/todo-store-postgres.ts"; /** Provides the migrated test database for adapter fixtures that insert prerequisite records. */ export const databaseLayer = Layer.effect( @@ -17,6 +18,8 @@ export const databaseLayer = Layer.effect( ).pipe(Layer.provide(PgliteClient.layer())); /** Provides every PostgreSQL persistence port through an isolated migrated PGlite database. */ -export const layer = WorkspaceStorePostgres.layer.pipe(Layer.provide(databaseLayer)); +export const layer = Layer.merge(WorkspaceStorePostgres.layer, TodoStorePostgres.layer).pipe( + Layer.provide(databaseLayer), +); export * as PersistencePglite from "./persistence-pglite.ts"; diff --git a/adapters/database-postgres/src/todo/schema.ts b/adapters/database-postgres/src/todo/schema.ts new file mode 100644 index 0000000..c09fab3 --- /dev/null +++ b/adapters/database-postgres/src/todo/schema.ts @@ -0,0 +1,16 @@ +import { boolean, index, pgTable, text, timestamp, uuid, varchar } from "drizzle-orm/pg-core"; +import { organization } from "../auth/schema.ts"; + +export const todos = pgTable( + "todos", + { + id: uuid("id").primaryKey(), + organizationId: text("organization_id") + .notNull() + .references(() => organization.id, { onDelete: "cascade" }), + title: varchar("title", { length: 200 }).notNull(), + completed: boolean("completed").notNull().default(false), + createdAt: timestamp("created_at").notNull().defaultNow(), + }, + (table) => [index("todos_organization_id_index").on(table.organizationId)], +); diff --git a/adapters/database-postgres/src/todo/todo-store-postgres.test.ts b/adapters/database-postgres/src/todo/todo-store-postgres.test.ts new file mode 100644 index 0000000..2bb6898 --- /dev/null +++ b/adapters/database-postgres/src/todo/todo-store-postgres.test.ts @@ -0,0 +1,120 @@ +import { assert, describe, it } from "@effect/vitest"; +import { TodoStore } from "@effect-forge/core/todo-store"; +import { OrganizationId } from "@effect-forge/domain/organization"; +import { Todo, TodoId, TodoTitle } from "@effect-forge/domain/todo"; +import { eq } from "drizzle-orm"; +import { DateTime, Effect, Layer } from "effect"; +import { organization } from "../auth/schema.ts"; +import { Database } from "../internal/database.ts"; +import { PersistencePglite } from "../test/persistence-pglite.ts"; +import { todos } from "./schema.ts"; + +const orgA = OrganizationId.make("org-a"); +const orgB = OrganizationId.make("org-b"); +const createdAt = DateTime.toDateUtc(DateTime.makeUnsafe(0)); +const todo = Todo.make({ + id: TodoId.make("550e8400-e29b-41d4-a716-446655440000"), + organizationId: orgA, + title: TodoTitle.make("Ship organizations"), + completed: false, +}); +const sibling = Todo.make({ ...todo, id: TodoId.make("550e8400-e29b-41d4-a716-446655440001") }); +const foreign = Todo.make({ + ...todo, + id: TodoId.make("550e8400-e29b-41d4-a716-446655440002"), + organizationId: orgB, +}); +const layer = PersistencePglite.layer.pipe(Layer.provideMerge(PersistencePglite.databaseLayer)); +const fixture = Effect.gen(function* () { + const database = yield* Database.Service; + const store = yield* TodoStore.Service; + yield* database.insert(organization).values([ + { id: orgA, name: "A", slug: "a", createdAt }, + { id: orgB, name: "B", slug: "b", createdAt }, + ]); + return { database, store }; +}); + +describe("PostgreSQL TodoStore", () => { + it.effect("persists creation, repeated completion, and reopening", () => + Effect.gen(function* () { + const { store } = yield* fixture; + assert.deepEqual(yield* store.create(todo), todo); + assert.deepEqual(yield* store.list(orgA), [todo]); + const done = { ...todo, completed: true }; + assert.deepEqual(yield* store.setCompleted(orgA, todo.id, true), done); + assert.deepEqual(yield* store.setCompleted(orgA, todo.id, true), done); + assert.deepEqual(yield* store.list(orgA), [done]); + assert.deepEqual(yield* store.setCompleted(orgA, todo.id, false), todo); + assert.deepEqual(yield* store.list(orgA), [todo]); + }).pipe(Effect.provide(layer)), + ); + + it.effect("lists only the requested organization's todos", () => + Effect.gen(function* () { + const { store } = yield* fixture; + yield* store.create(todo); + yield* store.create(foreign); + assert.deepEqual(yield* store.list(orgA), [todo]); + assert.deepEqual(yield* store.list(orgB), [foreign]); + }).pipe(Effect.provide(layer)), + ); + + it.effect("updates only the requested todo within its organization", () => + Effect.gen(function* () { + const { store } = yield* fixture; + for (const row of [todo, sibling, foreign]) yield* store.create(row); + assert.deepEqual(yield* store.setCompleted(orgA, todo.id, true), { + ...todo, + completed: true, + }); + assert.sameDeepMembers( + [...(yield* store.list(orgA))], + [{ ...todo, completed: true }, sibling], + ); + assert.deepEqual(yield* store.list(orgB), [foreign]); + assert.deepEqual( + yield* store.setCompleted(orgA, foreign.id, true).pipe(Effect.flip), + new TodoStore.NotFound({ organizationId: orgA, id: foreign.id }), + ); + const missingId = TodoId.make("550e8400-e29b-41d4-a716-446655440003"); + assert.deepEqual( + yield* store.setCompleted(orgA, missingId, false).pipe(Effect.flip), + new TodoStore.NotFound({ organizationId: orgA, id: missingId }), + ); + assert.sameDeepMembers( + [...(yield* store.list(orgA))], + [{ ...todo, completed: true }, sibling], + ); + assert.deepEqual(yield* store.list(orgB), [foreign]); + }).pipe(Effect.provide(layer)), + ); + + it.effect("cascades organization deletion without removing another organization's todos", () => + Effect.gen(function* () { + const { database, store } = yield* fixture; + yield* store.create(todo); + yield* store.create(foreign); + yield* database.delete(organization).where(eq(organization.id, orgA)); + assert.deepEqual(yield* store.list(orgA), []); + assert.deepEqual(yield* store.list(orgB), [foreign]); + }).pipe(Effect.provide(layer)), + ); + + it.effect("translates a database constraint failure into PersistenceError", () => + Effect.gen(function* () { + const { store } = yield* fixture; + yield* store.create(todo); + assert.instanceOf(yield* store.create(todo).pipe(Effect.flip), TodoStore.PersistenceError); + assert.deepEqual(yield* store.list(orgA), [todo]); + }).pipe(Effect.provide(layer)), + ); + + it.effect("rejects malformed persisted rows with PersistenceError", () => + Effect.gen(function* () { + const { database, store } = yield* fixture; + yield* database.insert(todos).values({ ...todo, title: "" }); + assert.instanceOf(yield* store.list(orgA).pipe(Effect.flip), TodoStore.PersistenceError); + }).pipe(Effect.provide(layer)), + ); +}); diff --git a/adapters/database-postgres/src/todo/todo-store-postgres.ts b/adapters/database-postgres/src/todo/todo-store-postgres.ts new file mode 100644 index 0000000..8863c58 --- /dev/null +++ b/adapters/database-postgres/src/todo/todo-store-postgres.ts @@ -0,0 +1,62 @@ +import { TodoStore } from "@effect-forge/core/todo-store"; +import type { OrganizationId } from "@effect-forge/domain/organization"; +import { Todo, type TodoId } from "@effect-forge/domain/todo"; +import { and, asc, eq } from "drizzle-orm"; +import { Effect, Layer, Schema } from "effect"; +import { Database } from "../internal/database.ts"; +import { todos } from "./schema.ts"; + +const columns = { + id: todos.id, + organizationId: todos.organizationId, + title: todos.title, + completed: todos.completed, +}; + +export const layer = Layer.effect( + TodoStore.Service, + Effect.gen(function* () { + const database = yield* Database.Service; + const create = Effect.fn("TodoStorePostgres.create")((todo: Todo) => + database + .insert(todos) + .values(todo) + .pipe( + Effect.as(todo), + Effect.mapError((cause) => new TodoStore.PersistenceError({ cause })), + ), + ); + const list = Effect.fn("TodoStorePostgres.list")((organizationId: OrganizationId) => + database + .select(columns) + .from(todos) + .where(eq(todos.organizationId, organizationId)) + .orderBy(asc(todos.createdAt), asc(todos.id)) + .pipe( + Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Todo))), + Effect.mapError((cause) => new TodoStore.PersistenceError({ cause })), + ), + ); + const setCompleted = Effect.fn("TodoStorePostgres.setCompleted")(function* ( + organizationId: OrganizationId, + id: TodoId, + completed: boolean, + ) { + const rows = yield* database + .update(todos) + .set({ completed }) + .where(and(eq(todos.organizationId, organizationId), eq(todos.id, id))) + .returning(columns) + .pipe( + Effect.flatMap(Schema.decodeUnknownEffect(Schema.Array(Todo))), + Effect.mapError((cause) => new TodoStore.PersistenceError({ cause })), + ); + const todo = rows[0]; + if (todo === undefined) return yield* new TodoStore.NotFound({ organizationId, id }); + return todo; + }); + return TodoStore.Service.of({ create, list, setCompleted }); + }), +); + +export * as TodoStorePostgres from "./todo-store-postgres.ts"; diff --git a/apps/web/src/lib/server/authentication.test.ts b/apps/web/src/lib/server/authentication.test.ts index e233ce0..739889a 100644 --- a/apps/web/src/lib/server/authentication.test.ts +++ b/apps/web/src/lib/server/authentication.test.ts @@ -1,11 +1,15 @@ import { assert, describe, it } from "@effect/vitest"; +import { OrganizationMembership } from "@effect-forge/core/organization-membership"; +import { UserId } from "@effect-forge/domain/identity"; +import { OrganizationId, OrganizationMember } from "@effect-forge/domain/organization"; import * as authSchema from "@effect-forge/database-postgres/auth-schema"; import { PgliteDatabase } from "@effect-forge/database-postgres/test/pglite-database"; -import { eq } from "drizzle-orm"; -import { Effect, Fiber, Redacted, Result, Schema } from "effect"; +import { eq, sql } from "drizzle-orm"; +import { DateTime, Effect, Fiber, Option, Redacted, Result, Schema } from "effect"; import { Authentication } from "./authentication.ts"; const baseURL = new URL("http://localhost:5173"); +const createdAt = DateTime.toDateUtc(DateTime.makeUnsafe(0)); const secret = Redacted.make("test-secret-value-with-at-least-32-characters"); const signUpBody = '{"name":"Ada Lovelace","email":"ada@example.com","password":"correct-horse-battery-staple"}'; @@ -49,6 +53,99 @@ const fixture = Effect.gen(function* () { }); describe("Authentication", () => { + describe("organization membership", () => { + const organizationId = OrganizationId.make("org-1"); + const userId = UserId.make("user-1"); + const membershipFixture = Effect.gen(function* () { + const auth = yield* fixture; + yield* Effect.promise(() => + auth.database + .insert(authSchema.user) + .values({ id: userId, name: "Ada", email: "ada@example.com" }), + ); + yield* Effect.promise(() => + auth.database + .insert(authSchema.organization) + .values({ id: organizationId, name: "Engine", slug: "engine", createdAt }), + ); + yield* Effect.promise(() => + auth.database.insert(authSchema.member).values({ + id: "member-1", + userId, + organizationId, + role: "owner", + createdAt, + }), + ); + const service = yield* auth.authentication(request("/organizations")); + return { ...auth, service }; + }); + + it.effect.each([ + { role: "owner", roles: ["owner"] }, + { role: "admin", roles: ["admin"] }, + { role: "member", roles: ["member"] }, + { role: "admin,member", roles: ["admin", "member"] }, + ] as const)("decodes provider role $role", ({ role, roles }) => + Effect.gen(function* () { + const { database, service } = yield* membershipFixture; + yield* Effect.promise(() => database.update(authSchema.member).set({ role })); + assert.deepEqual( + yield* service.member(organizationId, userId), + Option.some(OrganizationMember.make({ organizationId, userId, roles })), + ); + }), + ); + + it.effect("looks up the explicit organization/user pair without a session", () => + Effect.gen(function* () { + const { service } = yield* membershipFixture; + assert.isNull(yield* service.authenticate); + assert.deepEqual( + yield* service.member(organizationId, userId), + Option.some(OrganizationMember.make({ organizationId, userId, roles: ["owner"] })), + ); + assert.isTrue(Option.isNone(yield* service.member(OrganizationId.make("other"), userId))); + assert.isTrue(Option.isNone(yield* service.member(organizationId, UserId.make("other")))); + }), + ); + + it.effect("rereads membership after revocation in the same and next request", () => + Effect.gen(function* () { + const { database, service, authentication } = yield* membershipFixture; + assert.isTrue(Option.isSome(yield* service.member(organizationId, userId))); + yield* Effect.promise(() => database.delete(authSchema.member)); + assert.isTrue(Option.isNone(yield* service.member(organizationId, userId))); + const nextRequest = yield* authentication(request("/organizations")); + assert.isTrue(Option.isNone(yield* nextRequest.member(organizationId, userId))); + }), + ); + + it.effect.each(["unsupported", "", "owner,unsupported"])( + "rejects malformed provider roles instead of granting access: %s", + (role) => + Effect.gen(function* () { + const { database, service } = yield* membershipFixture; + yield* Effect.promise(() => database.update(authSchema.member).set({ role })); + assert.instanceOf( + yield* service.member(organizationId, userId).pipe(Effect.flip), + OrganizationMembership.Unavailable, + ); + }), + ); + + it.effect("preserves a database failure as Unavailable rather than missing membership", () => + Effect.gen(function* () { + const { database, service } = yield* membershipFixture; + yield* Effect.promise(() => database.execute(sql`drop table member`)); + assert.instanceOf( + yield* service.member(organizationId, userId).pipe(Effect.flip), + OrganizationMembership.Unavailable, + ); + }), + ); + }); + it.effect("settles provider work before interruption releases request resources", () => Effect.gen(function* () { const auth = yield* fixture; diff --git a/apps/web/src/lib/server/authentication.ts b/apps/web/src/lib/server/authentication.ts index 9164263..09d17e9 100644 --- a/apps/web/src/lib/server/authentication.ts +++ b/apps/web/src/lib/server/authentication.ts @@ -1,10 +1,12 @@ import * as authSchema from "@effect-forge/database-postgres/auth-schema"; +import { OrganizationMembership } from "@effect-forge/core/organization-membership"; import { EmailAddress } from "@effect-forge/domain/email-address"; import { Principal, UserId } from "@effect-forge/domain/identity"; +import { OrganizationId, OrganizationMember } from "@effect-forge/domain/organization"; import { drizzleAdapter } from "@better-auth/drizzle-adapter/relations-v2"; import { betterAuth, type BetterAuthOptions } from "better-auth/minimal"; import type { PgAsyncDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core"; -import { Context, Effect, Layer, Redacted, Schema } from "effect"; +import { Context, Effect, Layer, Option, Redacted, Schema } from "effect"; import { betterAuthOptions } from "./better-auth-options.ts"; export const Viewer = Schema.Struct({ @@ -34,8 +36,15 @@ const ProviderSession = Schema.NullOr( }), ); const decodeProviderSession = Schema.decodeUnknownEffect(ProviderSession); +const ProviderMember = Schema.NullOr( + Schema.Struct({ + organizationId: OrganizationId, + userId: UserId, + role: Schema.String, + }), +); -export interface Interface { +export interface Interface extends OrganizationMembership.Interface { readonly authenticate: Effect.Effect; readonly handle: Effect.Effect; } @@ -97,11 +106,45 @@ export const make = Effect.fnUntraced(function* ({ baseURL, database, request, s Effect.withSpan("Authentication.authenticate", { attributes: { "app.auth.reused": true } }), ); - return Service.of({ authenticate, handle }); + const member = Effect.fn("Authentication.member")( + function* (organizationId: OrganizationId, userId: UserId) { + // Read membership without re-entering session middleware or refreshing cookies. + const raw: unknown = yield* Effect.tryPromise({ + try: () => + auth.$context.then(({ adapter }) => + adapter.findOne({ + model: "member", + where: [ + { field: "organizationId", value: organizationId }, + { field: "userId", value: userId }, + ], + }), + ), + catch: (cause) => new OrganizationMembership.Unavailable({ cause }), + }).pipe(Effect.uninterruptible); + const row = yield* Schema.decodeUnknownEffect(ProviderMember)(raw); + if (row === null) return Option.none(); + const membership = yield* Schema.decodeUnknownEffect(OrganizationMember)({ + organizationId: row.organizationId, + userId: row.userId, + roles: row.role.split(","), + }); + return Option.some(membership); + }, + Effect.catchTag("SchemaError", (cause) => new OrganizationMembership.Unavailable({ cause })), + ); + + return Service.of({ authenticate, handle, member }); }); /** Builds request-scoped authentication over an acquired Drizzle database. */ -export const layer = (options: Options): Layer.Layer => - Layer.effect(Service, make(options)); +export const layer = (options: Options): Layer.Layer => + Layer.effectContext( + make(options).pipe( + Effect.map((service) => + Context.make(Service, service).pipe(Context.add(OrganizationMembership.Service, service)), + ), + ), + ); export * as Authentication from "./authentication.ts"; diff --git a/apps/web/src/lib/server/runtime.ts b/apps/web/src/lib/server/runtime.ts index aacb4f0..3ebea16 100644 --- a/apps/web/src/lib/server/runtime.ts +++ b/apps/web/src/lib/server/runtime.ts @@ -53,7 +53,8 @@ export const make = ({ const route = routeId === null ? null : routeId.replace(/\/\([^/)]+\)(?=\/|$)/g, "") || "/"; const runtime = ManagedRuntime.make( - Layer.merge(application, authentication).pipe( + application.pipe( + Layer.provideMerge(authentication), Layer.provideMerge( Layer.span(route === null ? "Web.requestScope" : `Web.requestScope ${route}`, { attributes: { diff --git a/packages/core/package.json b/packages/core/package.json index d52d8e0..f780e25 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -5,6 +5,9 @@ "type": "module", "exports": { "./application": "./src/application.ts", + "./organization-membership": "./src/organization-access/organization-membership.ts", + "./todo-directory": "./src/todo/todo-directory.ts", + "./todo-store": "./src/todo/todo-store.ts", "./test/crypto-deterministic": "./src/test/crypto-deterministic.ts", "./workspace-directory": "./src/workspace/workspace-directory.ts", "./workspace-store": "./src/workspace/workspace-store.ts" diff --git a/packages/core/src/application.ts b/packages/core/src/application.ts index 096f556..c32b57e 100644 --- a/packages/core/src/application.ts +++ b/packages/core/src/application.ts @@ -1,8 +1,9 @@ import { Layer } from "effect"; import { WorkspaceDirectory } from "./workspace/workspace-directory.ts"; +import { TodoDirectory } from "./todo/todo-directory.ts"; /** Canonical dependency-open graph for request-callable application services. */ -export const layer = WorkspaceDirectory.layer; +export const layer = Layer.merge(WorkspaceDirectory.layer, TodoDirectory.layer); /** Application services exposed to inbound composition boundaries. */ export type Services = Layer.Success; diff --git a/packages/core/src/organization-access/organization-access.test.ts b/packages/core/src/organization-access/organization-access.test.ts new file mode 100644 index 0000000..9230b0a --- /dev/null +++ b/packages/core/src/organization-access/organization-access.test.ts @@ -0,0 +1,103 @@ +import { assert, describe, it } from "@effect/vitest"; +import { Principal, UserId } from "@effect-forge/domain/identity"; +import { OrganizationId, OrganizationMember } from "@effect-forge/domain/organization"; +import { Effect, Layer, Option } from "effect"; +import { OrganizationAccess } from "./organization-access.ts"; +import { OrganizationMembership } from "./organization-membership.ts"; +import { Permission } from "./permission.ts"; + +const organizationId = OrganizationId.make("org-1"); +const principal = Principal.make({ userId: UserId.make("user-1") }); +const permission = Permission.define({ key: "todo:update", grantedTo: ["admin"] }); +const layerWith = ( + member: Effect.Effect, OrganizationMembership.Unavailable>, +) => + OrganizationAccess.layer.pipe( + Layer.provide( + Layer.succeed(OrganizationMembership.Service, { + member: (requestedOrganization, requestedUser) => + requestedOrganization === organizationId && requestedUser === principal.userId + ? member + : Effect.succeed(Option.none()), + }), + ), + ); + +describe("Permission", () => { + it("keeps owner authority and explicit delegated grants", () => { + const ownerOnly = Permission.define({ key: "todo:delete", grantedTo: [] }); + assert.isTrue(Permission.allows(ownerOnly, ["owner"])); + assert.isFalse(Permission.allows(ownerOnly, ["admin", "member"])); + assert.isTrue(Permission.allows(permission, ["member", "admin"])); + assert.isFalse(Permission.allows(permission, ["member"])); + assert.throws(() => Permission.define({ key: "todo", grantedTo: [] })); + }); +}); + +describe("OrganizationAccess", () => { + it.effect("returns current membership evidence for an authorized role", () => { + const member = OrganizationMember.make({ + organizationId, + userId: principal.userId, + roles: ["admin"], + }); + return Effect.gen(function* () { + const access = yield* OrganizationAccess.Service; + assert.deepEqual(yield* access.require(principal, organizationId, permission), member); + }).pipe(Effect.provide(layerWith(Effect.succeed(Option.some(member))))); + }); + + it.effect.each([ + { actor: Principal.make({ userId: UserId.make("user-2") }), target: organizationId }, + { actor: principal, target: OrganizationId.make("org-2") }, + ])("does not reuse membership for another user or organization %#", ({ actor, target }) => + Effect.gen(function* () { + const access = yield* OrganizationAccess.Service; + yield* access.require(principal, organizationId, permission); + assert.deepEqual( + yield* access.require(actor, target, permission).pipe(Effect.flip), + new OrganizationAccess.Denied({ organizationId: target, permission: permission.key }), + ); + }).pipe( + Effect.provide( + layerWith( + Effect.succeed( + Option.some( + OrganizationMember.make({ + organizationId, + userId: principal.userId, + roles: ["admin"], + }), + ), + ), + ), + ), + ), + ); + + it.effect.each([ + Option.none(), + Option.some( + OrganizationMember.make({ organizationId, userId: principal.userId, roles: ["member"] }), + ), + ])("denies absent membership or a role without the capability %#", (member) => + Effect.gen(function* () { + const access = yield* OrganizationAccess.Service; + assert.deepEqual( + yield* access.require(principal, organizationId, permission).pipe(Effect.flip), + new OrganizationAccess.Denied({ organizationId, permission: permission.key }), + ); + }).pipe(Effect.provide(layerWith(Effect.succeed(member)))), + ); + + it.effect("preserves membership availability failures", () => { + const failure = new OrganizationMembership.Unavailable({ cause: "offline" }); + return Effect.gen(function* () { + const access = yield* OrganizationAccess.Service; + assert.strictEqual( + yield* access.require(principal, organizationId, permission).pipe(Effect.flip), + failure, + ); + }).pipe(Effect.provide(layerWith(Effect.fail(failure)))); + }); +}); diff --git a/packages/core/src/organization-access/organization-access.ts b/packages/core/src/organization-access/organization-access.ts new file mode 100644 index 0000000..500762b --- /dev/null +++ b/packages/core/src/organization-access/organization-access.ts @@ -0,0 +1,43 @@ +import type { Principal } from "@effect-forge/domain/identity"; +import { OrganizationId, type OrganizationMember } from "@effect-forge/domain/organization"; +import { Context, Effect, Layer, Option, Schema } from "effect"; +import { OrganizationMembership } from "./organization-membership.ts"; +import { Permission } from "./permission.ts"; + +export interface Interface { + readonly require: ( + principal: Principal, + organizationId: OrganizationId, + permission: Permission.Definition, + ) => Effect.Effect; +} + +export class Service extends Context.Service()( + "@effect-forge/core/OrganizationAccess", +) {} + +export class Denied extends Schema.TaggedError()("OrganizationAccess.Denied", { + organizationId: OrganizationId, + permission: Permission.Key, +}) {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const memberships = yield* OrganizationMembership.Service; + const require = Effect.fn("OrganizationAccess.require")(function* ( + principal: Principal, + organizationId: OrganizationId, + permission: Permission.Definition, + ) { + const member = yield* memberships.member(organizationId, principal.userId); + if (Option.isNone(member) || !Permission.allows(permission, member.value.roles)) { + return yield* new Denied({ organizationId, permission: permission.key }); + } + return member.value; + }); + return Service.of({ require }); + }), +); + +export * as OrganizationAccess from "./organization-access.ts"; diff --git a/packages/core/src/organization-access/organization-membership.ts b/packages/core/src/organization-access/organization-membership.ts new file mode 100644 index 0000000..1d3a538 --- /dev/null +++ b/packages/core/src/organization-access/organization-membership.ts @@ -0,0 +1,24 @@ +import type { UserId } from "@effect-forge/domain/identity"; +import type { OrganizationId, OrganizationMember } from "@effect-forge/domain/organization"; +import { Context, Effect, type Option, Schema } from "effect"; + +export interface Interface { + readonly member: ( + organizationId: OrganizationId, + userId: UserId, + ) => Effect.Effect, Unavailable>; +} + +/** Reads current membership from the tenant authority, independently of session selection. */ +export class Service extends Context.Service()( + "@effect-forge/core/OrganizationMembership", +) {} + +export class Unavailable extends Schema.TaggedError()( + "OrganizationMembership.Unavailable", + { + cause: Schema.Defect(), + }, +) {} + +export * as OrganizationMembership from "./organization-membership.ts"; diff --git a/packages/core/src/organization-access/permission.ts b/packages/core/src/organization-access/permission.ts new file mode 100644 index 0000000..538ab2c --- /dev/null +++ b/packages/core/src/organization-access/permission.ts @@ -0,0 +1,27 @@ +import type { OrganizationRole } from "@effect-forge/domain/organization"; +import { Schema } from "effect"; + +export const Key = Schema.String.pipe( + Schema.check(Schema.isPattern(/^[a-z][a-z0-9-]*:[a-z][a-z0-9-]*$/)), + Schema.brand("PermissionKey"), +); +export type Key = typeof Key.Type; + +export interface Definition { + readonly key: Key; + readonly grantedTo: ReadonlyArray>; +} + +const decodeKey = Schema.decodeSync(Key); + +/** Capabilities declare delegated authority; owners always retain access. */ +export const define = (input: { + readonly key: string; + readonly grantedTo: Definition["grantedTo"]; +}): Definition => + Object.freeze({ key: decodeKey(input.key), grantedTo: Object.freeze([...input.grantedTo]) }); + +export const allows = (permission: Definition, roles: ReadonlyArray): boolean => + roles.some((role) => role === "owner" || permission.grantedTo.includes(role)); + +export * as Permission from "./permission.ts"; diff --git a/packages/core/src/todo/todo-directory.test.ts b/packages/core/src/todo/todo-directory.test.ts new file mode 100644 index 0000000..f56e526 --- /dev/null +++ b/packages/core/src/todo/todo-directory.test.ts @@ -0,0 +1,172 @@ +import { assert, describe, it } from "@effect/vitest"; +import { Principal, UserId } from "@effect-forge/domain/identity"; +import { OrganizationId, OrganizationMember } from "@effect-forge/domain/organization"; +import { TodoId, TodoTitle } from "@effect-forge/domain/todo"; +import { Effect, Layer, Option, PlatformError, Ref } from "effect"; +import { OrganizationAccess } from "../organization-access/organization-access.ts"; +import { OrganizationMembership } from "../organization-access/organization-membership.ts"; +import { Permission } from "../organization-access/permission.ts"; +import { CryptoDeterministic } from "../test/crypto-deterministic.ts"; +import { TodoDirectory } from "./todo-directory.ts"; +import { TodoStore } from "./todo-store.ts"; +import { TodoStoreMemory } from "./todo-store-memory.ts"; + +const organizationId = OrganizationId.make("org-1"); +const principal = Principal.make({ userId: UserId.make("user-1") }); +const colleague = Principal.make({ userId: UserId.make("user-2") }); +const title = TodoTitle.make("Ship organizations"); +const members = [principal, colleague].map(({ userId }) => + OrganizationMember.make({ organizationId, userId, roles: ["member"] }), +); +const memberships = Layer.succeed(OrganizationMembership.Service, { + member: (orgId, userId) => + Effect.succeed( + Option.fromUndefinedOr( + members.find((member) => member.organizationId === orgId && member.userId === userId), + ), + ), +}); +const testLayer = TodoDirectory.layer.pipe( + Layer.provideMerge(Layer.mergeAll(CryptoDeterministic.layer, TodoStoreMemory.layer, memberships)), +); +const cryptoError = PlatformError.badArgument({ + module: "Crypto", + method: "randomUUIDv4", + description: "failed", +}); +const failureLayer = TodoDirectory.layer.pipe( + Layer.provideMerge( + Layer.mergeAll( + CryptoDeterministic.randomUUIDFailureLayer(cryptoError), + TodoStoreMemory.layer, + memberships, + ), + ), +); + +describe("TodoDirectory", () => { + it.layer(testLayer)("shared organization todos", (it) => { + it.effect("creates distinct todos and lets another member complete them", () => + Effect.gen(function* () { + const directory = yield* TodoDirectory.Service; + const first = yield* directory.create(principal, organizationId, title); + const second = yield* directory.create(colleague, organizationId, title); + assert.notStrictEqual(first.id, second.id); + assert.deepEqual(first, { id: first.id, organizationId, title, completed: false }); + assert.deepEqual(second, { id: second.id, organizationId, title, completed: false }); + assert.sameDeepMembers( + [...(yield* directory.list(colleague, organizationId))], + [first, second], + ); + const done = yield* directory.setCompleted(colleague, organizationId, first.id, true); + const expected = { id: first.id, organizationId, title, completed: true }; + assert.deepEqual(done, expected); + assert.sameDeepMembers( + [...(yield* directory.list(principal, organizationId))], + [expected, second], + ); + }), + ); + }); + + it.effect("revoking one member denies persistence access while another retains access", () => + Effect.gen(function* () { + const currentMembers = yield* Ref.make(members); + const accesses = yield* Ref.make(0); + const recordedStore = Layer.effect( + TodoStore.Service, + Effect.gen(function* () { + const store = yield* TodoStore.Service; + const recordAccess = Ref.update(accesses, (count) => count + 1); + return TodoStore.Service.of({ + create: (todo) => recordAccess.pipe(Effect.andThen(store.create(todo))), + list: (orgId) => recordAccess.pipe(Effect.andThen(store.list(orgId))), + setCompleted: (orgId, id, completed) => + recordAccess.pipe(Effect.andThen(store.setCompleted(orgId, id, completed))), + }); + }), + ).pipe(Layer.provide(TodoStoreMemory.layer)); + const revocationLayer = TodoDirectory.layer.pipe( + Layer.provideMerge( + Layer.mergeAll( + CryptoDeterministic.layer, + recordedStore, + Layer.succeed(OrganizationMembership.Service, { + member: (orgId, userId) => + Ref.get(currentMembers).pipe( + Effect.map((rows) => + Option.fromUndefinedOr( + rows.find( + (member) => member.organizationId === orgId && member.userId === userId, + ), + ), + ), + ), + }), + ), + ), + ); + yield* Effect.gen(function* () { + const directory = yield* TodoDirectory.Service; + const store = yield* TodoStore.Service; + const todo = yield* directory.create(principal, organizationId, title); + yield* Ref.update(currentMembers, (rows) => + rows.filter((member) => member.userId !== principal.userId), + ); + const before = yield* Ref.get(accesses); + assert.deepEqual( + yield* directory.list(principal, organizationId).pipe(Effect.flip), + new OrganizationAccess.Denied({ + organizationId, + permission: Permission.Key.make("todo:list"), + }), + ); + assert.deepEqual( + yield* directory.create(principal, organizationId, title).pipe(Effect.flip), + new OrganizationAccess.Denied({ + organizationId, + permission: Permission.Key.make("todo:create"), + }), + ); + assert.deepEqual( + yield* directory.setCompleted(principal, organizationId, todo.id, true).pipe(Effect.flip), + new OrganizationAccess.Denied({ + organizationId, + permission: Permission.Key.make("todo:update"), + }), + ); + assert.strictEqual(yield* Ref.get(accesses), before); + assert.deepEqual(yield* store.list(organizationId), [todo]); + const done = yield* directory.setCompleted(colleague, organizationId, todo.id, true); + assert.deepEqual(done, { id: todo.id, organizationId, title, completed: true }); + assert.deepEqual(yield* directory.list(colleague, organizationId), [done]); + }).pipe(Effect.provide(revocationLayer)); + }), + ); + + it.layer(testLayer)("missing todo", (it) => { + it.effect("preserves the store's missing-todo failure", () => + Effect.gen(function* () { + const directory = yield* TodoDirectory.Service; + const id = TodoId.make("550e8400-e29b-41d4-a716-446655440000"); + assert.deepEqual( + yield* directory.setCompleted(principal, organizationId, id, true).pipe(Effect.flip), + new TodoStore.NotFound({ organizationId, id }), + ); + }), + ); + }); + + it.layer(failureLayer)("identifier generation", (it) => { + it.effect("preserves Crypto failure context without creating a todo", () => + Effect.gen(function* () { + const directory = yield* TodoDirectory.Service; + const store = yield* TodoStore.Service; + const failure = yield* directory.create(principal, organizationId, title).pipe(Effect.flip); + assert.instanceOf(failure, TodoDirectory.IdGenerationError); + assert.strictEqual(failure.cause, cryptoError); + assert.deepEqual(yield* store.list(organizationId), []); + }), + ); + }); +}); diff --git a/packages/core/src/todo/todo-directory.ts b/packages/core/src/todo/todo-directory.ts new file mode 100644 index 0000000..a02326d --- /dev/null +++ b/packages/core/src/todo/todo-directory.ts @@ -0,0 +1,85 @@ +import type { Principal } from "@effect-forge/domain/identity"; +import type { OrganizationId } from "@effect-forge/domain/organization"; +import { Todo, TodoId, TodoTitle } from "@effect-forge/domain/todo"; +import { Context, Crypto, Effect, Layer, Schema } from "effect"; +import { OrganizationAccess } from "../organization-access/organization-access.ts"; +import type { OrganizationMembership } from "../organization-access/organization-membership.ts"; +import { Permission } from "../organization-access/permission.ts"; +import { TodoStore } from "./todo-store.ts"; + +const permissions = { + list: Permission.define({ key: "todo:list", grantedTo: ["admin", "member"] }), + create: Permission.define({ key: "todo:create", grantedTo: ["admin", "member"] }), + update: Permission.define({ key: "todo:update", grantedTo: ["admin", "member"] }), +}; + +type AccessError = OrganizationAccess.Denied | OrganizationMembership.Unavailable; +export interface Interface { + readonly list: ( + principal: Principal, + organizationId: OrganizationId, + ) => Effect.Effect, AccessError | TodoStore.PersistenceError>; + readonly create: ( + principal: Principal, + organizationId: OrganizationId, + title: TodoTitle, + ) => Effect.Effect; + readonly setCompleted: ( + principal: Principal, + organizationId: OrganizationId, + id: TodoId, + completed: boolean, + ) => Effect.Effect; +} + +export class Service extends Context.Service()( + "@effect-forge/core/TodoDirectory", +) {} + +export class IdGenerationError extends Schema.TaggedError()( + "TodoDirectory.IdGenerationError", + { + cause: Schema.Defect(), + }, +) {} + +export const layer = Layer.effect( + Service, + Effect.gen(function* () { + const access = yield* OrganizationAccess.Service; + const store = yield* TodoStore.Service; + const crypto = yield* Crypto.Crypto; + + const list = Effect.fn("TodoDirectory.list")(function* ( + principal: Principal, + organizationId: OrganizationId, + ) { + yield* access.require(principal, organizationId, permissions.list); + return yield* store.list(organizationId); + }); + const create = Effect.fn("TodoDirectory.create")(function* ( + principal: Principal, + organizationId: OrganizationId, + title: TodoTitle, + ) { + yield* access.require(principal, organizationId, permissions.create); + const id = yield* crypto.randomUUIDv4.pipe( + Effect.flatMap(Schema.decodeEffect(TodoId)), + Effect.mapError((cause) => new IdGenerationError({ cause })), + ); + return yield* store.create(Todo.make({ id, organizationId, title, completed: false })); + }); + const setCompleted = Effect.fn("TodoDirectory.setCompleted")(function* ( + principal: Principal, + organizationId: OrganizationId, + id: TodoId, + completed: boolean, + ) { + yield* access.require(principal, organizationId, permissions.update); + return yield* store.setCompleted(organizationId, id, completed); + }); + return Service.of({ list, create, setCompleted }); + }), +).pipe(Layer.provide(OrganizationAccess.layer)); + +export * as TodoDirectory from "./todo-directory.ts"; diff --git a/packages/core/src/todo/todo-store-memory.test.ts b/packages/core/src/todo/todo-store-memory.test.ts new file mode 100644 index 0000000..926cc24 --- /dev/null +++ b/packages/core/src/todo/todo-store-memory.test.ts @@ -0,0 +1,46 @@ +import { assert, describe, it } from "@effect/vitest"; +import { OrganizationId } from "@effect-forge/domain/organization"; +import { Todo, TodoId, TodoTitle } from "@effect-forge/domain/todo"; +import { Effect } from "effect"; +import { TodoStore } from "./todo-store.ts"; +import { TodoStoreMemory } from "./todo-store-memory.ts"; + +const todo = Todo.make({ + id: TodoId.make("550e8400-e29b-41d4-a716-446655440000"), + organizationId: OrganizationId.make("org-a"), + title: TodoTitle.make("Ship organizations"), + completed: false, +}); + +describe("TodoStoreMemory", () => { + it.effect("rejects duplicate IDs without overwriting the existing todo", () => + Effect.gen(function* () { + const store = yield* TodoStore.Service; + yield* store.create(todo); + const duplicate = Todo.make({ ...todo, title: TodoTitle.make("Overwrite") }); + assert.instanceOf( + yield* store.create(duplicate).pipe(Effect.flip), + TodoStore.PersistenceError, + ); + assert.deepEqual(yield* store.list(todo.organizationId), [todo]); + }).pipe(Effect.provide(TodoStoreMemory.layer)), + ); + it.effect("scopes reads and rejects foreign or missing todo updates", () => + Effect.gen(function* () { + const store = yield* TodoStore.Service; + const other = OrganizationId.make("org-b"); + const missing = TodoId.make("550e8400-e29b-41d4-a716-446655440001"); + yield* store.create(todo); + assert.deepEqual(yield* store.list(other), []); + assert.deepEqual( + yield* store.setCompleted(other, todo.id, true).pipe(Effect.flip), + new TodoStore.NotFound({ organizationId: other, id: todo.id }), + ); + assert.deepEqual( + yield* store.setCompleted(todo.organizationId, missing, true).pipe(Effect.flip), + new TodoStore.NotFound({ organizationId: todo.organizationId, id: missing }), + ); + assert.deepEqual(yield* store.list(todo.organizationId), [todo]); + }).pipe(Effect.provide(TodoStoreMemory.layer)), + ); +}); diff --git a/packages/core/src/todo/todo-store-memory.ts b/packages/core/src/todo/todo-store-memory.ts new file mode 100644 index 0000000..2f0d8f7 --- /dev/null +++ b/packages/core/src/todo/todo-store-memory.ts @@ -0,0 +1,45 @@ +import { Todo, type TodoId } from "@effect-forge/domain/todo"; +import { Effect, Layer, Ref } from "effect"; +import { TodoStore } from "./todo-store.ts"; + +/** Provides atomic in-memory todo persistence for tests. */ +export const layer = Layer.effect( + TodoStore.Service, + Effect.gen(function* () { + const state = yield* Ref.make(new Map()); + return TodoStore.Service.of({ + create: (todo) => + Ref.modify( + state, + (rows): readonly [Effect.Effect, Map] => + rows.has(todo.id) + ? [ + Effect.fail( + new TodoStore.PersistenceError({ cause: new Error("Duplicate todo ID") }), + ), + rows, + ] + : [Effect.succeed(todo), new Map(rows).set(todo.id, todo)], + ).pipe(Effect.flatten), + list: (organizationId) => + Ref.get(state).pipe( + Effect.map((rows) => + [...rows.values()].filter((todo) => todo.organizationId === organizationId), + ), + ), + setCompleted: (organizationId, id, completed) => + Ref.modify( + state, + (rows): readonly [Effect.Effect, Map] => { + const previous = rows.get(id); + if (previous === undefined || previous.organizationId !== organizationId) + return [Effect.fail(new TodoStore.NotFound({ organizationId, id })), rows]; + const todo = Todo.make({ id, organizationId, title: previous.title, completed }); + return [Effect.succeed(todo), new Map(rows).set(id, todo)]; + }, + ).pipe(Effect.flatten), + }); + }), +); + +export * as TodoStoreMemory from "./todo-store-memory.ts"; diff --git a/packages/core/src/todo/todo-store.ts b/packages/core/src/todo/todo-store.ts new file mode 100644 index 0000000..3dea0b3 --- /dev/null +++ b/packages/core/src/todo/todo-store.ts @@ -0,0 +1,33 @@ +import { OrganizationId } from "@effect-forge/domain/organization"; +import { Todo, TodoId } from "@effect-forge/domain/todo"; +import { Context, Effect, Schema } from "effect"; + +export interface Interface { + readonly create: (todo: Todo) => Effect.Effect; + readonly list: ( + organizationId: OrganizationId, + ) => Effect.Effect, PersistenceError>; + readonly setCompleted: ( + organizationId: OrganizationId, + id: TodoId, + completed: boolean, + ) => Effect.Effect; +} + +export class Service extends Context.Service()( + "@effect-forge/core/TodoStore", +) {} + +export class NotFound extends Schema.TaggedError()("TodoStore.NotFound", { + organizationId: OrganizationId, + id: TodoId, +}) {} + +export class PersistenceError extends Schema.TaggedError()( + "TodoStore.PersistenceError", + { + cause: Schema.Defect(), + }, +) {} + +export * as TodoStore from "./todo-store.ts"; diff --git a/packages/domain/package.json b/packages/domain/package.json index c0ec9bd..d99ab28 100644 --- a/packages/domain/package.json +++ b/packages/domain/package.json @@ -6,6 +6,8 @@ "exports": { "./email-address": "./src/email-address/email-address.ts", "./identity": "./src/identity/identity.ts", + "./organization": "./src/organization/organization.ts", + "./todo": "./src/todo/todo.ts", "./workspace": "./src/workspace/workspace.ts", "./workspace-member": "./src/workspace/workspace-member.ts" }, diff --git a/packages/domain/src/organization/organization.ts b/packages/domain/src/organization/organization.ts new file mode 100644 index 0000000..6d80163 --- /dev/null +++ b/packages/domain/src/organization/organization.ts @@ -0,0 +1,19 @@ +import { Schema } from "effect"; +import { UserId } from "../identity/identity.ts"; + +/** Provider-owned opaque organization identifier. */ +export const OrganizationId = Schema.String.pipe( + Schema.check(Schema.isTrimmed(), Schema.isMinLength(1), Schema.isMaxLength(255)), + Schema.brand("OrganizationId"), +); +export type OrganizationId = typeof OrganizationId.Type; + +export const OrganizationRole = Schema.Literals(["owner", "admin", "member"]); +export type OrganizationRole = typeof OrganizationRole.Type; + +export const OrganizationMember = Schema.Struct({ + organizationId: OrganizationId, + userId: UserId, + roles: Schema.NonEmptyArray(OrganizationRole), +}); +export interface OrganizationMember extends Schema.Schema.Type {} diff --git a/packages/domain/src/todo/todo.test.ts b/packages/domain/src/todo/todo.test.ts new file mode 100644 index 0000000..a64f95b --- /dev/null +++ b/packages/domain/src/todo/todo.test.ts @@ -0,0 +1,14 @@ +import { assert, describe, it } from "@effect/vitest"; +import { Schema } from "effect"; +import { TodoTitle } from "./todo.ts"; + +describe("TodoTitle", () => { + it("requires a nonblank, trimmed title within the storage limit", () => { + const valid = Schema.is(TodoTitle); + assert.isTrue(valid("Ship organizations")); + assert.isTrue(valid("x".repeat(200))); + for (const title of ["", " ", " title", "title ", "x".repeat(201)]) { + assert.isFalse(valid(title)); + } + }); +}); diff --git a/packages/domain/src/todo/todo.ts b/packages/domain/src/todo/todo.ts new file mode 100644 index 0000000..b708462 --- /dev/null +++ b/packages/domain/src/todo/todo.ts @@ -0,0 +1,19 @@ +import { Schema } from "effect"; +import { OrganizationId } from "../organization/organization.ts"; + +export const TodoId = Schema.String.pipe(Schema.check(Schema.isUUID(4)), Schema.brand("TodoId")); +export type TodoId = typeof TodoId.Type; + +export const TodoTitle = Schema.String.pipe( + Schema.check(Schema.isTrimmed(), Schema.isMinLength(1), Schema.isMaxLength(200)), + Schema.brand("TodoTitle"), +); +export type TodoTitle = typeof TodoTitle.Type; + +export const Todo = Schema.Struct({ + id: TodoId, + organizationId: OrganizationId, + title: TodoTitle, + completed: Schema.Boolean, +}); +export interface Todo extends Schema.Schema.Type {} From f681784045d4b1a1c6c1878688247bcc44fa849e Mon Sep 17 00:00:00 2001 From: Mateo Roldos Date: Fri, 18 Sep 2026 17:12:03 -0300 Subject: [PATCH 03/19] chore(test): simplify validation to colocated Vitest and PGlite suites Remove the dedicated Playwright/PostgreSQL harness and its dependencies, CI steps, scripts, and configuration. Keep provider HTTP, application, domain, and SQL checks colocated with their owners, and align the validation guidance. --- .github/workflows/ci.yml | 16 - .gitignore | 2 - AGENTS.md | 2 +- README.md | 15 +- apps/web/package.json | 5 +- apps/web/playwright.config.ts | 19 -- apps/web/tests/auth.spec.ts | 44 --- apps/web/tests/server.ts | 59 ---- apps/web/tsconfig.json | 2 +- bun.lock | 320 ------------------ knip.jsonc | 3 +- mise.toml | 2 +- package.json | 3 +- .../references/feature-workflow.md | 2 +- skills/effect-forge/references/testing.md | 27 +- 15 files changed, 19 insertions(+), 502 deletions(-) delete mode 100644 apps/web/playwright.config.ts delete mode 100644 apps/web/tests/auth.spec.ts delete mode 100644 apps/web/tests/server.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 92d2d46..22ba959 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,22 +27,6 @@ jobs: - run: bun run check - run: bun run build - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: "24" - - name: Install Chromium - run: bun run --cwd apps/web playwright install --with-deps chromium - - name: Run browser acceptance test - id: e2e - run: bun run test:e2e - - name: Upload Playwright failure report - uses: actions/upload-artifact@v4 - if: ${{ failure() && steps.e2e.outcome == 'failure' }} - with: - name: playwright-report - path: apps/web/playwright-report/ - retention-days: 7 deploy_preview: name: Deploy labeled PR preview diff --git a/.gitignore b/.gitignore index 5b984dc..66bd5ca 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,3 @@ .turbo node_modules *.tsbuildinfo -playwright-report/ -test-results/ diff --git a/AGENTS.md b/AGENTS.md index 43e108b..777f69f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,6 +45,6 @@ ui → nothing ## Tests -Test through public interfaces using substitute Layers, PGlite, and real HTTP or SvelteKit boundaries. Do not mock modules or use arbitrary sleeps. +Test through public interfaces using substitute Layers, PGlite, and provider HTTP handlers. Do not mock modules or use arbitrary sleeps. A change is complete only after type checks, tests, formatting, architecture checks, and dead-code checks pass. diff --git a/README.md b/README.md index 7a5dd4d..3abf6bd 100644 --- a/README.md +++ b/README.md @@ -82,22 +82,13 @@ See [operation logging](skills/effect-forge/references/observability.md) for exa ## Validation ```sh -bun run test # tests only +bun run test # colocated tests bun run check # formatting, lint, types, and tests bun run build # production builds ``` -CI runs `check`, `build`, and the browser acceptance test. - -### Browser acceptance test - -With Docker running: - -```sh -bun run test:e2e -``` - -This runs the authentication journey against the built app and a disposable PostgreSQL database. +CI runs `check` and `build`. Colocated tests cover domain and application behavior, +provider HTTP handling, and SQL through PGlite. ### Database changes diff --git a/apps/web/package.json b/apps/web/package.json index 513c4f5..cdcdc83 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -11,8 +11,7 @@ "build": "vite build", "check-types": "svelte-kit sync && svelte-check --tsgo --tsconfig ./tsconfig.json", "prepare": "svelte-kit sync", - "test": "vitest run src", - "test:e2e": "playwright test" + "test": "vitest run src" }, "dependencies": { "@better-auth/drizzle-adapter": "catalog:", @@ -33,11 +32,9 @@ "@cloudflare/workers-types": "5.20260827.1", "@effect/vitest": "catalog:", "@electric-sql/pglite": "catalog:", - "@playwright/test": "1.63.0", "@sveltejs/kit": "3.0.0-next.25", "@sveltejs/vite-plugin-svelte": "7.3.0", "@tailwindcss/vite": "catalog:", - "@testcontainers/postgresql": "12.1.0", "@types/pg": "catalog:", "@typescript/native": "npm:typescript@7.0.2", "svelte": "catalog:", diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts deleted file mode 100644 index 08d4596..0000000 --- a/apps/web/playwright.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; - -export default defineConfig({ - testDir: "./tests", - forbidOnly: true, - reporter: [["list"], ["html", { open: "never" }]], - use: { - baseURL: "http://127.0.0.1:4173", - trace: "retain-on-failure", - }, - projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], - webServer: { - command: "bun run build && node tests/server.ts", - url: "http://127.0.0.1:4173/sign-in", - reuseExistingServer: false, - timeout: 120_000, - gracefulShutdown: { signal: "SIGTERM", timeout: 30_000 }, - }, -}); diff --git a/apps/web/tests/auth.spec.ts b/apps/web/tests/auth.spec.ts deleted file mode 100644 index 7a15d8d..0000000 --- a/apps/web/tests/auth.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect, test } from "@playwright/test"; - -// oxlint-disable-next-line effecttsgo/async-function -- Playwright owns the browser test lifecycle. -test("authentication survives SSR and clears protected state on sign-out", async ({ page }) => { - const email = "ada@example.com"; - const password = "correct-horse-battery-staple"; - - await page.goto("/workspaces"); - await expect(page).toHaveURL(/\/sign-in\?returnTo=%2Fworkspaces$/); - await page.getByRole("link", { name: "Create an account" }).click(); - await page.getByLabel("Name", { exact: true }).fill("Ada Lovelace"); - await page.getByLabel("Email", { exact: true }).fill(email); - await page.getByLabel("Password", { exact: true }).fill(password); - await page.getByRole("button", { name: "Create account", exact: true }).click(); - - await expect(page).toHaveURL("/workspaces"); - await expect(page.getByRole("heading", { name: "Workspaces", exact: true })).toBeVisible(); - await expect(page.getByText(email, { exact: true })).toBeVisible(); - - const response = await page.reload(); - if (response === null) throw new Error("Reload did not produce an HTTP response"); - expect(response.status()).toBe(200); - const html = await response.text(); - expect(html).toContain(email); - await expect(page.getByRole("button", { name: "Sign out", exact: true })).toBeVisible(); - - await page.getByRole("button", { name: "Sign out", exact: true }).click(); - await expect(page).toHaveURL(/\/sign-in(?:\?|$)/); - await expect(page.getByText(email, { exact: true })).toHaveCount(0); - await page.goBack(); - await expect(page).toHaveURL(/\/sign-in(?:\?|$)/); - await expect(page.getByRole("heading", { name: "Sign in", exact: true })).toBeVisible(); - - // Check server authorization too, independently of the client-side navigation cache. - const anonymous = await page.request.get("/workspaces", { maxRedirects: 0 }); - expect(anonymous.status()).toBe(303); - expect(anonymous.headers()["location"]).toBe("/sign-in?returnTo=%2Fworkspaces"); - - await page.getByLabel("Email", { exact: true }).fill(email); - await page.getByLabel("Password", { exact: true }).fill(password); - await page.getByRole("button", { name: "Sign in", exact: true }).click(); - await expect(page).toHaveURL("/workspaces"); - await expect(page.getByText(email, { exact: true })).toBeVisible(); -}); diff --git a/apps/web/tests/server.ts b/apps/web/tests/server.ts deleted file mode 100644 index 90ac835..0000000 --- a/apps/web/tests/server.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { makeSvelteKitConfigPlugin } from "@alchemy.run/frontend-frameworks/sveltekit"; -import { makeCloudflareAdapter } from "@alchemy.run/frontend-frameworks/sveltekit/cloudflare"; -import { NodeRuntime } from "@effect/platform-node"; -import { PostgreSqlContainer } from "@testcontainers/postgresql"; -import { drizzle } from "drizzle-orm/node-postgres"; -import { migrate } from "drizzle-orm/node-postgres/migrator"; -import { Effect } from "effect"; -import { Client } from "pg"; -import { preview } from "vite"; - -const server = Effect.gen(function* () { - const database = yield* Effect.acquireRelease( - Effect.promise(() => new PostgreSqlContainer("postgres:17-alpine").start()), - (container) => Effect.promise(() => container.stop()), - ); - - yield* Effect.gen(function* () { - const client = yield* Effect.acquireRelease( - Effect.sync(() => new Client({ connectionString: database.getConnectionUri() })), - (client) => Effect.promise(() => client.end()), - ); - yield* Effect.promise(() => client.connect()); - yield* Effect.promise(() => - migrate(drizzle({ client }), { - migrationsFolder: "../../adapters/database-postgres/drizzle", - }), - ); - }).pipe(Effect.scoped); - - const adapter = yield* Effect.acquireRelease( - Effect.sync(() => - makeCloudflareAdapter({ - platform: { - env: { - AUTH_ORIGIN: "http://127.0.0.1:4173", - AUTH_SECRET: "playwright-local-secret-not-for-deployment", - DATABASE: { connectionString: database.getConnectionUri() }, - SEARCH_INDEXABLE: "false", - DEPLOYMENT_ENVIRONMENT: "test", - }, - }, - }), - ), - (adapter) => Effect.promise(() => adapter.dispose()), - ); - - yield* Effect.acquireRelease( - Effect.promise(() => - preview({ - plugins: [makeSvelteKitConfigPlugin({ adapter })], - preview: { host: "127.0.0.1", port: 4173, strictPort: true }, - }), - ), - (server) => Effect.promise(() => server.close()), - ); - return yield* Effect.never; -}); - -NodeRuntime.runMain(server.pipe(Effect.scoped)); diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index f83826c..4b9606b 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -6,5 +6,5 @@ "rewriteRelativeImportExtensions": false, "sourceMap": true }, - "include": ["src", "tests", "playwright.config.ts", "vite.config.ts", "worker.ts"] + "include": ["src", "vite.config.ts", "worker.ts"] } diff --git a/bun.lock b/bun.lock index 541c4ea..d8efe08 100644 --- a/bun.lock +++ b/bun.lock @@ -85,11 +85,9 @@ "@cloudflare/workers-types": "5.20260827.1", "@effect/vitest": "catalog:", "@electric-sql/pglite": "catalog:", - "@playwright/test": "1.63.0", "@sveltejs/kit": "3.0.0-next.25", "@sveltejs/vite-plugin-svelte": "7.3.0", "@tailwindcss/vite": "catalog:", - "@testcontainers/postgresql": "12.1.0", "@types/pg": "catalog:", "@typescript/native": "npm:typescript@7.0.2", "svelte": "catalog:", @@ -322,8 +320,6 @@ "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], - "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], - "@better-auth/core": ["@better-auth/core@1.7.4", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.41.1", "@standard-schema/spec": "^1.1.0", "zod": "^4.5.4" }, "peerDependencies": { "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.4.0", "jose": "^6.1.0", "kysely": "^0.28.5 || ^0.29.0", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types", "@opentelemetry/api"] }, "sha512-g66dKOMB5PLxoujkxkZeYb6dh5wLhfAx/D+w9XtcO669Dslnyen2naBYqnD1r5ZwnxFKcr6aSrUPaH952V89Lg=="], "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.7.4", "", { "peerDependencies": { "@better-auth/core": "^1.7.4", "@better-auth/utils": "0.4.2", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-J9JMl0DGYNYwtDj5nq/fzycU9dzPUDEtTNn89KmToUrwPDGfByDgfmiQ9jGK+LN3EnfsG1Pt/dDmRocbnfihlA=="], @@ -526,10 +522,6 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.12", "", {}, "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.14.5", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-7VZM+SVdEcUUqSQeNI3zM8Qs/BhQKZndPo2h5VkYkAM8Iz0wJIa8mKV5ekQGqG8UUsnkQ0NMxIxwkIHYvj0qOw=="], - - "@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], - "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], @@ -588,8 +580,6 @@ "@internationalized/date": ["@internationalized/date@3.12.4", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ=="], - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -600,12 +590,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], - "@js-temporal/polyfill": ["@js-temporal/polyfill@0.5.1", "", { "dependencies": { "jsbi": "^4.3.0" } }, "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ=="], - "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="], - "@libsql/client": ["@libsql/client@0.17.4", "", { "dependencies": { "@libsql/core": "^0.17.4", "@libsql/hrana-client": "^0.10.0", "js-base64": "^3.7.5", "libsql": "^0.5.28", "promise-limit": "^2.7.0" } }, "sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw=="], "@libsql/core": ["@libsql/core@0.17.4", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ=="], @@ -866,10 +852,6 @@ "@oxlint/plugins": ["@oxlint/plugins@1.78.0", "", {}, "sha512-Ypt8KeRYw+4jUtlPirfcHWMrn5ms12VrrFPD+Mds477/7tJxG1Kcz2Yrg2nVcTQEUx/GdlhS+BUg1kmxNm04Ug=="], - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - - "@playwright/test": ["@playwright/test@1.63.0", "", { "dependencies": { "playwright": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ=="], - "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], "@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], @@ -880,24 +862,6 @@ "@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="], - "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], - - "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], - - "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], - - "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], - - "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], - - "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], - - "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], - - "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], - - "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], - "@puppeteer/browsers": ["@puppeteer/browsers@3.2.1", "", { "dependencies": { "modern-tar": "^0.8.0", "yargs": "^18.0.0" }, "peerDependencies": { "proxy-agent": ">=8.0.1", "yauzl": "^2.10.0 || ^3.4.0" }, "optionalPeers": ["proxy-agent", "yauzl"], "bin": { "browsers": "lib/main-cli.js" } }, "sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A=="], "@redis/bloom": ["@redis/bloom@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag=="], @@ -1022,8 +986,6 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], - "@testcontainers/postgresql": ["@testcontainers/postgresql@12.1.0", "", { "dependencies": { "testcontainers": "^12.1.0" } }, "sha512-Pjf2VSVNirEPfz36nidyrVAnZvc2YhajOznY4VgyEsvfTd5qiMNOuPq96drREvxAUtXl5SFLX7vXj7sSq4aTcA=="], - "@turbo/darwin-64": ["@turbo/darwin-64@2.10.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-v3R+1R/Ysozyo+p7Ri8MCIbndOvYt3DgPFrGLhrhQHfvyvbxyH3WyJj+A/2JTNmNleuAlh3JUyCV0iSVHIONTA=="], "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.10.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-R0a0CvGAeYYsBgPIgFNB3agGXh6qukjduNhFlwVVX1Ss2IdBJLXmgjytNGmo084bLKS0B6UdLRwKhXMHKKaObQ=="], @@ -1046,10 +1008,6 @@ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], - "@types/docker-modem": ["@types/docker-modem@3.0.6", "", { "dependencies": { "@types/node": "*", "@types/ssh2": "*" } }, "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg=="], - - "@types/dockerode": ["@types/dockerode@4.0.1", "", { "dependencies": { "@types/docker-modem": "*", "@types/node": "*", "@types/ssh2": "*" } }, "sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q=="], - "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -1066,10 +1024,6 @@ "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], - "@types/ssh2": ["@types/ssh2@1.15.6", "", { "dependencies": { "@types/node": "^18.11.18" } }, "sha512-oGdxhBqcRTwSTKFm+9EiKzkNVYRLEFkcW44lhguvBalGJbWfGnDt/ezwSUZc+SF9m9bMc3VyklNAtp7zICjS5w=="], - - "@types/ssh2-streams": ["@types/ssh2-streams@0.1.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA=="], - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], @@ -1132,8 +1086,6 @@ "@vitest/utils": ["@vitest/utils@4.1.11", "", { "dependencies": { "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ=="], - "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], "alchemy": ["alchemy@2.0.0-beta.77", "", { "dependencies": { "@alchemy.run/cloudflare-runtime": "2.0.0-beta.77", "@alchemy.run/floci": "2.0.0-beta.77", "@alchemy.run/node-utils": "2.0.0-beta.77", "@alchemy.run/sigil": "0.0.0-alpha.10", "@aws-sdk/credential-providers": "^3.0.0", "@distilled.cloud/aws": "1.0.0-rc.9", "@distilled.cloud/axiom": "1.0.0-rc.9", "@distilled.cloud/cloudflare": "1.0.0-rc.9", "@distilled.cloud/core": "1.0.0-rc.9", "@distilled.cloud/fly-io": "1.0.0-rc.9", "@distilled.cloud/hetzner": "1.0.0-rc.9", "@distilled.cloud/neon": "1.0.0-rc.9", "@distilled.cloud/planetscale": "1.0.0-rc.9", "@distilled.cloud/railway": "1.0.0-rc.9", "@effect/sql-d1": ">=4.0.0-rc.112 || >=4.0.0", "@effect/sql-sqlite-do": ">=4.0.0-rc.112 || >=4.0.0", "@effect/vitest": ">=4.0.0-rc.112 || >=4.0.0", "@libsql/client": "^0.17.0", "@octokit/rest": "^22.0.1", "@octokit/webhooks": "^14.2.0", "@prisma/dev": "^0.20.0", "@types/aws-lambda": "^8.10.152", "aws4fetch": "^1.0.20", "capnweb": "^0.6.1", "fast-glob": "^3.3.2", "jszip": "^3.10.1", "libsodium-wrappers": "^0.8.3", "pathe": "^2.0.3", "picomatch": "^4.0.4", "react": "^19.2.7", "rolldown": "1.2.5", "string-width": "^8.2.2", "undici": "^7.16.0", "yaml": "^2.0.0" }, "peerDependencies": { "@alchemy.run/frontend-frameworks": "2.0.0-beta.77", "@aws/durable-execution-sdk-js": "^2.1.0", "@effect/platform-bun": ">=4.0.0-rc.112 || >=4.0.0", "@effect/platform-node": ">=4.0.0-rc.112 || >=4.0.0", "@effect/sql-mysql2": ">=4.0.0-rc.112 || >=4.0.0", "@effect/sql-pg": ">=4.0.0-rc.112 || >=4.0.0", "@vercel/nft": "^1.10.2", "drizzle-kit": "1.0.0-rc.5-ab785fc", "drizzle-orm": "1.0.0-rc.5-ab785fc", "effect": ">=4.0.0-rc.112 || >=4.0.0", "mongodb": "^6.10.0", "mysql2": "^3.24.2", "pg": "^8.22.0", "vite": "^8.0.7", "ws": "^8.20.0" }, "optionalPeers": ["@alchemy.run/frontend-frameworks", "@aws/durable-execution-sdk-js", "@effect/platform-bun", "@effect/platform-node", "@effect/sql-mysql2", "@effect/sql-pg", "@vercel/nft", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "pg", "vite", "ws"], "bin": { "alchemy": "./bin/cli.js" } }, "sha512-8JXWIHFKw5F9kTYiDY2+vHW7Y9v9hoopS5PJVreYB+nJ7VhiKoJ7ApcA4hpWj78GmQoU5tgxy1Ue2OHmvTOt4A=="], @@ -1148,52 +1100,24 @@ "anynum": ["anynum@1.0.1", "", {}, "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A=="], - "archiver": ["archiver@7.0.1", "", { "dependencies": { "archiver-utils": "^5.0.2", "async": "^3.2.4", "buffer-crc32": "^1.0.0", "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", "zip-stream": "^6.0.1" } }, "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ=="], - - "archiver-utils": ["archiver-utils@5.0.2", "", { "dependencies": { "glob": "^10.0.0", "graceful-fs": "^4.2.0", "is-stream": "^2.0.1", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], - "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], "astro": ["astro@7.2.7", "", { "dependencies": { "@astrojs/compiler-rs": "^0.4.0", "@astrojs/internal-helpers": "0.10.4", "@astrojs/markdown-satteri": "0.3.8", "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^2.0.1", "devalue": "^5.8.1", "diff": "^9.0.0", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", "find-process": "^2.1.1", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.3.0", "jsonc-parser": "^3.3.1", "magic-string": "^1.0.0", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.5", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0 || ^0.35.0" }, "peerDependencies": { "@astrojs/markdown-remark": "7.2.4" }, "optionalPeers": ["@astrojs/markdown-remark"], "bin": { "astro": "./bin/astro.mjs" } }, "sha512-+XW7Z1x03hq/zV8tiEnvS8E7JYKo5iyYVPJFt5RhEj3dOE+zSwhVaUabcqNsEDVJcneQX2mqxo4fPgoSO+Jc2w=="], - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], - - "async-lock": ["async-lock@1.4.1", "", {}, "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="], - "auth": ["auth@1.7.4", "", { "dependencies": { "@babel/core": "^7.29.7", "@babel/preset-react": "^7.28.5", "@babel/preset-typescript": "^7.28.5", "@better-auth/core": "1.7.4", "@better-auth/telemetry": "1.7.4", "@better-auth/utils": "0.4.2", "@clack/prompts": "^1.6.0", "@mrleebo/prisma-ast": "^0.16.0", "better-auth": "1.7.4", "c12": "^4.0.0-beta.5", "chalk": "^5.6.2", "commander": "^15.0.0", "dotenv": "^17.3.1", "get-tsconfig": "^4.14.0", "jiti": "^2.7.0", "open": "^11.0.0", "prettier": "^3.8.1", "prompts": "^2.4.2", "semver": "^7.8.4", "yocto-spinner": "^1.2.0", "zod": "^4.5.4" }, "bin": { "auth": "./dist/index.mjs", "better-auth": "./dist/index.mjs" } }, "sha512-ZK0pMfjaaoH97E2v/QdV7/TTd/Wdb1xxILXOj12Mf2UgFCs2ipJwHFJ2513tR4i0RNSwgXTYqgNIzXpYnLjJtw=="], "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], - "b4a": ["b4a@1.9.0", "", { "peerDependencies": { "react-native-b4a": "*" }, "optionalPeers": ["react-native-b4a"] }, "sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ=="], - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "bare-events": ["bare-events@2.9.2", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA=="], - - "bare-fs": ["bare-fs@4.8.1", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg=="], - - "bare-path": ["bare-path@3.1.2", "", {}, "sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw=="], - - "bare-stream": ["bare-stream@2.13.4", "", { "dependencies": { "b4a": "^1.8.1", "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "optionalPeers": ["bare-abort-controller", "bare-buffer", "bare-events"] }, "sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA=="], - - "bare-url": ["bare-url@2.5.4", "", { "dependencies": { "bare-path": "^3.0.0" } }, "sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ=="], - "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], - "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], "better-auth": ["better-auth@1.7.4", "", { "dependencies": { "@better-auth/core": "1.7.4", "@better-auth/drizzle-adapter": "1.7.4", "@better-auth/kysely-adapter": "1.7.4", "@better-auth/memory-adapter": "1.7.4", "@better-auth/mongo-adapter": "1.7.4", "@better-auth/prisma-adapter": "1.7.4", "@better-auth/telemetry": "1.7.4", "@better-auth/utils": "0.4.2", "@better-fetch/fetch": "1.3.1", "@noble/ciphers": "^2.2.0", "@noble/hashes": "^2.2.0", "better-call": "1.4.0", "defu": "^6.1.4", "jose": "^6.2.3", "kysely": "^0.28.17 || ^0.29.0", "nanostores": "^1.3.0", "zod": "^4.5.4" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1", "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-rjL9g8b8UvdwK/ZSdawhqYzOm8YegurB6Fuzqtu/zFJamlFrPa+eEhP8Qj2vFWsMzHOk2mb/L4FRYh9pgaU7OA=="], @@ -1202,30 +1126,18 @@ "bits-ui": ["bits-ui@2.19.0", "", { "dependencies": { "@floating-ui/core": "^1.7.1", "@floating-ui/dom": "^1.7.1", "esm-env": "^1.1.2", "runed": "^0.35.1", "svelte-toolbelt": "^0.10.6", "tabbable": "^6.2.0" }, "peerDependencies": { "@internationalized/date": "^3.8.1", "svelte": "^5.33.0" } }, "sha512-dHDm5Jw2NZJgLRvHAwPeuR4BnZij+AqTIq33OqVfo+qiMXontoC/yIvGT5TZaviv7OuBdJ2QUy2JPQ2yKBvF6w=="], - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], - "brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.28.9", "", { "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.420", "node-releases": "^2.0.54", "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" } }, "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg=="], - "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - - "buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], - - "buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="], - "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], - "byline": ["byline@5.0.0", "", {}, "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q=="], - "c12": ["c12@4.0.0-rc.1", "", { "dependencies": { "confbox": "^0.3.1", "defu": "^6.1.7", "exsolve": "^1.1.1", "pathe": "^2.0.3", "pkg-types": "^2.3.2", "rc9": "^3.1.0" }, "peerDependencies": { "chokidar": "^5", "dotenv": "*", "giget": ">=3.1.0", "jiti": "*", "magicast": "*" }, "optionalPeers": ["chokidar", "dotenv", "giget", "jiti", "magicast"] }, "sha512-08UYGAVLTLqgiCzWNMNdBHdiWKzhIVwcapySI11mFTBacL5bP0dj1Vr7IcwOH0wcUTgMzm1VCh+by922viKWig=="], "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], @@ -1248,8 +1160,6 @@ "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], @@ -1268,8 +1178,6 @@ "common-ancestor-path": ["common-ancestor-path@2.0.0", "", {}, "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng=="], - "compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="], - "confbox": ["confbox@0.3.1", "", {}, "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA=="], "content-type": ["content-type@3.0.0", "", {}, "sha512-AIi5H6p0xk5uknXcN3/rmhP8jgp69OfSe/JuKiQAFprJ7UGw7mwj7m4XcmDzlrnJDG+cGpphAINGdU3g3g7kDw=="], @@ -1282,12 +1190,6 @@ "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], - "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], - - "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], - - "crc32-stream": ["crc32-stream@6.0.0", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^4.0.0" } }, "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], @@ -1326,12 +1228,6 @@ "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], - "docker-compose": ["docker-compose@1.5.0", "", { "dependencies": { "yaml": "^2.2.2" } }, "sha512-O+eD6c63+BJORUWddXA7uAlI6H1KLDPd/aS14k73nXYpFliM48C3xrrKB3sXsR1Fp/rHUZQe7+fJ+QA7+jepyw=="], - - "docker-modem": ["docker-modem@5.0.7", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA=="], - - "dockerode": ["dockerode@5.0.1", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.7", "protobufjs": "^7.3.2", "tar-fs": "^2.1.4" } }, "sha512-avsq/xk4YPIrn0CgleX5bjT9Y8IT1p9PxrNQ++RBQ2WEyFfHCTDsT9kmyxz+H/axnjAwg8wJWEIuPGOUuNupiA=="], - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], @@ -1348,16 +1244,12 @@ "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], - "effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="], "electron-to-chromium": ["electron-to-chromium@1.5.426", "", {}, "sha512-2Gcq6inCQs/AqfHP3f5ftCzk+pqeW2VlA1LgmPqEj2hfBO8HiZRspNYkD4HzFBe4u6lGqca4BspFr6Ix4Q400g=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - "enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="], "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], @@ -1374,14 +1266,8 @@ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], - "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - - "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], - "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], "exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="], @@ -1390,8 +1276,6 @@ "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], - "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], @@ -1426,8 +1310,6 @@ "formatly": ["formatly@0.7.0", "", { "dependencies": { "fd-package-json": "^2.0.0", "package-manager-detector": "^1.8.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-7CXJtIIA0zy/u12StsYk25qVKxvdLA2ep2sTNxK3ov0mGNIIDqIvAXDSgTnAfDJFsPfWjuz0WjfYSdpvnLA5Tg=="], - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], @@ -1436,16 +1318,12 @@ "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], - "get-port": ["get-port@5.1.1", "", {}, "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ=="], - "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], "get-tsconfig": ["get-tsconfig@4.14.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], - "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -1472,8 +1350,6 @@ "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], @@ -1486,8 +1362,6 @@ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], @@ -1500,8 +1374,6 @@ "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "is-unsafe": ["is-unsafe@2.0.2", "", {}, "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ=="], "is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="], @@ -1510,8 +1382,6 @@ "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], @@ -1540,8 +1410,6 @@ "kysely": ["kysely@0.29.5", "", {}, "sha512-ooa+eSbBNPTo3MycPEuW5jdrxQdQwdtB3LC3h43FiXQbIry5tR0C5lDG7eealK0E4D7XjrnOP5DIUg/LyjRMYQ=="], - "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], - "libsodium": ["libsodium@0.8.4", "", {}, "sha512-lMcYaRi0zcs7tarATsQUYC7rstliIXZuoq0c6zXSgNtSNtdvBgkSegjWhpMJAXzKX3SUSwIp7+zEsob+j3LuRw=="], "libsodium-wrappers": ["libsodium-wrappers@0.8.4", "", { "dependencies": { "libsodium": "^0.8.0" } }, "sha512-mu8aAWucZjTB5O/BtGXtW4e1agy7uHxNYG7zPthmmD1jU43LCDmSWZLN4JhflbdPXj3yDO4lxM1O9hLDgIOXDw=="], @@ -1580,12 +1448,8 @@ "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], - "loglevel": ["loglevel@1.9.2", "", {}, "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg=="], - "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -1614,14 +1478,6 @@ "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], - "minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], - - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - - "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], - - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], - "mode-watcher": ["mode-watcher@1.1.0", "", { "dependencies": { "runed": "^0.25.0", "svelte-toolbelt": "^0.7.1" }, "peerDependencies": { "svelte": "^5.27.0" } }, "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g=="], "modern-tar": ["modern-tar@0.8.4", "", {}, "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g=="], @@ -1636,8 +1492,6 @@ "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], - "nan": ["nan@2.29.0", "", {}, "sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA=="], - "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], "nanostores": ["nanostores@1.5.2", "", {}, "sha512-B0UbxzK1s0CN8Xht6r+7iT5+xV8PTaRERR1nATeplRv1Rw5YLWfVAid0hkqY3EceqpG4RjTk8GAwIxQY39Rnwg=="], @@ -1666,8 +1520,6 @@ "ohash": ["ohash@2.0.12", "", {}, "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "oniguruma-parser": ["oniguruma-parser@0.12.2", "", {}, "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw=="], "oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], @@ -1690,8 +1542,6 @@ "p-timeout": ["p-timeout@7.0.1", "", {}, "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg=="], - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - "package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="], "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], @@ -1700,8 +1550,6 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], @@ -1734,10 +1582,6 @@ "pkg-types": ["pkg-types@2.3.3", "", { "dependencies": { "confbox": "^0.3.1", "exsolve": "^1.1.1", "pathe": "^2.0.3" } }, "sha512-j/lCFdcppV0JxWpCEITdbDltBxPP6cHT+yNJ6Go2OgoSA9518X847X9z0p6LtA4Nc16+eQzCZjRrWanTGvHJ5w=="], - "playwright": ["playwright@1.63.0", "", { "dependencies": { "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg=="], - - "playwright-core": ["playwright-core@1.63.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg=="], - "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], @@ -1756,8 +1600,6 @@ "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], - "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], - "process-ancestry": ["process-ancestry@0.1.0", "", {}, "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg=="], "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], @@ -1768,14 +1610,8 @@ "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], - "properties-reader": ["properties-reader@3.0.1", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "mkdirp": "^3.0.1" } }, "sha512-WPn+h9RGEExOKdu4bsF4HksG/uzd3cFq3MFtq8PsFeExPse5Ha/VOjQNyHhjboBFwGXGev6muJYTSPAOkROq2g=="], - "property-information": ["property-information@7.2.0", "", {}, "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg=="], - "protobufjs": ["protobufjs@7.6.6", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg=="], - - "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], @@ -1790,8 +1626,6 @@ "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "redis": ["redis@6.2.1", "", { "dependencies": { "@redis/bloom": "6.2.1", "@redis/client": "6.2.1", "@redis/json": "6.2.1", "@redis/search": "6.2.1", "@redis/time-series": "6.2.1" } }, "sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg=="], @@ -1806,8 +1640,6 @@ "remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], "retext-smartypants": ["retext-smartypants@6.2.0", "", { "dependencies": { "@types/nlcst": "^2.0.0", "nlcst-to-string": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ=="], @@ -1830,8 +1662,6 @@ "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "satteri": ["satteri@0.10.5", "", { "dependencies": { "@types/estree-jsx": "^1.0.5", "@types/hast": "^3.0.5", "@types/mdast": "^4.0.4", "@types/unist": "^3.0.3" }, "optionalDependencies": { "@bruits/satteri-darwin-arm64": "0.10.5", "@bruits/satteri-darwin-x64": "0.10.5", "@bruits/satteri-linux-arm64-gnu": "0.10.5", "@bruits/satteri-linux-arm64-musl": "0.10.5", "@bruits/satteri-linux-x64-gnu": "0.10.5", "@bruits/satteri-linux-x64-musl": "0.10.5", "@bruits/satteri-wasm32-wasi": "0.10.5", "@bruits/satteri-win32-arm64-msvc": "0.10.5", "@bruits/satteri-win32-x64-msvc": "0.10.5" } }, "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ=="], "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], @@ -1870,32 +1700,20 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], - "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], - "ssh-remote-port-forward": ["ssh-remote-port-forward@1.0.4", "", { "dependencies": { "@types/ssh2": "^0.5.48", "ssh2": "^1.4.0" } }, "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ=="], - - "ssh2": ["ssh2@1.17.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.23.0" } }, "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ=="], - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], "std-env": ["std-env@4.2.0", "", {}, "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw=="], - "streamx": ["streamx@2.28.1", "", { "dependencies": { "events-universal": "^1.0.0", "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" } }, "sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA=="], - "string-width": ["string-width@8.2.2", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg=="], - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], "strnum": ["strnum@2.4.2", "", { "dependencies": { "anynum": "^1.0.1" } }, "sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw=="], @@ -1926,16 +1744,6 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar-fs": ["tar-fs@3.1.3", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ=="], - - "tar-stream": ["tar-stream@3.2.1", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ=="], - - "teex": ["teex@1.0.1", "", { "dependencies": { "streamx": "^2.12.5" } }, "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg=="], - - "testcontainers": ["testcontainers@12.1.0", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@types/dockerode": "^4.0.1", "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.4.3", "docker-compose": "^1.4.2", "dockerode": "^5.0.1", "get-port": "^5.1.1", "proper-lockfile": "^4.1.2", "properties-reader": "^3.0.1", "ssh-remote-port-forward": "^1.0.4", "tar-fs": "^3.1.3", "tmp": "^0.2.7", "undici": "^8.9.0" } }, "sha512-YjDLqIITuhGLMnM10yhg3oV6lIG5IMpz1R1DPBZoOOks83q7i7IVpeSWRTiyl7roozjiyLmwIoLK/KY8OnZmIA=="], - - "text-decoder": ["text-decoder@1.2.7", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ=="], - "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -1950,8 +1758,6 @@ "tinyrainbow": ["tinyrainbow@3.1.1", "", {}, "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw=="], - "tmp": ["tmp@0.2.7", "", {}, "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], @@ -1964,8 +1770,6 @@ "turbo": ["turbo@2.10.11", "", { "optionalDependencies": { "@turbo/darwin-64": "2.10.11", "@turbo/darwin-arm64": "2.10.11", "@turbo/linux-64": "2.10.11", "@turbo/linux-arm64": "2.10.11", "@turbo/windows-64": "2.10.11", "@turbo/windows-arm64": "2.10.11" }, "bin": { "turbo": "bin/turbo" } }, "sha512-yQfwQVoRXwOuyX1LxiJFBFNg6VfuYh+/RyZLd82+isgyLkBXw3S5XRRzvcck1FAjSCG5sVyLd+O1eDMvYa3J7g=="], - "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], - "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], @@ -2026,10 +1830,6 @@ "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "wsl-utils": ["wsl-utils@1.0.0", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA=="], @@ -2062,8 +1862,6 @@ "zimmerframe": ["zimmerframe@1.1.5", "", {}, "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA=="], - "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], - "zod": ["zod@4.6.1", "", {}, "sha512-341aRWQsve0rvronKNTqZpjmzdbUDlFuzHaI/XLg/Ej82qffDJRRfBTCuv7+9q/rMjB6LSLyEBnW4InJeMtt/Q=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -2096,16 +1894,8 @@ "@electric-sql/pglite-tools/@electric-sql/pglite": ["@electric-sql/pglite@0.3.15", "", {}, "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ=="], - "@grpc/grpc-js/@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="], - - "@grpc/proto-loader/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], - "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - "@octokit/plugin-paginate-rest/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], @@ -2134,16 +1924,10 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], - "alchemy/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "archiver/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - - "archiver-utils/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "astro/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], "astro/get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="], @@ -2154,30 +1938,16 @@ "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], - "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - - "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "compress-commons/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - - "crc32-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], - "docker-modem/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "dockerode/tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], - "figlet/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], "find-process/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "find-process/commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], - "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], - "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], "knip/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], @@ -2190,20 +1960,10 @@ "mode-watcher/svelte-toolbelt": ["svelte-toolbelt@0.7.1", "", { "dependencies": { "clsx": "^2.1.1", "runed": "^0.23.2", "style-to-object": "^1.0.8" }, "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ=="], - "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "rolldown/@oxc-project/types": ["@oxc-project/types@0.146.0", "", {}, "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA=="], - "ssh-remote-port-forward/@types/ssh2": ["@types/ssh2@0.5.52", "", { "dependencies": { "@types/node": "*", "@types/ssh2-streams": "*" } }, "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg=="], - - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "svelte/aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="], "svelte-sonner/runed": ["runed@0.28.0", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ=="], @@ -2218,14 +1978,8 @@ "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], - "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "wsl-utils/powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], - "zip-stream/readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], - "@alchemy.run/frontend-frameworks/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], "@alchemy.run/frontend-frameworks/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], @@ -2288,18 +2042,6 @@ "@effect/sql-pg/pg-types/postgres-interval": ["postgres-interval@3.0.0", "", {}, "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw=="], - "@grpc/grpc-js/@grpc/proto-loader/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], - - "@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@grpc/proto-loader/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@27.0.0", "", {}, "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA=="], @@ -2330,12 +2072,6 @@ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], - "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - - "archiver-utils/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "archiver/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "astro/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ=="], "astro/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.2", "", { "os": "android", "cpu": "arm" }, "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg=="], @@ -2388,18 +2124,10 @@ "astro/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], - "compress-commons/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "crc32-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], - "dockerode/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "mode-watcher/svelte-toolbelt/runed": ["runed@0.23.4", "", { "dependencies": { "esm-env": "^1.0.0" }, "peerDependencies": { "svelte": "^5.7.0" } }, "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "unstorage/chokidar/readdirp": ["readdirp@5.1.1", "", {}, "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA=="], "vite/rolldown/@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.6", "", { "os": "android", "cpu": "arm" }, "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ=="], @@ -2432,26 +2160,6 @@ "vite/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "zip-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "@grpc/grpc-js/@grpc/proto-loader/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - - "@grpc/grpc-js/@grpc/proto-loader/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "@grpc/grpc-js/@grpc/proto-loader/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - - "@grpc/proto-loader/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@grpc/proto-loader/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "@grpc/proto-loader/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@grpc/proto-loader/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@prisma/dev/@mrleebo/prisma-ast/chevrotain/@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="], "@prisma/dev/@mrleebo/prisma-ast/chevrotain/@chevrotain/gast": ["@chevrotain/gast@10.5.0", "", { "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A=="], @@ -2459,33 +2167,5 @@ "@prisma/dev/@mrleebo/prisma-ast/chevrotain/@chevrotain/types": ["@chevrotain/types@10.5.0", "", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="], "@prisma/dev/@mrleebo/prisma-ast/chevrotain/@chevrotain/utils": ["@chevrotain/utils@10.5.0", "", {}, "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="], - - "archiver-utils/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "archiver/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "compress-commons/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "crc32-stream/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "dockerode/tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "zip-stream/readable-stream/string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "@grpc/grpc-js/@grpc/proto-loader/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@grpc/grpc-js/@grpc/proto-loader/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "@grpc/grpc-js/@grpc/proto-loader/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "@grpc/grpc-js/@grpc/proto-loader/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "@grpc/proto-loader/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@grpc/proto-loader/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@grpc/grpc-js/@grpc/proto-loader/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "@grpc/grpc-js/@grpc/proto-loader/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], } } diff --git a/knip.jsonc b/knip.jsonc index d27848a..c535bce 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -16,8 +16,7 @@ }, "apps/web": { // SvelteKit discovers the explicit environment contract by convention. - // Playwright starts the isolated preview server through its webServer command. - "entry": ["auth.config.ts", "src/env.ts", "src/lib/server/runtime.ts", "tests/server.ts"], + "entry": ["auth.config.ts", "src/env.ts", "src/lib/server/runtime.ts"], // svelte-check resolves this compiler through `--tsgo`; knip cannot see // dependencies selected by a command-line flag. "ignoreDependencies": ["@typescript/native"], diff --git a/mise.toml b/mise.toml index 982c631..3ff9ebe 100644 --- a/mise.toml +++ b/mise.toml @@ -8,4 +8,4 @@ ALCHEMY_STAGE = "dev_{{ env.USER }}_{{ config_root | basename }}" [tasks.setup] description = "Bootstrap a fresh checkout" -run = ["mise install", "bun install", "bun run --cwd apps/web playwright install chromium"] +run = ["mise install", "bun install"] diff --git a/package.json b/package.json index 330bf39..df7b34d 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,7 @@ "lint": "oxlint .", "lint:fix": "oxlint --fix .", "prepare": "./scripts/prepare-effect.sh && effect-tsgo patch --oxlint", - "test": "turbo test && vitest run infra", - "test:e2e": "bun run --cwd apps/web test:e2e" + "test": "turbo test && vitest run infra" }, "dependencies": { "@effect/platform-bun": "catalog:", diff --git a/skills/effect-forge/references/feature-workflow.md b/skills/effect-forge/references/feature-workflow.md index bb8ce55..64dabaf 100644 --- a/skills/effect-forge/references/feature-workflow.md +++ b/skills/effect-forge/references/feature-workflow.md @@ -28,7 +28,7 @@ Web is the current application entrypoint. A future public API is a peer entrypo 4. Implement and test the production adapter. 5. Add a SvelteKit remote function for browser access. 6. Build the feature UI from the remote schema and result. -7. Add one integration test at each new framework boundary. +7. Verify new behavior through the existing colocated test seams. 8. Run every repository validation command. Stop when the slice is coherent. Do not add generalized policy, providers, transports, or UI state for anticipated features. diff --git a/skills/effect-forge/references/testing.md b/skills/effect-forge/references/testing.md index 2573037..92681dc 100644 --- a/skills/effect-forge/references/testing.md +++ b/skills/effect-forge/references/testing.md @@ -2,15 +2,12 @@ Test through public interfaces. Each seam verifies only the behavior it owns. -| Seam | What it verifies | -| ------------------- | ------------------------------------------------------------ | -| Domain | parsing, invariants, authorization, entitlement decisions | -| Application service | policy, authorization, effect ordering | -| PostgreSQL adapter | SQL, scoping, constraints, row decoding | -| Provider adapter | provider translation and error projection | -| Public HTTP API | decoding, authentication, and public error projection | -| SvelteKit | remote validation, redirects, forms, and safe failure output | -| End-to-end | representative critical flows | +| Seam | What it verifies | +| --------------------- | --------------------------------------------------------------------------- | +| Domain | parsing, invariants, authorization, entitlement decisions | +| Application service | policy, authorization, effect ordering | +| PostgreSQL adapter | SQL, scoping, constraints, row decoding | +| Provider HTTP handler | decoding, authentication, provider translation, and public error projection | ## Test doubles @@ -21,9 +18,6 @@ Provide substitute Layers instead of mocking modules. - Share migrated PGlite resource mechanics through an explicit test-only package export when provider adapters in another package need the same physical schema. - Use Effect test clocks and deterministic services for time and randomness. - Exercise HTTP capabilities through the production router as Fetch-compatible `Request → Response` handlers. -- Exercise remote functions and endpoints through built SvelteKit server boundaries. -- Run representative Playwright flows against the built app, Cloudflare adapter, and - disposable PostgreSQL; use accessible locators and no production test hooks. - Run a shared port contract suite against each adapter when several implementations exist. Choose Layer provisioning by lifecycle and isolation: @@ -40,10 +34,10 @@ Do not use module mocks, arbitrary sleeps, or assertions against private calls. Test each authorization rule once where it is owned: -- Pure role-to-permission decisions in domain tests. +- Pure role-to-permission decisions beside the permission definition. - Membership resolution and enforcement in core service tests. - Better Auth role translation in adapter tests. -- HTTP or SvelteKit status and message projection at the framework boundary. +- Provider HTTP status and message projection through its real handler. Browser permission checks are presentation behavior; they do not replace a server authorization test. @@ -61,16 +55,13 @@ In Effect tests, prefer `it.effect.prop`. Pass a Schema for valid domain inputs ## Placement -Test an expected failure where its policy is owned. A service authorization failure belongs in the service test; its HTTP status belongs in the handler test; its field presentation belongs in a form test only when the field can correct it. - -A future public API test and a SvelteKit test may cover the same capability boundary without repeating every core policy branch. +Test an expected failure where its policy is owned. A service authorization failure belongs in the service test; its HTTP status belongs in the handler test. ## Validation ```bash bun run check-types bun run test -bun run test:e2e bun run check bun run check:architecture bun run knip From b1abac6a246545c4cccb3e77785d0d016d65f66a Mon Sep 17 00:00:00 2001 From: Mateo Roldos Date: Fri, 18 Sep 2026 17:12:03 -0300 Subject: [PATCH 04/19] feat(web): replace workspaces with organizations and shared todos Resolve organization handles through the membership-filtered directory while keeping stable IDs explicit for authorization and persistence. Validate provider slugs and compose native remote forms with optimistic todo presentation and conservative mutation recovery. Remove the superseded workspace model and schema, keep public failure projections exhaustive, and document organization ownership and frontend recovery. --- AGENTS.md | 1 + README.md | 10 + adapters/database-postgres/drizzle.config.ts | 2 +- .../migration.sql | 3 + .../snapshot.json | 1270 +++++++++++++++++ .../src/persistence-postgres.ts | 5 +- .../src/test/persistence-pglite.ts | 5 +- .../database-postgres/src/workspace/schema.ts | 29 - .../workspace-store-postgres.test.ts | 56 - .../src/workspace/workspace-store-postgres.ts | 66 - apps/web/package.json | 5 +- .../src/lib/features/auth/return-path.test.ts | 12 +- apps/web/src/lib/features/auth/return-path.ts | 2 +- .../src/lib/features/auth/sign-in-form.svelte | 6 +- .../src/lib/features/auth/sign-up-form.svelte | 8 +- .../organizations/organizations.remote.ts | 27 + .../lib/features/todos/TodoCreateForm.svelte | 53 + .../src/lib/features/todos/TodoItem.svelte | 43 + .../src/lib/features/todos/TodoList.svelte | 20 + .../src/lib/features/todos/todos.remote.ts | 92 ++ .../features/workspaces/workspaces.remote.ts | 42 - apps/web/src/lib/server/auth-guard.ts | 29 + .../web/src/lib/server/authentication.test.ts | 235 ++- apps/web/src/lib/server/authentication.ts | 75 +- .../web/src/lib/server/better-auth-options.ts | 34 +- .../routes/(authenticated)/+layout.server.ts | 29 +- .../src/routes/(authenticated)/+layout.svelte | 8 +- .../organizations/+page.svelte | 92 ++ .../[organizationSlug]/todos/+page.svelte | 24 + .../(authenticated)/workspaces/+page.svelte | 58 - apps/web/src/routes/+error.svelte | 23 +- apps/web/src/routes/+page.svelte | 4 +- bun.lock | 1 + packages/core/package.json | 4 +- packages/core/src/application.ts | 3 +- .../organization-membership.ts | 2 +- .../src/workspace/workspace-directory.test.ts | 56 - .../core/src/workspace/workspace-directory.ts | 60 - .../src/workspace/workspace-store-memory.ts | 49 - .../core/src/workspace/workspace-store.ts | 30 - packages/domain/package.json | 4 +- .../domain/src/organization/organization.ts | 7 + .../src/workspace/workspace-member.test.ts | 38 - .../domain/src/workspace/workspace-member.ts | 19 - .../domain/src/workspace/workspace.test.ts | 30 - packages/domain/src/workspace/workspace.ts | 28 - .../effect-forge/references/architecture.md | 10 +- skills/effect-forge/references/frontend.md | 79 +- 48 files changed, 2123 insertions(+), 665 deletions(-) create mode 100644 adapters/database-postgres/drizzle/20260918201532_lively_harrier/migration.sql create mode 100644 adapters/database-postgres/drizzle/20260918201532_lively_harrier/snapshot.json delete mode 100644 adapters/database-postgres/src/workspace/schema.ts delete mode 100644 adapters/database-postgres/src/workspace/workspace-store-postgres.test.ts delete mode 100644 adapters/database-postgres/src/workspace/workspace-store-postgres.ts create mode 100644 apps/web/src/lib/features/organizations/organizations.remote.ts create mode 100644 apps/web/src/lib/features/todos/TodoCreateForm.svelte create mode 100644 apps/web/src/lib/features/todos/TodoItem.svelte create mode 100644 apps/web/src/lib/features/todos/TodoList.svelte create mode 100644 apps/web/src/lib/features/todos/todos.remote.ts delete mode 100644 apps/web/src/lib/features/workspaces/workspaces.remote.ts create mode 100644 apps/web/src/lib/server/auth-guard.ts create mode 100644 apps/web/src/routes/(authenticated)/organizations/+page.svelte create mode 100644 apps/web/src/routes/(authenticated)/organizations/[organizationSlug]/todos/+page.svelte delete mode 100644 apps/web/src/routes/(authenticated)/workspaces/+page.svelte delete mode 100644 packages/core/src/workspace/workspace-directory.test.ts delete mode 100644 packages/core/src/workspace/workspace-directory.ts delete mode 100644 packages/core/src/workspace/workspace-store-memory.ts delete mode 100644 packages/core/src/workspace/workspace-store.ts delete mode 100644 packages/domain/src/workspace/workspace-member.test.ts delete mode 100644 packages/domain/src/workspace/workspace-member.ts delete mode 100644 packages/domain/src/workspace/workspace.test.ts delete mode 100644 packages/domain/src/workspace/workspace.ts diff --git a/AGENTS.md b/AGENTS.md index 777f69f..cbc5a38 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -29,6 +29,7 @@ ui → nothing - Remote functions own application calls from the browser. A `query` is the cache; a `form` is the mutation. Do not add a client-side state library. - Remote handlers invoke core capabilities directly. - Better Auth is the deliberate exception: its Svelte client calls the same-origin `/api/auth/*` routes directly so the provider owns its browser protocol and cookies. +- Better Auth owns organizations and memberships. Core owns application permissions; require the capability through `OrganizationAccess` and scope persistence by the explicit organization ID. - Effect Schema validates remote form input through Standard Schema. Remote handlers map expected failures to SvelteKit `error` or `invalid` results. - Svelte runes are limited to component-local presentation state. - Server layouts resolve principals required by their pages; remote handlers and endpoints own authorization for their operations. diff --git a/README.md b/README.md index 3abf6bd..2dc422c 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,10 @@ alchemy.run.ts application Stack summary SvelteKit remote functions call application services directly. The Web Worker hosts Better Auth behind same-origin routes and projects authenticated principals into application operations. Independent API and CLI clients are added only when their concrete contracts and credentials are known. +The example flow is a shared todo list: sign up, create an organization, and add or +complete todos with its members. Better Auth owns organizations and memberships; +`TodoDirectory` owns application permissions and `TodoStore` owns organization-scoped persistence. + ## Development Bootstrap a checkout and authenticate the local Alchemy profile: @@ -79,6 +83,12 @@ single-line logfmt; production uses JSON. Normal `bun run dev` needs no collecto See [operation logging](skills/effect-forge/references/observability.md) for examples. +## Organization URLs + +Organization URLs use handles, for example `/organizations/analytical-engine/todos`. +Authorization and persistence use the stable organization ID. Handles can be changed +through Better Auth; changing one changes its URL without redirecting old links. + ## Validation ```sh diff --git a/adapters/database-postgres/drizzle.config.ts b/adapters/database-postgres/drizzle.config.ts index c6ff0e0..550a8f4 100644 --- a/adapters/database-postgres/drizzle.config.ts +++ b/adapters/database-postgres/drizzle.config.ts @@ -3,5 +3,5 @@ import { defineConfig } from "drizzle-kit"; export default defineConfig({ dialect: "postgresql", out: "./drizzle", - schema: ["./src/auth/schema.ts", "./src/workspace/schema.ts", "./src/todo/schema.ts"], + schema: ["./src/auth/schema.ts", "./src/todo/schema.ts"], }); diff --git a/adapters/database-postgres/drizzle/20260918201532_lively_harrier/migration.sql b/adapters/database-postgres/drizzle/20260918201532_lively_harrier/migration.sql new file mode 100644 index 0000000..f5e08c9 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918201532_lively_harrier/migration.sql @@ -0,0 +1,3 @@ +ALTER TABLE "workspace_members" DROP CONSTRAINT "workspace_members_workspace_id_workspaces_id_fkey";--> statement-breakpoint +DROP TABLE "workspace_members";--> statement-breakpoint +DROP TABLE "workspaces"; \ No newline at end of file diff --git a/adapters/database-postgres/drizzle/20260918201532_lively_harrier/snapshot.json b/adapters/database-postgres/drizzle/20260918201532_lively_harrier/snapshot.json new file mode 100644 index 0000000..a3c28d5 --- /dev/null +++ b/adapters/database-postgres/drizzle/20260918201532_lively_harrier/snapshot.json @@ -0,0 +1,1270 @@ +{ + "version": "8", + "dialect": "postgres", + "id": "24c5b99c-89ba-4bcd-bcf3-7034e36ca21e", + "prevIds": ["b2323111-6852-49eb-ac65-d97a9b51df79"], + "ddl": [ + { + "isRlsEnabled": false, + "name": "account", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "invitation", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "member", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "organization", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "rate_limit", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "session", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "user", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "verification", + "entityType": "tables", + "schema": "public" + }, + { + "isRlsEnabled": false, + "name": "todos", + "entityType": "tables", + "schema": "public" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "account_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "provider_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id_token", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "access_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "refresh_token_expires_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "scope", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "password", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "account" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'pending'", + "generated": null, + "identity": null, + "name": "status", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "inviter_id", + "entityType": "columns", + "schema": "public", + "table": "invitation" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "'member'", + "generated": null, + "identity": null, + "name": "role", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "member" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "slug", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "logo", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "metadata", + "entityType": "columns", + "schema": "public", + "table": "organization" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "key", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "integer", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "count", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "bigint", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "last_request", + "entityType": "columns", + "schema": "public", + "table": "rate_limit" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "token", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "ip_address", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_agent", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "user_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "active_organization_id", + "entityType": "columns", + "schema": "public", + "table": "session" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "name", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "email", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "email_verified", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": false, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "image", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "user" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "identifier", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "value", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "expires_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "updated_at", + "entityType": "columns", + "schema": "public", + "table": "verification" + }, + { + "type": "uuid", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "id", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "text", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "organization_id", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "varchar(200)", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": null, + "generated": null, + "identity": null, + "name": "title", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "boolean", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "false", + "generated": null, + "identity": null, + "name": "completed", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "type": "timestamp", + "typeSchema": null, + "notNull": true, + "dimensions": 0, + "default": "now()", + "generated": null, + "identity": null, + "name": "created_at", + "entityType": "columns", + "schema": "public", + "table": "todos" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "account_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "email", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "invitation_email_idx", + "entityType": "indexes", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_organizationId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "member_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "slug", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": true, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "organization_slug_uidx", + "entityType": "indexes", + "schema": "public", + "table": "organization" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "user_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "session_userId_idx", + "entityType": "indexes", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "identifier", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "verification_identifier_idx", + "entityType": "indexes", + "schema": "public", + "table": "verification" + }, + { + "nameExplicit": true, + "columns": [ + { + "value": "organization_id", + "isExpression": false, + "asc": true, + "nullsFirst": false, + "opclass": null + } + ], + "isUnique": false, + "where": null, + "with": "", + "method": "btree", + "concurrently": false, + "name": "todos_organization_id_index", + "entityType": "indexes", + "schema": "public", + "table": "todos" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "account_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "account" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["inviter_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "invitation_inviter_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "invitation" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "member_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "member" + }, + { + "nameExplicit": false, + "columns": ["user_id"], + "schemaTo": "public", + "tableTo": "user", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "session_user_id_user_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "session" + }, + { + "nameExplicit": false, + "columns": ["organization_id"], + "schemaTo": "public", + "tableTo": "organization", + "columnsTo": ["id"], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "name": "todos_organization_id_organization_id_fkey", + "entityType": "fks", + "schema": "public", + "table": "todos" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "account_pkey", + "schema": "public", + "table": "account", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "invitation_pkey", + "schema": "public", + "table": "invitation", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "member_pkey", + "schema": "public", + "table": "member", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "organization_pkey", + "schema": "public", + "table": "organization", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "rate_limit_pkey", + "schema": "public", + "table": "rate_limit", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "session_pkey", + "schema": "public", + "table": "session", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "user_pkey", + "schema": "public", + "table": "user", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "verification_pkey", + "schema": "public", + "table": "verification", + "entityType": "pks" + }, + { + "columns": ["id"], + "nameExplicit": false, + "name": "todos_pkey", + "schema": "public", + "table": "todos", + "entityType": "pks" + }, + { + "nameExplicit": false, + "columns": ["slug"], + "nullsNotDistinct": false, + "name": "organization_slug_key", + "schema": "public", + "table": "organization", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["key"], + "nullsNotDistinct": false, + "name": "rate_limit_key_key", + "schema": "public", + "table": "rate_limit", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["token"], + "nullsNotDistinct": false, + "name": "session_token_key", + "schema": "public", + "table": "session", + "entityType": "uniques" + }, + { + "nameExplicit": false, + "columns": ["email"], + "nullsNotDistinct": false, + "name": "user_email_key", + "schema": "public", + "table": "user", + "entityType": "uniques" + } + ], + "renames": [] +} diff --git a/adapters/database-postgres/src/persistence-postgres.ts b/adapters/database-postgres/src/persistence-postgres.ts index 0e55f86..5321366 100644 --- a/adapters/database-postgres/src/persistence-postgres.ts +++ b/adapters/database-postgres/src/persistence-postgres.ts @@ -1,7 +1,6 @@ import { Layer } from "effect"; import { type CustomTypesConfig, types } from "pg"; import { Database } from "./internal/database.ts"; -import { WorkspaceStorePostgres } from "./workspace/workspace-store-postgres.ts"; import { TodoStorePostgres } from "./todo/todo-store-postgres.ts"; const drizzleRawStringOids = new Set([1082, 1114, 1184, 1186, 1231, 1115, 1185, 1187, 1182]); @@ -13,8 +12,6 @@ export const typeParsers: CustomTypesConfig = { }; /** Provides every PostgreSQL-backed application persistence port. */ -export const layer = Layer.merge(WorkspaceStorePostgres.layer, TodoStorePostgres.layer).pipe( - Layer.provide(Database.layer), -); +export const layer = TodoStorePostgres.layer.pipe(Layer.provide(Database.layer)); export * as PersistencePostgres from "./persistence-postgres.ts"; diff --git a/adapters/database-postgres/src/test/persistence-pglite.ts b/adapters/database-postgres/src/test/persistence-pglite.ts index 0042a5b..26b0221 100644 --- a/adapters/database-postgres/src/test/persistence-pglite.ts +++ b/adapters/database-postgres/src/test/persistence-pglite.ts @@ -4,7 +4,6 @@ import { migrate } from "drizzle-orm/effect-pglite/migrator"; import { Effect, Layer } from "effect"; import { Database } from "../internal/database.ts"; import { migrationConfig } from "../migrations.ts"; -import { WorkspaceStorePostgres } from "../workspace/workspace-store-postgres.ts"; import { TodoStorePostgres } from "../todo/todo-store-postgres.ts"; /** Provides the migrated test database for adapter fixtures that insert prerequisite records. */ @@ -18,8 +17,6 @@ export const databaseLayer = Layer.effect( ).pipe(Layer.provide(PgliteClient.layer())); /** Provides every PostgreSQL persistence port through an isolated migrated PGlite database. */ -export const layer = Layer.merge(WorkspaceStorePostgres.layer, TodoStorePostgres.layer).pipe( - Layer.provide(databaseLayer), -); +export const layer = TodoStorePostgres.layer.pipe(Layer.provide(databaseLayer)); export * as PersistencePglite from "./persistence-pglite.ts"; diff --git a/adapters/database-postgres/src/workspace/schema.ts b/adapters/database-postgres/src/workspace/schema.ts deleted file mode 100644 index 3aeee0b..0000000 --- a/adapters/database-postgres/src/workspace/schema.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { sql } from "drizzle-orm"; -import { check, index, pgTable, primaryKey, unique, uuid, varchar } from "drizzle-orm/pg-core"; - -/** PostgreSQL representation of persisted workspaces. */ -export const workspaces = pgTable( - "workspaces", - { - id: uuid("id").primaryKey(), - name: varchar("name", { length: 100 }).notNull(), - }, - (table) => [unique("workspaces_name_unique").on(table.name)], -); - -/** PostgreSQL representation of a user's role in one workspace. */ -export const workspaceMembers = pgTable( - "workspace_members", - { - workspaceId: uuid("workspace_id") - .notNull() - .references(() => workspaces.id, { onDelete: "cascade" }), - userId: varchar("user_id", { length: 255 }).notNull(), - role: varchar("role", { length: 20 }).notNull(), - }, - (table) => [ - primaryKey({ columns: [table.workspaceId, table.userId] }), - index("workspace_members_user_id_index").on(table.userId), - check("workspace_members_role_check", sql`${table.role} in ('owner', 'admin', 'member')`), - ], -); diff --git a/adapters/database-postgres/src/workspace/workspace-store-postgres.test.ts b/adapters/database-postgres/src/workspace/workspace-store-postgres.test.ts deleted file mode 100644 index f2b60ae..0000000 --- a/adapters/database-postgres/src/workspace/workspace-store-postgres.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { assert, describe, it } from "@effect/vitest"; -import { WorkspaceStore } from "@effect-forge/core/workspace-store"; -import { UserId } from "@effect-forge/domain/identity"; -import { Workspace, WorkspaceId, WorkspaceName } from "@effect-forge/domain/workspace"; -import { Effect, Layer } from "effect"; -import { Database } from "../internal/database.ts"; -import { PersistencePglite } from "../test/persistence-pglite.ts"; -import { workspaceMembers, workspaces } from "./schema.ts"; - -const ownerId = UserId.make("better-auth-user-1"); -const otherId = UserId.make("better-auth-user-2"); -const workspace = Workspace.make({ - id: WorkspaceId.make("123e4567-e89b-42d3-a456-426614174000"), - name: WorkspaceName.make("Effect Forge"), -}); -const testLayer = Layer.merge(PersistencePglite.layer, PersistencePglite.databaseLayer); - -describe("PostgreSQL WorkspaceStore", () => { - it.layer(testLayer)("principal scope", (it) => { - it.effect("atomically creates the workspace and owner membership", () => - Effect.gen(function* () { - const store = yield* WorkspaceStore.Service; - yield* store.create(workspace, ownerId); - - assert.deepEqual(yield* store.list(ownerId), [workspace]); - assert.deepEqual(yield* store.list(otherId), []); - const database = yield* Database.Service; - assert.deepEqual(yield* database.select().from(workspaceMembers), [ - { workspaceId: workspace.id, userId: ownerId, role: "owner" }, - ]); - }), - ); - }); - - it.layer(testLayer)("name conflict", (it) => { - it.effect("returns NameTaken without creating another membership", () => - Effect.gen(function* () { - const store = yield* WorkspaceStore.Service; - yield* store.create(workspace, ownerId); - const duplicate = Workspace.make({ - id: WorkspaceId.make("123e4567-e89b-42d3-a456-426614174001"), - name: workspace.name, - }); - - assert.instanceOf( - yield* store.create(duplicate, ownerId).pipe(Effect.flip), - WorkspaceStore.NameTaken, - ); - - const database = yield* Database.Service; - assert.lengthOf(yield* database.select().from(workspaces), 1); - assert.lengthOf(yield* database.select().from(workspaceMembers), 1); - }), - ); - }); -}); diff --git a/adapters/database-postgres/src/workspace/workspace-store-postgres.ts b/adapters/database-postgres/src/workspace/workspace-store-postgres.ts deleted file mode 100644 index 473b779..0000000 --- a/adapters/database-postgres/src/workspace/workspace-store-postgres.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { WorkspaceStore } from "@effect-forge/core/workspace-store"; -import { UserId } from "@effect-forge/domain/identity"; -import { Workspace } from "@effect-forge/domain/workspace"; -import { WorkspaceRole } from "@effect-forge/domain/workspace-member"; -import { asc, eq } from "drizzle-orm"; -import { Effect, Layer, Schema } from "effect"; -import { Database } from "../internal/database.ts"; -import { workspaceMembers, workspaces } from "./schema.ts"; - -const ownerRole: WorkspaceRole = "owner"; - -/** Provides PostgreSQL-backed principal-scoped workspace persistence. */ -export const layer = Layer.effect( - WorkspaceStore.Service, - Effect.gen(function* () { - const database = yield* Database.Service; - - const create = Effect.fn("WorkspaceStorePostgres.create")( - (workspace: Workspace, ownerId: UserId) => - database - .transaction((transaction) => - Effect.gen(function* () { - const inserted = yield* transaction - .insert(workspaces) - .values(workspace) - .onConflictDoNothing({ target: workspaces.name }) - .returning({ id: workspaces.id }); - if (inserted.length === 0) { - return yield* new WorkspaceStore.NameTaken({ name: workspace.name }); - } - - yield* transaction.insert(workspaceMembers).values({ - workspaceId: workspace.id, - userId: ownerId, - role: ownerRole, - }); - return workspace; - }), - ) - .pipe( - Effect.mapError((cause) => - Schema.is(WorkspaceStore.NameTaken)(cause) - ? cause - : new WorkspaceStore.PersistenceError({ cause }), - ), - ), - ); - - const list = Effect.fn("WorkspaceStorePostgres.list")((userId: UserId) => - database - .select({ id: workspaces.id, name: workspaces.name }) - .from(workspaceMembers) - .innerJoin(workspaces, eq(workspaceMembers.workspaceId, workspaces.id)) - .where(eq(workspaceMembers.userId, userId)) - .orderBy(asc(workspaces.name)) - .pipe( - Effect.flatMap(Schema.decodeEffect(Schema.Array(Workspace))), - Effect.mapError((cause) => new WorkspaceStore.PersistenceError({ cause })), - ), - ); - - return WorkspaceStore.Service.of({ create, list }); - }), -); - -export * as WorkspaceStorePostgres from "./workspace-store-postgres.ts"; diff --git a/apps/web/package.json b/apps/web/package.json index cdcdc83..b10c7c0 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -9,7 +9,7 @@ }, "scripts": { "build": "vite build", - "check-types": "svelte-kit sync && svelte-check --tsgo --tsconfig ./tsconfig.json", + "check-types": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "prepare": "svelte-kit sync", "test": "vitest run src" }, @@ -25,7 +25,8 @@ "better-auth": "catalog:", "drizzle-orm": "catalog:", "effect": "catalog:", - "pg": "catalog:" + "pg": "catalog:", + "svelte-sonner": "1.2.1" }, "devDependencies": { "@alchemy.run/frontend-frameworks": "2.0.0-beta.77", diff --git a/apps/web/src/lib/features/auth/return-path.test.ts b/apps/web/src/lib/features/auth/return-path.test.ts index 57f60eb..f7dd6c9 100644 --- a/apps/web/src/lib/features/auth/return-path.test.ts +++ b/apps/web/src/lib/features/auth/return-path.test.ts @@ -5,7 +5,7 @@ import { fromURL, ReturnPath } from "./return-path.ts"; const isReturnPath = Schema.is(ReturnPath); describe("ReturnPath", () => { - it.each(["/workspaces", "/workspaces?tab=recent", "/workspaces#recent"])( + it.each(["/organizations", "/organizations?tab=recent", "/organizations#recent"])( "accepts the same-document path %s", (path) => expect(isReturnPath(path)).toBe(true), ); @@ -18,15 +18,15 @@ describe("ReturnPath", () => { "/\t/evil.example", "/\n/evil.example", "/\r/evil.example", - "/workspaces\u0000", + "/organizations\u0000", ])("rejects the external destination %s", (path) => expect(isReturnPath(path)).toBe(false)); it("reads a safe return path and rejects an external one", () => { - expect(fromURL(new URL("https://app.example/sign-in?returnTo=%2Fworkspaces%3Ftab%3Dnew"))).toBe( - "/workspaces?tab=new", - ); + expect( + fromURL(new URL("https://app.example/sign-in?returnTo=%2Forganizations%3Ftab%3Dnew")), + ).toBe("/organizations?tab=new"); expect( fromURL(new URL("https://app.example/sign-in?returnTo=https%3A%2F%2Fevil.example")), - ).toBe("/workspaces"); + ).toBe("/organizations"); }); }); diff --git a/apps/web/src/lib/features/auth/return-path.ts b/apps/web/src/lib/features/auth/return-path.ts index d926f89..ca3d8bd 100644 --- a/apps/web/src/lib/features/auth/return-path.ts +++ b/apps/web/src/lib/features/auth/return-path.ts @@ -13,5 +13,5 @@ export const fromURL = (url: { const candidate = url.searchParams.get("returnTo"); return candidate !== null && Schema.is(ReturnPath)(candidate) ? candidate - : ReturnPath.make("/workspaces"); + : ReturnPath.make("/organizations"); }; diff --git a/apps/web/src/lib/features/auth/sign-in-form.svelte b/apps/web/src/lib/features/auth/sign-in-form.svelte index 86d770c..d85d6a7 100644 --- a/apps/web/src/lib/features/auth/sign-in-form.svelte +++ b/apps/web/src/lib/features/auth/sign-in-form.svelte @@ -28,7 +28,9 @@ : 'We couldn’t sign you in right now. Please try again.'; return; } - } catch { + } catch (failure) { + // oxlint-disable-next-line effecttsgo/global-console -- Locally recovered failures do not reach Kit's error hook. + console.error(failure); message = 'We couldn’t sign you in right now. Please try again.'; return; } finally { @@ -50,7 +52,7 @@

Sign in

-

Continue to your workspaces.

+

Continue to your organizations.