From a1b4e1d2b27bca699207c62cf2348fe53da3c0b2 Mon Sep 17 00:00:00 2001 From: "Alessandro (Ale) Segala" <43508+ItalyPaleAle@users.noreply.github.com> Date: Sun, 26 Jul 2026 03:32:43 -1000 Subject: [PATCH] feat: migrate one-time and signup tokens to an actor (#1611) Co-authored-by: Elias Schneider --- backend/go.mod | 12 +- backend/go.sum | 24 +- .../internal/bootstrap/actors_bootstrap.go | 28 + backend/internal/bootstrap/bootstrap.go | 3 + .../bootstrap/e2etest_router_bootstrap.go | 2 +- .../internal/bootstrap/router_bootstrap.go | 8 +- .../internal/bootstrap/services_bootstrap.go | 68 +- .../internal/cmds/one_time_access_token.go | 74 +- .../internal/controller/user_controller.go | 199 +- backend/internal/job/db_cleanup_job.go | 29 - .../internal/model/one_time_access_token.go | 13 - backend/internal/onetimeaccess/actor.go | 167 + .../dto.go} | 12 +- backend/internal/onetimeaccess/handler.go | 198 + backend/internal/onetimeaccess/module.go | 86 + backend/internal/onetimeaccess/service.go | 283 + .../internal/onetimeaccess/service_test.go | 172 + backend/internal/service/e2etest_service.go | 196 +- .../service/one_time_access_email_sender.go | 29 + .../service/one_time_access_service.go | 261 - .../service/one_time_access_service_test.go | 53 - backend/internal/usersignup/actor.go | 217 + backend/internal/usersignup/actor_test.go | 173 + backend/internal/usersignup/cleanup.go | 19 - backend/internal/usersignup/handler.go | 20 +- backend/internal/usersignup/migration.go | 105 + backend/internal/usersignup/migration_test.go | 207 + backend/internal/usersignup/models.go | 16 +- backend/internal/usersignup/module.go | 29 +- backend/internal/usersignup/service.go | 386 +- backend/internal/usersignup/service_test.go | 127 + .../20260723000000_actor_tokens.down.sql | 56 + .../20260723000000_actor_tokens.up.sql | 28 + .../20260723000000_actor_tokens.down.sql | 62 + .../sqlite/20260723000000_actor_tokens.up.sql | 35 + frontend/package-lock.json | 5429 +++++++++++++++++ tests/resources/export/database.json | 68 +- 37 files changed, 7989 insertions(+), 905 deletions(-) delete mode 100644 backend/internal/model/one_time_access_token.go create mode 100644 backend/internal/onetimeaccess/actor.go rename backend/internal/{dto/one_time_access_dto.go => onetimeaccess/dto.go} (51%) create mode 100644 backend/internal/onetimeaccess/handler.go create mode 100644 backend/internal/onetimeaccess/module.go create mode 100644 backend/internal/onetimeaccess/service.go create mode 100644 backend/internal/onetimeaccess/service_test.go create mode 100644 backend/internal/service/one_time_access_email_sender.go delete mode 100644 backend/internal/service/one_time_access_service.go delete mode 100644 backend/internal/service/one_time_access_service_test.go create mode 100644 backend/internal/usersignup/actor.go create mode 100644 backend/internal/usersignup/actor_test.go delete mode 100644 backend/internal/usersignup/cleanup.go create mode 100644 backend/internal/usersignup/migration.go create mode 100644 backend/internal/usersignup/migration_test.go create mode 100644 backend/internal/usersignup/service_test.go create mode 100644 backend/resources/migrations/postgres/20260723000000_actor_tokens.down.sql create mode 100644 backend/resources/migrations/postgres/20260723000000_actor_tokens.up.sql create mode 100644 backend/resources/migrations/sqlite/20260723000000_actor_tokens.down.sql create mode 100644 backend/resources/migrations/sqlite/20260723000000_actor_tokens.up.sql create mode 100644 frontend/package-lock.json diff --git a/backend/go.mod b/backend/go.mod index e18eaf34..8aea1871 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -24,8 +24,8 @@ require ( github.com/go-webauthn/webauthn v0.17.4 github.com/golang-migrate/migrate/v4 v4.19.1 github.com/google/uuid v1.6.0 - github.com/italypaleale/francis v0.1.0-beta.11 - github.com/italypaleale/go-kit v0.0.0-20260708054611-e276b65dd3be + github.com/italypaleale/francis v0.1.0-beta.15 + github.com/italypaleale/go-kit v0.0.0-20260725195228-78f113702f86 github.com/italypaleale/go-sql-utils v0.2.4 github.com/jackc/pgx/v5 v5.10.0 github.com/jinzhu/copier v0.4.0 @@ -227,13 +227,13 @@ require ( go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.27.0 // indirect - golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/mod v0.37.0 // indirect + golang.org/x/exp v0.0.0-20260718201538-764159d718ef // indirect + golang.org/x/mod v0.38.0 // indirect golang.org/x/net v0.57.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.47.0 // indirect + golang.org/x/tools v0.48.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect google.golang.org/grpc v1.82.0 // indirect @@ -242,7 +242,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect gorm.io/driver/clickhouse v0.7.0 // indirect gorm.io/driver/mysql v1.5.7 // indirect - k8s.io/utils v0.0.0-20260617174310-a95e086a2553 // indirect + k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect modernc.org/libc v1.74.1 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index fdd57ba0..bc7c44c8 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -263,10 +263,10 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/italypaleale/francis v0.1.0-beta.11 h1:FurXV2vMkRzJRFldQ6Z/bhLSJz8YXHm84uiASGeyWWU= -github.com/italypaleale/francis v0.1.0-beta.11/go.mod h1:vqKhwdLs5Sx+n6JCNknEKAODtEU51E9/LC1q9JAG3zk= -github.com/italypaleale/go-kit v0.0.0-20260708054611-e276b65dd3be h1:jgu+Mdsda++LqPxz8cj8vvgiFINQ8PhFB4Q1VZpyPjs= -github.com/italypaleale/go-kit v0.0.0-20260708054611-e276b65dd3be/go.mod h1:pl0r3F+thZIyDsyDo8aOUsAIVcsRuAeP1bB4GuAHLoY= +github.com/italypaleale/francis v0.1.0-beta.15 h1:yVFJCcD1pP91rIesAb06Gp1K1tPj0ruh6hha5YEhtRw= +github.com/italypaleale/francis v0.1.0-beta.15/go.mod h1:KKwS+57OBD/MoHBVfbbMelA2vUx65fPiG9fhdWWapFc= +github.com/italypaleale/go-kit v0.0.0-20260725195228-78f113702f86 h1:719T7W8hLVjelch856Sern60QAPMn0fIE87i91YVcfw= +github.com/italypaleale/go-kit v0.0.0-20260725195228-78f113702f86/go.mod h1:0Sy3bN3qnSy2kgcJ05A2CsP6os5wmLC9lPraFr+0jGk= github.com/italypaleale/go-sql-utils v0.2.4 h1:6CN8y3qEdNzvYlS/JK6N65E8cL9F8a6OBCJjzaQIv3c= github.com/italypaleale/go-sql-utils v0.2.4/go.mod h1:BJStxMfB6fzYVcOe0oZQCjGIPZQu76UBmg1Wuy6Z/7I= github.com/jackc/pgerrcode v0.0.0-20250907135507-afb5586c32a6 h1:D/V0gu4zQ3cL2WKeVNVM4r2gLxGGf6McLwgXzRTo2RQ= @@ -620,8 +620,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= -golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= +golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A= +golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= @@ -633,8 +633,8 @@ golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= -golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -724,8 +724,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= -golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -767,8 +767,8 @@ gorm.io/gorm v1.31.2 h1:3o8FXNo9v9S858gil+3LlZA1LkCOzgb4g5BL64FgaCo= gorm.io/gorm v1.31.2/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/plugin/opentelemetry v0.1.16 h1:Kypj2YYAliJqkIczDZDde6P6sFMhKSlG5IpngMFQGpc= gorm.io/plugin/opentelemetry v0.1.16/go.mod h1:P3RmTeZXT+9n0F1ccUqR5uuTvEXDxF8k2UpO7mTIB2Y= -k8s.io/utils v0.0.0-20260617174310-a95e086a2553 h1:hmGqDecjc8d7HVzWzRFl0QD9bYuYKbBEG7t8xwnVxfI= -k8s.io/utils v0.0.0-20260617174310-a95e086a2553/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE= +k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM= modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc= modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= diff --git a/backend/internal/bootstrap/actors_bootstrap.go b/backend/internal/bootstrap/actors_bootstrap.go index d497364b..abdd833e 100644 --- a/backend/internal/bootstrap/actors_bootstrap.go +++ b/backend/internal/bootstrap/actors_bootstrap.go @@ -98,6 +98,34 @@ func (o *NewActorsOpts) getPSK() ([]byte, error) { return crypto.DeriveKey(o.EnvConfig.EncryptionKey, "pocketid/actors-psk/"+o.InstanceID) } +// NewActorStateStore creates a minimal actor host that can read and write actor state directly, without joining the cluster or binding a network port. +// It's meant for short-lived contexts such as CLI commands that need to persist actor state (for example, one-time access tokens) without running the full actor host. +// The returned host must NOT be Run(): only direct state operations (Get/Set/Delete on state) are supported, and they require the actor state tables to already exist, which is the case whenever the server has run at least once against this database. +func NewActorStateStore(db *gorm.DB, pg *pgxpool.Pool) (*local.Host, error) { + opts := &NewActorsOpts{DB: db, Postgres: pg} + if pg == nil { + sqlDB, err := db.DB() + if err != nil { + return nil, fmt.Errorf("failed to get *sql.DB connection from Gorm: %w", err) + } + opts.SQLite = sqlDB + } + + providerOpt, err := opts.getProvider() + if err != nil { + return nil, err + } + + return local.NewHost( + // The address is required by the host but never bound, since the host is not Run + local.WithAddress("127.0.0.1:1"), + local.WithLogger(slog.Default().With("scope", "actor-state-store")), + // The health-check deadline only needs to exceed the provider's query timeout to pass validation + local.WithHostHealthCheckDeadline(90*time.Second), + providerOpt, + ) +} + func (o *NewActorsOpts) getProvider() (local.HostOption, error) { switch { case o.Postgres != nil && o.SQLite != nil: diff --git a/backend/internal/bootstrap/bootstrap.go b/backend/internal/bootstrap/bootstrap.go index 8fd171e8..10b7e383 100644 --- a/backend/internal/bootstrap/bootstrap.go +++ b/backend/internal/bootstrap/bootstrap.go @@ -106,6 +106,9 @@ func Bootstrap(ctx context.Context) error { } services = append(services, svc.appLockService.RunRenewal) + // Migrate the pre-actor signup tokens into their actors, once the actor host is ready + services = append(services, actorsReady.Await(svc.userSignUpModule.RunSignupTokenMigration)) + // Acquire the lock from the app lock service waitUntil, err := svc.appLockService.Acquire(ctx, false) if errors.Is(err, service.ErrLockUnavailable) { diff --git a/backend/internal/bootstrap/e2etest_router_bootstrap.go b/backend/internal/bootstrap/e2etest_router_bootstrap.go index b87d4644..db1eb663 100644 --- a/backend/internal/bootstrap/e2etest_router_bootstrap.go +++ b/backend/internal/bootstrap/e2etest_router_bootstrap.go @@ -17,7 +17,7 @@ import ( func init() { registerTestControllers = []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services){ func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) { - testService, err := service.NewTestService(db, svc.appConfigService, svc.jwtService, svc.ldapService, svc.appLockService, svc.fileStorage) + testService, err := service.NewTestService(db, svc.actors, svc.appConfigService, svc.jwtService, svc.ldapService, svc.appLockService, svc.fileStorage) if err != nil { slog.Error("Failed to initialize test service", slog.Any("error", err)) os.Exit(1) diff --git a/backend/internal/bootstrap/router_bootstrap.go b/backend/internal/bootstrap/router_bootstrap.go index 11a71b96..fe68f019 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -158,7 +158,7 @@ func registerRoutes(r *gin.Engine, db *gorm.DB, svc *services, rateLimitServices rateLimitMiddleware.Add(middleware.RateLimitWebauthnReauthenticate), ) controller.NewOidcController(apiGroup, authMiddleware, fileSizeLimitMiddleware, svc.oidcService) - controller.NewUserController(apiGroup, authMiddleware, rateLimitMiddleware, svc.appConfigService, svc.userService, svc.oneTimeAccessService, svc.webauthnModule) + controller.NewUserController(apiGroup, authMiddleware, rateLimitMiddleware, svc.appConfigService, svc.userService, svc.webauthnModule) controller.NewAppConfigController(apiGroup, authMiddleware, svc.appConfigService, svc.emailService, svc.ldapService) controller.NewAppImagesController(apiGroup, authMiddleware, svc.appImagesService) controller.NewAuditLogController(apiGroup, svc.auditLogService, authMiddleware) @@ -171,6 +171,12 @@ func registerRoutes(r *gin.Engine, db *gorm.DB, svc *services, rateLimitServices authMiddleware.Add(), rateLimitMiddleware.Add(middleware.RateLimitSignup), ) + svc.oneTimeAccessModule.RegisterRoutes(apiGroup, + authMiddleware.Add(), + authMiddleware.WithAdminNotRequired().Add(), + rateLimitMiddleware.Add(middleware.RateLimitOneTimeAccessToken), + rateLimitMiddleware.Add(middleware.RateLimitOneTimeAccessEmail), + ) optionalBrowserAuth := authMiddleware.WithAdminNotRequired().WithSuccessOptional().WithApiKeyAuthDisabled().Add() browserAuth := authMiddleware.WithAdminNotRequired().WithApiKeyAuthDisabled().Add() diff --git a/backend/internal/bootstrap/services_bootstrap.go b/backend/internal/bootstrap/services_bootstrap.go index c3600f4f..8e2b363b 100644 --- a/backend/internal/bootstrap/services_bootstrap.go +++ b/backend/internal/bootstrap/services_bootstrap.go @@ -14,6 +14,7 @@ import ( "github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/job" "github.com/pocket-id/pocket-id/backend/internal/oidc" + "github.com/pocket-id/pocket-id/backend/internal/onetimeaccess" "github.com/pocket-id/pocket-id/backend/internal/service" "github.com/pocket-id/pocket-id/backend/internal/storage" "github.com/pocket-id/pocket-id/backend/internal/usersignup" @@ -21,28 +22,29 @@ import ( ) type services struct { - appConfigService *appconfig.AppConfigService - appImagesService *service.AppImagesService - emailService *service.EmailService - geoLiteService *service.GeoLiteService - auditLogService *service.AuditLogService - jwtService *service.JwtService - scimService *service.ScimService - userService *service.UserService - customClaimService *service.CustomClaimService - oidcService *service.OidcService - userGroupService *service.UserGroupService - ldapService *service.LdapService - versionService *service.VersionService - fileStorage storage.FileStorage - appLockService *service.AppLockService - oneTimeAccessService *service.OneTimeAccessService + appConfigService *appconfig.AppConfigService + appImagesService *service.AppImagesService + emailService *service.EmailService + geoLiteService *service.GeoLiteService + auditLogService *service.AuditLogService + jwtService *service.JwtService + scimService *service.ScimService + userService *service.UserService + customClaimService *service.CustomClaimService + oidcService *service.OidcService + userGroupService *service.UserGroupService + ldapService *service.LdapService + versionService *service.VersionService + fileStorage storage.FileStorage + appLockService *service.AppLockService - apiKeyModule *apikey.Module - oidcModule *oidc.Module - webauthnModule *webauthn.Module - userSignUpModule *usersignup.Module - apiModule *api.Module + apiKeyModule *apikey.Module + oidcModule *oidc.Module + webauthnModule *webauthn.Module + userSignUpModule *usersignup.Module + oneTimeAccessModule *onetimeaccess.Module + apiModule *api.Module + actors *local.Host } // Initializes all services @@ -56,7 +58,9 @@ func initServices( fileStorage storage.FileStorage, scheduler *job.Scheduler, ) (svc *services, err error) { - svc = &services{} + svc = &services{ + actors: actors, + } // Init the app config service svc.appConfigService, err = appconfig.NewService(ctx, actors, db) @@ -132,14 +136,30 @@ func initServices( return nil, fmt.Errorf("failed to create API key module: %w", err) } - svc.userSignUpModule = usersignup.New(usersignup.Dependencies{ + svc.userSignUpModule, err = usersignup.New(usersignup.Dependencies{ DB: db, + Actors: actors, Signer: svc.jwtService, AuditLog: svc.auditLogService, UserCreator: svc.userService, AppConfig: svc.appConfigService, }) - svc.oneTimeAccessService = service.NewOneTimeAccessService(db, svc.userService, svc.jwtService, svc.auditLogService, svc.emailService) + if err != nil { + return nil, fmt.Errorf("failed to create user signup module: %w", err) + } + + svc.oneTimeAccessModule, err = onetimeaccess.New(onetimeaccess.Dependencies{ + DB: db, + Actors: actors, + Signer: svc.jwtService, + AuditLog: svc.auditLogService, + UserProvider: svc.userService, + EmailSender: service.NewOneTimeAccessEmailSender(svc.emailService), + AppConfig: svc.appConfigService, + }) + if err != nil { + return nil, fmt.Errorf("failed to create one-time access module: %w", err) + } svc.versionService = service.NewVersionService(httpClient) diff --git a/backend/internal/cmds/one_time_access_token.go b/backend/internal/cmds/one_time_access_token.go index dd3f392c..dd64024d 100644 --- a/backend/internal/cmds/one_time_access_token.go +++ b/backend/internal/cmds/one_time_access_token.go @@ -12,7 +12,7 @@ import ( "github.com/pocket-id/pocket-id/backend/internal/bootstrap" "github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/model" - "github.com/pocket-id/pocket-id/backend/internal/service" + "github.com/pocket-id/pocket-id/backend/internal/onetimeaccess" ) var oneTimeAccessTokenCmd = &cobra.Command{ @@ -24,57 +24,47 @@ var oneTimeAccessTokenCmd = &cobra.Command{ userArg := args[0] // Connect to the database - db, _, err := bootstrap.NewDatabase(cmd.Context()) + db, pg, err := bootstrap.NewDatabase(cmd.Context()) if err != nil { return err } - // Create the access token - var oneTimeAccessToken *model.OneTimeAccessToken - err = db.Transaction(func(tx *gorm.DB) error { - // Load the user to retrieve the user ID - var user model.User - queryCtx, queryCancel := context.WithTimeout(cmd.Context(), 10*time.Second) - defer queryCancel() - txErr := tx. - WithContext(queryCtx). - Where("username = ? OR email = ?", userArg, userArg). - First(&user). - Error - switch { - case errors.Is(txErr, gorm.ErrRecordNotFound): - return errors.New("user not found") - case txErr != nil: - return fmt.Errorf("failed to query for user: %w", txErr) - case user.ID == "": - return errors.New("invalid user loaded: ID is empty") - } + // Load the user to retrieve the user ID + var user model.User + queryCtx, queryCancel := context.WithTimeout(cmd.Context(), 10*time.Second) + defer queryCancel() + err = db. + WithContext(queryCtx). + Where("username = ? OR email = ?", userArg, userArg). + First(&user). + Error + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + return errors.New("user not found") + case err != nil: + return fmt.Errorf("failed to query for user: %w", err) + case user.ID == "": + return errors.New("invalid user loaded: ID is empty") + } - // Create a new access token that expires in 1 hour - oneTimeAccessToken, txErr = service.NewOneTimeAccessToken(user.ID, time.Hour, false) - if txErr != nil { - return fmt.Errorf("failed to generate access token: %w", txErr) - } - - queryCtx, queryCancel = context.WithTimeout(cmd.Context(), 10*time.Second) - defer queryCancel() - txErr = tx. - WithContext(queryCtx). - Create(oneTimeAccessToken). - Error - if txErr != nil { - return fmt.Errorf("failed to save access token: %w", txErr) - } - - return nil - }) + // One-time access tokens are stored in the actor state store + // The CLI doesn't run the full actor host, so it uses a minimal state store to persist the token directly + actorStore, err := bootstrap.NewActorStateStore(db, pg) if err != nil { - return err + return fmt.Errorf("failed to initialize the actor state store: %w", err) + } + + // Create a new access token that expires in 1 hour + tokenCtx, tokenCancel := context.WithTimeout(cmd.Context(), 10*time.Second) + defer tokenCancel() + token, _, err := onetimeaccess.StoreToken(tokenCtx, actorStore, user.ID, time.Hour, false) + if err != nil { + return fmt.Errorf("failed to create access token: %w", err) } // Print the result fmt.Printf(`A one-time access token valid for 1 hour has been created for "%s".`+"\n", userArg) - fmt.Printf("Use the following URL to sign in once: %s/lc/%s\n", common.EnvConfig.AppURL, oneTimeAccessToken.Token) + fmt.Printf("Use the following URL to sign in once: %s/lc/%s\n", common.EnvConfig.AppURL, token) return nil }, diff --git a/backend/internal/controller/user_controller.go b/backend/internal/controller/user_controller.go index 57ea9095..5aab14d7 100644 --- a/backend/internal/controller/user_controller.go +++ b/backend/internal/controller/user_controller.go @@ -6,8 +6,6 @@ import ( "time" "github.com/pocket-id/pocket-id/backend/internal/appconfig" - "github.com/pocket-id/pocket-id/backend/internal/common" - "github.com/pocket-id/pocket-id/backend/internal/utils/cookie" "github.com/gin-gonic/gin" "github.com/pocket-id/pocket-id/backend/internal/dto" @@ -17,18 +15,15 @@ import ( "github.com/pocket-id/pocket-id/backend/internal/webauthn" ) -const defaultOneTimeAccessTokenDuration = 15 * time.Minute - // NewUserController creates a new controller for user management endpoints // @Summary User management controller // @Description Initializes all user-related API endpoints // @Tags Users -func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, appConfigService *appconfig.AppConfigService, userService *service.UserService, oneTimeAccessService *service.OneTimeAccessService, webAuthnService *webauthn.Module) { +func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMiddleware, rateLimitMiddleware *middleware.RateLimitMiddleware, appConfigService *appconfig.AppConfigService, userService *service.UserService, webAuthnService *webauthn.Module) { uc := UserController{ - appConfigService: appConfigService, - userService: userService, - oneTimeAccessService: oneTimeAccessService, - webAuthnService: webAuthnService, + appConfigService: appConfigService, + userService: userService, + webAuthnService: webAuthnService, } group.GET("/users", authMiddleware.Add(), uc.listUsersHandler) @@ -49,12 +44,6 @@ func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi group.PUT("/users/:id/profile-picture", authMiddleware.Add(), uc.updateUserProfilePictureHandler) group.PUT("/users/me/profile-picture", authMiddleware.WithAdminNotRequired().Add(), uc.updateCurrentUserProfilePictureHandler) - group.POST("/users/me/one-time-access-token", authMiddleware.WithAdminNotRequired().Add(), uc.createOwnOneTimeAccessTokenHandler) - group.POST("/users/:id/one-time-access-token", authMiddleware.Add(), uc.createAdminOneTimeAccessTokenHandler) - group.POST("/users/:id/one-time-access-email", authMiddleware.Add(), uc.RequestOneTimeAccessEmailAsAdminHandler) - group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(middleware.RateLimitOneTimeAccessToken), uc.exchangeOneTimeAccessTokenHandler) - group.POST("/one-time-access-email", rateLimitMiddleware.Add(middleware.RateLimitOneTimeAccessEmail), uc.RequestOneTimeAccessEmailAsUnauthenticatedUserHandler) - group.DELETE("/users/:id/profile-picture", authMiddleware.Add(), uc.resetUserProfilePictureHandler) group.DELETE("/users/me/profile-picture", authMiddleware.WithAdminNotRequired().Add(), uc.resetCurrentUserProfilePictureHandler) @@ -63,10 +52,9 @@ func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi } type UserController struct { - appConfigService *appconfig.AppConfigService - userService *service.UserService - oneTimeAccessService *service.OneTimeAccessService - webAuthnService *webauthn.Module + appConfigService *appconfig.AppConfigService + userService *service.UserService + webAuthnService *webauthn.Module } // getUserGroupsHandler godoc @@ -394,179 +382,6 @@ func (uc *UserController) updateCurrentUserProfilePictureHandler(c *gin.Context) c.Status(http.StatusNoContent) } -func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context, own bool) { - var input dto.OneTimeAccessTokenCreateDto - err := c.ShouldBindJSON(&input) - if err != nil { - _ = c.Error(err) - return - } - - var ( - userID string - ttl time.Duration - ) - if own { - // Get user ID from context and force the default TTL - userID = c.GetString("userID") - ttl = defaultOneTimeAccessTokenDuration - } else { - // Get user ID from URL parameter, and optional TTL from body - userID = c.Param("id") - ttl = input.TTL.Duration - if ttl <= 0 { - ttl = defaultOneTimeAccessTokenDuration - } - } - if userID == "" { - _ = c.Error(&common.UserIdNotProvidedError{}) - return - } - - token, err := uc.oneTimeAccessService.CreateOneTimeAccessToken(c.Request.Context(), userID, ttl) - if err != nil { - _ = c.Error(err) - return - } - - c.JSON(http.StatusCreated, gin.H{"token": token}) -} - -// createOwnOneTimeAccessTokenHandler godoc -// @Summary Create one-time access token for current user -// @Description Generate a one-time access token for the currently authenticated user -// @Tags Users -// @Param id path string true "User ID" -// @Param body body dto.OneTimeAccessTokenCreateDto true "Token options" -// @Success 201 {object} object "{ \"token\": \"string\" }" -// @Router /api/users/{id}/one-time-access-token [post] -func (uc *UserController) createOwnOneTimeAccessTokenHandler(c *gin.Context) { - uc.createOneTimeAccessTokenHandler(c, true) -} - -// createAdminOneTimeAccessTokenHandler godoc -// @Summary Create one-time access token for user (admin) -// @Description Generate a one-time access token for a specific user (admin only) -// @Tags Users -// @Param id path string true "User ID" -// @Param body body dto.OneTimeAccessTokenCreateDto true "Token options" -// @Success 201 {object} object "{ \"token\": \"string\" }" -// @Router /api/users/{id}/one-time-access-token [post] -func (uc *UserController) createAdminOneTimeAccessTokenHandler(c *gin.Context) { - uc.createOneTimeAccessTokenHandler(c, false) -} - -// RequestOneTimeAccessEmailAsUnauthenticatedUserHandler godoc -// @Summary Request one-time access email -// @Description Request a one-time access email for unauthenticated users -// @Tags Users -// @Accept json -// @Produce json -// @Param body body dto.OneTimeAccessEmailAsUnauthenticatedUserDto true "Email request information" -// @Success 204 "No Content" -// @Router /api/one-time-access-email [post] -func (uc *UserController) RequestOneTimeAccessEmailAsUnauthenticatedUserHandler(c *gin.Context) { - dbConfig, err := uc.appConfigService.GetConfig(c.Request.Context()) - if err != nil { - _ = c.Error(fmt.Errorf("error loading app configuration: %w", err)) - return - } - - var input dto.OneTimeAccessEmailAsUnauthenticatedUserDto - if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil { - _ = c.Error(err) - return - } - - deviceToken, err := uc.oneTimeAccessService.RequestOneTimeAccessEmailAsUnauthenticatedUser(c.Request.Context(), dbConfig, input.Email, input.RedirectPath) - if err != nil { - _ = c.Error(err) - return - } - - cookie.AddDeviceTokenCookie(c, deviceToken) - c.Status(http.StatusNoContent) -} - -// RequestOneTimeAccessEmailAsAdminHandler godoc -// @Summary Request one-time access email (admin) -// @Description Request a one-time access email for a specific user (admin only) -// @Tags Users -// @Accept json -// @Produce json -// @Param id path string true "User ID" -// @Param body body dto.OneTimeAccessEmailAsAdminDto true "Email request options" -// @Success 204 "No Content" -// @Router /api/users/{id}/one-time-access-email [post] -func (uc *UserController) RequestOneTimeAccessEmailAsAdminHandler(c *gin.Context) { - dbConfig, err := uc.appConfigService.GetConfig(c.Request.Context()) - if err != nil { - _ = c.Error(fmt.Errorf("error loading app configuration: %w", err)) - return - } - - var input dto.OneTimeAccessEmailAsAdminDto - if err := c.ShouldBindJSON(&input); err != nil { - _ = c.Error(err) - return - } - - userID := c.Param("id") - - ttl := input.TTL.Duration - if ttl <= 0 { - ttl = defaultOneTimeAccessTokenDuration - } - err = uc.oneTimeAccessService.RequestOneTimeAccessEmailAsAdmin(c.Request.Context(), dbConfig, userID, ttl) - if err != nil { - _ = c.Error(err) - return - } - - c.Status(http.StatusNoContent) -} - -// exchangeOneTimeAccessTokenHandler godoc -// @Summary Exchange one-time access token -// @Description Exchange a one-time access token for a session token -// @Tags Users -// @Param token path string true "One-time access token" -// @Success 200 {object} dto.UserDto -// @Router /api/one-time-access-token/{token} [post] -func (uc *UserController) exchangeOneTimeAccessTokenHandler(c *gin.Context) { - cfg, err := uc.appConfigService.GetConfig(c.Request.Context()) - if err != nil { - _ = c.Error(fmt.Errorf("error loading app configuration: %w", err)) - return - } - - loginCode := c.Param("token") - // reject invalid length login codes - if len(loginCode) != 6 && len(loginCode) != 16 { - _ = c.Error(&common.TokenInvalidOrExpiredError{}) - return - } - - deviceToken, _ := c.Cookie(cookie.DeviceTokenCookieName) - user, token, err := uc.oneTimeAccessService.ExchangeOneTimeAccessToken(c.Request.Context(), cfg, loginCode, deviceToken, c.ClientIP(), c.Request.UserAgent()) - if err != nil { - _ = c.Error(err) - return - } - - var userDto dto.UserDto - err = dto.MapStruct(user, &userDto) - if err != nil { - _ = c.Error(err) - return - } - - maxAge := int(cfg.SessionDuration.AsDurationMinutes().Seconds()) - cookie.AddAccessTokenCookie(c, maxAge, token) - - c.JSON(http.StatusOK, userDto) -} - // updateUserGroups godoc // @Summary Update user groups // @Description Update the groups a specific user belongs to diff --git a/backend/internal/job/db_cleanup_job.go b/backend/internal/job/db_cleanup_job.go index 24bbc4d3..7c682891 100644 --- a/backend/internal/job/db_cleanup_job.go +++ b/backend/internal/job/db_cleanup_job.go @@ -15,7 +15,6 @@ import ( datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" "github.com/pocket-id/pocket-id/backend/internal/oidc" "github.com/pocket-id/pocket-id/backend/internal/service" - "github.com/pocket-id/pocket-id/backend/internal/usersignup" "github.com/pocket-id/pocket-id/backend/internal/webauthn" ) @@ -34,8 +33,6 @@ func (s *Scheduler) RegisterDbCleanupJobs(ctx context.Context, db *gorm.DB) erro // Use exponential backoff for each DB cleanup job so transient query failures are retried automatically rather than causing an immediate job failure return errors.Join( s.RegisterJob(ctx, "ClearWebauthnSessions", jobDefWithJitter(24*time.Hour), jobs.clearWebauthnSessions, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}), - s.RegisterJob(ctx, "ClearOneTimeAccessTokens", jobDefWithJitter(24*time.Hour), jobs.clearOneTimeAccessTokens, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}), - s.RegisterJob(ctx, "ClearSignupTokens", jobDefWithJitter(24*time.Hour), jobs.clearSignupTokens, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}), s.RegisterJob(ctx, "ClearEmailVerificationTokens", jobDefWithJitter(24*time.Hour), jobs.clearEmailVerificationTokens, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}), s.RegisterJob(ctx, "ClearOAuth2Sessions", jobDefWithJitter(24*time.Hour), jobs.clearOAuth2Sessions, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}), s.RegisterJob(ctx, "ClearOAuth2JTIs", jobDefWithJitter(24*time.Hour), jobs.clearOAuth2JTIs, service.RegisterJobOpts{RunImmediately: true, BackOff: newBackOff()}), @@ -61,32 +58,6 @@ func (j *DbCleanupJobs) clearWebauthnSessions(ctx context.Context) error { return nil } -// ClearOneTimeAccessTokens deletes one-time access tokens that have expired -func (j *DbCleanupJobs) clearOneTimeAccessTokens(ctx context.Context) error { - st := j.db. - WithContext(ctx). - Delete(&model.OneTimeAccessToken{}, "expires_at < ?", datatype.DateTime(time.Now())) - if st.Error != nil { - return fmt.Errorf("failed to clean expired one-time access tokens: %w", st.Error) - } - - slog.InfoContext(ctx, "Cleaned expired one-time access tokens", slog.Int64("count", st.RowsAffected)) - - return nil -} - -// clearSignupTokens deletes signup tokens that have expired -func (j *DbCleanupJobs) clearSignupTokens(ctx context.Context) error { - count, err := usersignup.CleanupExpiredSignupTokens(ctx, j.db) - if err != nil { - return fmt.Errorf("failed to clean expired signup tokens: %w", err) - } - - slog.InfoContext(ctx, "Cleaned expired signup tokens", slog.Int64("count", count)) - - return nil -} - // clearOAuth2Sessions deletes expired and invalidated OAuth2 sessions. func (j *DbCleanupJobs) clearOAuth2Sessions(ctx context.Context) error { count, err := oidc.CleanupExpiredOAuth2Sessions(ctx, j.db) diff --git a/backend/internal/model/one_time_access_token.go b/backend/internal/model/one_time_access_token.go deleted file mode 100644 index 3a3c095d..00000000 --- a/backend/internal/model/one_time_access_token.go +++ /dev/null @@ -1,13 +0,0 @@ -package model - -import datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" - -type OneTimeAccessToken struct { - Base - Token string - DeviceToken *string - ExpiresAt datatype.DateTime - - UserID string - User User -} diff --git a/backend/internal/onetimeaccess/actor.go b/backend/internal/onetimeaccess/actor.go new file mode 100644 index 00000000..61f2d91c --- /dev/null +++ b/backend/internal/onetimeaccess/actor.go @@ -0,0 +1,167 @@ +package onetimeaccess + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/italypaleale/francis/actor" + + "github.com/pocket-id/pocket-id/backend/internal/common" +) + +// One-time access tokens are stored entirely in the actor state store. +// Each token is its own actor, whose actor ID is the token value itself. +// The state is persisted with a TTL equal to the token's lifetime, so it's purged automatically when the token expires (there's no separate cleanup job). + +// TokenActorType is the actor type for the one-time access token actor +const TokenActorType = "OneTimeAccessToken" + +// Methods exposed by the one-time access token actor +// Because we cannot invoke an actor while a DB transaction is open (that would deadlock on SQLite), consuming a token is done by invoking the actor first (which atomically validates and deletes the token), and only afterwards performing the remaining work. +// On failure, the caller compensates by restoring the token via the "restore" method as best-effort. +const ( + // TokenMethodRestore stores a token's state, and is also how a consumed token is put back + TokenMethodRestore = "restore" + + tokenMethodConsume = "consume" +) + +// tokenConsumeStatus is the outcome of a "consume" invocation. +type tokenConsumeStatus string + +const ( + // tokenConsumeOK indicates the token was valid and has been consumed + tokenConsumeOK tokenConsumeStatus = "ok" + // tokenConsumeNotFound indicates the token doesn't exist (or has expired) + tokenConsumeNotFound tokenConsumeStatus = "not_found" + // tokenConsumeDeviceMismatch indicates the provided device token doesn't match + tokenConsumeDeviceMismatch tokenConsumeStatus = "device_mismatch" +) + +// TokenState is the persisted state of a one-time access token actor. +// The token value itself is the actor's ID, so it isn't repeated here. +type TokenState struct { + UserID string + DeviceToken *string + ExpiresAt time.Time +} + +// tokenConsumeRequest is the payload for the "consume" method +type tokenConsumeRequest struct { + DeviceToken string +} + +// tokenConsumeResponse is the response of the "consume" method +type tokenConsumeResponse struct { + Status tokenConsumeStatus + // State is included only when Status is "ok", so the caller can restore it if a later step fails + State TokenState +} + +// tokenActor is the actor that manages a single one-time access token +type tokenActor struct { + log *slog.Logger + client actor.Client[TokenState] +} + +// NewTokenActor allocates a new one-time access token actor +// It satisfies actor.Factory +func NewTokenActor(actorID string, service *actor.Service) actor.Actor { + return &tokenActor{ + log: slog.With( + slog.String("scope", "actor"), + slog.String("actorType", TokenActorType), + ), + client: actor.NewActorClient[TokenState](TokenActorType, actorID, service), + } +} + +// Invoke implements actor.ActorInvoke +func (a *tokenActor) Invoke(parentCtx context.Context, method string, data actor.Envelope) (any, error) { + switch method { + case tokenMethodConsume: + return a.consume(parentCtx, data) + case TokenMethodRestore: + return nil, a.restore(parentCtx, data) + default: + return nil, common.ErrUnsupportedActorMethod{Method: method} + } +} + +// consume atomically validates the token and, if valid, deletes it. +func (a *tokenActor) consume(parentCtx context.Context, data actor.Envelope) (tokenConsumeResponse, error) { + var req tokenConsumeRequest + if data != nil { + err := data.Decode(&req) + if err != nil { + return tokenConsumeResponse{}, fmt.Errorf("request body is not valid for method '%s': %w", tokenMethodConsume, err) + } + } + + ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second) + defer cancel() + state, err := a.client.GetState(ctx) + if err != nil { + return tokenConsumeResponse{}, fmt.Errorf("error retrieving actor state: %w", err) + } + + // An empty UserID means there's no state: the token doesn't exist (or its state already expired and was purged) + if state.UserID == "" || state.ExpiresAt.Before(time.Now()) { + return tokenConsumeResponse{ + Status: tokenConsumeNotFound, + }, nil + } + + // If the token requires a device token, it must match + // A mismatch leaves the token untouched, mirroring the pre-actor behavior + if state.DeviceToken != nil && req.DeviceToken != *state.DeviceToken { + return tokenConsumeResponse{ + Status: tokenConsumeDeviceMismatch, + }, nil + } + + // The token is valid: delete the state (one-time use) + ctx, cancel = context.WithTimeout(parentCtx, 10*time.Second) + defer cancel() + err = a.client.DeleteState(ctx) + if err != nil { + return tokenConsumeResponse{}, fmt.Errorf("error deleting actor state: %w", err) + } + + return tokenConsumeResponse{ + Status: tokenConsumeOK, + State: state, + }, nil +} + +// restore re-creates the token state, used to compensate when a step after consuming the token fails. +func (a *tokenActor) restore(parentCtx context.Context, data actor.Envelope) error { + if data == nil { + return fmt.Errorf("request body is empty for method '%s'", TokenMethodRestore) + } + + var state TokenState + err := data.Decode(&state) + if err != nil { + return fmt.Errorf("request body is not valid for method '%s': %w", TokenMethodRestore, err) + } + + // If the token has meanwhile expired, there's nothing to restore + ttl := time.Until(state.ExpiresAt) + if ttl <= 0 { + return nil + } + + ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second) + defer cancel() + err = a.client.SetState(ctx, state, &actor.SetStateOpts{ + TTL: ttl, + }) + if err != nil { + return fmt.Errorf("error saving actor state: %w", err) + } + + return nil +} diff --git a/backend/internal/dto/one_time_access_dto.go b/backend/internal/onetimeaccess/dto.go similarity index 51% rename from backend/internal/dto/one_time_access_dto.go rename to backend/internal/onetimeaccess/dto.go index a99dc5ac..d5eabbd3 100644 --- a/backend/internal/dto/one_time_access_dto.go +++ b/backend/internal/onetimeaccess/dto.go @@ -1,16 +1,18 @@ -package dto +package onetimeaccess -import "github.com/pocket-id/pocket-id/backend/internal/utils" +import ( + "github.com/pocket-id/pocket-id/backend/internal/utils" +) -type OneTimeAccessTokenCreateDto struct { +type tokenCreateDto struct { TTL utils.JSONDuration `json:"ttl" binding:"ttl"` } -type OneTimeAccessEmailAsUnauthenticatedUserDto struct { +type emailAsUnauthenticatedUserDto struct { Email string `json:"email" binding:"required,email" unorm:"nfc"` RedirectPath string `json:"redirectPath"` } -type OneTimeAccessEmailAsAdminDto struct { +type emailAsAdminDto struct { TTL utils.JSONDuration `json:"ttl" binding:"ttl"` } diff --git a/backend/internal/onetimeaccess/handler.go b/backend/internal/onetimeaccess/handler.go new file mode 100644 index 00000000..55cf5f62 --- /dev/null +++ b/backend/internal/onetimeaccess/handler.go @@ -0,0 +1,198 @@ +package onetimeaccess + +import ( + "fmt" + "net/http" + "time" + + "github.com/gin-gonic/gin" + + "github.com/pocket-id/pocket-id/backend/internal/common" + "github.com/pocket-id/pocket-id/backend/internal/dto" + "github.com/pocket-id/pocket-id/backend/internal/utils/cookie" +) + +const defaultTokenDuration = 15 * time.Minute + +type handler struct { + service *Service + appConfig AppConfigResolver +} + +func newHandler(service *Service, appConfig AppConfigResolver) *handler { + return &handler{service: service, appConfig: appConfig} +} + +func (h *handler) createToken(c *gin.Context, own bool) { + var input tokenCreateDto + err := c.ShouldBindJSON(&input) + if err != nil { + _ = c.Error(err) + return + } + + var ( + userID string + ttl time.Duration + ) + if own { + // Get user ID from context and force the default TTL + userID = c.GetString("userID") + ttl = defaultTokenDuration + } else { + // Get user ID from URL parameter, and optional TTL from body + userID = c.Param("id") + ttl = input.TTL.Duration + if ttl <= 0 { + ttl = defaultTokenDuration + } + } + if userID == "" { + _ = c.Error(&common.UserIdNotProvidedError{}) + return + } + + token, err := h.service.CreateToken(c.Request.Context(), userID, ttl) + if err != nil { + _ = c.Error(err) + return + } + + c.JSON(http.StatusCreated, gin.H{"token": token}) +} + +// createOwnToken godoc +// @Summary Create one-time access token for current user +// @Description Generate a one-time access token for the currently authenticated user +// @Tags Users +// @Param body body tokenCreateDto true "Token options" +// @Success 201 {object} object "{ \"token\": \"string\" }" +// @Router /api/users/me/one-time-access-token [post] +func (h *handler) createOwnToken(c *gin.Context) { + h.createToken(c, true) +} + +// createTokenForUser godoc +// @Summary Create one-time access token for user (admin) +// @Description Generate a one-time access token for a specific user (admin only) +// @Tags Users +// @Param id path string true "User ID" +// @Param body body tokenCreateDto true "Token options" +// @Success 201 {object} object "{ \"token\": \"string\" }" +// @Router /api/users/{id}/one-time-access-token [post] +func (h *handler) createTokenForUser(c *gin.Context) { + h.createToken(c, false) +} + +// requestEmailAsUnauthenticatedUser godoc +// @Summary Request one-time access email +// @Description Request a one-time access email for unauthenticated users +// @Tags Users +// @Accept json +// @Produce json +// @Param body body emailAsUnauthenticatedUserDto true "Email request information" +// @Success 204 "No Content" +// @Router /api/one-time-access-email [post] +func (h *handler) requestEmailAsUnauthenticatedUser(c *gin.Context) { + dbConfig, err := h.appConfig.GetConfig(c.Request.Context()) + if err != nil { + _ = c.Error(fmt.Errorf("error loading app configuration: %w", err)) + return + } + + var input emailAsUnauthenticatedUserDto + err = dto.ShouldBindWithNormalizedJSON(c, &input) + if err != nil { + _ = c.Error(err) + return + } + + deviceToken, err := h.service.RequestOneTimeAccessEmailAsUnauthenticatedUser(c.Request.Context(), dbConfig, input.Email, input.RedirectPath) + if err != nil { + _ = c.Error(err) + return + } + + cookie.AddDeviceTokenCookie(c, deviceToken) + c.Status(http.StatusNoContent) +} + +// requestEmailAsAdmin godoc +// @Summary Request one-time access email (admin) +// @Description Request a one-time access email for a specific user (admin only) +// @Tags Users +// @Accept json +// @Produce json +// @Param id path string true "User ID" +// @Param body body emailAsAdminDto true "Email request options" +// @Success 204 "No Content" +// @Router /api/users/{id}/one-time-access-email [post] +func (h *handler) requestEmailAsAdmin(c *gin.Context) { + dbConfig, err := h.appConfig.GetConfig(c.Request.Context()) + if err != nil { + _ = c.Error(fmt.Errorf("error loading app configuration: %w", err)) + return + } + + var input emailAsAdminDto + err = c.ShouldBindJSON(&input) + if err != nil { + _ = c.Error(err) + return + } + + userID := c.Param("id") + + ttl := input.TTL.Duration + if ttl <= 0 { + ttl = defaultTokenDuration + } + err = h.service.RequestOneTimeAccessEmailAsAdmin(c.Request.Context(), dbConfig, userID, ttl) + if err != nil { + _ = c.Error(err) + return + } + + c.Status(http.StatusNoContent) +} + +// exchangeToken godoc +// @Summary Exchange one-time access token +// @Description Exchange a one-time access token for a session token +// @Tags Users +// @Param token path string true "One-time access token" +// @Success 200 {object} dto.UserDto +// @Router /api/one-time-access-token/{token} [post] +func (h *handler) exchangeToken(c *gin.Context) { + cfg, err := h.appConfig.GetConfig(c.Request.Context()) + if err != nil { + _ = c.Error(fmt.Errorf("error loading app configuration: %w", err)) + return + } + + loginCode := c.Param("token") + // reject invalid length login codes + if len(loginCode) != 6 && len(loginCode) != 16 { + _ = c.Error(&common.TokenInvalidOrExpiredError{}) + return + } + + deviceToken, _ := c.Cookie(cookie.DeviceTokenCookieName) + user, token, err := h.service.ExchangeToken(c.Request.Context(), cfg, loginCode, deviceToken, c.ClientIP(), c.Request.UserAgent()) + if err != nil { + _ = c.Error(err) + return + } + + var userDto dto.UserDto + err = dto.MapStruct(user, &userDto) + if err != nil { + _ = c.Error(err) + return + } + + maxAge := int(cfg.SessionDuration.AsDurationMinutes().Seconds()) + cookie.AddAccessTokenCookie(c, maxAge, token) + + c.JSON(http.StatusOK, userDto) +} diff --git a/backend/internal/onetimeaccess/module.go b/backend/internal/onetimeaccess/module.go new file mode 100644 index 00000000..9654921c --- /dev/null +++ b/backend/internal/onetimeaccess/module.go @@ -0,0 +1,86 @@ +package onetimeaccess + +import ( + "context" + "fmt" + "time" + + "github.com/gin-gonic/gin" + "github.com/italypaleale/francis/host/local" + "gorm.io/gorm" + + "github.com/pocket-id/pocket-id/backend/internal/appconfig" + "github.com/pocket-id/pocket-id/backend/internal/model" + "github.com/pocket-id/pocket-id/backend/internal/utils/email" +) + +// EmailData is the data rendered in the one-time access email +type EmailData struct { + Code string + LoginLink string + LoginLinkWithCode string + ExpirationString string +} + +// EmailSender sends the one-time access email +type EmailSender interface { + SendOneTimeAccessEmail(ctx context.Context, dbConfig *appconfig.AppConfigModel, to email.Address, data EmailData) error +} + +type TokenService interface { + GenerateAccessToken(user model.User, authenticationMethod string, sessionDuration time.Duration) (string, error) +} + +type AuditLogger interface { + Create(ctx context.Context, event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData, tx *gorm.DB) (model.AuditLog, bool) +} + +type UserProvider interface { + GetUser(ctx context.Context, userID string) (model.User, error) +} + +// AppConfigResolver loads the current application configuration, so handlers can pass it explicitly to the service methods that need it +type AppConfigResolver interface { + GetConfig(ctx context.Context) (*appconfig.AppConfigModel, error) +} + +type Dependencies struct { + DB *gorm.DB + Actors *local.Host + + Signer TokenService + AuditLog AuditLogger + UserProvider UserProvider + EmailSender EmailSender + AppConfig AppConfigResolver +} + +type Module struct { + service *Service + handler *handler +} + +func New(deps Dependencies) (*Module, error) { + // Register the actor that manages a one-time access token + // Each token is its own actor, whose actor ID is the token's value + err := deps.Actors.RegisterActor(TokenActorType, NewTokenActor) + if err != nil { + return nil, fmt.Errorf("error registering the %s actor: %w", TokenActorType, err) + } + + service := newService(deps, deps.Actors.Service()) + return &Module{ + service: service, + handler: newHandler(service, deps.AppConfig), + }, nil +} + +// RegisterRoutes mounts the one-time access token endpoints +// auth guards the admin routes and ownAuth the current user's own token, while the rate limiters throttle the public exchange and email endpoints +func (m *Module) RegisterRoutes(apiGroup *gin.RouterGroup, auth, ownAuth, exchangeRateLimit, emailRateLimit gin.HandlerFunc) { + apiGroup.POST("/users/me/one-time-access-token", ownAuth, m.handler.createOwnToken) + apiGroup.POST("/users/:id/one-time-access-token", auth, m.handler.createTokenForUser) + apiGroup.POST("/users/:id/one-time-access-email", auth, m.handler.requestEmailAsAdmin) + apiGroup.POST("/one-time-access-token/:token", exchangeRateLimit, m.handler.exchangeToken) + apiGroup.POST("/one-time-access-email", emailRateLimit, m.handler.requestEmailAsUnauthenticatedUser) +} diff --git a/backend/internal/onetimeaccess/service.go b/backend/internal/onetimeaccess/service.go new file mode 100644 index 00000000..4c4a9cfd --- /dev/null +++ b/backend/internal/onetimeaccess/service.go @@ -0,0 +1,283 @@ +package onetimeaccess + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net/url" + "strings" + "time" + + "github.com/italypaleale/francis/actor" + "gorm.io/gorm" + + "github.com/pocket-id/pocket-id/backend/internal/appconfig" + "github.com/pocket-id/pocket-id/backend/internal/common" + "github.com/pocket-id/pocket-id/backend/internal/model" + "github.com/pocket-id/pocket-id/backend/internal/utils" + "github.com/pocket-id/pocket-id/backend/internal/utils/email" +) + +// authenticationMethodOneTimePassword identifies one-time password/code authentication +// It must match the value emitted by the JWT service in the access token's "amr" claim +const authenticationMethodOneTimePassword = "otp" + +// TokenStore is the minimal interface needed to persist a one-time access token in the actor state store. +// It's satisfied by both *actor.Service (used by the running application) and *local.Host (used by CLI commands, which don't run the full actor host). +type TokenStore interface { + SetState(ctx context.Context, actorType string, actorID string, state any, opts *actor.SetStateOpts) error +} + +type Service struct { + db *gorm.DB + actorService *actor.Service + userProvider UserProvider + signer TokenService + auditLog AuditLogger + emailSender EmailSender +} + +func newService(deps Dependencies, actorService *actor.Service) *Service { + return &Service{ + db: deps.DB, + actorService: actorService, + userProvider: deps.UserProvider, + signer: deps.Signer, + auditLog: deps.AuditLog, + emailSender: deps.EmailSender, + } +} + +func (s *Service) RequestOneTimeAccessEmailAsAdmin(ctx context.Context, dbConfig *appconfig.AppConfigModel, userID string, ttl time.Duration) error { + if !dbConfig.EmailOneTimeAccessAsAdminEnabled.IsTrue() { + return &common.OneTimeAccessDisabledError{} + } + + _, err := s.requestOneTimeAccessEmailInternal(ctx, userID, "", ttl, false, dbConfig) + return err +} + +func (s *Service) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context.Context, dbConfig *appconfig.AppConfigModel, userID, redirectPath string) (string, error) { + if !dbConfig.EmailOneTimeAccessAsUnauthenticatedEnabled.IsTrue() { + return "", &common.OneTimeAccessDisabledError{} + } + + var userId string + err := s.db.Model(&model.User{}).Select("id").Where("email = ?", userID).First(&userId).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + // Do not return error if user not found to prevent email enumeration + return "", nil + } else if err != nil { + return "", err + } + + deviceToken, err := s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, 15*time.Minute, true, dbConfig) + if err != nil { + return "", err + } else if deviceToken == nil { + return "", errors.New("device token expected but not returned") + } + + return *deviceToken, nil +} + +func (s *Service) requestOneTimeAccessEmailInternal(ctx context.Context, userID, redirectPath string, ttl time.Duration, withDeviceToken bool, dbConfig *appconfig.AppConfigModel) (*string, error) { + // Load the user to ensure it exists and has an email address + user, err := s.userProvider.GetUser(ctx, userID) + if err != nil { + return nil, err + } + + if user.Email == nil { + return nil, &common.UserEmailNotSetError{} + } + + oneTimeAccessToken, deviceToken, err := StoreToken(ctx, s.actorService, user.ID, ttl, withDeviceToken) + if err != nil { + return nil, err + } + + go func() { + // This runs in background, so use a context without cancellation (or it would be stopped when the request ends) + // We still want to have a context derived from the request's to carry over tracing info + innerCtx := context.WithoutCancel(ctx) + + link := common.EnvConfig.AppURL + "/lc" + linkWithCode := link + "/" + oneTimeAccessToken + + // Add redirect path to the link + if strings.HasPrefix(redirectPath, "/") { + encodedRedirectPath := url.QueryEscape(redirectPath) + linkWithCode = linkWithCode + "?redirect=" + encodedRedirectPath + } + + innerErr := s.emailSender.SendOneTimeAccessEmail(innerCtx, dbConfig, email.Address{ + Name: user.FullName(), + Email: *user.Email, + }, EmailData{ + Code: oneTimeAccessToken, + LoginLink: link, + LoginLinkWithCode: linkWithCode, + ExpirationString: utils.DurationToString(ttl), + }) + if innerErr != nil { + slog.ErrorContext(innerCtx, "Failed to send one-time access token email", slog.Any("error", innerErr), slog.String("address", *user.Email)) + return + } + }() + + return deviceToken, nil +} + +func (s *Service) CreateToken(ctx context.Context, userID string, ttl time.Duration) (token string, err error) { + // Load the user to ensure it exists + _, err = s.userProvider.GetUser(ctx, userID) + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", &common.UserNotFoundError{} + } else if err != nil { + return "", err + } + + token, _, err = StoreToken(ctx, s.actorService, userID, ttl, false) + if err != nil { + return "", err + } + + return token, nil +} + +func (s *Service) ExchangeToken(ctx context.Context, dbConfig *appconfig.AppConfigModel, token, deviceToken, ipAddress, userAgent string) (model.User, string, error) { + // Consume the token by invoking its actor: this atomically validates it and, if valid, deletes it. + // It must happen outside of a DB transaction, since invoking an actor while a transaction is open would deadlock on SQLite. + res, err := s.actorService.Invoke(ctx, TokenActorType, token, tokenMethodConsume, tokenConsumeRequest{ + DeviceToken: deviceToken, + }) + if err != nil { + return model.User{}, "", fmt.Errorf("error invoking one-time access token actor: %w", err) + } + + var consumeRes tokenConsumeResponse + err = res.Decode(&consumeRes) + if err != nil { + return model.User{}, "", fmt.Errorf("error decoding one-time access token actor response: %w", err) + } + + switch consumeRes.Status { + case tokenConsumeNotFound: + return model.User{}, "", &common.TokenInvalidOrExpiredError{} + case tokenConsumeDeviceMismatch: + return model.User{}, "", &common.DeviceCodeInvalid{} + case tokenConsumeOK: + // All good, continue below + default: + return model.User{}, "", fmt.Errorf("unexpected status from one-time access token actor: %s", consumeRes.Status) + } + + // The token has now been consumed. From this point on, if we hit an error we compensate by restoring the token (this is best-effort). + user, accessToken, err := s.completeTokenExchange(ctx, dbConfig, consumeRes.State, ipAddress, userAgent) + if err != nil { + s.restoreToken(ctx, token, consumeRes.State) + return model.User{}, "", err + } + + return user, accessToken, nil +} + +// completeTokenExchange performs the work that follows consuming a token: loading the user, validating it, and issuing an access token. +func (s *Service) completeTokenExchange(ctx context.Context, dbConfig *appconfig.AppConfigModel, state TokenState, ipAddress, userAgent string) (model.User, string, error) { + var user model.User + err := s.db. + WithContext(ctx). + Where("id = ?", state.UserID). + First(&user). + Error + if errors.Is(err, gorm.ErrRecordNotFound) { + return model.User{}, "", &common.TokenInvalidOrExpiredError{} + } else if err != nil { + return model.User{}, "", err + } + + if user.Disabled { + return model.User{}, "", &common.UserDisabledError{} + } + + accessToken, err := s.signer.GenerateAccessToken( + user, + authenticationMethodOneTimePassword, + dbConfig.SessionDuration.AsDurationMinutes(), + ) + if err != nil { + return model.User{}, "", err + } + + s.auditLog.Create( + ctx, model.AuditLogEventOneTimeAccessTokenSignIn, + ipAddress, userAgent, + user.ID, + model.AuditLogData{}, + s.db, + ) + + return user, accessToken, nil +} + +// restoreToken restores a token that was consumed but whose exchange could not be completed. +// It's a best-effort compensation: if it fails (or the process crashes before it runs) we accept that the token was consumed unnecessarily. +func (s *Service) restoreToken(parentCtx context.Context, token string, state TokenState) { + // Use a context that is not canceled when the original request ends + ctx, cancel := context.WithTimeout(context.WithoutCancel(parentCtx), 10*time.Second) + defer cancel() + + _, err := s.actorService.Invoke(ctx, TokenActorType, token, TokenMethodRestore, state) + if err != nil { + slog.ErrorContext(ctx, "Failed to restore one-time access token after a failed exchange", slog.Any("error", err)) + } +} + +// StoreToken generates a new one-time access token and persists it in the actor state store, with a TTL matching its lifetime. +// It returns the token value and, when requested, the associated device token. +func StoreToken(ctx context.Context, store TokenStore, userID string, ttl time.Duration, withDeviceToken bool) (token string, deviceToken *string, err error) { + token, deviceToken, err = generateToken(ttl, withDeviceToken) + if err != nil { + return "", nil, err + } + + now := time.Now().Round(time.Second) + state := TokenState{ + UserID: userID, + DeviceToken: deviceToken, + ExpiresAt: now.Add(ttl), + } + + err = store.SetState(ctx, TokenActorType, token, state, &actor.SetStateOpts{TTL: ttl}) + if err != nil { + return "", nil, fmt.Errorf("error saving one-time access token state: %w", err) + } + + return token, deviceToken, nil +} + +// generateToken generates the random token value (and optional device token) for a one-time access token. +func generateToken(ttl time.Duration, withDeviceToken bool) (token string, deviceToken *string, err error) { + // If expires at is less than 15 minutes, use a 6-character token instead of 16 + tokenLength := 16 + if ttl <= 15*time.Minute { + tokenLength = 6 + } + + token, err = utils.GenerateRandomUnambiguousString(tokenLength) + if err != nil { + return "", nil, err + } + + if withDeviceToken { + dt, err := utils.GenerateRandomAlphanumericString(16) + if err != nil { + return "", nil, err + } + deviceToken = &dt + } + + return token, deviceToken, nil +} diff --git a/backend/internal/onetimeaccess/service_test.go b/backend/internal/onetimeaccess/service_test.go new file mode 100644 index 00000000..ccd1b42f --- /dev/null +++ b/backend/internal/onetimeaccess/service_test.go @@ -0,0 +1,172 @@ +package onetimeaccess + +import ( + "context" + "testing" + "time" + + "github.com/italypaleale/francis/actor" + "github.com/italypaleale/francis/host/local" + "github.com/stretchr/testify/require" + "gorm.io/gorm" + + "github.com/pocket-id/pocket-id/backend/internal/appconfig" + "github.com/pocket-id/pocket-id/backend/internal/common" + "github.com/pocket-id/pocket-id/backend/internal/model" + "github.com/pocket-id/pocket-id/backend/internal/utils/email" + testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing" +) + +type fakeSigner struct{} + +func (fakeSigner) GenerateAccessToken(_ model.User, _ string, _ time.Duration) (string, error) { + return "access-token", nil +} + +type fakeAuditLogger struct { + events []model.AuditLogEvent +} + +func (f *fakeAuditLogger) Create(_ context.Context, event model.AuditLogEvent, _, _, _ string, _ model.AuditLogData, _ *gorm.DB) (model.AuditLog, bool) { + f.events = append(f.events, event) + return model.AuditLog{}, true +} + +type fakeUserProvider struct { + db *gorm.DB +} + +func (f fakeUserProvider) GetUser(ctx context.Context, userID string) (model.User, error) { + var user model.User + err := f.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error + return user, err +} + +type fakeEmailSender struct{} + +func (fakeEmailSender) SendOneTimeAccessEmail(_ context.Context, _ *appconfig.AppConfigModel, _ email.Address, _ EmailData) error { + return nil +} + +// newServiceForTest sets up a Service backed by an in-memory test actor host, and returns it together with the host and the audit logger it records into +func newServiceForTest(t *testing.T, db *gorm.DB) (*Service, *local.Host, *fakeAuditLogger) { + t.Helper() + + auditLog := &fakeAuditLogger{} + + var svc *Service + host := testutils.NewActorHostForTest(t, func(t *testing.T, h *local.Host) { + err := h.RegisterActor(TokenActorType, NewTokenActor) + require.NoError(t, err) + + svc = newService(Dependencies{ + DB: db, + Signer: fakeSigner{}, + AuditLog: auditLog, + UserProvider: fakeUserProvider{db: db}, + EmailSender: fakeEmailSender{}, + }, h.Service()) + }) + require.NotNil(t, svc) + + return svc, host, auditLog +} + +func TestExchangeTokenSuccess(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + svc, host, auditLog := newServiceForTest(t, db) + + user := model.User{ + Base: model.Base{ID: "enabled-user"}, + Username: "enabled-user", + } + require.NoError(t, db.Create(&user).Error) + + token, _, err := StoreToken(t.Context(), svc.actorService, user.ID, time.Minute, false) + require.NoError(t, err) + + dbConfig := appconfig.NewTestConfig(nil) + exchangedUser, accessToken, err := svc.ExchangeToken(t.Context(), dbConfig, token, "", "1.2.3.4", "test-agent") + require.NoError(t, err) + require.Equal(t, user.ID, exchangedUser.ID) + require.NotEmpty(t, accessToken) + + // The token must have been consumed + var state TokenState + err = host.GetState(t.Context(), TokenActorType, token, &state) + require.ErrorIs(t, err, actor.ErrStateNotFound) + + // A sign-in audit log must have been created + require.Equal(t, []model.AuditLogEvent{model.AuditLogEventOneTimeAccessTokenSignIn}, auditLog.events) +} + +func TestExchangeTokenInvalidToken(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + svc, _, _ := newServiceForTest(t, db) + + dbConfig := appconfig.NewTestConfig(nil) + _, _, err := svc.ExchangeToken(t.Context(), dbConfig, "does-not-exist", "", "", "") + + var invalidErr *common.TokenInvalidOrExpiredError + require.ErrorAs(t, err, &invalidErr) +} + +func TestExchangeTokenDeviceMismatch(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + svc, host, _ := newServiceForTest(t, db) + + user := model.User{ + Base: model.Base{ID: "device-user"}, + Username: "device-user", + } + require.NoError(t, db.Create(&user).Error) + + // Store a token that requires a device token + token, deviceToken, err := StoreToken(t.Context(), svc.actorService, user.ID, time.Minute, true) + require.NoError(t, err) + require.NotNil(t, deviceToken) + + dbConfig := appconfig.NewTestConfig(nil) + _, _, err = svc.ExchangeToken(t.Context(), dbConfig, token, "wrong-device-token", "", "") + + var deviceErr *common.DeviceCodeInvalid + require.ErrorAs(t, err, &deviceErr) + + // The token must not have been consumed on a device-token mismatch + var state TokenState + err = host.GetState(t.Context(), TokenActorType, token, &state) + require.NoError(t, err) + require.Equal(t, user.ID, state.UserID) +} + +func TestExchangeTokenRejectsDisabledUser(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + svc, host, auditLog := newServiceForTest(t, db) + + user := model.User{ + Base: model.Base{ID: "disabled-user"}, + Username: "disabled-user", + Disabled: true, + } + require.NoError(t, db.Create(&user).Error) + + // Store a one-time access token for the disabled user in the actor state store + token, _, err := StoreToken(t.Context(), svc.actorService, user.ID, time.Minute, false) + require.NoError(t, err) + + dbConfig := appconfig.NewTestConfig(nil) + exchangedUser, accessToken, err := svc.ExchangeToken(t.Context(), dbConfig, token, "", "", "") + + var userDisabledErr *common.UserDisabledError + require.ErrorAs(t, err, &userDisabledErr) + require.Empty(t, exchangedUser.ID) + require.Empty(t, accessToken) + + // The token must have been restored (not consumed), since the exchange failed because the user is disabled + var state TokenState + err = host.GetState(t.Context(), TokenActorType, token, &state) + require.NoError(t, err) + require.Equal(t, user.ID, state.UserID) + + require.Empty(t, auditLog.events) +} diff --git a/backend/internal/service/e2etest_service.go b/backend/internal/service/e2etest_service.go index 22b36638..aacbc1fd 100644 --- a/backend/internal/service/e2etest_service.go +++ b/backend/internal/service/e2etest_service.go @@ -15,6 +15,8 @@ import ( "github.com/go-webauthn/webauthn/protocol" "github.com/google/uuid" + "github.com/italypaleale/francis/actor" + "github.com/italypaleale/francis/host/local" "github.com/lestrrat-go/jwx/v3/jwa" "github.com/lestrrat-go/jwx/v3/jwk" "github.com/lestrrat-go/jwx/v3/jwt" @@ -31,6 +33,7 @@ import ( "github.com/pocket-id/pocket-id/backend/internal/model" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" "github.com/pocket-id/pocket-id/backend/internal/oidc" + "github.com/pocket-id/pocket-id/backend/internal/onetimeaccess" "github.com/pocket-id/pocket-id/backend/internal/storage" "github.com/pocket-id/pocket-id/backend/internal/usersignup" "github.com/pocket-id/pocket-id/backend/internal/utils" @@ -41,6 +44,7 @@ import ( type TestService struct { db *gorm.DB + actors *local.Host jwtService *JwtService appConfigService *appconfig.AppConfigService ldapService *LdapService @@ -56,9 +60,10 @@ const ( e2eRefreshTokenExpiredFixtureToken = "X4vqwtRyCUaq51UafHea4Fsg8Km6CAns6vp3tuX4" ) -func NewTestService(db *gorm.DB, appConfigService *appconfig.AppConfigService, jwtService *JwtService, ldapService *LdapService, appLockService *AppLockService, fileStorage storage.FileStorage) (*TestService, error) { +func NewTestService(db *gorm.DB, actors *local.Host, appConfigService *appconfig.AppConfigService, jwtService *JwtService, ldapService *LdapService, appLockService *AppLockService, fileStorage storage.FileStorage) (*TestService, error) { s := &TestService{ db: db, + actors: actors, appConfigService: appConfigService, jwtService: jwtService, ldapService: ldapService, @@ -136,29 +141,6 @@ func (s *TestService) SeedDatabase(baseURL string) error { } } - oneTimeAccessTokens := []model.OneTimeAccessToken{{ - Base: model.Base{ - ID: "bf877753-4ea4-4c9c-bbbd-e198bb201cb8", - }, - Token: "HPe6k6uiDRRVuAQV", - ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)), - UserID: users[0].ID, - }, - { - Base: model.Base{ - ID: "d3afae24-fe2d-4a98-abec-cf0b8525096a", - }, - Token: "YCGDtftvsvYWiXd0", - ExpiresAt: datatype.DateTime(time.Now().Add(-1 * time.Second)), // expired - UserID: users[0].ID, - }, - } - for _, token := range oneTimeAccessTokens { - if err := tx.Create(&token).Error; err != nil { - return err - } - } - userGroups := []model.UserGroup{ { Base: model.Base{ @@ -340,15 +322,6 @@ func (s *TestService) SeedDatabase(baseURL string) error { return err } - accessToken := model.OneTimeAccessToken{ - Token: "one-time-token", - ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)), - UserID: users[0].ID, - } - if err := tx.Create(&accessToken).Error; err != nil { - return err - } - userAuthorizedClients := []model.UserAuthorizedOidcClient{ { Scope: datatype.StringList{"openid", "profile", "email"}, @@ -506,53 +479,6 @@ func (s *TestService) SeedDatabase(baseURL string) error { } } - signupTokens := []usersignup.SignupToken{ - { - Base: model.Base{ - ID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - }, - Token: "VALID1234567890A", - ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)), - UsageLimit: 1, - UsageCount: 0, - UserGroups: []model.UserGroup{ - userGroups[0], - }, - }, - { - Base: model.Base{ - ID: "dc3c9c96-714e-48eb-926e-2d7c7858e6cf", - }, - Token: "PARTIAL567890ABC", - ExpiresAt: datatype.DateTime(time.Now().Add(7 * 24 * time.Hour)), - UsageLimit: 5, - UsageCount: 2, - }, - { - Base: model.Base{ - ID: "44de1863-ffa5-4db1-9507-4887cd7a1e3f", - }, - Token: "EXPIRED34567890B", - ExpiresAt: datatype.DateTime(time.Now().Add(-24 * time.Hour)), // Expired - UsageLimit: 3, - UsageCount: 1, - }, - { - Base: model.Base{ - ID: "f1b1678b-7720-4d8b-8f91-1dbff1e2d02b", - }, - Token: "FULLYUSED567890C", - ExpiresAt: datatype.DateTime(time.Now().Add(24 * time.Hour)), - UsageLimit: 1, - UsageCount: 1, // Usage limit reached - }, - } - for _, token := range signupTokens { - if err := tx.Create(&token).Error; err != nil { - return err - } - } - emailVerificationTokens := []model.EmailVerificationToken{ { Base: model.Base{ @@ -599,6 +525,116 @@ func (s *TestService) SeedDatabase(baseURL string) error { return err } + // One-time access tokens and signup tokens live in the actor state store, so they're seeded separately from the DB transaction above. + err = s.seedOneTimeAccessTokens(context.Background()) + if err != nil { + return fmt.Errorf("failed to seed one-time access tokens: %w", err) + } + + err = s.seedSignupTokens(context.Background()) + if err != nil { + return fmt.Errorf("failed to seed signup tokens: %w", err) + } + + return nil +} + +// seedSignupTokens seeds the signup tokens used by E2E tests into the signup token singleton actor. +// The already-expired fixture token is intentionally not seeded, since the actor would purge it right away via its cleanup alarm. +func (s *TestService) seedSignupTokens(ctx context.Context) error { + now := time.Now().Round(time.Second) + tokens := map[string]usersignup.SignupTokenState{ + "VALID1234567890A": { + ID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + ExpiresAt: now.Add(24 * time.Hour), + UsageLimit: 1, + UsageCount: 0, + UserGroupIDs: []string{"c7ae7c01-28a3-4f3c-9572-1ee734ea8368"}, + CreatedAt: now, + }, + "PARTIAL567890ABC": { + ID: "dc3c9c96-714e-48eb-926e-2d7c7858e6cf", + ExpiresAt: now.Add(7 * 24 * time.Hour), + UsageLimit: 5, + UsageCount: 2, + CreatedAt: now, + }, + "FULLYUSED567890C": { + ID: "f1b1678b-7720-4d8b-8f91-1dbff1e2d02b", + ExpiresAt: now.Add(24 * time.Hour), + UsageLimit: 1, + UsageCount: 1, // Usage limit reached + CreatedAt: now, + }, + } + + // The actor state store isn't wiped by ResetDatabase, so remove any signup token left over from a previous test first + err := s.deleteAllSignupTokens(ctx) + if err != nil { + return err + } + + // Each signup token is its own actor, whose actor ID is the token's value + for token, state := range tokens { + _, err = s.actors.Service().Invoke(ctx, usersignup.SignupTokenActorType, token, usersignup.SignupTokenMethodCreate, state) + if err != nil { + return fmt.Errorf("failed to seed signup token %q: %w", token, err) + } + } + + return nil +} + +// deleteAllSignupTokens removes every signup token currently stored in the actor state store +func (s *TestService) deleteAllSignupTokens(ctx context.Context) error { + var after string + for { + res, err := s.actors.Service().ListStates(ctx, usersignup.SignupTokenActorType, &actor.ListStatesOpts{After: after}) + if err != nil { + return fmt.Errorf("failed to list signup tokens: %w", err) + } + + for _, st := range res.States { + _, err = s.actors.Service().Invoke(ctx, usersignup.SignupTokenActorType, st.ActorID, usersignup.SignupTokenMethodDelete, nil) + if err != nil { + return fmt.Errorf("failed to delete signup token %q: %w", st.ActorID, err) + } + } + + // An empty cursor means we've just read the last page + after = res.AfterID() + if after == "" { + return nil + } + } +} + +// seedOneTimeAccessTokens seeds the one-time access tokens used by E2E tests into the actor state store. +// Expired tokens are intentionally not seeded: with actor-backed storage an expired token is simply one that has no state, which the exchange flow already reports as invalid/expired. +func (s *TestService) seedOneTimeAccessTokens(ctx context.Context) error { + tokens := []struct { + token string + ttl time.Duration + }{ + {token: "HPe6k6uiDRRVuAQV", ttl: time.Hour}, + {token: "one-time-token", ttl: time.Hour}, + } + + for _, t := range tokens { + state := onetimeaccess.TokenState{ + UserID: e2eRefreshTokenUserID, + ExpiresAt: time.Now().Add(t.ttl).Round(time.Second), + } + // Seed through the actor's "restore" method (which sets the state) rather than writing the + // state directly: if an actor for this token is still active from a previous test (for + // example, one whose token was already consumed), invoking it refreshes its in-memory cache + // too, whereas a direct state write would leave that cache stale. + _, err := s.actors.Service().Invoke(ctx, onetimeaccess.TokenActorType, t.token, onetimeaccess.TokenMethodRestore, state) + if err != nil { + return fmt.Errorf("failed to seed one-time access token %q: %w", t.token, err) + } + } + return nil } diff --git a/backend/internal/service/one_time_access_email_sender.go b/backend/internal/service/one_time_access_email_sender.go new file mode 100644 index 00000000..1c558c4d --- /dev/null +++ b/backend/internal/service/one_time_access_email_sender.go @@ -0,0 +1,29 @@ +package service + +import ( + "context" + + "github.com/pocket-id/pocket-id/backend/internal/appconfig" + "github.com/pocket-id/pocket-id/backend/internal/onetimeaccess" + "github.com/pocket-id/pocket-id/backend/internal/utils/email" +) + +// OneTimeAccessEmailSender sends the one-time access email. +// It adapts the email service, which owns the email templates, to the interface the onetimeaccess module depends on. +type OneTimeAccessEmailSender struct { + emailService *EmailService +} + +func NewOneTimeAccessEmailSender(emailService *EmailService) *OneTimeAccessEmailSender { + return &OneTimeAccessEmailSender{emailService: emailService} +} + +// SendOneTimeAccessEmail implements onetimeaccess.EmailSender +func (s *OneTimeAccessEmailSender) SendOneTimeAccessEmail(ctx context.Context, dbConfig *appconfig.AppConfigModel, to email.Address, data onetimeaccess.EmailData) error { + return SendEmail(ctx, s.emailService, dbConfig, to, OneTimeAccessTemplate, &OneTimeAccessTemplateData{ + Code: data.Code, + LoginLink: data.LoginLink, + LoginLinkWithCode: data.LoginLinkWithCode, + ExpirationString: data.ExpirationString, + }) +} diff --git a/backend/internal/service/one_time_access_service.go b/backend/internal/service/one_time_access_service.go deleted file mode 100644 index af561ee3..00000000 --- a/backend/internal/service/one_time_access_service.go +++ /dev/null @@ -1,261 +0,0 @@ -package service - -import ( - "context" - "errors" - "fmt" - "log/slog" - "net/url" - "strings" - "time" - - "github.com/pocket-id/pocket-id/backend/internal/appconfig" - "github.com/pocket-id/pocket-id/backend/internal/common" - "github.com/pocket-id/pocket-id/backend/internal/model" - datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" - "github.com/pocket-id/pocket-id/backend/internal/utils" - "github.com/pocket-id/pocket-id/backend/internal/utils/email" - "gorm.io/gorm" - "gorm.io/gorm/clause" -) - -type OneTimeAccessService struct { - db *gorm.DB - userService *UserService - jwtService *JwtService - auditLogService *AuditLogService - emailService *EmailService -} - -func NewOneTimeAccessService(db *gorm.DB, userService *UserService, jwtService *JwtService, auditLogService *AuditLogService, emailService *EmailService) *OneTimeAccessService { - return &OneTimeAccessService{ - db: db, - userService: userService, - jwtService: jwtService, - auditLogService: auditLogService, - emailService: emailService, - } -} - -func (s *OneTimeAccessService) RequestOneTimeAccessEmailAsAdmin(ctx context.Context, dbConfig *appconfig.AppConfigModel, userID string, ttl time.Duration) error { - if !dbConfig.EmailOneTimeAccessAsAdminEnabled.IsTrue() { - return &common.OneTimeAccessDisabledError{} - } - - _, err := s.requestOneTimeAccessEmailInternal(ctx, userID, "", ttl, false, dbConfig) - return err -} - -func (s *OneTimeAccessService) RequestOneTimeAccessEmailAsUnauthenticatedUser(ctx context.Context, dbConfig *appconfig.AppConfigModel, userID, redirectPath string) (string, error) { - if !dbConfig.EmailOneTimeAccessAsUnauthenticatedEnabled.IsTrue() { - return "", &common.OneTimeAccessDisabledError{} - } - - var userId string - err := s.db.Model(&model.User{}).Select("id").Where("email = ?", userID).First(&userId).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - // Do not return error if user not found to prevent email enumeration - return "", nil - } else if err != nil { - return "", err - } - - deviceToken, err := s.requestOneTimeAccessEmailInternal(ctx, userId, redirectPath, 15*time.Minute, true, dbConfig) - if err != nil { - return "", err - } else if deviceToken == nil { - return "", errors.New("device token expected but not returned") - } - - return *deviceToken, nil -} - -func (s *OneTimeAccessService) requestOneTimeAccessEmailInternal(ctx context.Context, userID, redirectPath string, ttl time.Duration, withDeviceToken bool, dbConfig *appconfig.AppConfigModel) (*string, error) { - tx := s.db.Begin() - defer func() { - tx.Rollback() - }() - - user, err := s.userService.getUserInternal(ctx, userID, tx) - if err != nil { - return nil, err - } - - if user.Email == nil { - return nil, &common.UserEmailNotSetError{} - } - - oneTimeAccessToken, deviceToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, ttl, withDeviceToken, tx) - if err != nil { - return nil, err - } - err = tx.Commit().Error - if err != nil { - return nil, err - } - - go func() { - // This runs in background, so use a context without cancellation (or it would be stopped when the request ends) - // We still want to have a context derived from the request's to carry over tracing info - innerCtx := context.WithoutCancel(ctx) - - link := common.EnvConfig.AppURL + "/lc" - linkWithCode := link + "/" + oneTimeAccessToken - - // Add redirect path to the link - if strings.HasPrefix(redirectPath, "/") { - encodedRedirectPath := url.QueryEscape(redirectPath) - linkWithCode = linkWithCode + "?redirect=" + encodedRedirectPath - } - - errInternal := SendEmail(innerCtx, s.emailService, dbConfig, email.Address{ - Name: user.FullName(), - Email: *user.Email, - }, OneTimeAccessTemplate, &OneTimeAccessTemplateData{ - Code: oneTimeAccessToken, - LoginLink: link, - LoginLinkWithCode: linkWithCode, - ExpirationString: utils.DurationToString(ttl), - }) - if errInternal != nil { - slog.ErrorContext(innerCtx, "Failed to send one-time access token email", slog.Any("error", errInternal), slog.String("address", *user.Email)) - return - } - }() - - return deviceToken, nil -} - -func (s *OneTimeAccessService) CreateOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (token string, err error) { - tx := s.db.Begin() - defer func() { - tx.Rollback() - }() - - // Load the user to ensure it exists - _, err = s.userService.getUserInternal(ctx, userID, tx) - if errors.Is(err, gorm.ErrRecordNotFound) { - return "", &common.UserNotFoundError{} - } else if err != nil { - return "", err - } - - // Create the one-time access token - token, _, err = s.createOneTimeAccessTokenInternal(ctx, userID, ttl, false, tx) - if err != nil { - return "", err - } - - // Commit - err = tx.Commit().Error - if err != nil { - return "", fmt.Errorf("error committing transaction: %w", err) - } - - return token, nil -} - -func (s *OneTimeAccessService) createOneTimeAccessTokenInternal(ctx context.Context, userID string, ttl time.Duration, withDeviceToken bool, tx *gorm.DB) (token string, deviceToken *string, err error) { - oneTimeAccessToken, err := NewOneTimeAccessToken(userID, ttl, withDeviceToken) - if err != nil { - return "", nil, err - } - - err = tx.WithContext(ctx).Create(oneTimeAccessToken).Error - if err != nil { - return "", nil, err - } - - return oneTimeAccessToken.Token, oneTimeAccessToken.DeviceToken, nil -} - -func (s *OneTimeAccessService) ExchangeOneTimeAccessToken(ctx context.Context, dbConfig *appconfig.AppConfigModel, token, deviceToken, ipAddress, userAgent string) (model.User, string, error) { - tx := s.db.Begin() - defer func() { - tx.Rollback() - }() - - var oneTimeAccessToken model.OneTimeAccessToken - err := tx. - WithContext(ctx). - Where("token = ? AND expires_at > ?", token, datatype.DateTime(time.Now())). - Preload("User"). - Clauses(clause.Locking{Strength: "UPDATE"}). - First(&oneTimeAccessToken). - Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return model.User{}, "", &common.TokenInvalidOrExpiredError{} - } - return model.User{}, "", err - } - if oneTimeAccessToken.DeviceToken != nil && deviceToken != *oneTimeAccessToken.DeviceToken { - return model.User{}, "", &common.DeviceCodeInvalid{} - } - if oneTimeAccessToken.User.Disabled { - return model.User{}, "", &common.UserDisabledError{} - } - - accessToken, err := s.jwtService.GenerateAccessToken( - oneTimeAccessToken.User, - AuthenticationMethodOneTimePassword, - dbConfig.SessionDuration.AsDurationMinutes(), - ) - if err != nil { - return model.User{}, "", err - } - - err = tx. - WithContext(ctx). - Delete(&oneTimeAccessToken). - Error - if err != nil { - return model.User{}, "", err - } - - s.auditLogService.Create( - ctx, model.AuditLogEventOneTimeAccessTokenSignIn, - ipAddress, userAgent, - oneTimeAccessToken.User.ID, model.AuditLogData{}, - tx, - ) - - err = tx.Commit().Error - if err != nil { - return model.User{}, "", fmt.Errorf("error committing transaction: %w", err) - } - - return oneTimeAccessToken.User, accessToken, nil -} - -func NewOneTimeAccessToken(userID string, ttl time.Duration, withDeviceToken bool) (*model.OneTimeAccessToken, error) { - // If expires at is less than 15 minutes, use a 6-character token instead of 16 - tokenLength := 16 - if ttl <= 15*time.Minute { - tokenLength = 6 - } - - token, err := utils.GenerateRandomUnambiguousString(tokenLength) - if err != nil { - return nil, err - } - - var deviceToken *string - if withDeviceToken { - dt, err := utils.GenerateRandomAlphanumericString(16) - if err != nil { - return nil, err - } - deviceToken = &dt - } - - now := time.Now().Round(time.Second) - o := &model.OneTimeAccessToken{ - UserID: userID, - ExpiresAt: datatype.DateTime(now.Add(ttl)), - Token: token, - DeviceToken: deviceToken, - } - - return o, nil -} diff --git a/backend/internal/service/one_time_access_service_test.go b/backend/internal/service/one_time_access_service_test.go deleted file mode 100644 index ff946043..00000000 --- a/backend/internal/service/one_time_access_service_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package service - -import ( - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/pocket-id/pocket-id/backend/internal/appconfig" - "github.com/pocket-id/pocket-id/backend/internal/common" - "github.com/pocket-id/pocket-id/backend/internal/model" - datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" - testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing" -) - -func TestExchangeOneTimeAccessTokenRejectsDisabledUser(t *testing.T) { - db := testutils.NewDatabaseForTest(t) - appConfig := appconfig.NewTestAppConfigService(nil) - instanceID := newInstanceID(t, db) - jwtService := initJwtService(t, db, instanceID, appConfig, newTestEnvConfig()) - auditLogService := NewAuditLogService(db, nil, &GeoLiteService{}, appConfig) - oneTimeAccessService := NewOneTimeAccessService(db, nil, jwtService, auditLogService, nil) - - user := model.User{ - Base: model.Base{ID: "disabled-user"}, - Username: "disabled-user", - Disabled: true, - } - require.NoError(t, db.Create(&user).Error) - - loginCode := model.OneTimeAccessToken{ - Base: model.Base{ID: "disabled-user-login-code"}, - Token: "ABCDEF", - ExpiresAt: datatype.DateTime(time.Now().Add(time.Minute)), - UserID: user.ID, - } - require.NoError(t, db.Create(&loginCode).Error) - - dbConfig := appconfig.NewTestConfig(nil) - exchangedUser, accessToken, err := oneTimeAccessService.ExchangeOneTimeAccessToken(t.Context(), dbConfig, loginCode.Token, "", "", "") - - var userDisabledErr *common.UserDisabledError - require.ErrorAs(t, err, &userDisabledErr) - require.Empty(t, exchangedUser.ID) - require.Empty(t, accessToken) - - var remainingLoginCode model.OneTimeAccessToken - require.NoError(t, db.Where("token = ?", loginCode.Token).First(&remainingLoginCode).Error) - - var auditLogCount int64 - require.NoError(t, db.Model(&model.AuditLog{}).Where("user_id = ?", user.ID).Count(&auditLogCount).Error) - require.Zero(t, auditLogCount) -} diff --git a/backend/internal/usersignup/actor.go b/backend/internal/usersignup/actor.go new file mode 100644 index 00000000..68ba1b45 --- /dev/null +++ b/backend/internal/usersignup/actor.go @@ -0,0 +1,217 @@ +package usersignup + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/italypaleale/francis/actor" + + "github.com/pocket-id/pocket-id/backend/internal/common" +) + +// Signup tokens are stored entirely in the actor state store. +// Each token is its own actor, whose actor ID is the token value itself. +// The state is persisted with a TTL equal to the token's lifetime, so it's purged automatically when the token expires (there's no separate cleanup job and no expiration alarm). +// Listing tokens uses ListStates, which only returns states that haven't expired yet. + +// SignupTokenActorType is the actor type for the signup token actor +const SignupTokenActorType = "SignupToken" + +// Methods exposed by the signup token actor +// Because we cannot invoke an actor while a DB transaction is open (that would deadlock on SQLite), consuming a token is done by invoking the actor first (which atomically validates it and increments its usage count), and only afterwards performing the remaining work. +// On failure, the caller compensates by releasing the token via the "release" method as best-effort. +const ( + // SignupTokenMethodCreate stores a new signup token, replacing any existing state + SignupTokenMethodCreate = "create" + // SignupTokenMethodDelete removes a signup token + SignupTokenMethodDelete = "delete" + + signupTokenMethodMigrate = "migrate" + signupTokenMethodConsume = "consume" + signupTokenMethodRelease = "release" +) + +// signupTokenConsumeStatus is the outcome of a "consume" invocation. +type signupTokenConsumeStatus string + +const ( + // signupTokenConsumeOK indicates the token was valid and one use has been consumed + signupTokenConsumeOK signupTokenConsumeStatus = "ok" + // signupTokenConsumeNotFound indicates the token doesn't exist (or has expired) + signupTokenConsumeNotFound signupTokenConsumeStatus = "not_found" + // signupTokenConsumeLimitReached indicates the token has no uses left + signupTokenConsumeLimitReached signupTokenConsumeStatus = "limit_reached" +) + +// SignupTokenState is the persisted state of a signup token actor. +// The token value itself is the actor's ID, so it isn't repeated here. +type SignupTokenState struct { + ID string + UsageLimit int + UsageCount int + UserGroupIDs []string + CreatedAt time.Time + ExpiresAt time.Time +} + +// signupTokenConsumeResponse is the response of the "consume" method +type signupTokenConsumeResponse struct { + Status signupTokenConsumeStatus + // UserGroupIDs is set only when Status is "ok", and contains the groups the new user should join + UserGroupIDs []string +} + +// signupTokenActor is the actor that manages a single signup token +type signupTokenActor struct { + log *slog.Logger + client actor.Client[SignupTokenState] +} + +// NewSignupTokenActor allocates a new signup token actor +// It satisfies actor.Factory +func NewSignupTokenActor(actorID string, service *actor.Service) actor.Actor { + return &signupTokenActor{ + log: slog.With( + slog.String("scope", "actor"), + slog.String("actorType", SignupTokenActorType), + ), + client: actor.NewActorClient[SignupTokenState](SignupTokenActorType, actorID, service), + } +} + +// Invoke implements actor.ActorInvoke +func (a *signupTokenActor) Invoke(parentCtx context.Context, method string, data actor.Envelope) (any, error) { + switch method { + case SignupTokenMethodCreate: + return nil, a.create(parentCtx, data, false) + case signupTokenMethodMigrate: + return nil, a.create(parentCtx, data, true) + case signupTokenMethodConsume: + return a.consume(parentCtx) + case signupTokenMethodRelease: + return nil, a.release(parentCtx) + case SignupTokenMethodDelete: + return nil, a.delete(parentCtx) + default: + return nil, common.ErrUnsupportedActorMethod{Method: method} + } +} + +// create stores the token's state. +// When onlyIfMissing is true the write is skipped if the actor already has state: this is used by the one-time migration of the pre-actor tokens, so a token that has already been migrated is never reset. +func (a *signupTokenActor) create(parentCtx context.Context, data actor.Envelope, onlyIfMissing bool) error { + if data == nil { + return fmt.Errorf("request body is empty for method '%s'", SignupTokenMethodCreate) + } + + var state SignupTokenState + err := data.Decode(&state) + if err != nil { + return fmt.Errorf("request body is not valid for method '%s': %w", SignupTokenMethodCreate, err) + } + + if onlyIfMissing { + ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second) + defer cancel() + current, err := a.client.GetState(ctx) + if err != nil { + return fmt.Errorf("error retrieving actor state: %w", err) + } + + // An empty ID means there's no state yet + if current.ID != "" { + return nil + } + } + + return a.setState(parentCtx, state) +} + +// consume atomically validates the token and, if it's still usable, records one more use. +func (a *signupTokenActor) consume(parentCtx context.Context) (signupTokenConsumeResponse, error) { + ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second) + defer cancel() + state, err := a.client.GetState(ctx) + if err != nil { + return signupTokenConsumeResponse{}, fmt.Errorf("error retrieving actor state: %w", err) + } + + // An empty ID means there's no state: the token doesn't exist (or its state already expired and was purged) + if state.ID == "" || state.ExpiresAt.Before(time.Now()) { + return signupTokenConsumeResponse{ + Status: signupTokenConsumeNotFound, + }, nil + } + + if state.UsageCount >= state.UsageLimit { + return signupTokenConsumeResponse{ + Status: signupTokenConsumeLimitReached, + }, nil + } + + // Consume one use of the token + state.UsageCount++ + err = a.setState(parentCtx, state) + if err != nil { + return signupTokenConsumeResponse{}, err + } + + return signupTokenConsumeResponse{ + Status: signupTokenConsumeOK, + UserGroupIDs: state.UserGroupIDs, + }, nil +} + +// release reverts the usage count increment performed while consuming the token, to compensate when the signup could not be completed. +func (a *signupTokenActor) release(parentCtx context.Context) error { + ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second) + defer cancel() + state, err := a.client.GetState(ctx) + if err != nil { + return fmt.Errorf("error retrieving actor state: %w", err) + } + + // The token is gone (for example, it expired and was purged) or was never consumed: nothing to compensate + if state.ID == "" || state.UsageCount <= 0 { + return nil + } + + state.UsageCount-- + return a.setState(parentCtx, state) +} + +// delete removes the token. +func (a *signupTokenActor) delete(parentCtx context.Context) error { + ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second) + defer cancel() + err := a.client.DeleteState(ctx) + if err != nil && !errors.Is(err, actor.ErrStateNotFound) { + // Deleting a token that doesn't exist (for example, one that expired in the meanwhile) already reaches the desired end state + return fmt.Errorf("error deleting actor state: %w", err) + } + + return nil +} + +// setState saves the state with a TTL matching the token's remaining lifetime, so it's purged automatically once the token expires. +// Saving is skipped if the token has already expired, since there would be nothing left to store. +func (a *signupTokenActor) setState(parentCtx context.Context, state SignupTokenState) error { + ttl := time.Until(state.ExpiresAt) + if ttl <= 0 { + return nil + } + + ctx, cancel := context.WithTimeout(parentCtx, 10*time.Second) + defer cancel() + err := a.client.SetState(ctx, state, &actor.SetStateOpts{ + TTL: ttl, + }) + if err != nil { + return fmt.Errorf("error saving actor state: %w", err) + } + + return nil +} diff --git a/backend/internal/usersignup/actor_test.go b/backend/internal/usersignup/actor_test.go new file mode 100644 index 00000000..c147a5bd --- /dev/null +++ b/backend/internal/usersignup/actor_test.go @@ -0,0 +1,173 @@ +package usersignup + +import ( + "testing" + "time" + + "github.com/italypaleale/francis/actor" + "github.com/italypaleale/francis/host/local" + "github.com/stretchr/testify/require" + + testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing" +) + +// newSignupTokenActorService starts a test actor host with the signup token actor registered and returns its service +func newSignupTokenActorService(t *testing.T) *actor.Service { + t.Helper() + + var svc *actor.Service + testutils.NewActorHostForTest(t, func(t *testing.T, h *local.Host) { + err := h.RegisterActor(SignupTokenActorType, NewSignupTokenActor) + require.NoError(t, err) + svc = h.Service() + }) + require.NotNil(t, svc) + + return svc +} + +func createSignupTokenForTest(t *testing.T, svc *actor.Service, token string, state SignupTokenState) { + t.Helper() + _, err := svc.Invoke(t.Context(), SignupTokenActorType, token, SignupTokenMethodCreate, state) + require.NoError(t, err) +} + +func consumeSignupTokenForTest(t *testing.T, svc *actor.Service, token string) signupTokenConsumeResponse { + t.Helper() + res, err := svc.Invoke(t.Context(), SignupTokenActorType, token, signupTokenMethodConsume, nil) + require.NoError(t, err) + + var out signupTokenConsumeResponse + err = res.Decode(&out) + require.NoError(t, err) + + return out +} + +// listSignupTokenIDsForTest returns the actor IDs (that is, the token values) of every stored signup token +func listSignupTokenIDsForTest(t *testing.T, svc *actor.Service) []string { + t.Helper() + res, err := svc.ListStates(t.Context(), SignupTokenActorType, nil) + require.NoError(t, err) + + ids := make([]string, len(res.States)) + for i, st := range res.States { + ids[i] = st.ActorID + } + + return ids +} + +func TestSignupTokenActorConsume(t *testing.T) { + svc := newSignupTokenActorService(t) + + createSignupTokenForTest(t, svc, "token-1", SignupTokenState{ + ID: "id-1", + ExpiresAt: time.Now().Add(time.Hour), + UsageLimit: 1, + UserGroupIDs: []string{"group-a", "group-b"}, + CreatedAt: time.Now(), + }) + + // First consume succeeds and returns the token's user groups + res := consumeSignupTokenForTest(t, svc, "token-1") + require.Equal(t, signupTokenConsumeOK, res.Status) + require.Equal(t, []string{"group-a", "group-b"}, res.UserGroupIDs) + + // Second consume fails: the usage limit (1) has been reached + res = consumeSignupTokenForTest(t, svc, "token-1") + require.Equal(t, signupTokenConsumeLimitReached, res.Status) +} + +func TestSignupTokenActorConsumeNotFound(t *testing.T) { + svc := newSignupTokenActorService(t) + + res := consumeSignupTokenForTest(t, svc, "does-not-exist") + require.Equal(t, signupTokenConsumeNotFound, res.Status) +} + +// TestSignupTokenActorCreateExpired verifies that a token that has already expired is never stored, since its state TTL would be in the past +func TestSignupTokenActorCreateExpired(t *testing.T) { + svc := newSignupTokenActorService(t) + + createSignupTokenForTest(t, svc, "token-expired", SignupTokenState{ + ID: "id-expired", + ExpiresAt: time.Now().Add(-time.Minute), + UsageLimit: 1, + CreatedAt: time.Now().Add(-time.Hour), + }) + + require.Empty(t, listSignupTokenIDsForTest(t, svc)) + require.Equal(t, signupTokenConsumeNotFound, consumeSignupTokenForTest(t, svc, "token-expired").Status) +} + +func TestSignupTokenActorRelease(t *testing.T) { + svc := newSignupTokenActorService(t) + + createSignupTokenForTest(t, svc, "token-2", SignupTokenState{ + ID: "id-2", + ExpiresAt: time.Now().Add(time.Hour), + UsageLimit: 2, + CreatedAt: time.Now(), + }) + + // Consume both uses + require.Equal(t, signupTokenConsumeOK, consumeSignupTokenForTest(t, svc, "token-2").Status) + require.Equal(t, signupTokenConsumeOK, consumeSignupTokenForTest(t, svc, "token-2").Status) + require.Equal(t, signupTokenConsumeLimitReached, consumeSignupTokenForTest(t, svc, "token-2").Status) + + // Release one use (compensation) + _, err := svc.Invoke(t.Context(), SignupTokenActorType, "token-2", signupTokenMethodRelease, nil) + require.NoError(t, err) + + // Consuming succeeds again now that a use was released + require.Equal(t, signupTokenConsumeOK, consumeSignupTokenForTest(t, svc, "token-2").Status) +} + +func TestSignupTokenActorDelete(t *testing.T) { + svc := newSignupTokenActorService(t) + + createSignupTokenForTest(t, svc, "token-3", SignupTokenState{ + ID: "id-3", + ExpiresAt: time.Now().Add(time.Hour), + UsageLimit: 1, + CreatedAt: time.Now(), + }) + require.Equal(t, []string{"token-3"}, listSignupTokenIDsForTest(t, svc)) + + _, err := svc.Invoke(t.Context(), SignupTokenActorType, "token-3", SignupTokenMethodDelete, nil) + require.NoError(t, err) + + require.Empty(t, listSignupTokenIDsForTest(t, svc)) + + // The token can no longer be consumed + require.Equal(t, signupTokenConsumeNotFound, consumeSignupTokenForTest(t, svc, "token-3").Status) + + // Deleting a token that no longer exists is a no-op + _, err = svc.Invoke(t.Context(), SignupTokenActorType, "token-3", SignupTokenMethodDelete, nil) + require.NoError(t, err) +} + +// TestSignupTokenActorMigrateDoesNotOverwrite verifies that the one-time migration never resets a token that was already migrated and used since +func TestSignupTokenActorMigrateDoesNotOverwrite(t *testing.T) { + svc := newSignupTokenActorService(t) + + state := SignupTokenState{ + ID: "id-4", + ExpiresAt: time.Now().Add(time.Hour), + UsageLimit: 2, + CreatedAt: time.Now(), + } + createSignupTokenForTest(t, svc, "token-4", state) + + // Use the token once + require.Equal(t, signupTokenConsumeOK, consumeSignupTokenForTest(t, svc, "token-4").Status) + + // Re-running the migration must not reset the usage count + _, err := svc.Invoke(t.Context(), SignupTokenActorType, "token-4", signupTokenMethodMigrate, state) + require.NoError(t, err) + + // Only one use is left, so a single consume succeeds and the next one doesn't + require.Equal(t, signupTokenConsumeOK, consumeSignupTokenForTest(t, svc, "token-4").Status) + require.Equal(t, signupTokenConsumeLimitReached, consumeSignupTokenForTest(t, svc, "token-4").Status) +} diff --git a/backend/internal/usersignup/cleanup.go b/backend/internal/usersignup/cleanup.go deleted file mode 100644 index bcbc0971..00000000 --- a/backend/internal/usersignup/cleanup.go +++ /dev/null @@ -1,19 +0,0 @@ -package usersignup - -import ( - "context" - "time" - - "gorm.io/gorm" - - datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" -) - -// CleanupExpiredSignupTokens deletes signup tokens that have expired -// It returns the number of rows removed -func CleanupExpiredSignupTokens(ctx context.Context, db *gorm.DB) (int64, error) { - st := db. - WithContext(ctx). - Delete(&SignupToken{}, "expires_at < ?", datatype.DateTime(time.Now())) - return st.RowsAffected, st.Error -} diff --git a/backend/internal/usersignup/handler.go b/backend/internal/usersignup/handler.go index c8d96ef2..ec1552c2 100644 --- a/backend/internal/usersignup/handler.go +++ b/backend/internal/usersignup/handler.go @@ -56,7 +56,8 @@ func (h *handler) signUpInitialAdmin(c *gin.Context) { } var input signUpDto - if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil { + err = dto.ShouldBindWithNormalizedJSON(c, &input) + if err != nil { _ = c.Error(err) return } @@ -68,7 +69,8 @@ func (h *handler) signUpInitialAdmin(c *gin.Context) { } var userDto dto.UserDto - if err := dto.MapStruct(user, &userDto); err != nil { + err = dto.MapStruct(user, &userDto) + if err != nil { _ = c.Error(err) return } @@ -136,7 +138,8 @@ func (h *handler) listSignupTokens(c *gin.Context) { } var tokensDto []signupTokenDto - if err := dto.MapStructList(tokens, &tokensDto); err != nil { + err = dto.MapStructList(tokens, &tokensDto) + if err != nil { _ = c.Error(err) return } @@ -183,15 +186,13 @@ func (h *handler) signup(c *gin.Context) { } var input signUpDto - if err := dto.ShouldBindWithNormalizedJSON(c, &input); err != nil { + err = dto.ShouldBindWithNormalizedJSON(c, &input) + if err != nil { _ = c.Error(err) return } - ipAddress := c.ClientIP() - userAgent := c.GetHeader("User-Agent") - - user, accessToken, err := h.service.SignUp(c.Request.Context(), config, input, ipAddress, userAgent) + user, accessToken, err := h.service.SignUp(c.Request.Context(), config, input, c.ClientIP(), c.GetHeader("User-Agent")) if err != nil { _ = c.Error(err) return @@ -201,7 +202,8 @@ func (h *handler) signup(c *gin.Context) { cookie.AddAccessTokenCookie(c, maxAge, accessToken) var userDto dto.UserDto - if err := dto.MapStruct(user, &userDto); err != nil { + err = dto.MapStruct(user, &userDto) + if err != nil { _ = c.Error(err) return } diff --git a/backend/internal/usersignup/migration.go b/backend/internal/usersignup/migration.go new file mode 100644 index 00000000..7d2d2cbb --- /dev/null +++ b/backend/internal/usersignup/migration.go @@ -0,0 +1,105 @@ +package usersignup + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "time" + + "gorm.io/gorm" + + "github.com/pocket-id/pocket-id/backend/internal/model" +) + +// This file holds the one-time migration of the pre-actor signup tokens. +// The "actor tokens" migration freezes the signup_tokens table (and its user-group associations) into a JSON document stored in the "kv" table under the "signup_tokens_migrated" key. +// It's loaded here to create the per-token actors on first startup. + +// signupTokensMigratedKey is the kv key under which the pre-actor signup tokens were frozen. +const signupTokensMigratedKey = "signup_tokens_migrated" //nolint:gosec // G101 false positive: this is the name of a kv key, not a credential + +// migratedSignupToken is the JSON shape of a signup token frozen into the kv table by the migration. +// All timestamps are expressed as Unix seconds. +type migratedSignupToken struct { + ID string `json:"id"` + Token string `json:"token"` + ExpiresAt int64 `json:"expiresAt"` + UsageLimit int `json:"usageLimit"` + UsageCount int `json:"usageCount"` + UserGroupIDs []string `json:"userGroupIds"` + CreatedAt int64 `json:"createdAt"` +} + +// migrateSignupTokens creates an actor for every signup token frozen into the kv table by the migration. +// It requires the actor state store to be available, so it must run after the actor host is ready. +// It is idempotent: tokens that have already been migrated are left untouched, so a token that has been used since it was migrated is never reset. +func (s *Service) migrateSignupTokens(ctx context.Context) error { + migrated, err := loadMigratedSignupTokens(ctx, s.db) + if err != nil { + return err + } + if len(migrated) == 0 { + return nil + } + + var count int + for _, m := range migrated { + // Skip tokens that have already expired, since there would be nothing left to store + expiresAt := time.Unix(m.ExpiresAt, 0) + if !expiresAt.After(time.Now()) { + continue + } + + state := SignupTokenState{ + ID: m.ID, + ExpiresAt: expiresAt, + UsageLimit: m.UsageLimit, + UsageCount: m.UsageCount, + UserGroupIDs: m.UserGroupIDs, + CreatedAt: time.Unix(m.CreatedAt, 0), + } + + // The token's value is the actor's ID + // The "migrate" method only writes the state if the actor doesn't have one already + _, err = s.actorService.Invoke(ctx, SignupTokenActorType, m.Token, signupTokenMethodMigrate, state) + if err != nil { + return fmt.Errorf("error migrating signup token '%s': %w", m.ID, err) + } + count++ + } + + slog.InfoContext(ctx, "Migrated signup tokens to actors", slog.Int("count", count)) + + return nil +} + +// loadMigratedSignupTokens reads the signup tokens frozen into the kv table by the migration +// It returns nil if there's nothing to migrate +func loadMigratedSignupTokens(ctx context.Context, db *gorm.DB) ([]migratedSignupToken, error) { + row := model.KV{ + Key: signupTokensMigratedKey, + } + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + err := db.WithContext(ctx).First(&row).Error + switch { + case errors.Is(err, gorm.ErrRecordNotFound): + // There are no migrated signup tokens in the database, nothing to do + return nil, nil + case err != nil: + return nil, fmt.Errorf("failed to load migrated signup tokens from the database: %w", err) + case row.Value == nil || len(*row.Value) == 0: + // Also no migrated signup tokens, nothing to do + return nil, nil + } + + var migrated []migratedSignupToken + err = json.Unmarshal([]byte(*row.Value), &migrated) + if err != nil { + return nil, fmt.Errorf("error parsing migrated signup tokens: %w", err) + } + + return migrated, nil +} diff --git a/backend/internal/usersignup/migration_test.go b/backend/internal/usersignup/migration_test.go new file mode 100644 index 00000000..35515cfe --- /dev/null +++ b/backend/internal/usersignup/migration_test.go @@ -0,0 +1,207 @@ +package usersignup + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "gorm.io/gorm" + + "github.com/pocket-id/pocket-id/backend/internal/utils" + testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing" +) + +// versionBeforeMoveTokens is the migration version right before the "actor tokens" migration. +const versionBeforeMoveTokens = 20260722120000 + +// seedSignupTokensForMigration seeds two signup tokens (one with a user group, one without) into the pre-migration schema. +func seedSignupTokensForMigration(t *testing.T, db *gorm.DB, createdAt, expiresAt time.Time) { + t.Helper() + + // An unrelated, non-JSON kv entry, to ensure the freeze/restore queries don't choke on other kv keys + err := db.Exec( + `INSERT INTO kv ("key", "value") VALUES ('instance_id', ?)`, + "not-json-instance-id", + ).Error + require.NoError(t, err) + + // A user group referenced by one of the tokens + err = db.Exec( + `INSERT INTO user_groups (id, created_at, friendly_name, name) VALUES (?, ?, ?, ?)`, + "grp-1", createdAt.Unix(), "Group One", "group-one", + ).Error + require.NoError(t, err) + + // A token with a user group + err = db.Exec( + `INSERT INTO signup_tokens (id, created_at, token, expires_at, usage_limit, usage_count) VALUES (?, ?, ?, ?, ?, ?)`, + "tok-1", createdAt.Unix(), "TOKENWITHGROUP01", expiresAt.Unix(), 3, 1, + ).Error + require.NoError(t, err) + err = db.Exec( + `INSERT INTO signup_tokens_user_groups (signup_token_id, user_group_id) VALUES (?, ?)`, + "tok-1", "grp-1", + ).Error + require.NoError(t, err) + + // A token without user groups + err = db.Exec( + `INSERT INTO signup_tokens (id, created_at, token, expires_at, usage_limit, usage_count) VALUES (?, ?, ?, ?, ?, ?)`, + "tok-2", createdAt.Unix(), "TOKENNOGROUP0002", expiresAt.Unix(), 1, 0, + ).Error + require.NoError(t, err) +} + +func TestLoadMigratedSignupTokens(t *testing.T) { + createdAt := time.Now().Add(-time.Hour).Truncate(time.Second) + expiresAt := time.Now().Add(24 * time.Hour).Truncate(time.Second) + + db := testutils.NewDatabaseForTestWithMigrationSeed(t, versionBeforeMoveTokens, func(t *testing.T, db *gorm.DB) { + seedSignupTokensForMigration(t, db, createdAt, expiresAt) + }) + + // The migration must have dropped the signup_tokens tables + ok := db.Migrator().HasTable("signup_tokens") + require.False(t, ok, "signup_tokens table should have been dropped") + ok = db.Migrator().HasTable("signup_tokens_user_groups") + require.False(t, ok, "signup_tokens_user_groups table should have been dropped") + + tokens, err := loadMigratedSignupTokens(t.Context(), db) + require.NoError(t, err) + require.Len(t, tokens, 2) + + byID := make(map[string]migratedSignupToken, len(tokens)) + for _, tok := range tokens { + byID[tok.ID] = tok + } + + tok1 := byID["tok-1"] + require.Equal(t, "TOKENWITHGROUP01", tok1.Token) + require.Equal(t, 3, tok1.UsageLimit) + require.Equal(t, 1, tok1.UsageCount) + require.Equal(t, []string{"grp-1"}, tok1.UserGroupIDs) + require.Equal(t, expiresAt.Unix(), tok1.ExpiresAt) + require.Equal(t, createdAt.Unix(), tok1.CreatedAt) + + tok2 := byID["tok-2"] + require.Equal(t, "TOKENNOGROUP0002", tok2.Token) + require.Equal(t, 1, tok2.UsageLimit) + require.Equal(t, 0, tok2.UsageCount) + require.Empty(t, tok2.UserGroupIDs) +} + +// TestMigrateSignupTokens verifies that the frozen signup tokens are turned into per-token actors, and that already-expired ones are skipped +func TestMigrateSignupTokens(t *testing.T) { + createdAt := time.Now().Add(-time.Hour).Truncate(time.Second) + expiresAt := time.Now().Add(24 * time.Hour).Truncate(time.Second) + + db := testutils.NewDatabaseForTestWithMigrationSeed(t, versionBeforeMoveTokens, func(t *testing.T, db *gorm.DB) { + seedSignupTokensForMigration(t, db, createdAt, expiresAt) + + // A token that has already expired: it must not be migrated + err := db.Exec( + `INSERT INTO signup_tokens (id, created_at, token, expires_at, usage_limit, usage_count) VALUES (?, ?, ?, ?, ?, ?)`, + "tok-expired", createdAt.Unix(), "EXPIREDTOKEN0003", time.Now().Add(-time.Hour).Unix(), 1, 0, + ).Error + require.NoError(t, err) + }) + + svc := newSignupServiceForTest(t, db, fakeUserCreator{}) + + err := svc.migrateSignupTokens(t.Context()) + require.NoError(t, err) + + entries, err := svc.listSignupTokenStates(t.Context()) + require.NoError(t, err) + require.Len(t, entries, 2) + + byToken := make(map[string]SignupTokenState, len(entries)) + for _, e := range entries { + byToken[e.Token] = e.State + } + + tok1 := byToken["TOKENWITHGROUP01"] + require.Equal(t, "tok-1", tok1.ID) + require.Equal(t, 3, tok1.UsageLimit) + require.Equal(t, 1, tok1.UsageCount) + require.Equal(t, []string{"grp-1"}, tok1.UserGroupIDs) + require.Equal(t, expiresAt.Unix(), tok1.ExpiresAt.Unix()) + require.Equal(t, createdAt.Unix(), tok1.CreatedAt.Unix()) + + tok2 := byToken["TOKENNOGROUP0002"] + require.Equal(t, "tok-2", tok2.ID) + require.Empty(t, tok2.UserGroupIDs) + + // The expired token must not have been migrated + require.NotContains(t, byToken, "EXPIREDTOKEN0003") + + // The migration is idempotent: re-running it doesn't reset a token that has been used since + require.Equal(t, signupTokenConsumeOK, consumeSignupTokenForTest(t, svc.actorService, "TOKENNOGROUP0002").Status) + err = svc.migrateSignupTokens(t.Context()) + require.NoError(t, err) + require.Equal(t, signupTokenConsumeLimitReached, consumeSignupTokenForTest(t, svc.actorService, "TOKENNOGROUP0002").Status) +} + +// TestLoadMigratedSignupTokensEmpty verifies that when there were no signup tokens, nothing is frozen and nothing is loaded. +func TestLoadMigratedSignupTokensEmpty(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + + tokens, err := loadMigratedSignupTokens(t.Context(), db) + require.NoError(t, err) + require.Empty(t, tokens) +} + +// TestMoveTokensToActorStateDown verifies that rolling the migration back recreates the signup token tables and restores their contents from the frozen kv document. +func TestMoveTokensToActorStateDown(t *testing.T) { + createdAt := time.Now().Add(-time.Hour).Truncate(time.Second) + expiresAt := time.Now().Add(24 * time.Hour).Truncate(time.Second) + + db := testutils.NewDatabaseForTestWithMigrationSeed(t, versionBeforeMoveTokens, func(t *testing.T, db *gorm.DB) { + seedSignupTokensForMigration(t, db, createdAt, expiresAt) + }) + + // The tables were frozen and dropped by the up migration + ok := db.Migrator().HasTable("signup_tokens") + require.False(t, ok) + + // Roll the migration back + sqlDB, err := db.DB() + require.NoError(t, err) + m, cleanup, err := utils.GetEmbeddedMigrateInstance(t.Context(), sqlDB) + require.NoError(t, err) + defer cleanup() + + err = m.Migrate(versionBeforeMoveTokens) + require.NoError(t, err) + + // The tables must have been recreated and repopulated from the frozen document + ok = db.Migrator().HasTable("signup_tokens") + require.True(t, ok) + ok = db.Migrator().HasTable("signup_tokens_user_groups") + require.True(t, ok) + + type row struct { + ID string + Token string + UsageLimit int + UsageCount int + } + var rows []row + err = db.Raw(`SELECT id, token, usage_limit, usage_count FROM signup_tokens ORDER BY id`).Scan(&rows).Error + require.NoError(t, err) + require.Equal(t, []row{ + {ID: "tok-1", Token: "TOKENWITHGROUP01", UsageLimit: 3, UsageCount: 1}, + {ID: "tok-2", Token: "TOKENNOGROUP0002", UsageLimit: 1, UsageCount: 0}, + }, rows) + + var groupID string + err = db.Raw(`SELECT user_group_id FROM signup_tokens_user_groups WHERE signup_token_id = ?`, "tok-1").Scan(&groupID).Error + require.NoError(t, err) + require.Equal(t, "grp-1", groupID) + + // The frozen document must have been removed from the kv table + var kvCount int64 + err = db.Raw(`SELECT count(*) FROM kv WHERE "key" = ?`, signupTokensMigratedKey).Scan(&kvCount).Error + require.NoError(t, err) + require.Zero(t, kvCount) +} diff --git a/backend/internal/usersignup/models.go b/backend/internal/usersignup/models.go index ddbf5b3e..7c3b31cb 100644 --- a/backend/internal/usersignup/models.go +++ b/backend/internal/usersignup/models.go @@ -1,8 +1,6 @@ package usersignup import ( - "time" - "github.com/pocket-id/pocket-id/backend/internal/model" datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" ) @@ -15,17 +13,5 @@ type SignupToken struct { ExpiresAt datatype.DateTime `json:"expiresAt" sortable:"true"` UsageLimit int `json:"usageLimit" sortable:"true"` UsageCount int `json:"usageCount" sortable:"true"` - UserGroups []model.UserGroup `gorm:"many2many:signup_tokens_user_groups;"` -} - -func (st *SignupToken) IsExpired() bool { - return time.Time(st.ExpiresAt).Before(time.Now()) -} - -func (st *SignupToken) IsUsageLimitReached() bool { - return st.UsageCount >= st.UsageLimit -} - -func (st *SignupToken) IsValid() bool { - return !st.IsExpired() && !st.IsUsageLimitReached() + UserGroups []model.UserGroup `json:"userGroups"` } diff --git a/backend/internal/usersignup/module.go b/backend/internal/usersignup/module.go index 8a7acacf..7698ba6d 100644 --- a/backend/internal/usersignup/module.go +++ b/backend/internal/usersignup/module.go @@ -2,9 +2,11 @@ package usersignup import ( "context" + "fmt" "time" "github.com/gin-gonic/gin" + "github.com/italypaleale/francis/host/local" "gorm.io/gorm" "github.com/pocket-id/pocket-id/backend/internal/appconfig" @@ -30,7 +32,8 @@ type AppConfigResolver interface { } type Dependencies struct { - DB *gorm.DB + DB *gorm.DB + Actors *local.Host Signer TokenService AuditLog AuditLogger @@ -43,12 +46,32 @@ type Module struct { handler *handler } -func New(deps Dependencies) *Module { - service := newService(deps) +func New(deps Dependencies) (*Module, error) { + // Register the actor that manages a signup token + // Each token is its own actor, whose actor ID is the token's value + err := deps.Actors.RegisterActor(SignupTokenActorType, NewSignupTokenActor) + if err != nil { + return nil, fmt.Errorf("error registering the %s actor: %w", SignupTokenActorType, err) + } + + service := newService(deps, deps.Actors.Service()) return &Module{ service: service, handler: newHandler(service, deps.AppConfig), + }, nil +} + +// RunSignupTokenMigration performs the one-time migration of the pre-actor signup tokens, then blocks until the context is canceled. +// It's meant to be started as a background service gated on the actor host being ready, since the migration needs the actor state store. +// Note that it must not return before the context is canceled, as the service runner stops the application as soon as any of its services returns. +func (m *Module) RunSignupTokenMigration(ctx context.Context) error { + err := m.service.migrateSignupTokens(ctx) + if err != nil { + return fmt.Errorf("failed to migrate signup tokens: %w", err) } + + <-ctx.Done() + return ctx.Err() } // RegisterRoutes mounts the signup and signup-token management endpoints diff --git a/backend/internal/usersignup/service.go b/backend/internal/usersignup/service.go index 26b23157..0a26f6b3 100644 --- a/backend/internal/usersignup/service.go +++ b/backend/internal/usersignup/service.go @@ -2,12 +2,15 @@ package usersignup import ( "context" - "errors" + "fmt" + "log/slog" + "sort" "strings" "time" + "github.com/google/uuid" + "github.com/italypaleale/francis/actor" "gorm.io/gorm" - "gorm.io/gorm/clause" "github.com/pocket-id/pocket-id/backend/internal/appconfig" "github.com/pocket-id/pocket-id/backend/internal/common" @@ -22,57 +25,49 @@ import ( const authenticationMethodOneTimePassword = "otp" type Service struct { - db *gorm.DB - userCreator UserCreator - signer TokenService - auditLog AuditLogger + db *gorm.DB + actorService *actor.Service + userCreator UserCreator + signer TokenService + auditLog AuditLogger } -func newService(deps Dependencies) *Service { +func newService(deps Dependencies, actorService *actor.Service) *Service { return &Service{ - db: deps.DB, - userCreator: deps.UserCreator, - signer: deps.Signer, - auditLog: deps.AuditLog, + db: deps.DB, + actorService: actorService, + userCreator: deps.UserCreator, + signer: deps.Signer, + auditLog: deps.AuditLog, } } func (s *Service) SignUp(ctx context.Context, config *appconfig.AppConfigModel, signupData signUpDto, ipAddress, userAgent string) (model.User, string, error) { - tx := s.db.Begin() - defer func() { - tx.Rollback() - }() - tokenProvided := signupData.Token != "" if config.AllowUserSignups.String() != "open" && !tokenProvided { return model.User{}, "", &common.OpenSignupDisabledError{} } - var signupToken SignupToken var userGroupIDs []string if tokenProvided { - err := tx. - WithContext(ctx). - Preload("UserGroups"). - Where("token = ?", signupData.Token). - Clauses(clause.Locking{Strength: "UPDATE"}). - First(&signupToken). - Error + // Consume the signup token by invoking its actor: this atomically validates it and increments its usage count + // Note: must invoke outside of a DB transaction, since invoking an actor while a transaction is open would deadlock on SQLite + res, err := s.actorService.Invoke(ctx, SignupTokenActorType, signupData.Token, signupTokenMethodConsume, nil) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return model.User{}, "", &common.TokenInvalidOrExpiredError{} - } - return model.User{}, "", err + return model.User{}, "", fmt.Errorf("error invoking signup token actor: %w", err) } - if !signupToken.IsValid() { + var consumeRes signupTokenConsumeResponse + err = res.Decode(&consumeRes) + if err != nil { + return model.User{}, "", fmt.Errorf("error decoding signup token actor response: %w", err) + } + + if consumeRes.Status != signupTokenConsumeOK { return model.User{}, "", &common.TokenInvalidOrExpiredError{} } - - for _, group := range signupToken.UserGroups { - userGroupIDs = append(userGroupIDs, group.ID) - } + userGroupIDs = consumeRes.UserGroupIDs } userToCreate := dto.UserCreateDto{ @@ -85,6 +80,27 @@ func (s *Service) SignUp(ctx context.Context, config *appconfig.AppConfigModel, EmailVerified: config.EmailsVerified.IsTrue(), } + // The token has now been consumed + // From this point on, if we hit an error we compensate by releasing the token (best-effort) + user, accessToken, err := s.createSignedUpUser(ctx, config, userToCreate, signupData.Token, tokenProvided, ipAddress, userAgent) + if err != nil { + if tokenProvided { + s.releaseSignupToken(ctx, signupData.Token) + } + return model.User{}, "", err + } + + return user, accessToken, nil +} + +// createSignedUpUser creates the user and issues an access token within a single transaction. +// It performs no actor calls, so it's safe to keep the transaction open for its whole duration. +func (s *Service) createSignedUpUser(ctx context.Context, config *appconfig.AppConfigModel, userToCreate dto.UserCreateDto, token string, tokenProvided bool, ipAddress, userAgent string) (model.User, string, error) { + tx := s.db.Begin() + defer func() { + tx.Rollback() + }() + user, err := s.userCreator.CreateUserInternal(ctx, config, userToCreate, false, tx) if err != nil { return model.User{}, "", err @@ -97,15 +113,8 @@ func (s *Service) SignUp(ctx context.Context, config *appconfig.AppConfigModel, if tokenProvided { s.auditLog.Create(ctx, model.AuditLogEventAccountCreated, ipAddress, userAgent, user.ID, model.AuditLogData{ - "signupToken": signupToken.Token, + "signupToken": token, }, tx) - - signupToken.UsageCount++ - - err = tx.WithContext(ctx).Save(&signupToken).Error - if err != nil { - return model.User{}, "", err - } } else { s.auditLog.Create(ctx, model.AuditLogEventAccountCreated, ipAddress, userAgent, user.ID, model.AuditLogData{ "method": "open_signup", @@ -120,6 +129,19 @@ func (s *Service) SignUp(ctx context.Context, config *appconfig.AppConfigModel, return user, accessToken, nil } +// releaseSignupToken reverts the usage count increment performed while consuming a token, used to compensate when the signup could not be completed. +// It's a best-effort compensation: if it fails (or the process crashes before it runs) we accept that a token use was consumed unnecessarily +func (s *Service) releaseSignupToken(parentCtx context.Context, token string) { + // Use a context that is not canceled when the original request ends + ctx, cancel := context.WithTimeout(context.WithoutCancel(parentCtx), 10*time.Second) + defer cancel() + + _, err := s.actorService.Invoke(ctx, SignupTokenActorType, token, signupTokenMethodRelease, nil) + if err != nil { + slog.ErrorContext(ctx, "Failed to release signup token after a failed signup", slog.Any("error", err)) + } +} + func (s *Service) SignUpInitialAdmin(ctx context.Context, config *appconfig.AppConfigModel, signUpData signUpDto) (model.User, string, error) { tx := s.db.Begin() defer func() { @@ -177,55 +199,273 @@ func (s *Service) isInitialAdminSetupCompleted(ctx context.Context, db *gorm.DB) } func (s *Service) ListSignupTokens(ctx context.Context, listRequestOptions utils.ListRequestOptions) ([]SignupToken, utils.PaginationResponse, error) { - var tokens []SignupToken - query := s.db.WithContext(ctx).Preload("UserGroups").Model(&SignupToken{}) + // Each signup token is its own actor, so we enumerate the stored states (expired ones are filtered out by the state store), then sort and paginate in memory + entries, err := s.listSignupTokenStates(ctx) + if err != nil { + return nil, utils.PaginationResponse{}, err + } - pagination, err := utils.PaginateFilterAndSort(listRequestOptions, query, &tokens) - return tokens, pagination, err + // Resolve the referenced user groups so they can be included in the response + groupsByID, err := s.loadUserGroupsByID(ctx, entries) + if err != nil { + return nil, utils.PaginationResponse{}, err + } + + tokens := make([]SignupToken, len(entries)) + for i, e := range entries { + tokens[i] = signupTokenModelFromState(e.Token, e.State, resolveUserGroups(e.State.UserGroupIDs, groupsByID)) + } + + return paginateSignupTokens(tokens, listRequestOptions) } func (s *Service) DeleteSignupToken(ctx context.Context, tokenID string) error { - return s.db.WithContext(ctx).Delete(&SignupToken{}, "id = ?", tokenID).Error + // Tokens are addressed by their value (the actor ID), while the API deletes them by ID, so we look up the matching token first + entries, err := s.listSignupTokenStates(ctx) + if err != nil { + return err + } + + for _, e := range entries { + if e.State.ID != tokenID { + continue + } + + _, err = s.actorService.Invoke(ctx, SignupTokenActorType, e.Token, SignupTokenMethodDelete, nil) + if err != nil { + return fmt.Errorf("error deleting signup token via actor: %w", err) + } + return nil + } + + // The token doesn't exist (or has expired): deleting it already reaches the desired end state + return nil +} + +// signupTokenEntry pairs a signup token's value (which is its actor ID) with its stored state +type signupTokenEntry struct { + Token string + State SignupTokenState +} + +// listSignupTokenStates returns every signup token currently stored in the actor state store. +// Expired tokens are not returned, since the state store filters out states whose TTL has passed. +func (s *Service) listSignupTokenStates(ctx context.Context) ([]signupTokenEntry, error) { + var ( + entries []signupTokenEntry + after string + ) + for { + res, err := s.actorService.ListStates(ctx, SignupTokenActorType, &actor.ListStatesOpts{ + IncludeData: true, + After: after, + }) + if err != nil { + return nil, fmt.Errorf("error listing signup token states: %w", err) + } + + for _, st := range res.States { + if st.Data == nil { + continue + } + + var state SignupTokenState + err = st.Data.Decode(&state) + if err != nil { + return nil, fmt.Errorf("error decoding state of signup token actor '%s': %w", st.ActorID, err) + } + + entries = append(entries, signupTokenEntry{ + Token: st.ActorID, + State: state, + }) + } + + // An empty cursor means we've just read the last page + after = res.AfterID() + if after == "" { + break + } + } + + return entries, nil } func (s *Service) CreateSignupToken(ctx context.Context, ttl time.Duration, usageLimit int, userGroupIDs []string) (SignupToken, error) { - signupToken, err := newSignupToken(ttl, usageLimit) - if err != nil { - return SignupToken{}, err - } - + // Load the referenced user groups to validate them and to include them in the response var userGroups []model.UserGroup - err = s.db.WithContext(ctx). - Where("id IN ?", userGroupIDs). - Find(&userGroups). - Error - if err != nil { - return SignupToken{}, err - } - signupToken.UserGroups = userGroups - - err = s.db.WithContext(ctx).Create(signupToken).Error - if err != nil { - return SignupToken{}, err + if len(userGroupIDs) > 0 { + err := s.db.WithContext(ctx). + Where("id IN ?", userGroupIDs). + Find(&userGroups). + Error + if err != nil { + return SignupToken{}, err + } } - return *signupToken, nil -} + validGroupIDs := make([]string, len(userGroups)) + for i, g := range userGroups { + validGroupIDs[i] = g.ID + } -func newSignupToken(ttl time.Duration, usageLimit int) (*SignupToken, error) { // Generate a random token randomString, err := utils.GenerateRandomAlphanumericString(16) + if err != nil { + return SignupToken{}, err + } + + now := time.Now().Round(time.Second) + state := SignupTokenState{ + ID: uuid.NewString(), + ExpiresAt: now.Add(ttl), + UsageLimit: usageLimit, + UsageCount: 0, + UserGroupIDs: validGroupIDs, + CreatedAt: now, + } + + // The token's value is the actor's ID + _, err = s.actorService.Invoke(ctx, SignupTokenActorType, randomString, SignupTokenMethodCreate, state) + if err != nil { + return SignupToken{}, fmt.Errorf("error creating signup token via actor: %w", err) + } + + return signupTokenModelFromState(randomString, state, userGroups), nil +} + +// loadUserGroupsByID loads every user group referenced by the given tokens, keyed by ID. +func (s *Service) loadUserGroupsByID(ctx context.Context, entries []signupTokenEntry) (map[string]model.UserGroup, error) { + idSet := make(map[string]struct{}) + for _, e := range entries { + for _, id := range e.State.UserGroupIDs { + idSet[id] = struct{}{} + } + } + if len(idSet) == 0 { + return map[string]model.UserGroup{}, nil + } + + ids := make([]string, 0, len(idSet)) + for id := range idSet { + ids = append(ids, id) + } + + var groups []model.UserGroup + err := s.db.WithContext(ctx). + Where("id IN ?", ids). + Find(&groups). + Error if err != nil { return nil, err } - now := time.Now().Round(time.Second) - token := &SignupToken{ - Token: randomString, - ExpiresAt: datatype.DateTime(now.Add(ttl)), - UsageLimit: usageLimit, - UsageCount: 0, + byID := make(map[string]model.UserGroup, len(groups)) + for _, g := range groups { + byID[g.ID] = g } - return token, nil + return byID, nil +} + +// resolveUserGroups maps the given group IDs to the corresponding UserGroup objects, preserving order and skipping any that no longer exist. +func resolveUserGroups(ids []string, byID map[string]model.UserGroup) []model.UserGroup { + if len(ids) == 0 { + return nil + } + + groups := make([]model.UserGroup, 0, len(ids)) + for _, id := range ids { + g, ok := byID[id] + if ok { + groups = append(groups, g) + } + } + + return groups +} + +// signupTokenModelFromState builds the API/model representation of a signup token from its actor ID (the token's value) and stored state. +func signupTokenModelFromState(token string, state SignupTokenState, groups []model.UserGroup) SignupToken { + return SignupToken{ + Base: model.Base{ + ID: state.ID, + CreatedAt: datatype.DateTime(state.CreatedAt), + }, + Token: token, + ExpiresAt: datatype.DateTime(state.ExpiresAt), + UsageLimit: state.UsageLimit, + UsageCount: state.UsageCount, + UserGroups: groups, + } +} + +// paginateSignupTokens sorts and paginates the in-memory list of signup tokens, mirroring the behavior of the DB-backed pagination utility. +func paginateSignupTokens(tokens []SignupToken, params utils.ListRequestOptions) ([]SignupToken, utils.PaginationResponse, error) { + sortSignupTokens(tokens, params.Sort.Column, params.Sort.Direction) + + page := max(params.Pagination.Page, 1) + pageSize := params.Pagination.Limit + switch { + case pageSize < 1: + pageSize = 20 + case pageSize > 100: + pageSize = 100 + } + + totalItems := int64(len(tokens)) + totalPages := (totalItems + int64(pageSize) - 1) / int64(pageSize) + if totalItems == 0 { + totalPages = 1 + } + if int64(page) > totalPages { + page = int(totalPages) + } + + start := min((page-1)*pageSize, len(tokens)) + end := min(start+pageSize, len(tokens)) + + return tokens[start:end], utils.PaginationResponse{ + TotalPages: totalPages, + TotalItems: totalItems, + CurrentPage: page, + ItemsPerPage: pageSize, + }, nil +} + +// sortSignupTokens sorts the tokens by the given column and direction. +// It defaults to sorting by creation date ascending, matching the DB-backed listing. +func sortSignupTokens(tokens []SignupToken, column, direction string) { + desc := utils.NormalizeSortDirection(direction) == "desc" + + less := func(i, j int) bool { + caI := time.Time(tokens[i].CreatedAt) + caJ := time.Time(tokens[j].CreatedAt) + return caI.Before(caJ) + } + switch column { + case "expiresAt": + less = func(i, j int) bool { + eaI := time.Time(tokens[i].ExpiresAt) + eaJ := time.Time(tokens[j].ExpiresAt) + return eaI.Before(eaJ) + } + case "usageLimit": + less = func(i, j int) bool { return tokens[i].UsageLimit < tokens[j].UsageLimit } + case "usageCount": + less = func(i, j int) bool { + return tokens[i].UsageCount < tokens[j].UsageCount + } + case "createdAt", "": + // Use the default comparator (creation date) + default: + // Unknown or non-sortable column: keep the default (creation date) ordering + } + + sort.SliceStable(tokens, func(i, j int) bool { + if desc { + return less(j, i) + } + return less(i, j) + }) } diff --git a/backend/internal/usersignup/service_test.go b/backend/internal/usersignup/service_test.go new file mode 100644 index 00000000..bc3a424b --- /dev/null +++ b/backend/internal/usersignup/service_test.go @@ -0,0 +1,127 @@ +package usersignup + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/require" + "gorm.io/gorm" + + "github.com/pocket-id/pocket-id/backend/internal/appconfig" + "github.com/pocket-id/pocket-id/backend/internal/common" + "github.com/pocket-id/pocket-id/backend/internal/dto" + "github.com/pocket-id/pocket-id/backend/internal/model" + "github.com/pocket-id/pocket-id/backend/internal/utils" + testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing" +) + +type fakeUserCreator struct { + err error + user model.User +} + +func (f fakeUserCreator) CreateUserInternal(_ context.Context, _ *appconfig.AppConfigModel, _ dto.UserCreateDto, _ bool, _ *gorm.DB) (model.User, error) { + if f.err != nil { + return model.User{}, f.err + } + return f.user, nil +} + +type fakeSigner struct{} + +func (fakeSigner) GenerateAccessToken(_ model.User, _ string, _ time.Duration) (string, error) { + return "access-token", nil +} + +type fakeAuditLogger struct{} + +func (fakeAuditLogger) Create(_ context.Context, _ model.AuditLogEvent, _, _, _ string, _ model.AuditLogData, _ *gorm.DB) (model.AuditLog, bool) { + return model.AuditLog{}, true +} + +func newSignupServiceForTest(t *testing.T, db *gorm.DB, userCreator UserCreator) *Service { + t.Helper() + actorService := newSignupTokenActorService(t) + return newService(Dependencies{ + DB: db, + UserCreator: userCreator, + Signer: fakeSigner{}, + AuditLog: fakeAuditLogger{}, + }, actorService) +} + +func signupTokenUsageCount(t *testing.T, svc *Service, tokenID string) int { + t.Helper() + tokens, _, err := svc.ListSignupTokens(t.Context(), listAllOptions()) + require.NoError(t, err) + for _, tok := range tokens { + if tok.ID == tokenID { + return tok.UsageCount + } + } + t.Fatalf("signup token %q not found", tokenID) + return 0 +} + +func TestSignUpConsumesTokenOnSuccess(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + svc := newSignupServiceForTest(t, db, fakeUserCreator{user: model.User{Base: model.Base{ID: "new-user"}}}) + + token, err := svc.CreateSignupToken(t.Context(), time.Hour, 2, nil) + require.NoError(t, err) + + config := appconfig.NewTestConfig(nil) + user, accessToken, err := svc.SignUp(t.Context(), config, signUpDto{ + Username: "newuser", + Token: token.Token, + }, "1.2.3.4", "test-agent") + require.NoError(t, err) + require.Equal(t, "new-user", user.ID) + require.Equal(t, "access-token", accessToken) + + // The token's usage count must have been incremented and not rolled back + require.Equal(t, 1, signupTokenUsageCount(t, svc, token.ID)) +} + +func TestSignUpCompensatesTokenOnFailure(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + boom := errors.New("could not create user") + svc := newSignupServiceForTest(t, db, fakeUserCreator{err: boom}) + + token, err := svc.CreateSignupToken(t.Context(), time.Hour, 2, nil) + require.NoError(t, err) + + config := appconfig.NewTestConfig(nil) + _, _, err = svc.SignUp(t.Context(), config, signUpDto{ + Username: "newuser", + Token: token.Token, + }, "1.2.3.4", "test-agent") + require.ErrorIs(t, err, boom) + + // The usage count increment must have been compensated (reverted back to 0) + require.Equal(t, 0, signupTokenUsageCount(t, svc, token.ID)) +} + +func TestSignUpRejectsInvalidToken(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + svc := newSignupServiceForTest(t, db, fakeUserCreator{user: model.User{Base: model.Base{ID: "new-user"}}}) + + config := appconfig.NewTestConfig(nil) + _, _, err := svc.SignUp(t.Context(), config, signUpDto{ + Username: "newuser", + Token: "not-a-real-token", + }, "1.2.3.4", "test-agent") + + var invalidErr *common.TokenInvalidOrExpiredError + require.ErrorAs(t, err, &invalidErr) +} + +// listAllOptions returns list options that return every token on a single page. +func listAllOptions() utils.ListRequestOptions { + var opts utils.ListRequestOptions + opts.Pagination.Page = 1 + opts.Pagination.Limit = 100 + return opts +} diff --git a/backend/resources/migrations/postgres/20260723000000_actor_tokens.down.sql b/backend/resources/migrations/postgres/20260723000000_actor_tokens.down.sql new file mode 100644 index 00000000..516579fb --- /dev/null +++ b/backend/resources/migrations/postgres/20260723000000_actor_tokens.down.sql @@ -0,0 +1,56 @@ +-- Recreate the one_time_access_tokens table with the schema it had before it was dropped. +CREATE TABLE one_time_access_tokens +( + id UUID NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ, + token VARCHAR(255) NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE, + device_token VARCHAR(16) +); +CREATE INDEX IF NOT EXISTS idx_one_time_access_tokens_expires_at ON one_time_access_tokens (expires_at); + +-- Recreate the signup token tables with the schema they had before they were frozen. +CREATE TABLE signup_tokens ( + id UUID NOT NULL PRIMARY KEY, + created_at TIMESTAMPTZ NOT NULL, + token VARCHAR(255) NOT NULL UNIQUE, + expires_at TIMESTAMPTZ NOT NULL, + usage_limit INTEGER NOT NULL DEFAULT 1, + usage_count INTEGER NOT NULL DEFAULT 0 +); +CREATE INDEX idx_signup_tokens_token ON signup_tokens(token); +CREATE INDEX idx_signup_tokens_expires_at ON signup_tokens(expires_at); + +CREATE TABLE signup_tokens_user_groups +( + signup_token_id UUID NOT NULL, + user_group_id UUID NOT NULL, + PRIMARY KEY (signup_token_id, user_group_id), + FOREIGN KEY (signup_token_id) REFERENCES signup_tokens (id) ON DELETE CASCADE, + FOREIGN KEY (user_group_id) REFERENCES user_groups (id) ON DELETE CASCADE +); + +-- Restore the signup tokens from the frozen JSON document stored in the "kv" table. +-- json_array_elements expands the JSON array into one row per token object. +INSERT INTO signup_tokens (id, created_at, token, expires_at, usage_limit, usage_count) +SELECT + (e ->> 'id')::uuid, + to_timestamp((e ->> 'createdAt')::bigint), + e ->> 'token', + to_timestamp((e ->> 'expiresAt')::bigint), + (e ->> 'usageLimit')::int, + (e ->> 'usageCount')::int +FROM kv, json_array_elements(kv."value"::json) AS e +WHERE kv."key" = 'signup_tokens_migrated'; + +-- Restore the token/user-group associations, expanding each token's nested userGroupIds array. +INSERT INTO signup_tokens_user_groups (signup_token_id, user_group_id) +SELECT + (e ->> 'id')::uuid, + g.value::uuid +FROM kv, json_array_elements(kv."value"::json) AS e, json_array_elements_text(e -> 'userGroupIds') AS g +WHERE kv."key" = 'signup_tokens_migrated'; + +-- Remove the frozen signup tokens from the "kv" table. +DELETE FROM kv WHERE "key" = 'signup_tokens_migrated'; diff --git a/backend/resources/migrations/postgres/20260723000000_actor_tokens.up.sql b/backend/resources/migrations/postgres/20260723000000_actor_tokens.up.sql new file mode 100644 index 00000000..bb5cd847 --- /dev/null +++ b/backend/resources/migrations/postgres/20260723000000_actor_tokens.up.sql @@ -0,0 +1,28 @@ +-- One-time access tokens are now stored in the actor state store, so the table is no longer needed. +DROP TABLE IF EXISTS one_time_access_tokens; + +-- Freeze the signup tokens. +-- Encode every signup token (with its user group IDs) as a single JSON array and store it in the "kv" table under the "signup_tokens_migrated" key, so the singleton signup token actor can seed its state from it on first startup. +-- The "HAVING count(*) > 0" clause ensures nothing is written to the "kv" table when there are no signup tokens. +-- Timestamps are stored as Unix seconds so the frozen format is identical across databases. +INSERT INTO kv ("key", "value") +SELECT 'signup_tokens_migrated', json_agg( + json_build_object( + 'id', st.id, + 'token', st.token, + 'expiresAt', extract(epoch FROM st.expires_at)::bigint, + 'usageLimit', st.usage_limit, + 'usageCount', st.usage_count, + 'createdAt', extract(epoch FROM st.created_at)::bigint, + 'userGroupIds', COALESCE( + (SELECT json_agg(stug.user_group_id) FROM signup_tokens_user_groups stug WHERE stug.signup_token_id = st.id), + '[]'::json + ) + ) +)::text +FROM signup_tokens st +HAVING count(*) > 0; + +-- Drop the now-frozen signup token tables. +DROP TABLE signup_tokens_user_groups; +DROP TABLE signup_tokens; diff --git a/backend/resources/migrations/sqlite/20260723000000_actor_tokens.down.sql b/backend/resources/migrations/sqlite/20260723000000_actor_tokens.down.sql new file mode 100644 index 00000000..b7627995 --- /dev/null +++ b/backend/resources/migrations/sqlite/20260723000000_actor_tokens.down.sql @@ -0,0 +1,62 @@ +PRAGMA foreign_keys=OFF; +BEGIN; + +-- Recreate the one_time_access_tokens table with the schema it had before it was dropped. +CREATE TABLE one_time_access_tokens +( + id TEXT PRIMARY KEY, + created_at DATETIME NOT NULL, + token TEXT NOT NULL UNIQUE, + expires_at DATETIME NOT NULL, + user_id TEXT NOT NULL REFERENCES users ON DELETE CASCADE, + device_token TEXT +); +CREATE INDEX IF NOT EXISTS idx_one_time_access_tokens_expires_at ON one_time_access_tokens (expires_at); + +-- Recreate the signup token tables with the schema they had before they were frozen. +CREATE TABLE signup_tokens ( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME NOT NULL, + token TEXT NOT NULL UNIQUE, + expires_at DATETIME NOT NULL, + usage_limit INTEGER NOT NULL DEFAULT 1, + usage_count INTEGER NOT NULL DEFAULT 0 +); +CREATE INDEX idx_signup_tokens_token ON signup_tokens(token); +CREATE INDEX idx_signup_tokens_expires_at ON signup_tokens(expires_at); + +CREATE TABLE signup_tokens_user_groups +( + signup_token_id TEXT NOT NULL, + user_group_id TEXT NOT NULL, + PRIMARY KEY (signup_token_id, user_group_id), + FOREIGN KEY (signup_token_id) REFERENCES signup_tokens (id) ON DELETE CASCADE, + FOREIGN KEY (user_group_id) REFERENCES user_groups (id) ON DELETE CASCADE +); + +-- Restore the signup tokens from the frozen JSON document stored in the "kv" table. +-- json_each expands the JSON array into one row per token object. +INSERT INTO signup_tokens (id, created_at, token, expires_at, usage_limit, usage_count) +SELECT + json_extract(e.value, '$.id'), + json_extract(e.value, '$.createdAt'), + json_extract(e.value, '$.token'), + json_extract(e.value, '$.expiresAt'), + json_extract(e.value, '$.usageLimit'), + json_extract(e.value, '$.usageCount') +FROM kv, json_each(kv."value") AS e +WHERE kv."key" = 'signup_tokens_migrated'; + +-- Restore the token/user-group associations, expanding each token's nested userGroupIds array. +INSERT INTO signup_tokens_user_groups (signup_token_id, user_group_id) +SELECT + json_extract(e.value, '$.id'), + g.value +FROM kv, json_each(kv."value") AS e, json_each(json_extract(e.value, '$.userGroupIds')) AS g +WHERE kv."key" = 'signup_tokens_migrated'; + +-- Remove the frozen signup tokens from the "kv" table. +DELETE FROM kv WHERE "key" = 'signup_tokens_migrated'; + +COMMIT; +PRAGMA foreign_keys=ON; diff --git a/backend/resources/migrations/sqlite/20260723000000_actor_tokens.up.sql b/backend/resources/migrations/sqlite/20260723000000_actor_tokens.up.sql new file mode 100644 index 00000000..ddaff1cf --- /dev/null +++ b/backend/resources/migrations/sqlite/20260723000000_actor_tokens.up.sql @@ -0,0 +1,35 @@ +PRAGMA foreign_keys=OFF; +BEGIN; + +-- One-time access tokens are now stored in the actor state store, so the table is no longer needed. +DROP TABLE IF EXISTS one_time_access_tokens; + +-- Freeze the signup tokens. +-- Encode every signup token (with its user group IDs) as a single JSON array and store it in the "kv" table under the "signup_tokens_migrated" key, so the singleton signup token actor can seed its state from it on first startup. +-- The "HAVING count(*) > 0" clause ensures nothing is written to the "kv" table when there are no signup tokens. +-- Timestamps are stored as Unix seconds, matching how DateTime values are persisted on SQLite. +INSERT INTO kv ("key", "value") +SELECT 'signup_tokens_migrated', json_group_array( + json_object( + 'id', st.id, + 'token', st.token, + 'expiresAt', st.expires_at, + 'usageLimit', st.usage_limit, + 'usageCount', st.usage_count, + 'createdAt', st.created_at, + 'userGroupIds', json(( + SELECT COALESCE(json_group_array(stug.user_group_id), json_array()) + FROM signup_tokens_user_groups stug + WHERE stug.signup_token_id = st.id + )) + ) +) +FROM signup_tokens st +HAVING count(*) > 0; + +-- Drop the now-frozen signup token tables. +DROP TABLE signup_tokens_user_groups; +DROP TABLE signup_tokens; + +COMMIT; +PRAGMA foreign_keys=ON; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 00000000..0e4bc1f7 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5429 @@ +{ + "name": "pocket-id-frontend", + "version": "2.11.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pocket-id-frontend", + "version": "2.11.0", + "hasInstallScript": true, + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", + "@opentelemetry/resources": "^2.8.0", + "@opentelemetry/sdk-trace-web": "^2.8.0", + "@opentelemetry/semantic-conventions": "^1.41.1", + "@simplewebauthn/browser": "^13.3.0", + "@tailwindcss/vite": "^4.3.0", + "axios": "^1.16.1", + "clsx": "^2.1.1", + "date-fns": "^4.2.1", + "qrcode": "^1.5.4", + "runed": "^0.37.1", + "sveltekit-superforms": "^2.30.1", + "tailwind-merge": "^3.6.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "@inlang/paraglide-js": "^2.18.0", + "@inlang/plugin-m-function-matcher": "^2.2.6", + "@inlang/plugin-message-format": "^4.4.0", + "@internationalized/date": "^3.12.1", + "@lucide/svelte": "^1.16.0", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.60.1", + "@sveltejs/vite-plugin-svelte": "^7.1.2", + "@types/node": "^25.9.0", + "@types/qrcode": "^1.5.6", + "bits-ui": "^2.18.1", + "eslint": "^10.4.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.17.1", + "formsnap": "^2.0.1", + "globals": "^17.6.0", + "mode-watcher": "^1.1.0", + "prettier": "^3.8.3", + "prettier-plugin-svelte": "^3.5.2", + "prettier-plugin-tailwindcss": "^0.8.0", + "shadcn-svelte": "^1.3.0", + "svelte": "^5.55.8", + "svelte-check": "^4.4.8", + "svelte-sonner": "^1.1.1", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.3.0", + "tslib": "^2.8.1", + "tw-animate-css": "^1.4.0", + "typescript": "^6.0.3", + "typescript-eslint": "^8.59.4", + "vite": "^8.0.16", + "vite-plugin-compression": "^0.5.1" + } + }, + "node_modules/@ark/schema": { + "version": "0.56.2", + "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.56.2.tgz", + "integrity": "sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@ark/util": "0.56.2" + } + }, + "node_modules/@ark/util": { + "version": "0.56.2", + "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.56.2.tgz", + "integrity": "sha512-9kU2sUE38FZEGG7l3hamYMBieLYEJh2L1mrYD2eXpT+78EnQSV1bhjxJhnxGBMSTbtwpBSDNSK+K60WvaI/DTQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "license": "MIT", + "optional": true + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inlang/paraglide-js": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/@inlang/paraglide-js/-/paraglide-js-2.22.0.tgz", + "integrity": "sha512-GSzG7KEKcYAhwuPNJczIPB+DzyndYxr4lsXAkkB7xh00jTrt80NF2KfgjEAkxuJvWcnscqXf7y7d1Q0SWtSu7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inlang/recommend-sherlock": "^0.2.1", + "@inlang/sdk": "^2.10.0", + "commander": "11.1.0", + "consola": "3.4.0", + "json5": "2.2.3", + "unplugin": "^2.1.2", + "urlpattern-polyfill": "^10.0.0" + }, + "bin": { + "paraglide-js": "bin/run.js" + }, + "peerDependencies": { + "typescript": ">=5.6", + "vite": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@inlang/plugin-m-function-matcher": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@inlang/plugin-m-function-matcher/-/plugin-m-function-matcher-2.2.9.tgz", + "integrity": "sha512-FqrEw6p5UKn0fVLedILx5vjEAPAl01Uuv1xlF6pQ9XMxpDsTUSOAPrjWuus1jiPBnYEdbK8cP43Ni0e3IPz9aQ==", + "dev": true, + "dependencies": { + "@inlang/sdk": "2.10.2" + } + }, + "node_modules/@inlang/plugin-message-format": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@inlang/plugin-message-format/-/plugin-message-format-4.4.0.tgz", + "integrity": "sha512-n4aXt6XVg5kxhKoLAhi9nMgZtCA9iS0QOaXte56VqxWHcfj9O4c4gOkyVQZH7H9D8h7OZufCrO1sZGYOypPwEA==", + "dev": true, + "dependencies": { + "flat": "^6.0.1" + } + }, + "node_modules/@inlang/recommend-sherlock": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@inlang/recommend-sherlock/-/recommend-sherlock-0.2.1.tgz", + "integrity": "sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "comment-json": "^4.2.3" + } + }, + "node_modules/@inlang/sdk": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@inlang/sdk/-/sdk-2.10.2.tgz", + "integrity": "sha512-O1ki72SNK6LPagaGrvlioBb1mWKvump7cO7P85hfGZjdFTmDdn3icI0A6MvaBsB3P9KQHAjzyubnN1OslGufTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@lix-js/sdk": "0.4.10", + "@sinclair/typebox": "^0.31.17", + "kysely": "^0.28.12", + "sqlite-wasm-kysely": "0.3.0", + "uuid": "^14.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@internationalized/date": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.2.tgz", + "integrity": "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lix-js/sdk": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/@lix-js/sdk/-/sdk-0.4.10.tgz", + "integrity": "sha512-0dMInAJK/67guTG5rRZaCEhvzC5cCXENOjaePA5AqMXrCE97kaY7SRor9e2vnoGsFIiGqXKlT0MCIoZj36G0gg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@lix-js/server-protocol-schema": "0.1.1", + "dedent": "1.5.1", + "human-id": "^4.1.1", + "js-sha256": "^0.11.0", + "kysely": "^0.28.12", + "sqlite-wasm-kysely": "0.3.0", + "uuid": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@lix-js/server-protocol-schema": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@lix-js/server-protocol-schema/-/server-protocol-schema-0.1.1.tgz", + "integrity": "sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@lucide/svelte": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.25.0.tgz", + "integrity": "sha512-v9m+dD68jxVnqkU3K59mG/RSRFlPGzmKCGSyMfnXcaGv9jODDQMyQkcp1CGvk3Y/cUj9v7f8rw1n//K0B53xGQ==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "svelte": "^5" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.9.0.tgz", + "integrity": "sha512-m2nckMT80NnmjTYSPjJQObBJ+8dgkoajEOUbznL8AHZ3T3yHRk2P7gI1PhEBc1+lOnrYE9UWrWHqJDsmqjmNbw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-http": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.220.0.tgz", + "integrity": "sha512-/+ExB3lRkf+erv4PnoywyL7RHKITidxtUpUTS55k7OQ0dB42S7gEF1gry7swb9MSm1hYLUhJg4QQh9W8SpwwqA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-exporter-base": "0.220.0", + "@opentelemetry/otlp-transformer": "0.220.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-exporter-base": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.220.0.tgz", + "integrity": "sha512-CXYo8UD5Mn9YbgebO2EL4wejtA+gxLmLiu6HCk2KH2BR7XhFN6/6p1UlCb23DYCjeYkndevLHuejCCN1yx4+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/otlp-transformer": "0.220.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.220.0.tgz", + "integrity": "sha512-lXGrv7KXZ0gNH9SVNUaa6vv6phVYGvJxfXAlMbzbakiXru75f5MZl8Z7oqiMMQD77riVHJCFlQvbZs/VVN2/4A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-logs": "0.220.0", + "@opentelemetry/sdk-metrics": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", + "integrity": "sha512-jyA5MBLQ+Dkl3+JsZkUoUvL7yHvU64kLsvpXKarWm6347Sl1t1bXFTFykUePNpT5WH5pm9a2Qtt03iIYQhZ1Fg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.220.0.tgz", + "integrity": "sha512-WywcTkQtv2iNmt+6y5Kcd4rzvx9bLVsBa2Nwcmg01IUaBTkTow3W4d9KE5vNBpEDtb9tp21WcRBY/lANRrApYA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.4.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", + "integrity": "sha512-Xx8RGS4H5XEBl01WuCreMIpiah9cCXMbSkeuIePPdD2cUpq/vUzYmj8E/MK1OsbOc93FuAD4jfn2WOacKwLn7Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.9.0.tgz", + "integrity": "sha512-sGA19HvtrrSKYsseHphluH6j3p6Xa3fqc7c7y8f/7mYWejc1lyDFcpSdD1kYa50HCLUeEo4zA5bW0pniaPszuw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.9.0.tgz", + "integrity": "sha512-cp9zmTl62R8PJrpvFcmc8N2JQU/xfa0S+61q511Nji+QxCfZ8Ifvg7H27G8cANe4crg4RTrWsVvanHiXjSp6ag==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/resources": "2.9.0", + "@opentelemetry/sdk-trace": "2.9.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-web": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-web/-/sdk-trace-web-2.9.0.tgz", + "integrity": "sha512-LS4XlzOK3e6YYdt84m15AmRR04121rKipmifi4XFZToH1h75f7F2bzHLbB1NMgIEYJ0jSKYm4VsK5mws/5kfTQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.9.0", + "@opentelemetry/sdk-trace-base": "2.9.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.139.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", + "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "license": "MIT" + }, + "node_modules/@poppinss/macroable": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@poppinss/macroable/-/macroable-1.1.2.tgz", + "integrity": "sha512-FAVBRzzWhYP5mA3lCwLH1A0fKBqq5anyjGet90Z81aRK5c/+LTGUE1zJhZrErjaenBSOOI9BVUs3WVmotneFQA==", + "license": "MIT", + "optional": true + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", + "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", + "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", + "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", + "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", + "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", + "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", + "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", + "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", + "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", + "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", + "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", + "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", + "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", + "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", + "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@simplewebauthn/browser": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", + "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.31.30", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.31.30.tgz", + "integrity": "sha512-MGsM7bVmHg3sUKCphlu3SGQ+T+5JTbygZWYArfKQCW5anJeACHeqLhcnf+R7XlCWZ+QR3C8JTBx3kCKJTN69ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sqlite.org/sqlite-wasm": { + "version": "3.48.0-build4", + "resolved": "https://registry.npmjs.org/@sqlite.org/sqlite-wasm/-/sqlite-wasm-3.48.0-build4.tgz", + "integrity": "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "sqlite-wasm": "bin/index.js" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", + "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.10.tgz", + "integrity": "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.70.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.70.1.tgz", + "integrity": "sha512-nY9SPHGOZro3doud9vZXDBwl9tCZIouuJztjgSHs6PAIrv9M/z5O7eOhPV5xU7CgVHA976Jwu3BA1hIFvXztkA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.9", + "@types/cookie": "^0.6.0", + "acorn": "^8.16.0", + "cookie": "^0.6.0", + "devalue": "^5.8.1", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.0.tgz", + "integrity": "sha512-1LgZ/qUqSoq+QorD83lk2hka79Px0wXNW2q5V1nZlxGhQgw1jrsIbVz5YiCeucVLo4XvFLjXukUaQjIiqowkcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.2.0.tgz", + "integrity": "sha512-1SpkuMSRLfugrVX+IrKfE1RUegzo8AQzKQ6qQPfVzbcWi5IhuTPaKb5ZrLpucleFznkc4/RTeSPoRnGWFxX+EQ==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT", + "optional": true + }, + "node_modules/@typeschema/class-validator": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@typeschema/class-validator/-/class-validator-0.3.0.tgz", + "integrity": "sha512-OJSFeZDIQ8EK1HTljKLT5CItM2wsbgczLN8tMEfz3I1Lmhc5TBfkZ0eikFzUC16tI3d1Nag7um6TfCgp2I2Bww==", + "license": "MIT", + "optional": true, + "dependencies": { + "@typeschema/core": "0.14.0" + }, + "peerDependencies": { + "class-validator": "^0.14.1" + }, + "peerDependenciesMeta": { + "class-validator": { + "optional": true + } + } + }, + "node_modules/@typeschema/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@typeschema/core/-/core-0.14.0.tgz", + "integrity": "sha512-Ia6PtZHcL3KqsAWXjMi5xIyZ7XMH4aSnOQes8mfMLx+wGFGtGRNlwe6Y7cYvX+WfNK67OL0/HSe9t8QDygV0/w==", + "license": "MIT", + "optional": true, + "peerDependencies": { + "@types/json-schema": "^7.0.15" + }, + "peerDependenciesMeta": { + "@types/json-schema": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz", + "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/type-utils": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.65.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz", + "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz", + "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.65.0", + "@typescript-eslint/types": "^8.65.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz", + "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz", + "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz", + "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", + "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz", + "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.65.0", + "@typescript-eslint/tsconfig-utils": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/visitor-keys": "8.65.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz", + "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.65.0", + "@typescript-eslint/types": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz", + "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.65.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@valibot/to-json-schema": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@valibot/to-json-schema/-/to-json-schema-1.7.1.tgz", + "integrity": "sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A==", + "license": "MIT", + "optional": true, + "peerDependencies": { + "valibot": "^1.4.0" + } + }, + "node_modules/@vinejs/compiler": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vinejs/compiler/-/compiler-3.0.0.tgz", + "integrity": "sha512-v9Lsv59nR56+bmy2p0+czjZxsLHwaibJ+SV5iK9JJfehlJMa501jUJQqqz4X/OqKXrxtE3uTQmSqjUqzF3B2mw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@vinejs/vine": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@vinejs/vine/-/vine-3.0.1.tgz", + "integrity": "sha512-ZtvYkYpZOYdvbws3uaOAvTFuvFXoQGAtmzeiXu+XSMGxi5GVsODpoI9Xu9TplEMuD/5fmAtBbKb9cQHkWkLXDQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@poppinss/macroable": "^1.0.4", + "@types/validator": "^13.12.2", + "@vinejs/compiler": "^3.0.0", + "camelcase": "^8.0.0", + "dayjs": "^1.11.13", + "dlv": "^1.1.3", + "normalize-url": "^8.0.1", + "validator": "^13.12.0" + }, + "engines": { + "node": ">=18.16.0" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arkregex": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/arkregex/-/arkregex-0.0.8.tgz", + "integrity": "sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@ark/util": "0.56.2" + } + }, + "node_modules/arktype": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.2.3.tgz", + "integrity": "sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@ark/schema": "0.56.2", + "@ark/util": "0.56.2", + "arkregex": "0.0.8" + } + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bits-ui": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.1.tgz", + "integrity": "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" + } + }, + "node_modules/bits-ui/node_modules/runed": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", + "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + } + } + }, + "node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/class-validator": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", + "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/validator": "^13.15.3", + "libphonenumber-js": "^1.11.1", + "validator": "^13.15.22" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/comment-json": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.6.2.tgz", + "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/consola": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.0.tgz", + "integrity": "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT", + "optional": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.2.tgz", + "integrity": "sha512-DObPPAfdtFbXjxLqK8s2Xk9ZuWz5+ZoFEhC7J76es4GU/rEiXwHTmbImoCdyoCOcBH1UF3+Cz6Z2sYD4hyl5TA==", + "license": "MIT" + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT", + "optional": true + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/effect": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.22.0.tgz", + "integrity": "sha512-jhYFe0zTlIRqYFrKTS+6luhmS/Tm0f+JLo0K9KUxvtFab1SUGEszQi2ehOP6QzAZvy831lDmTwwzvVDZSPNz3g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.3.tgz", + "integrity": "sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.7.0.tgz", + "integrity": "sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.22.0.tgz", + "integrity": "sha512-O3qn0NePTWta+1o25dIThqeEP/hEQ3VxDK2LVO8SQ5wG9umLMvulK+m1yQ4JGOb2Pkl8IB0G1lpRV/HXDXSLTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.6.1", + "@jridgewell/sourcemap-codec": "^1.5.0", + "esutils": "^2.0.3", + "globals": "^16.0.0", + "known-css-properties": "^0.37.0", + "postcss": "^8.4.49", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.7.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-svelte/node_modules/globals": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz", + "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/flat/-/flat-6.0.1.tgz", + "integrity": "sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formsnap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/formsnap/-/formsnap-2.0.1.tgz", + "integrity": "sha512-iJSe4YKd/W6WhLwKDVJU9FQeaJRpEFuolhju7ZXlRpUVyDdqFdMP8AUBICgnVvQPyP41IPAlBa/v0Eo35iE6wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "svelte-toolbelt": "^0.5.0" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "sveltekit-superforms": "^2.19.0" + } + }, + "node_modules/formsnap/node_modules/svelte-toolbelt": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.5.0.tgz", + "integrity": "sha512-t3tenZcnfQoIeRuQf/jBU7bvTeT3TGkcEE+1EUr5orp0lR7NEpprflpuie3x9Dn0W9nOKqs3HwKGJeeN5Ok1sQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0-next.126" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.7.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", + "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-id": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.2.0.tgz", + "integrity": "sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/joi": { + "version": "17.13.4", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.4.tgz", + "integrity": "sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-sha256": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz", + "integrity": "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/kysely": { + "version": "0.28.17", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.17.tgz", + "integrity": "sha512-nbD8lB9EB3wNdMhOCdx5Li8DxnLbvKByylRLcJ1h+4SkrowVeECAyZlyiKMThF7xFdRz0jSQ2MoJr+wXux2y0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.13.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.9.tgz", + "integrity": "sha512-VNS5vWMM7r0P66BYv+TQJATxExEgLxN+34hfHDVhDkUsGAE4cRg0shCNSLTXNKm7nIUscC7AfB51TjxEeF7msQ==", + "license": "MIT", + "optional": true + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-weak": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/memoize-weak/-/memoize-weak-1.0.2.tgz", + "integrity": "sha512-gj39xkrjEw7nCn4nJ1M5ms6+MyMlyiGmttzsqAUsAKn6bYKwuTHh/AO3cKPF8IBrTIYTxb0wWXFs3E//Y8VoWQ==", + "license": "ISC" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mode-watcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz", + "integrity": "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "runed": "^0.25.0", + "svelte-toolbelt": "^0.7.1" + }, + "peerDependencies": { + "svelte": "^5.27.0" + } + }, + "node_modules/mode-watcher/node_modules/runed": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.25.0.tgz", + "integrity": "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/mode-watcher/node_modules/svelte-toolbelt": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz", + "integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.23.2", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/mode-watcher/node_modules/svelte-toolbelt/node_modules/runed": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz", + "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.20", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", + "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", + "integrity": "sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.5.tgz", + "integrity": "sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.2.tgz", + "integrity": "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.8.1.tgz", + "integrity": "sha512-iaFMYqDsE4ffdDkn5qup0j5f2aCEBFZrdrZnvu9QKTlWx/iGPeQ4HHu7b7fCPMxeo9nwQBiOAh2nSypdFYWJkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT", + "optional": true + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/rolldown": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", + "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.139.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.5", + "@rolldown/binding-darwin-arm64": "1.1.5", + "@rolldown/binding-darwin-x64": "1.1.5", + "@rolldown/binding-freebsd-x64": "1.1.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", + "@rolldown/binding-linux-arm64-gnu": "1.1.5", + "@rolldown/binding-linux-arm64-musl": "1.1.5", + "@rolldown/binding-linux-ppc64-gnu": "1.1.5", + "@rolldown/binding-linux-s390x-gnu": "1.1.5", + "@rolldown/binding-linux-x64-gnu": "1.1.5", + "@rolldown/binding-linux-x64-musl": "1.1.5", + "@rolldown/binding-openharmony-arm64": "1.1.5", + "@rolldown/binding-wasm32-wasi": "1.1.5", + "@rolldown/binding-win32-arm64-msvc": "1.1.5", + "@rolldown/binding-win32-x64-msvc": "1.1.5" + } + }, + "node_modules/runed": { + "version": "0.37.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.37.1.tgz", + "integrity": "sha512-MeFY73xBW8IueWBm012nNFIGy19WUGPLtknavyUPMpnyt350M47PhGSGrGoSLbidwn+Zlt/O0cp8/OZE3LASWA==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0", + "zod": "^4.1.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-cookie-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", + "license": "MIT" + }, + "node_modules/shadcn-svelte": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/shadcn-svelte/-/shadcn-svelte-1.4.2.tgz", + "integrity": "sha512-j7oDhXRmFuZ8bAhvF7Y65moMi9qM0iA68j5wEDNBiH0be9NWOEg1qo7myDeIf+h2oUSLBYHpVmeJIKYBmNT4Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.0", + "node-fetch-native": "^1.6.4", + "postcss": "^8.5.10", + "tailwind-merge": "^3.0.0" + }, + "bin": { + "shadcn-svelte": "dist/index.mjs" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/shadcn-svelte/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sqlite-wasm-kysely": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/sqlite-wasm-kysely/-/sqlite-wasm-kysely-0.3.0.tgz", + "integrity": "sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==", + "dev": true, + "dependencies": { + "@sqlite.org/sqlite-wasm": "^3.48.0-build2" + }, + "peerDependencies": { + "kysely": "*" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svelte": { + "version": "5.56.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.7.tgz", + "integrity": "sha512-5qERUZX80oQj6XrDMUmD2Uhd/cIpCPDWWKBK3ZHmyRUC9apPyamWM8xMo31mbWsIQxwG2hVoSnOJ/EcnhVkkzQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.10", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.8.1", + "esm-env": "^1.2.1", + "esrap": "^2.2.12", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.3.tgz", + "integrity": "sha512-DHdTCGX62R0fCxBEaT+USdASAnoaRBaaNczkRJl0K7o3WyoCeVUbVxo6fKqpOll/B+WMWCsiFK0eFrJSNBKZIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.0", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.8.0.tgz", + "integrity": "sha512-mikR1qwIVy3t5WthUoAXkMwxkXvabZP9FJgdx35Ei7EbGWmctva1Pih16Koeor/bdNNq8NXHlwKGS6NkYTawLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0", + "pnpm": "10.34.1" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-sonner": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.1.1.tgz", + "integrity": "sha512-5cd3p7wa4cq0NsqslMwdlPb7x1JglEZ/GKrLePWNr5bCxR1nagAVrY01FRFrXfUGs41miLt3C327+8XJo5BzZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "runed": "^0.28.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/svelte-sonner/node_modules/runed": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz", + "integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/svelte-toolbelt": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", + "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.35.1", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, + "node_modules/svelte-toolbelt/node_modules/runed": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", + "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + } + } + }, + "node_modules/sveltekit-superforms": { + "version": "2.30.2", + "resolved": "https://registry.npmjs.org/sveltekit-superforms/-/sveltekit-superforms-2.30.2.tgz", + "integrity": "sha512-6sR70ZfjFMAfdNure/Bu26o1rY32+WGxebko1L8jUFS+qZw5fxIHBPkUaTpNBkOIlJZ/UJwoNCYCm5xK2Z7Kqw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ciscoheat" + }, + { + "type": "ko-fi", + "url": "https://ko-fi.com/ciscoheat" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=NY7F5ALHHSVQS" + } + ], + "license": "MIT", + "dependencies": { + "devalue": "^5.8.1", + "memoize-weak": "^1.0.2", + "ts-deepmerge": "^8.0.0" + }, + "optionalDependencies": { + "@exodus/schemasafe": "^1.3.0", + "@standard-schema/spec": "^1.1.0", + "@typeschema/class-validator": "^0.3.0", + "@valibot/to-json-schema": "^1.7.1", + "@vinejs/vine": "^3.0.1", + "arktype": "^2.2.2", + "class-validator": "^0.14.4", + "effect": "^3.21.4", + "joi": "^17.13.4", + "json-schema-to-ts": "^3.1.1", + "superstruct": "^2.0.2", + "typebox": "^1.3.3", + "valibot": "^1.4.2", + "yup": "^1.7.1", + "zod": "^4.4.3", + "zod-v3-to-json-schema": "^4.0.0" + }, + "peerDependencies": { + "@exodus/schemasafe": "^1.3.0", + "@sveltejs/kit": "1.x || 2.x", + "@typeschema/class-validator": "^0.3.0", + "@vinejs/vine": "^1.8.0 || ^2.0.0 || ^3.0.0", + "arktype": ">=2.0.0-rc.23", + "class-validator": "^0.14.1", + "effect": "^3.21.0", + "joi": "^17.13.1", + "superstruct": "^2.0.2", + "svelte": "3.x || 4.x || >=5.0.0-next.51", + "typebox": "^1.0.36", + "valibot": "^1.2.0", + "yup": "^1.4.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@exodus/schemasafe": { + "optional": true + }, + "@typeschema/class-validator": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "arktype": { + "optional": true + }, + "class-validator": { + "optional": true + }, + "effect": { + "optional": true + }, + "joi": { + "optional": true + }, + "superstruct": { + "optional": true + }, + "typebox": { + "optional": true + }, + "valibot": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", + "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwind-merge": ">=3.0.0", + "tailwindcss": "*" + }, + "peerDependenciesMeta": { + "tailwind-merge": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT", + "optional": true + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT", + "optional": true + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT", + "optional": true + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-deepmerge": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-8.0.0.tgz", + "integrity": "sha512-133O+10nJmVI8w5xeVZPEv5PIrv7iaUae07wv1aH8XJH95Ur6YIhWAPhPyP1YPlbPS9fCVcNIZTu7m8urRVF0A==", + "license": "ISC", + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "devOptional": true, + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typebox": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.6.tgz", + "integrity": "sha512-Sc8RA0NCMEFmApHNU9ZMzqcpQj46She44J8ffpLM/bdhLNUZKq7DJumcLcsFx1gRmDfQPgCgOmFFJ7rcnfWNyA==", + "license": "MIT", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.65.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz", + "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.65.0", + "@typescript-eslint/parser": "8.65.0", + "@typescript-eslint/typescript-estree": "8.65.0", + "@typescript-eslint/utils": "8.65.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/urlpattern-polyfill": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.1.0.tgz", + "integrity": "sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/valibot": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.4.2.tgz", + "integrity": "sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==", + "license": "MIT", + "optional": true, + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vite": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", + "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.17", + "rolldown": "~1.1.5", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-compression": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/vite-plugin-compression/-/vite-plugin-compression-0.5.1.tgz", + "integrity": "sha512-5QJKBDc+gNYVqL/skgFAP81Yuzo9R+EAf19d+EtsMF/i8kFUpNi3J/H01QD3Oo8zBQn+NzoCIFkpPLynoOzaJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "debug": "^4.3.3", + "fs-extra": "^10.0.0" + }, + "peerDependencies": { + "vite": ">=2.0.0" + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs-parser/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yup": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz", + "integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==", + "license": "MIT", + "optional": true, + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-v3-to-json-schema": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/zod-v3-to-json-schema/-/zod-v3-to-json-schema-4.0.0.tgz", + "integrity": "sha512-KixLrhX/uPmRFnDgsZrzrk4x5SSJA+PmaE5adbfID9+3KPJcdxqRobaHU397EfWBqfQircrjKqvEqZ/mW5QH6w==", + "license": "ISC", + "optional": true, + "peerDependencies": { + "zod": "^3.25 || ^4.0.14" + } + } + } +} diff --git a/tests/resources/export/database.json b/tests/resources/export/database.json index 23fdbba9..1cfde9a1 100644 --- a/tests/resources/export/database.json +++ b/tests/resources/export/database.json @@ -1,6 +1,6 @@ { "provider": "sqlite", - "version": 20260722120000, + "version": 20260723000000, "tableOrder": [ "users", "user_groups", @@ -250,32 +250,6 @@ "user_group_id": "c7ae7c01-28a3-4f3c-9572-1ee734ea8368" } ], - "one_time_access_tokens": [ - { - "created_at": "2025-11-25T12:39:02Z", - "expires_at": "2025-11-25T13:39:02Z", - "id": "bf877753-4ea4-4c9c-bbbd-e198bb201cb8", - "token": "HPe6k6uiDRRVuAQV", - "device_token": null, - "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" - }, - { - "created_at": "2025-11-25T12:39:02Z", - "expires_at": "2025-11-25T12:39:01Z", - "id": "d3afae24-fe2d-4a98-abec-cf0b8525096a", - "token": "YCGDtftvsvYWiXd0", - "device_token": null, - "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" - }, - { - "created_at": "2025-11-25T12:39:02Z", - "expires_at": "2025-11-25T13:39:02Z", - "id": "defd5164-9d9b-4228-bbce-708e33f49360", - "token": "one-time-token", - "device_token": null, - "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" - } - ], "oauth2_jtis": [ { "id": "bd0c8bf2-66ec-487a-9dd5-7d9d78d73543", @@ -322,46 +296,6 @@ "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" } ], - "signup_tokens": [ - { - "created_at": "2025-11-25T12:39:02Z", - "expires_at": "2025-11-26T12:39:02Z", - "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "token": "VALID1234567890A", - "usage_count": 0, - "usage_limit": 1 - }, - { - "created_at": "2025-11-25T12:39:02Z", - "expires_at": "2025-12-02T12:39:02Z", - "id": "dc3c9c96-714e-48eb-926e-2d7c7858e6cf", - "token": "PARTIAL567890ABC", - "usage_count": 2, - "usage_limit": 5 - }, - { - "created_at": "2025-11-25T12:39:02Z", - "expires_at": "2025-11-24T12:39:02Z", - "id": "44de1863-ffa5-4db1-9507-4887cd7a1e3f", - "token": "EXPIRED34567890B", - "usage_count": 1, - "usage_limit": 3 - }, - { - "created_at": "2025-11-25T12:39:02Z", - "expires_at": "2025-11-26T12:39:02Z", - "id": "f1b1678b-7720-4d8b-8f91-1dbff1e2d02b", - "token": "FULLYUSED567890C", - "usage_count": 1, - "usage_limit": 1 - } - ], - "signup_tokens_user_groups": [ - { - "signup_token_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "user_group_id": "c7ae7c01-28a3-4f3c-9572-1ee734ea8368" - } - ], "user_authorized_oidc_clients": [ { "client_id": "3654a746-35d4-4321-ac61-0bdcff2b4055",