diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ab558411..5caf5a2c 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -171,7 +171,7 @@ jobs: run: | DOCKER_COMPOSE_FILE=docker-compose.yml - export FILE_BACKEND="${{ matrix.storage }}" + echo "FILE_BACKEND=${{ matrix.storage }}" > .env if [ "${{ matrix.db }}" = "postgres" ]; then DOCKER_COMPOSE_FILE=docker-compose-postgres.yml elif [ "${{ matrix.storage }}" = "s3" ]; then @@ -179,7 +179,20 @@ jobs: fi docker compose -f "$DOCKER_COMPOSE_FILE" up -d - docker compose -f "$DOCKER_COMPOSE_FILE" logs -f pocket-id &> /tmp/backend.log & + + { + LOG_FILE="/tmp/backend.log" + while true; do + CID=$(docker compose -f "$DOCKER_COMPOSE_FILE" ps -q pocket-id) + if [ -n "$CID" ]; then + echo "[$(date)] Attaching logs for $CID" >> "$LOG_FILE" + docker logs -f --since=0 "$CID" >> "$LOG_FILE" 2>&1 + else + echo "[$(date)] Container not yet running…" >> "$LOG_FILE" + fi + sleep 1 + done + } & - name: Run Playwright tests working-directory: ./tests diff --git a/.gitignore b/.gitignore index abf848c2..7d3bbc73 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ node_modules /backend/bin pocket-id /tests/test-results/*.json +.tmp/ # OS .DS_Store diff --git a/backend/go.mod b/backend/go.mod index d9bd1b46..0effca66 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -14,7 +14,6 @@ require ( github.com/disintegration/imaging v1.6.2 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 github.com/emersion/go-smtp v0.24.0 - github.com/fxamacker/cbor/v2 v2.9.0 github.com/gin-contrib/slog v1.2.0 github.com/gin-gonic/gin v1.11.0 github.com/glebarez/go-sqlite v1.22.0 @@ -84,6 +83,7 @@ require ( github.com/disintegration/gift v1.2.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.11 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect @@ -121,7 +121,6 @@ require ( github.com/lestrrat-go/option/v2 v2.0.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-sqlite3 v1.14.32 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -160,11 +159,9 @@ require ( go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/arch v0.23.0 // indirect golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 // indirect - golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sys v0.38.0 // indirect - golang.org/x/tools v0.39.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect google.golang.org/grpc v1.77.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index a5d17faa..668102fc 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,75 +1,45 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= -github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A= github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk= -github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2 v1.40.0 h1:/WMUA0kjhZExjOQN2z3oLALDREea1A7TobfuiBrKlwc= github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 h1:DHctwEM8P8iTXFxC/QK0MRjwEpWQeM9yzidCRjldUz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3/go.mod h1:xdCzcZEtnSTKVDOmUZs4l/j3pSV6rpo1WXl5ugNsL8Y= -github.com/aws/aws-sdk-go-v2/config v1.31.17 h1:QFl8lL6RgakNK86vusim14P2k8BFSxjvUkcWLDjgz9Y= -github.com/aws/aws-sdk-go-v2/config v1.31.17/go.mod h1:V8P7ILjp/Uef/aX8TjGk6OHZN6IKPM5YW6S78QnRD5c= github.com/aws/aws-sdk-go-v2/config v1.32.2 h1:4liUsdEpUUPZs5WVapsJLx5NPmQhQdez7nYFcovrytk= github.com/aws/aws-sdk-go-v2/config v1.32.2/go.mod h1:l0hs06IFz1eCT+jTacU/qZtC33nvcnLADAPL/XyrkZI= -github.com/aws/aws-sdk-go-v2/credentials v1.18.21 h1:56HGpsgnmD+2/KpG0ikvvR8+3v3COCwaF4r+oWwOeNA= -github.com/aws/aws-sdk-go-v2/credentials v1.18.21/go.mod h1:3YELwedmQbw7cXNaII2Wywd+YY58AmLPwX4LzARgmmA= github.com/aws/aws-sdk-go-v2/credentials v1.19.2 h1:qZry8VUyTK4VIo5aEdUcBjPZHL2v4FyQ3QEOaWcFLu4= github.com/aws/aws-sdk-go-v2/credentials v1.19.2/go.mod h1:YUqm5a1/kBnoK+/NY5WEiMocZihKSo15/tJdmdXnM5g= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 h1:T1brd5dR3/fzNFAQch/iBKeX07/ffu/cLu+q+RuzEWk= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13/go.mod h1:Peg/GBAQ6JDt+RoBf4meB1wylmAipb7Kg2ZFakZTlwk= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14 h1:WZVR5DbDgxzA0BJeudId89Kmgy6DIU4ORpxwsVHz0qA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.14/go.mod h1:Dadl9QO0kHgbrH1GRqGiZdYtW5w+IXXaBNCHTIaheM4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14 h1:PZHqQACxYb8mYgms4RZbhZG0a7dPW06xOjmaH0EJC/I= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.14/go.mod h1:VymhrMJUWs69D8u0/lZ7jSB6WgaG/NqHi3gX0aYf6U0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14 h1:bOS19y6zlJwagBfHxs0ESzr1XCOU2KXJCWcq3E2vfjY= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.14/go.mod h1:1ipeGBMAxZ0xcTm6y6paC2C/J6f6OO7LBODV9afuAyM= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13 h1:eg/WYAa12vqTphzIdWMzqYRVKKnCboVPRlvaybNCqPA= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.13/go.mod h1:/FDdxWhz1486obGrKKC1HONd7krpk38LBt+dutLcN9k= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14 h1:ITi7qiDSv/mSGDSWNpZ4k4Ve0DQR6Ug2SJQ8zEHoDXg= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.14/go.mod h1:k1xtME53H1b6YpZt74YmwlONMWf4ecM+lut1WQLAF/U= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3 h1:x2Ibm/Af8Fi+BH+Hsn9TXGdT+hKbDd5XOTZxTMxDk7o= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.3/go.mod h1:IW1jwyrQgMdhisceG8fQLmQIydcT/jWY21rFhzgaKwo= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4 h1:NvMjwvv8hpGUILarKw7Z4Q0w1H9anXKsesMxtw++MA4= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.4/go.mod h1:455WPHSwaGj2waRSpQp7TsnpOnBfw8iDfPfbwl7KPJE= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5 h1:Hjkh7kE6D81PgrHlE/m9gx+4TyyeLHuY8xJs7yXN5C4= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.5/go.mod h1:nPRXgyCfAurhyaTMoBMwRBYBhaHI4lNPAnJmjM0Tslc= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY4AtKoACfzIGD8A0+hbT41KTKF//gq7jITfM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14 h1:FIouAnCE46kyYqyhs0XEBDFFSREtdnr8HQuLPQPLCrY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.14/go.mod h1:UTwDc5COa5+guonQU8qBikJo1ZJ4ln2r1MkF7Dqag1E= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14 h1:FzQE21lNtUor0Fb7QNgnEyiRCBlolLTX/Z1j65S7teM= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.14/go.mod h1:s1ydyWG9pm3ZwmmYN21HKyG9WzAZhYVW85wMHs5FV6w= -github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8= -github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw= github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1 h1:OgQy/+0+Kc3khtqiEOk23xQAglXi3Tj0y5doOxbi5tg= github.com/aws/aws-sdk-go-v2/service/s3 v1.92.1/go.mod h1:wYNqY3L02Z3IgRYxOBPH9I1zD9Cjh9hI5QOy/eOjQvw= github.com/aws/aws-sdk-go-v2/service/signin v1.0.2 h1:MxMBdKTYBjPQChlJhi4qlEueqB1p1KcbTEa7tD5aqPs= github.com/aws/aws-sdk-go-v2/service/signin v1.0.2/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.1/go.mod h1:fKvyjJcz63iL/ftA6RaM8sRCtN4r4zl4tjL3qw5ec7k= github.com/aws/aws-sdk-go-v2/service/sso v1.30.5 h1:ksUT5KtgpZd3SAiFJNJ0AFEJVva3gjBmN7eXUZjzUwQ= github.com/aws/aws-sdk-go-v2/service/sso v1.30.5/go.mod h1:av+ArJpoYf3pgyrj6tcehSFW+y9/QvAY8kMooR9bZCw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 h1:OWs0/j2UYR5LOGi88sD5/lhN6TDLG6SfA7CqsQO9zF0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5/go.mod h1:klO+ejMvYsB4QATfEOIXk8WAEwN4N0aBfJpvC+5SZBo= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10 h1:GtsxyiF3Nd3JahRBJbxLCCdYW9ltGQYrFWg8XdkGDd8= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.10/go.mod h1:/j67Z5XBVDx8nZVp9EuFM9/BS5dvBznbqILGuu73hug= -github.com/aws/aws-sdk-go-v2/service/sts v1.39.1 h1:mLlUgHn02ue8whiR4BmxxGJLR2gwU6s6ZzJ5wDamBUs= -github.com/aws/aws-sdk-go-v2/service/sts v1.39.1/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= github.com/aws/aws-sdk-go-v2/service/sts v1.41.2 h1:a5UTtD4mHBU3t0o6aHQZFJTNKVfxFWfPX7J0Lr7G+uY= github.com/aws/aws-sdk-go-v2/service/sts v1.41.2/go.mod h1:6TxbXoDSgBQ225Qd8Q+MbxUxUh6TtNKwbRt/EPS9xso= github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= @@ -78,12 +48,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= -github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= @@ -131,12 +97,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= -github.com/gin-contrib/slog v1.1.0 h1:K9MVNrETT6r/C3u2Aheer/gxwVeVqrGL0hXlsmv3fm4= -github.com/gin-contrib/slog v1.1.0/go.mod h1:PvNXQVXcVOAaaiJR84LV1/xlQHIaXi9ygEXyBkmjdkY= github.com/gin-contrib/slog v1.2.0 h1:vAxZfr7knD1ZYK5+pMJLP52sZXIkJXkcRPa/0dx9hSk= github.com/gin-contrib/slog v1.2.0/go.mod h1:vYK6YltmpsEFkO0zfRMLTKHrWS3DwUSn0TMpT+kMagI= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= @@ -149,8 +111,6 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-co-op/gocron/v2 v2.17.0 h1:e/oj6fcAM8vOOKZxv2Cgfmjo+s8AXC46po5ZPtaSea4= -github.com/go-co-op/gocron/v2 v2.17.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI= github.com/go-co-op/gocron/v2 v2.18.1 h1:VVxgAghLW1Q6VHi/rc+B0ZSpFoUVlWgkw09Yximvn58= github.com/go-co-op/gocron/v2 v2.18.1/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI= github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4= @@ -170,12 +130,8 @@ github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0 github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-webauthn/webauthn v0.14.0 h1:ZLNPUgPcDlAeoxe+5umWG/tEeCoQIDr7gE2Zx2QnhL0= -github.com/go-webauthn/webauthn v0.14.0/go.mod h1:QZzPFH3LJ48u5uEPAu+8/nWJImoLBWM7iAH/kSVSo6k= github.com/go-webauthn/webauthn v0.15.0 h1:LR1vPv62E0/6+sTenX35QrCmpMCzLeVAcnXeH4MrbJY= github.com/go-webauthn/webauthn v0.15.0/go.mod h1:hcAOhVChPRG7oqG7Xj6XKN1mb+8eXTGP/B7zBLzkX5A= -github.com/go-webauthn/x v0.1.25 h1:g/0noooIGcz/yCVqebcFgNnGIgBlJIccS+LYAa+0Z88= -github.com/go-webauthn/x v0.1.25/go.mod h1:ieblaPY1/BVCV0oQTsA/VAo08/TWayQuJuo5Q+XxmTY= github.com/go-webauthn/x v0.1.26 h1:eNzreFKnwNLDFoywGh9FA8YOMebBWTUNlNSdolQRebs= github.com/go-webauthn/x v0.1.26/go.mod h1:jmf/phPV6oIsF6hmdVre+ovHkxjDOmNH0t6fekWUxvg= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= @@ -200,8 +156,6 @@ github.com/google/go-github/v39 v39.2.0 h1:rNNM311XtPOz5rDdsJXAp2o8F67X9FnROXTvt github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/go-tpm v0.9.6 h1:Ku42PT4LmjDu1H5C5ISWLlpI1mj+Zq7sPGKoRw2XROA= -github.com/google/go-tpm v0.9.6/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/go-tpm v0.9.7 h1:u89J4tUUeDTlH8xxC3CTW7OHZjbjKoHdQ9W7gCUhtxA= github.com/google/go-tpm v0.9.7/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -218,6 +172,8 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -290,8 +246,6 @@ github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuE github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws= github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -313,8 +267,6 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/orandin/slog-gorm v1.4.0 h1:FgA8hJufF9/jeNSYoEXmHPPBwET2gwlF3B85JdpsTUU= github.com/orandin/slog-gorm v1.4.0/go.mod h1:MoZ51+b7xE9lwGNPYEhxcUtRNrYzjdcKvA8QXQQGEPA= -github.com/oschwald/maxminddb-golang/v2 v2.0.0 h1:Gyljxck1kHbBxDgLM++NfDWBqvu1pWWfT8XbosSo0bo= -github.com/oschwald/maxminddb-golang/v2 v2.0.0/go.mod h1:gG4V88LsawPEqtbL1Veh1WRh+nVSYwXzJ1P5Fcn77g0= github.com/oschwald/maxminddb-golang/v2 v2.1.0 h1:2Iv7lmG9XtxuZA/jFAsd7LnZaC1E59pFsj5O/nU15pw= github.com/oschwald/maxminddb-golang/v2 v2.1.0/go.mod h1:gG4V88LsawPEqtbL1Veh1WRh+nVSYwXzJ1P5Fcn77g0= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -327,22 +279,14 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI= -github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q= github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= -github.com/prometheus/procfs v0.18.0 h1:2QTA9cKdznfYJz7EDaa7IiJobHuV7E1WzeBwcrhk0ao= -github.com/prometheus/procfs v0.18.0/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= -github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= -github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= @@ -368,15 +312,12 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= -github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= @@ -435,8 +376,6 @@ go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6 go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= -go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE= -go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -445,25 +384,17 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= -golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= -golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= -golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ= golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -471,8 +402,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= @@ -492,24 +421,16 @@ golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4= -google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846 h1:ZdyUkS9po3H7G0tuh955QVyyotWvOD4W0aEapeGeUYk= google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= -google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= -google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= @@ -522,24 +443,20 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= -gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= -gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= -modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= -modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= -modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= -modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= -modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk= modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= @@ -550,8 +467,6 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.39.1 h1:H+/wGFzuSCIEVCvXYVHX5RQglwhMOvtHSv+VtidL2r4= -modernc.org/sqlite v1.39.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY= modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= diff --git a/backend/internal/bootstrap/bootstrap.go b/backend/internal/bootstrap/bootstrap.go index e59a1562..733add97 100644 --- a/backend/internal/bootstrap/bootstrap.go +++ b/backend/internal/bootstrap/bootstrap.go @@ -7,6 +7,7 @@ import ( "time" _ "github.com/golang-migrate/migrate/v4/source/file" + "gorm.io/gorm" "github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/job" @@ -15,6 +16,16 @@ import ( ) func Bootstrap(ctx context.Context) error { + var shutdownFns []utils.Service + defer func() { //nolint:contextcheck + // Invoke all shutdown functions on exit + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := utils.NewServiceRunner(shutdownFns...).Run(shutdownCtx); err != nil { + slog.Error("Error during graceful shutdown", "error", err) + } + }() + // Initialize the observability stack, including the logger, distributed tracing, and metrics shutdownFns, httpClient, err := initObservability(ctx, common.EnvConfig.MetricsEnabled, common.EnvConfig.TracingEnabled) if err != nil { @@ -22,15 +33,80 @@ func Bootstrap(ctx context.Context) error { } slog.InfoContext(ctx, "Pocket ID is starting") - // Connect to the database db, err := NewDatabase() if err != nil { return fmt.Errorf("failed to initialize database: %w", err) } - // Initialize the file storage backend - var fileStorage storage.FileStorage + fileStorage, err := InitStorage(ctx, db) + if err != nil { + return fmt.Errorf("failed to initialize file storage (backend: %s): %w", common.EnvConfig.FileBackend, err) + } + imageExtensions, err := initApplicationImages(ctx, fileStorage) + if err != nil { + return fmt.Errorf("failed to initialize application images: %w", err) + } + + // Create all services + svc, err := initServices(ctx, db, httpClient, imageExtensions, fileStorage) + if err != nil { + return fmt.Errorf("failed to initialize services: %w", err) + } + + waitUntil, err := svc.appLockService.Acquire(ctx, false) + if err != nil { + return fmt.Errorf("failed to acquire application lock: %w", err) + } + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(time.Until(waitUntil)): + } + + shutdownFn := func(shutdownCtx context.Context) error { + sErr := svc.appLockService.Release(shutdownCtx) + if sErr != nil { + return fmt.Errorf("failed to release application lock: %w", sErr) + } + return nil + } + shutdownFns = append(shutdownFns, shutdownFn) + + // Init the job scheduler + scheduler, err := job.NewScheduler() + if err != nil { + return fmt.Errorf("failed to create job scheduler: %w", err) + } + err = registerScheduledJobs(ctx, db, svc, httpClient, scheduler) + if err != nil { + return fmt.Errorf("failed to register scheduled jobs: %w", err) + } + + // Init the router + router, err := initRouter(db, svc) + if err != nil { + return fmt.Errorf("failed to initialize router: %w", err) + } + + // Run all background services + // This call blocks until the context is canceled + services := []utils.Service{svc.appLockService.RunRenewal, router} + + if common.EnvConfig.AppEnv != "test" { + services = append(services, scheduler.Run) + } + + err = utils.NewServiceRunner(services...).Run(ctx) + if err != nil { + return fmt.Errorf("failed to run services: %w", err) + } + + return nil +} + +func InitStorage(ctx context.Context, db *gorm.DB) (fileStorage storage.FileStorage, err error) { switch common.EnvConfig.FileBackend { case storage.TypeFileSystem: fileStorage, err = storage.NewFilesystemStorage(common.EnvConfig.UploadPath) @@ -52,53 +128,8 @@ func Bootstrap(ctx context.Context) error { err = fmt.Errorf("unknown file storage backend: %s", common.EnvConfig.FileBackend) } if err != nil { - return fmt.Errorf("failed to initialize file storage (backend: %s): %w", common.EnvConfig.FileBackend, err) + return fileStorage, err } - imageExtensions, err := initApplicationImages(ctx, fileStorage) - if err != nil { - return fmt.Errorf("failed to initialize application images: %w", err) - } - - // Create all services - svc, err := initServices(ctx, db, httpClient, imageExtensions, fileStorage) - if err != nil { - return fmt.Errorf("failed to initialize services: %w", err) - } - - // Init the job scheduler - scheduler, err := job.NewScheduler() - if err != nil { - return fmt.Errorf("failed to create job scheduler: %w", err) - } - err = registerScheduledJobs(ctx, db, svc, httpClient, scheduler) - if err != nil { - return fmt.Errorf("failed to register scheduled jobs: %w", err) - } - - // Init the router - router := initRouter(db, svc) - - // Run all background services - // This call blocks until the context is canceled - err = utils. - NewServiceRunner(router, scheduler.Run). - Run(ctx) - if err != nil { - return fmt.Errorf("failed to run services: %w", err) - } - - // Invoke all shutdown functions - // We give these a timeout of 5s - // Note: we use a background context because the run context has been canceled already - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer shutdownCancel() - err = utils. - NewServiceRunner(shutdownFns...). - Run(shutdownCtx) //nolint:contextcheck - if err != nil { - slog.Error("Error shutting down services", slog.Any("error", err)) - } - - return nil + return fileStorage, nil } diff --git a/backend/internal/bootstrap/db_bootstrap.go b/backend/internal/bootstrap/db_bootstrap.go index 7f020b07..335cf606 100644 --- a/backend/internal/bootstrap/db_bootstrap.go +++ b/backend/internal/bootstrap/db_bootstrap.go @@ -12,12 +12,7 @@ import ( "time" "github.com/glebarez/sqlite" - "github.com/golang-migrate/migrate/v4" - "github.com/golang-migrate/migrate/v4/database" - postgresMigrate "github.com/golang-migrate/migrate/v4/database/postgres" - sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3" _ "github.com/golang-migrate/migrate/v4/source/github" - "github.com/golang-migrate/migrate/v4/source/iofs" slogGorm "github.com/orandin/slog-gorm" "gorm.io/driver/postgres" "gorm.io/gorm" @@ -26,11 +21,10 @@ import ( "github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/utils" sqliteutil "github.com/pocket-id/pocket-id/backend/internal/utils/sqlite" - "github.com/pocket-id/pocket-id/backend/resources" ) func NewDatabase() (db *gorm.DB, err error) { - db, err = connectDatabase() + db, err = ConnectDatabase() if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) } @@ -39,105 +33,15 @@ func NewDatabase() (db *gorm.DB, err error) { return nil, fmt.Errorf("failed to get sql.DB: %w", err) } - // Choose the correct driver for the database provider - var driver database.Driver - switch common.EnvConfig.DbProvider { - case common.DbProviderSqlite: - driver, err = sqliteMigrate.WithInstance(sqlDb, &sqliteMigrate.Config{ - NoTxWrap: true, - }) - case common.DbProviderPostgres: - driver, err = postgresMigrate.WithInstance(sqlDb, &postgresMigrate.Config{}) - default: - // Should never happen at this point - return nil, fmt.Errorf("unsupported database provider: %s", common.EnvConfig.DbProvider) - } - if err != nil { - return nil, fmt.Errorf("failed to create migration driver: %w", err) - } - // Run migrations - if err := migrateDatabase(driver); err != nil { + if err := utils.MigrateDatabase(sqlDb); err != nil { return nil, fmt.Errorf("failed to run migrations: %w", err) } return db, nil } -func migrateDatabase(driver database.Driver) error { - // Embedded migrations via iofs - path := "migrations/" + string(common.EnvConfig.DbProvider) - source, err := iofs.New(resources.FS, path) - if err != nil { - return fmt.Errorf("failed to create embedded migration source: %w", err) - } - - m, err := migrate.NewWithInstance("iofs", source, "pocket-id", driver) - if err != nil { - return fmt.Errorf("failed to create migration instance: %w", err) - } - - requiredVersion, err := getRequiredMigrationVersion(path) - if err != nil { - return fmt.Errorf("failed to get last migration version: %w", err) - } - - currentVersion, _, _ := m.Version() - if currentVersion > requiredVersion { - slog.Warn("Database version is newer than the application supports, possible downgrade detected", slog.Uint64("db_version", uint64(currentVersion)), slog.Uint64("app_version", uint64(requiredVersion))) - if !common.EnvConfig.AllowDowngrade { - return fmt.Errorf("database version (%d) is newer than application version (%d), downgrades are not allowed (set ALLOW_DOWNGRADE=true to enable)", currentVersion, requiredVersion) - } - slog.Info("Fetching migrations from GitHub to handle possible downgrades") - return migrateDatabaseFromGitHub(driver, requiredVersion) - } - - if err := m.Migrate(requiredVersion); err != nil && !errors.Is(err, migrate.ErrNoChange) { - return fmt.Errorf("failed to apply embedded migrations: %w", err) - } - return nil -} - -func migrateDatabaseFromGitHub(driver database.Driver, version uint) error { - srcURL := "github://pocket-id/pocket-id/backend/resources/migrations/" + string(common.EnvConfig.DbProvider) - - m, err := migrate.NewWithDatabaseInstance(srcURL, "pocket-id", driver) - if err != nil { - return fmt.Errorf("failed to create GitHub migration instance: %w", err) - } - - if err := m.Migrate(version); err != nil && !errors.Is(err, migrate.ErrNoChange) { - return fmt.Errorf("failed to apply GitHub migrations: %w", err) - } - return nil -} - -// getRequiredMigrationVersion reads the embedded migration files and returns the highest version number found. -func getRequiredMigrationVersion(path string) (uint, error) { - entries, err := resources.FS.ReadDir(path) - if err != nil { - return 0, fmt.Errorf("failed to read migration directory: %w", err) - } - - var maxVersion uint - for _, entry := range entries { - if entry.IsDir() { - continue - } - name := entry.Name() - var version uint - n, err := fmt.Sscanf(name, "%d_", &version) - if err == nil && n == 1 { - if version > maxVersion { - maxVersion = version - } - } - } - - return maxVersion, nil -} - -func connectDatabase() (db *gorm.DB, err error) { +func ConnectDatabase() (db *gorm.DB, err error) { var dialector gorm.Dialector // Choose the correct database provider diff --git a/backend/internal/bootstrap/e2etest_router_bootstrap.go b/backend/internal/bootstrap/e2etest_router_bootstrap.go index b4eb9fc7..b87d4644 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.fileStorage) + testService, err := service.NewTestService(db, 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 3c1072be..f307932d 100644 --- a/backend/internal/bootstrap/router_bootstrap.go +++ b/backend/internal/bootstrap/router_bootstrap.go @@ -29,16 +29,7 @@ import ( // This is used to register additional controllers for tests var registerTestControllers []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) -func initRouter(db *gorm.DB, svc *services) utils.Service { - runner, err := initRouterInternal(db, svc) - if err != nil { - slog.Error("Failed to init router", "error", err) - os.Exit(1) - } - return runner -} - -func initRouterInternal(db *gorm.DB, svc *services) (utils.Service, error) { +func initRouter(db *gorm.DB, svc *services) (utils.Service, error) { // Set the appropriate Gin mode based on the environment switch common.EnvConfig.AppEnv { case common.AppEnvProduction: diff --git a/backend/internal/bootstrap/services_bootstrap.go b/backend/internal/bootstrap/services_bootstrap.go index 21254627..e5fc805a 100644 --- a/backend/internal/bootstrap/services_bootstrap.go +++ b/backend/internal/bootstrap/services_bootstrap.go @@ -27,6 +27,7 @@ type services struct { apiKeyService *service.ApiKeyService versionService *service.VersionService fileStorage storage.FileStorage + appLockService *service.AppLockService } // Initializes all services @@ -40,6 +41,7 @@ func initServices(ctx context.Context, db *gorm.DB, httpClient *http.Client, ima svc.fileStorage = fileStorage svc.appImagesService = service.NewAppImagesService(imageExtensions, fileStorage) + svc.appLockService = service.NewAppLockService(db) svc.emailService, err = service.NewEmailService(db, svc.appConfigService) if err != nil { diff --git a/backend/internal/cmds/export.go b/backend/internal/cmds/export.go new file mode 100644 index 00000000..55194e2f --- /dev/null +++ b/backend/internal/cmds/export.go @@ -0,0 +1,70 @@ +package cmds + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/pocket-id/pocket-id/backend/internal/bootstrap" + "github.com/pocket-id/pocket-id/backend/internal/service" + "github.com/spf13/cobra" +) + +type exportFlags struct { + Path string +} + +func init() { + var flags exportFlags + + exportCmd := &cobra.Command{ + Use: "export", + Short: "Exports all data of Pocket ID into a ZIP file", + RunE: func(cmd *cobra.Command, args []string) error { + return runExport(cmd.Context(), flags) + }, + } + + exportCmd.Flags().StringVarP(&flags.Path, "path", "p", "pocket-id-export.zip", "Path to the ZIP file to export the data to, or '-' to write to stdout") + + rootCmd.AddCommand(exportCmd) +} + +// runExport orchestrates the export flow +func runExport(ctx context.Context, flags exportFlags) error { + db, err := bootstrap.NewDatabase() + if err != nil { + return fmt.Errorf("failed to connect to database: %w", err) + } + + storage, err := bootstrap.InitStorage(ctx, db) + if err != nil { + return fmt.Errorf("failed to initialize storage: %w", err) + } + + exportService := service.NewExportService(db, storage) + + var w io.Writer + if flags.Path == "-" { + w = os.Stdout + } else { + file, err := os.Create(flags.Path) + if err != nil { + return fmt.Errorf("failed to create export file: %w", err) + } + defer file.Close() + + w = file + } + + if err := exportService.ExportToZip(ctx, w); err != nil { + return fmt.Errorf("failed to export data: %w", err) + } + + if flags.Path != "-" { + fmt.Printf("Exported data to %s\n", flags.Path) + } + + return nil +} diff --git a/backend/internal/cmds/import.go b/backend/internal/cmds/import.go new file mode 100644 index 00000000..974d049e --- /dev/null +++ b/backend/internal/cmds/import.go @@ -0,0 +1,191 @@ +package cmds + +import ( + "archive/zip" + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "time" + + "github.com/spf13/cobra" + "gorm.io/gorm" + + "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/service" + "github.com/pocket-id/pocket-id/backend/internal/utils" +) + +type importFlags struct { + Path string + Yes bool + ForcefullyAcquireLock bool +} + +func init() { + var flags importFlags + + importCmd := &cobra.Command{ + Use: "import", + Short: "Imports all data of Pocket ID from a ZIP file", + RunE: func(cmd *cobra.Command, args []string) error { + return runImport(cmd.Context(), flags) + }, + } + + importCmd.Flags().StringVarP(&flags.Path, "path", "p", "pocket-id-export.zip", "Path to the ZIP file to import the data from, or '-' to read from stdin") + importCmd.Flags().BoolVarP(&flags.Yes, "yes", "y", false, "Skip confirmation prompts") + importCmd.Flags().BoolVarP(&flags.ForcefullyAcquireLock, "forcefully-acquire-lock", "", false, "Forcefully acquire the application lock by terminating the Pocket ID instance") + + rootCmd.AddCommand(importCmd) +} + +// runImport handles the high-level orchestration of the import process +func runImport(ctx context.Context, flags importFlags) error { + if !flags.Yes { + ok, err := askForConfirmation() + if err != nil { + return fmt.Errorf("failed to get confirmation: %w", err) + } + if !ok { + fmt.Println("Aborted") + os.Exit(1) + } + } + + var ( + zipReader *zip.ReadCloser + cleanup func() + err error + ) + + if flags.Path == "-" { + zipReader, cleanup, err = readZipFromStdin() + defer cleanup() + } else { + zipReader, err = zip.OpenReader(flags.Path) + } + if err != nil { + return fmt.Errorf("failed to open zip: %w", err) + } + defer zipReader.Close() + + db, err := bootstrap.ConnectDatabase() + if err != nil { + return err + } + + err = acquireImportLock(ctx, db, flags.ForcefullyAcquireLock) + if err != nil { + return err + } + + storage, err := bootstrap.InitStorage(ctx, db) + if err != nil { + return fmt.Errorf("failed to initialize storage: %w", err) + } + + importService := service.NewImportService(db, storage) + err = importService.ImportFromZip(ctx, &zipReader.Reader) + if err != nil { + return fmt.Errorf("failed to import data from zip: %w", err) + } + + fmt.Println("Import completed successfully.") + return nil +} + +func acquireImportLock(ctx context.Context, db *gorm.DB, force bool) error { + // Check if the kv table exists, in case we are starting from an empty database + exists, err := utils.DBTableExists(db, "kv") + if err != nil { + return fmt.Errorf("failed to check if kv table exists: %w", err) + } + if !exists { + // This either means the database is empty, or the import is into an old version of PocketID that doesn't support locks + // In either case, there's no lock to acquire + fmt.Println("Could not acquire a lock because the 'kv' table does not exist. This is fine if you're importing into a new database, but make sure that there isn't an instance of Pocket ID currently running and using the same database.") + return nil + } + + // Note that we do not call a deferred Release if the data was imported + // This is because we are overriding the contents of the database, so the lock is automatically lost + appLockService := service.NewAppLockService(db) + + opCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + waitUntil, err := appLockService.Acquire(opCtx, force) + if err != nil { + if errors.Is(err, service.ErrLockUnavailable) { + //nolint:staticcheck + return errors.New("Pocket ID must be stopped before importing data; please stop the running instance or run with --forcefully-acquire-lock to terminate the other instance") + } + return fmt.Errorf("failed to acquire application lock: %w", err) + } + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(time.Until(waitUntil)): + } + + return nil +} + +func askForConfirmation() (bool, error) { + fmt.Println("WARNING: This feature is experimental and may not work correctly. Please create a backup before proceeding and report any issues you encounter.") + fmt.Println() + fmt.Println("WARNING: Import will erase all existing data at the following locations:") + fmt.Printf("Database: %s\n", absolutePathOrOriginal(common.EnvConfig.DbConnectionString)) + fmt.Printf("Uploads Path: %s\n", absolutePathOrOriginal(common.EnvConfig.UploadPath)) + + ok, err := utils.PromptForConfirmation("Do you want to continue?") + if err != nil { + return false, err + } + + return ok, nil +} + +// absolutePathOrOriginal returns the absolute path of the given path, or the original if it fails +func absolutePathOrOriginal(path string) string { + abs, err := filepath.Abs(path) + if err != nil { + return path + } + return abs +} + +func readZipFromStdin() (*zip.ReadCloser, func(), error) { + tmpFile, err := os.CreateTemp("", "pocket-id-import-*.zip") + if err != nil { + return nil, nil, fmt.Errorf("failed to create temporary file: %w", err) + } + + cleanup := func() { + _ = os.Remove(tmpFile.Name()) + } + + if _, err := io.Copy(tmpFile, os.Stdin); err != nil { + tmpFile.Close() + cleanup() + return nil, nil, fmt.Errorf("failed to read data from stdin: %w", err) + } + + if err := tmpFile.Close(); err != nil { + cleanup() + return nil, nil, fmt.Errorf("failed to close temporary file: %w", err) + } + + r, err := zip.OpenReader(tmpFile.Name()) + if err != nil { + cleanup() + return nil, nil, err + } + + return r, cleanup, nil +} diff --git a/backend/internal/cmds/key_rotate.go b/backend/internal/cmds/key_rotate.go index 1a219b7d..5225c623 100644 --- a/backend/internal/cmds/key_rotate.go +++ b/backend/internal/cmds/key_rotate.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "strings" "github.com/lestrrat-go/jwx/v3/jwa" @@ -78,7 +79,7 @@ func keyRotate(ctx context.Context, flags keyRotateFlags, db *gorm.DB, envConfig } if !ok { fmt.Println("Aborted") - return nil + os.Exit(1) } } diff --git a/backend/internal/cmds/root.go b/backend/internal/cmds/root.go index f3488cbb..01391ec2 100644 --- a/backend/internal/cmds/root.go +++ b/backend/internal/cmds/root.go @@ -12,9 +12,10 @@ import ( ) var rootCmd = &cobra.Command{ - Use: "pocket-id", - Short: "A simple and easy-to-use OIDC provider that allows users to authenticate with their passkeys to your services.", - Long: "By default, this command starts the pocket-id server.", + Use: "pocket-id", + Short: "A simple and easy-to-use OIDC provider that allows users to authenticate with their passkeys to your services.", + Long: "By default, this command starts the pocket-id server.", + SilenceUsage: true, Run: func(cmd *cobra.Command, args []string) { // Start the server err := bootstrap.Bootstrap(cmd.Context()) diff --git a/backend/internal/controller/e2etest_controller.go b/backend/internal/controller/e2etest_controller.go index 54d74e2c..62937c66 100644 --- a/backend/internal/controller/e2etest_controller.go +++ b/backend/internal/controller/e2etest_controller.go @@ -40,6 +40,11 @@ func (tc *TestController) resetAndSeedHandler(c *gin.Context) { return } + if err := tc.TestService.ResetLock(c.Request.Context()); err != nil { + _ = c.Error(err) + return + } + if err := tc.TestService.ResetApplicationImages(c.Request.Context()); err != nil { _ = c.Error(err) return @@ -69,8 +74,6 @@ func (tc *TestController) resetAndSeedHandler(c *gin.Context) { } } - tc.TestService.SetJWTKeys() - c.Status(http.StatusNoContent) } diff --git a/backend/internal/model/types/date_time.go b/backend/internal/model/types/date_time.go index 6cea0268..bcc5f9f5 100644 --- a/backend/internal/model/types/date_time.go +++ b/backend/internal/model/types/date_time.go @@ -11,6 +11,15 @@ import ( // DateTime custom type for time.Time to store date as unix timestamp for sqlite and as date for postgres type DateTime time.Time //nolint:recvcheck +func DateTimeFromString(str string) (DateTime, error) { + t, err := time.Parse(time.RFC3339Nano, str) + if err != nil { + return DateTime{}, fmt.Errorf("failed to parse date string: %w", err) + } + + return DateTime(t), nil +} + func (date *DateTime) Scan(value any) (err error) { switch v := value.(type) { case time.Time: diff --git a/backend/internal/service/app_lock_service.go b/backend/internal/service/app_lock_service.go new file mode 100644 index 00000000..339e41cd --- /dev/null +++ b/backend/internal/service/app_lock_service.go @@ -0,0 +1,296 @@ +package service + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "os" + "time" + + "github.com/google/uuid" + "github.com/pocket-id/pocket-id/backend/internal/model" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +var ( + ErrLockUnavailable = errors.New("lock is already held by another process") + ErrLockLost = errors.New("lock ownership lost") +) + +const ( + ttl = 30 * time.Second + renewInterval = 20 * time.Second + renewRetries = 3 + lockKey = "application_lock" +) + +type AppLockService struct { + db *gorm.DB + lockID string + processID int64 + hostID string +} + +func NewAppLockService(db *gorm.DB) *AppLockService { + host, err := os.Hostname() + if err != nil || host == "" { + host = "unknown-host" + } + + return &AppLockService{ + db: db, + processID: int64(os.Getpid()), + hostID: host, + lockID: uuid.NewString(), + } +} + +type lockValue struct { + ProcessID int64 `json:"process_id"` + HostID string `json:"host_id"` + LockID string `json:"lock_id"` + ExpiresAt int64 `json:"expires_at"` +} + +func (lv *lockValue) Marshal() (string, error) { + data, err := json.Marshal(lv) + if err != nil { + return "", err + } + return string(data), nil +} + +func (lv *lockValue) Unmarshal(raw string) error { + if raw == "" { + return nil + } + return json.Unmarshal([]byte(raw), lv) +} + +// Acquire obtains the lock. When force is true, the lock is stolen from any existing owner. +// If the lock is forcefully acquired, it blocks until the previous lock has expired. +func (s *AppLockService) Acquire(ctx context.Context, force bool) (waitUntil time.Time, err error) { + tx := s.db.Begin() + defer func() { + tx.Rollback() + }() + + var prevLockRaw string + err = tx. + WithContext(ctx). + Model(&model.KV{}). + Where("key = ?", lockKey). + Clauses(clause.Locking{Strength: "UPDATE"}). + Select("value"). + Scan(&prevLockRaw). + Error + if err != nil { + return time.Time{}, fmt.Errorf("query existing lock: %w", err) + } + + var prevLock lockValue + if prevLockRaw != "" { + if err := prevLock.Unmarshal(prevLockRaw); err != nil { + return time.Time{}, fmt.Errorf("decode existing lock value: %w", err) + } + } + + now := time.Now() + nowUnix := now.Unix() + + value := lockValue{ + ProcessID: s.processID, + HostID: s.hostID, + LockID: s.lockID, + ExpiresAt: now.Add(ttl).Unix(), + } + raw, err := value.Marshal() + if err != nil { + return time.Time{}, fmt.Errorf("encode lock value: %w", err) + } + + var query string + switch s.db.Name() { + case "sqlite": + query = ` + INSERT INTO kv (key, value) + VALUES (?, ?) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value + WHERE (json_extract(kv.value, '$.expires_at') < ?) OR ? + ` + case "postgres": + query = ` + INSERT INTO kv (key, value) + VALUES ($1, $2) + ON CONFLICT(key) DO UPDATE SET + value = excluded.value + WHERE ((kv.value::json->>'expires_at')::bigint < $3) OR ($4::boolean IS TRUE) + ` + default: + return time.Time{}, fmt.Errorf("unsupported database dialect: %s", s.db.Name()) + } + + res := tx.WithContext(ctx).Exec(query, lockKey, raw, nowUnix, force) + if res.Error != nil { + return time.Time{}, fmt.Errorf("lock acquisition failed: %w", res.Error) + } + + if err := tx.Commit().Error; err != nil { + return time.Time{}, fmt.Errorf("commit lock acquisition: %w", err) + } + + // If there is a lock that is not expired and force is false, no rows will be affected + if res.RowsAffected == 0 { + return time.Time{}, ErrLockUnavailable + } + + if force && prevLock.ExpiresAt > nowUnix && prevLock.LockID != s.lockID { + waitUntil = time.Unix(prevLock.ExpiresAt, 0) + } + + attrs := []any{ + slog.Int64("process_id", s.processID), + slog.String("host_id", s.hostID), + } + if wait := time.Until(waitUntil); wait > 0 { + attrs = append(attrs, slog.Duration("wait_before_proceeding", wait)) + } + slog.Info("Acquired application lock", attrs...) + + return waitUntil, nil +} + +// RunRenewal keeps renewing the lock until the context is canceled. +func (s *AppLockService) RunRenewal(ctx context.Context) error { + ticker := time.NewTicker(renewInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + if err := s.renew(ctx); err != nil { + return fmt.Errorf("renew lock: %w", err) + } + } + } +} + +// Release releases the lock if it is held by this process. +func (s *AppLockService) Release(ctx context.Context) error { + opCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + var query string + switch s.db.Name() { + case "sqlite": + query = ` + DELETE FROM kv + WHERE key = ? + AND json_extract(value, '$.lock_id') = ? + ` + case "postgres": + query = ` + DELETE FROM kv + WHERE key = $1 + AND value::json->>'lock_id' = $2 + ` + default: + return fmt.Errorf("unsupported database dialect: %s", s.db.Name()) + } + + res := s.db.WithContext(opCtx).Exec(query, lockKey, s.lockID) + if res.Error != nil { + return fmt.Errorf("release lock failed: %w", res.Error) + } + + if res.RowsAffected == 0 { + slog.Warn("Application lock not held by this process, cannot release", + slog.Int64("process_id", s.processID), + slog.String("host_id", s.hostID), + ) + } + + slog.Info("Released application lock", + slog.Int64("process_id", s.processID), + slog.String("host_id", s.hostID), + ) + return nil +} + +// renew tries to renew the lock, retrying up to renewRetries times (sleeping 1s between attempts). +func (s *AppLockService) renew(ctx context.Context) error { + var lastErr error + for attempt := 1; attempt <= renewRetries; attempt++ { + now := time.Now() + nowUnix := now.Unix() + expiresAt := now.Add(ttl).Unix() + + value := lockValue{ + LockID: s.lockID, + ProcessID: s.processID, + HostID: s.hostID, + ExpiresAt: expiresAt, + } + raw, err := value.Marshal() + if err != nil { + return fmt.Errorf("encode lock value: %w", err) + } + + var query string + switch s.db.Name() { + case "sqlite": + query = ` + UPDATE kv + SET value = ? + WHERE key = ? + AND json_extract(value, '$.lock_id') = ? + AND json_extract(value, '$.expires_at') > ? + ` + case "postgres": + query = ` + UPDATE kv + SET value = $1 + WHERE key = $2 + AND value::json->>'lock_id' = $3 + AND ((value::json->>'expires_at')::bigint > $4) + ` + default: + return fmt.Errorf("unsupported database dialect: %s", s.db.Name()) + } + + opCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + res := s.db.WithContext(opCtx).Exec(query, raw, lockKey, s.lockID, nowUnix) + cancel() + + switch { + case res.Error != nil: + lastErr = fmt.Errorf("lock renewal failed: %w", res.Error) + case res.RowsAffected == 0: + // Must be after checking res.Error + return ErrLockLost + default: + slog.Debug("Renewed application lock", + slog.Int64("process_id", s.processID), + slog.String("host_id", s.hostID), + ) + return nil + } + + // Wait before next attempt or cancel if context is done + if attempt < renewRetries { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(1 * time.Second): + } + } + } + + return lastErr +} diff --git a/backend/internal/service/app_lock_service_test.go b/backend/internal/service/app_lock_service_test.go new file mode 100644 index 00000000..95b22f51 --- /dev/null +++ b/backend/internal/service/app_lock_service_test.go @@ -0,0 +1,189 @@ +package service + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "gorm.io/gorm" + + "github.com/pocket-id/pocket-id/backend/internal/model" + testutils "github.com/pocket-id/pocket-id/backend/internal/utils/testing" +) + +func newTestAppLockService(t *testing.T, db *gorm.DB) *AppLockService { + t.Helper() + + return &AppLockService{ + db: db, + processID: 1, + hostID: "test-host", + lockID: "a13c7673-c7ae-49f1-9112-2cd2d0d4b0c1", + } +} + +func insertLock(t *testing.T, db *gorm.DB, value lockValue) { + t.Helper() + + raw, err := value.Marshal() + require.NoError(t, err) + + err = db.Create(&model.KV{Key: lockKey, Value: &raw}).Error + require.NoError(t, err) +} + +func readLockValue(t *testing.T, db *gorm.DB) lockValue { + t.Helper() + + var row model.KV + err := db.Take(&row, "key = ?", lockKey).Error + require.NoError(t, err) + + require.NotNil(t, row.Value) + + var value lockValue + err = value.Unmarshal(*row.Value) + require.NoError(t, err) + + return value +} + +func TestAppLockServiceAcquire(t *testing.T) { + t.Run("creates new lock when none exists", func(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + service := newTestAppLockService(t, db) + + _, err := service.Acquire(context.Background(), false) + require.NoError(t, err) + + stored := readLockValue(t, db) + require.Equal(t, service.processID, stored.ProcessID) + require.Equal(t, service.hostID, stored.HostID) + require.Greater(t, stored.ExpiresAt, time.Now().Unix()) + }) + + t.Run("returns ErrLockUnavailable when lock held by another process", func(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + service := newTestAppLockService(t, db) + + existing := lockValue{ + ProcessID: 99, + HostID: "other-host", + ExpiresAt: time.Now().Add(ttl).Unix(), + } + insertLock(t, db, existing) + + _, err := service.Acquire(context.Background(), false) + require.ErrorIs(t, err, ErrLockUnavailable) + + current := readLockValue(t, db) + require.Equal(t, existing, current) + }) + + t.Run("force acquisition steals lock", func(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + service := newTestAppLockService(t, db) + + insertLock(t, db, lockValue{ + ProcessID: 99, + HostID: "other-host", + ExpiresAt: time.Now().Unix(), + }) + + _, err := service.Acquire(context.Background(), true) + require.NoError(t, err) + + stored := readLockValue(t, db) + require.Equal(t, service.processID, stored.ProcessID) + require.Equal(t, service.hostID, stored.HostID) + require.Greater(t, stored.ExpiresAt, time.Now().Unix()) + }) +} + +func TestAppLockServiceRelease(t *testing.T) { + t.Run("removes owned lock", func(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + service := newTestAppLockService(t, db) + + _, err := service.Acquire(context.Background(), false) + require.NoError(t, err) + + err = service.Release(context.Background()) + require.NoError(t, err) + + var row model.KV + err = db.Take(&row, "key = ?", lockKey).Error + require.ErrorIs(t, err, gorm.ErrRecordNotFound) + }) + + t.Run("ignores lock held by another owner", func(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + service := newTestAppLockService(t, db) + + existing := lockValue{ + ProcessID: 2, + HostID: "other-host", + ExpiresAt: time.Now().Add(ttl).Unix(), + } + insertLock(t, db, existing) + + err := service.Release(context.Background()) + require.NoError(t, err) + + stored := readLockValue(t, db) + require.Equal(t, existing, stored) + }) +} + +func TestAppLockServiceRenew(t *testing.T) { + t.Run("extends expiration when lock is still owned", func(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + service := newTestAppLockService(t, db) + + _, err := service.Acquire(context.Background(), false) + require.NoError(t, err) + + before := readLockValue(t, db) + + err = service.renew(context.Background()) + require.NoError(t, err) + + after := readLockValue(t, db) + require.Equal(t, service.processID, after.ProcessID) + require.Equal(t, service.hostID, after.HostID) + require.GreaterOrEqual(t, after.ExpiresAt, before.ExpiresAt) + }) + + t.Run("returns ErrLockLost when lock is missing", func(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + service := newTestAppLockService(t, db) + + err := service.renew(context.Background()) + require.ErrorIs(t, err, ErrLockLost) + }) + + t.Run("returns ErrLockLost when ownership changed", func(t *testing.T) { + db := testutils.NewDatabaseForTest(t) + service := newTestAppLockService(t, db) + + _, err := service.Acquire(context.Background(), false) + require.NoError(t, err) + + // Simulate a different process taking the lock. + newOwner := lockValue{ + ProcessID: 9, + HostID: "stolen-host", + ExpiresAt: time.Now().Add(ttl).Unix(), + } + raw, marshalErr := newOwner.Marshal() + require.NoError(t, marshalErr) + updateErr := db.Model(&model.KV{}). + Where("key = ?", lockKey). + Update("value", raw).Error + require.NoError(t, updateErr) + + err = service.renew(context.Background()) + require.ErrorIs(t, err, ErrLockLost) + }) +} diff --git a/backend/internal/service/e2etest_service.go b/backend/internal/service/e2etest_service.go index 5b9549e1..c15f4d61 100644 --- a/backend/internal/service/e2etest_service.go +++ b/backend/internal/service/e2etest_service.go @@ -7,14 +7,12 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" - "crypto/x509" "encoding/base64" "fmt" "log/slog" "path" "time" - "github.com/fxamacker/cbor/v2" "github.com/go-webauthn/webauthn/protocol" "github.com/lestrrat-go/jwx/v3/jwa" "github.com/lestrrat-go/jwx/v3/jwk" @@ -36,15 +34,17 @@ type TestService struct { appConfigService *AppConfigService ldapService *LdapService fileStorage storage.FileStorage + appLockService *AppLockService externalIdPKey jwk.Key } -func NewTestService(db *gorm.DB, appConfigService *AppConfigService, jwtService *JwtService, ldapService *LdapService, fileStorage storage.FileStorage) (*TestService, error) { +func NewTestService(db *gorm.DB, appConfigService *AppConfigService, jwtService *JwtService, ldapService *LdapService, appLockService *AppLockService, fileStorage storage.FileStorage) (*TestService, error) { s := &TestService{ db: db, appConfigService: appConfigService, jwtService: jwtService, ldapService: ldapService, + appLockService: appLockService, fileStorage: fileStorage, } err := s.initExternalIdP() @@ -288,8 +288,8 @@ func (s *TestService) SeedDatabase(baseURL string) error { // openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 | \ // openssl pkcs8 -topk8 -nocrypt | tee >(openssl pkey -pubout) - publicKeyPasskey1, _ := s.getCborPublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwcOo5KV169KR67QEHrcYkeXE3CCxv2BgwnSq4VYTQxyLtdmKxegexa8JdwFKhKXa2BMI9xaN15BoL6wSCRFJhg==") - publicKeyPasskey2, _ := s.getCborPublicKey("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEj4qA0PrZzg8Co1C27nyUbzrp8Ewjr7eOlGI2LfrzmbL5nPhZRAdJ3hEaqrHMSnJBhfMqtQGKwDYpaLIQFAKLhw==") + publicKeyPasskey1, _ := base64.StdEncoding.DecodeString("pQMmIAEhWCDBw6jkpXXr0pHrtAQetxiR5cTcILG/YGDCdKrhVhNDHCJYIIu12YrF6B7Frwl3AUqEpdrYEwj3Fo3XkGgvrBIJEUmGAQI=") + publicKeyPasskey2, _ := base64.StdEncoding.DecodeString("pSJYIPmc+FlEB0neERqqscxKckGF8yq1AYrANiloshAUAouHAQIDJiABIVggj4qA0PrZzg8Co1C27nyUbzrp8Ewjr7eOlGI2LfrzmbI=") webauthnCredentials := []model.WebauthnCredential{ { Name: "Passkey 1", @@ -318,6 +318,10 @@ func (s *TestService) SeedDatabase(baseURL string) error { Challenge: "challenge", ExpiresAt: datatype.DateTime(time.Now().Add(1 * time.Hour)), UserVerification: "preferred", + CredentialParams: model.CredentialParameters{ + {Type: "public-key", Algorithm: -7}, + {Type: "public-key", Algorithm: -257}, + }, } if err := tx.Create(&webauthnSession).Error; err != nil { return err @@ -327,9 +331,10 @@ func (s *TestService) SeedDatabase(baseURL string) error { Base: model.Base{ ID: "5f1fa856-c164-4295-961e-175a0d22d725", }, - Name: "Test API Key", - Key: "6c34966f57ef2bb7857649aff0e7ab3ad67af93c846342ced3f5a07be8706c20", - UserID: users[0].ID, + Name: "Test API Key", + Key: "6c34966f57ef2bb7857649aff0e7ab3ad67af93c846342ced3f5a07be8706c20", + UserID: users[0].ID, + ExpiresAt: datatype.DateTime(time.Now().Add(30 * 24 * time.Hour)), } if err := tx.Create(&apiKey).Error; err != nil { return err @@ -379,6 +384,20 @@ func (s *TestService) SeedDatabase(baseURL string) error { } } + keyValues := []model.KV{ + { + Key: jwkutils.PrivateKeyDBKey, + // {"alg":"RS256","d":"mvMDWSdPPvcum0c0iEHE2gbqtV2NKMmLwrl9E6K7g8lTV95SePLnW_bwyMPV7EGp7PQk3l17I5XRhFjze7GqTnFIOgKzMianPs7jv2ELtBMGK0xOPATgu1iGb70xZ6vcvuEfRyY3dJ0zr4jpUdVuXwKmx9rK4IdZn2dFCKfvSuspqIpz11RhF1ALrqDLkxGVv7ZwNh0_VhJZU9hcjG5l6xc7rQEKpPRkZp0IdjkGS8Z0FskoVaiRIWAbZuiVFB9WCW8k1czC4HQTPLpII01bUQx2ludbm0UlXRgVU9ptUUbU7GAImQqTOW8LfPGklEvcgzlIlR_oqw4P9yBxLi-yMQ","dp":"pvNCSnnhbo8Igw9psPR-DicxFnkXlu_ix4gpy6efTrxA-z1VDFDioJ814vKQNioYDzpyAP1gfMPhRkvG_q0hRZsJah3Sb9dfA-WkhSWY7lURQP4yIBTMU0PF_rEATuS7lRciYk1SOx5fqXZd3m_LP0vpBC4Ujlq6NAq6CIjCnms","dq":"TtUVGCCkPNgfOLmkYXu7dxxUCV5kB01-xAEK2OY0n0pG8vfDophH4_D_ZC7nvJ8J9uDhs_3JStexq1lIvaWtG99RNTChIEDzpdn6GH9yaVcb_eB4uJjrNm64FhF8PGCCwxA-xMCZMaARKwhMB2_IOMkxUbWboL3gnhJ2rDO_QO0","e":"AQAB","kid":"8uHDw3M6rf8","kty":"RSA","n":"yaeEL0VKoPBXIAaWXsUgmu05lAvEIIdJn0FX9lHh4JE5UY9B83C5sCNdhs9iSWzpeP11EVjWp8i3Yv2CF7c7u50BXnVBGtxpZpFC-585UXacoJ0chUmarL9GRFJcM1nPHBTFu68aRrn1rIKNHUkNaaxFo0NFGl_4EDDTO8HwawTjwkPoQlRzeByhlvGPVvwgB3Fn93B8QJ_cZhXKxJvjjrC_8Pk76heC_ntEMru71Ix77BoC3j2TuyiN7m9RNBW8BU5q6lKoIdvIeZfTFLzi37iufyfvMrJTixp9zhNB1NxlLCeOZl2MXegtiGqd2H3cbAyqoOiv9ihUWTfXj7SxJw","p":"_Yylc9e07CKdqNRD2EosMC2mrhrEa9j5oY_l00Qyy4-jmCA59Q9viyqvveRo0U7cRvFA5BWgWN6GGLh1DG3X-QBqVr0dnk3uzbobb55RYUXyPLuBZI2q6w2oasbiDwPdY7KpkVv_H-bpITQlyDvO8hhucA6rUV7F6KTQVz8M3Ms","q":"y5p3hch-7jJ21TkAhp_Vk1fLCAuD4tbErwQs2of9ja8sB4iJOs5Wn6HD3P7Mc8Plye7qaLHvzc8I5g0tPKWvC0DPd_FLPXiWwMVAzee3NUX_oGeJNOQp11y1w_KqdO9qZqHSEPZ3NcFL_SZMFgggxhM1uzRiPzsVN0lnD_6prZU","qi":"2Grt6uXHm61ji3xSdkBWNtUnj19vS1-7rFJp5SoYztVQVThf_W52BAiXKBdYZDRVoItC_VS2NvAOjeJjhYO_xQ_q3hK7MdtuXfEPpLnyXKkmWo3lrJ26wbeF6l05LexCkI7ShsOuSt-dsyaTJTszuKDIA6YOfWvfo3aVZmlWRaI","use":"sig"} + Value: utils.Ptr("7d/5hl7diJ2rnFL14hEAQf9tzpu29aqXQ8jpJ2iqqKUNFZpdOkEpud0CmRv4H3r8yyk2u/Gqqj9klSy58DJkYXGF5PAYgLyoBIb7L3JXWRbxg4cQ3QJCug13l2OTmpAKoVc+rmX8c3j3h1sNqyJ+7Ql5sS0jSeyiYgIsFNCdnK5alBDyvtcpe/QDpklmP4JCeVpvmf2rLGplk3g5UO5ydJ8UiDXxfDmi+gF6NKJvrGnnah8Ar3G/x88z+tTJtp0DIQFwxXwUM2XZqzEVGm8K2r0w5o9/Keh6bBBaiuH2C78ZOaijGV3DovhR+e9J0cYUYGwT42MZMx9fSWQ/lvWGGnf+Uq3MXJfjWSREfhkp8KTQwR9F7+dnVJWswOEk7jPR8I7hCWTMxJyvaFX3wgAXIVmhrgXZQQbYOqTt56IoqUl0xOJku8dA8opg2UcLlmmuOh6+hfkXKsiiS/H/9c1BVIGj1fCOiT6IePh4wKKSTbwJnPD5EKmdJpgTsUpjcDnXQKY4ReO0UpdRdKxwRDDLeQuG6j+ljGxR9GPudCU9Nmci6rFVI6n5LWYkQxBA1O73RpmXRZPDzntDfpXMEonkmSvOoxaCK2Id7CRKMdqvR0kEouwnhk5WSFtsfi3sA0pkXzPFxwZeWM8vFtbffZOZzXaOhxCOfcj1NClZohlZhyc4jvkxmrpY7PSaAzih0AmHI7y0LYFi6fZu/K4EheVa1+KF55nWZ8ARikHMWKAKkyExkTak7xyN884TDmzURRaPlQg4jzQte5WMNjAG/hlHibdMBNvgwiYd49ZxteJ8ABdbiXVRl+2JGbdjl2ubpQZwOn7bJKlqO56bIwsZ+e4+pXsuOGdBahkHrUjtMEmH3DZbGc6CJLbcmdhdpApLQRRcLAazxJhzAwJ47FRYsHsj57LnYNvmcKdIxw8rxCdLUuzz95uw0T3ankEO5J9sjem+HMEuKdwXK1UcuOn2rjR8Sd/BuvQmeso27dFbPXqXYNS90Ml45YyTvcKSiopD181oZR703TFUSpR7dsiqROMr+p/2jN9h6a8WbQ8xpksyclaQByY/M77AssbXnG6wfhRsntNIINCZLbBnjXOyz6ZHIC5K4tSTdcnWaiYPeRPQmnw9UUvHAcNU2yMWsy0eU377yDS0WstTxOdQutTdkczl8kv5Lo26JiEK7mSIuRK19ffF9Zz8FG8+eKv5zdyIPjyQRDYBysUoDv5huKe2eoxJu/MWS2Pql/ZtUGeD6Ozm3mCvh0vQ9ceagBkY6Ocm3du0ziAKP29Ri0mjg4DizVorbLzsh+EQH/s2Pi9MnjUZDlEmuLl2Xfp7/w4j/8u0N0tVR70VDFuGdKpTjFY3vS8EJrPtyMTM51x1D9rb8gIql8aR/rJw4YF+huxg1mv5n6+tGVqg5msbPmF12eJijP4lkmaRwIpLW5pJTtaDkUj7uOeu1mm4k+Dt5nh0/0jPHzrv6bcTCcbV7UjMHDoTXXqEpFAAJ66rHR7zdAJu+YKsnTIZyLmOpcowq7LL8G9qTvV0OSpyQWUIavRSgbDHFqEqRs+JU94jAzkq8nCY5MTd9m5sIv9InfdT3k+pwpsE/FKge8nghFLtbUrafGkzTky8SE2druvVcIvbfXMfLIKRUYjJgnWc0gQzF5J6pzXM7D2r/RG6JDzASqjlbURq6v9bhNerlOVdMujWKEEVcKWIzlbt4RkihRjM8AUqIZQOyicGQ+4yfIjAHw5viuABONYs3OIWULnFqJxdvS9rNKhfxSjIq9cfqyzevq2xrRoMXEonobh6M3bD2Vang8OAeVeD1OXWPERi4pepCYFS9RJ/Xa/UWxptsqSNuGcb3fAzQSmLpXLGdWRoKXvSe7EYgc0bGcLOjSTu5RURKo+EF9i4KT9EJauf6VXw5dTf/CCIJRXE1bWzXhSCFYntohYhX2ldOCDYpi/jFBC6Vtkw0ud3/xq8Nmhd5gUk+SpngByCZH3Pm3H+jvlbMpiqkDkm1v74hDX13Xhrcw2eWyuqKBVoRCCniUvwpYNbGvBfjC6Hcizv0Aybciwj+4nybt5EPoEUm6S6Gs7fG7QpPdvrzpAxX70MlmdkF/gwyuhbEeJhLK+WL7qAsN5CvHPzVbsIf90x+nGTtMJPgpxVr0tJMj+vprXV4WxutfARBiOnqe58MhA857sd+MzKBgKnoLOBRTiC3qc/0/ULwbG2HCCD7nmwzz7M4nUuMvo8rgS7z0BF68OClT8X3JwSXbL5Wg=="), + }, + } + + for _, kv := range keyValues { + if err := tx.Create(&kv).Error; err != nil { + return err + } + } + return nil }) @@ -464,47 +483,29 @@ func (s *TestService) ResetAppConfig(ctx context.Context) error { return err } + // Manually set instance ID + err = s.appConfigService.UpdateAppConfigValues(ctx, "instanceId", "test-instance-id") + if err != nil { + return err + } + // Reload the app config from the database after resetting the values - return s.appConfigService.LoadDbConfig(ctx) + err = s.appConfigService.LoadDbConfig(ctx) + if err != nil { + return err + } + + // Reload the JWK + if err := s.jwtService.LoadOrGenerateKey(); err != nil { + return err + } + + return nil } -func (s *TestService) SetJWTKeys() { - const privateKeyString = `{"alg":"RS256","d":"mvMDWSdPPvcum0c0iEHE2gbqtV2NKMmLwrl9E6K7g8lTV95SePLnW_bwyMPV7EGp7PQk3l17I5XRhFjze7GqTnFIOgKzMianPs7jv2ELtBMGK0xOPATgu1iGb70xZ6vcvuEfRyY3dJ0zr4jpUdVuXwKmx9rK4IdZn2dFCKfvSuspqIpz11RhF1ALrqDLkxGVv7ZwNh0_VhJZU9hcjG5l6xc7rQEKpPRkZp0IdjkGS8Z0FskoVaiRIWAbZuiVFB9WCW8k1czC4HQTPLpII01bUQx2ludbm0UlXRgVU9ptUUbU7GAImQqTOW8LfPGklEvcgzlIlR_oqw4P9yBxLi-yMQ","dp":"pvNCSnnhbo8Igw9psPR-DicxFnkXlu_ix4gpy6efTrxA-z1VDFDioJ814vKQNioYDzpyAP1gfMPhRkvG_q0hRZsJah3Sb9dfA-WkhSWY7lURQP4yIBTMU0PF_rEATuS7lRciYk1SOx5fqXZd3m_LP0vpBC4Ujlq6NAq6CIjCnms","dq":"TtUVGCCkPNgfOLmkYXu7dxxUCV5kB01-xAEK2OY0n0pG8vfDophH4_D_ZC7nvJ8J9uDhs_3JStexq1lIvaWtG99RNTChIEDzpdn6GH9yaVcb_eB4uJjrNm64FhF8PGCCwxA-xMCZMaARKwhMB2_IOMkxUbWboL3gnhJ2rDO_QO0","e":"AQAB","kid":"8uHDw3M6rf8","kty":"RSA","n":"yaeEL0VKoPBXIAaWXsUgmu05lAvEIIdJn0FX9lHh4JE5UY9B83C5sCNdhs9iSWzpeP11EVjWp8i3Yv2CF7c7u50BXnVBGtxpZpFC-585UXacoJ0chUmarL9GRFJcM1nPHBTFu68aRrn1rIKNHUkNaaxFo0NFGl_4EDDTO8HwawTjwkPoQlRzeByhlvGPVvwgB3Fn93B8QJ_cZhXKxJvjjrC_8Pk76heC_ntEMru71Ix77BoC3j2TuyiN7m9RNBW8BU5q6lKoIdvIeZfTFLzi37iufyfvMrJTixp9zhNB1NxlLCeOZl2MXegtiGqd2H3cbAyqoOiv9ihUWTfXj7SxJw","p":"_Yylc9e07CKdqNRD2EosMC2mrhrEa9j5oY_l00Qyy4-jmCA59Q9viyqvveRo0U7cRvFA5BWgWN6GGLh1DG3X-QBqVr0dnk3uzbobb55RYUXyPLuBZI2q6w2oasbiDwPdY7KpkVv_H-bpITQlyDvO8hhucA6rUV7F6KTQVz8M3Ms","q":"y5p3hch-7jJ21TkAhp_Vk1fLCAuD4tbErwQs2of9ja8sB4iJOs5Wn6HD3P7Mc8Plye7qaLHvzc8I5g0tPKWvC0DPd_FLPXiWwMVAzee3NUX_oGeJNOQp11y1w_KqdO9qZqHSEPZ3NcFL_SZMFgggxhM1uzRiPzsVN0lnD_6prZU","qi":"2Grt6uXHm61ji3xSdkBWNtUnj19vS1-7rFJp5SoYztVQVThf_W52BAiXKBdYZDRVoItC_VS2NvAOjeJjhYO_xQ_q3hK7MdtuXfEPpLnyXKkmWo3lrJ26wbeF6l05LexCkI7ShsOuSt-dsyaTJTszuKDIA6YOfWvfo3aVZmlWRaI","use":"sig"}` - - privateKey, _ := jwk.ParseKey([]byte(privateKeyString)) - _ = s.jwtService.SetKey(privateKey) -} - -// getCborPublicKey decodes a Base64 encoded public key and returns the CBOR encoded COSE key -func (s *TestService) getCborPublicKey(base64PublicKey string) ([]byte, error) { - decodedKey, err := base64.StdEncoding.DecodeString(base64PublicKey) - if err != nil { - return nil, fmt.Errorf("failed to decode base64 key: %w", err) - } - pubKey, err := x509.ParsePKIXPublicKey(decodedKey) - if err != nil { - return nil, fmt.Errorf("failed to parse public key: %w", err) - } - - ecdsaPubKey, ok := pubKey.(*ecdsa.PublicKey) - if !ok { - return nil, fmt.Errorf("not an ECDSA public key") - } - - coseKey := map[int]interface{}{ - 1: 2, // Key type: EC2 - 3: -7, // Algorithm: ECDSA with SHA-256 - -1: 1, // Curve: P-256 - -2: ecdsaPubKey.X.Bytes(), // X coordinate - -3: ecdsaPubKey.Y.Bytes(), // Y coordinate - } - - cborPublicKey, err := cbor.Marshal(coseKey) - if err != nil { - return nil, fmt.Errorf("failed to marshal COSE key: %w", err) - } - - return cborPublicKey, nil +func (s *TestService) ResetLock(ctx context.Context) error { + _, err := s.appLockService.Acquire(ctx, true) + return err } // SyncLdap triggers an LDAP synchronization diff --git a/backend/internal/service/export_service.go b/backend/internal/service/export_service.go new file mode 100644 index 00000000..31e2ceae --- /dev/null +++ b/backend/internal/service/export_service.go @@ -0,0 +1,217 @@ +package service + +import ( + "archive/zip" + "context" + "encoding/json" + "fmt" + "io" + "path/filepath" + + "gorm.io/gorm" + + datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" + "github.com/pocket-id/pocket-id/backend/internal/storage" + "github.com/pocket-id/pocket-id/backend/internal/utils" +) + +// ExportService handles exporting Pocket ID data into a ZIP archive. +type ExportService struct { + db *gorm.DB + storage storage.FileStorage +} + +func NewExportService(db *gorm.DB, storage storage.FileStorage) *ExportService { + return &ExportService{ + db: db, + storage: storage, + } +} + +// ExportToZip performs the full export process and writes the ZIP data to the given writer. +func (s *ExportService) ExportToZip(ctx context.Context, w io.Writer) error { + dbData, err := s.extractDatabase() + if err != nil { + return err + } + + return s.writeExportZipStream(ctx, w, dbData) +} + +// extractDatabase reads all tables into a DatabaseExport struct +func (s *ExportService) extractDatabase() (DatabaseExport, error) { + schema, err := utils.LoadDBSchemaTypes(s.db) + if err != nil { + return DatabaseExport{}, fmt.Errorf("failed to load schema types: %w", err) + } + + version, err := s.schemaVersion() + if err != nil { + return DatabaseExport{}, err + } + + out := DatabaseExport{ + Provider: s.db.Name(), + Version: version, + Tables: map[string][]map[string]any{}, + // These tables need to be inserted in a specific order because of foreign key constraints + // Not all tables are listed here, because not all tables are order-dependent + TableOrder: []string{"users", "user_groups", "oidc_clients"}, + } + + for table := range schema { + if table == "storage" || table == "schema_migrations" { + continue + } + err = s.dumpTable(table, schema[table], &out) + if err != nil { + return DatabaseExport{}, err + } + } + + return out, nil +} + +func (s *ExportService) schemaVersion() (uint, error) { + var version uint + if err := s.db.Raw("SELECT version FROM schema_migrations").Row().Scan(&version); err != nil { + return 0, fmt.Errorf("failed to query schema version: %w", err) + } + return version, nil +} + +// dumpTable selects all rows from a table and appends them to out.Tables +func (s *ExportService) dumpTable(table string, types utils.DBSchemaTableTypes, out *DatabaseExport) error { + rows, err := s.db.Raw("SELECT * FROM " + table).Rows() + if err != nil { + return fmt.Errorf("failed to read table %s: %w", table, err) + } + defer rows.Close() + + cols, _ := rows.Columns() + if len(cols) != len(types) { + // Should never happen... + return fmt.Errorf("mismatched columns in table (%d) and schema (%d)", len(cols), len(types)) + } + + for rows.Next() { + vals := s.getScanValuesForTable(cols, types) + err = rows.Scan(vals...) + if err != nil { + return fmt.Errorf("failed to scan row in table %s: %w", table, err) + } + + rowMap := make(map[string]any, len(cols)) + for i, col := range cols { + rowMap[col] = vals[i] + } + + // Skip the app lock row in the kv table + if table == "kv" { + if keyPtr, ok := rowMap["key"].(*string); ok && keyPtr != nil && *keyPtr == lockKey { + continue + } + } + + out.Tables[table] = append(out.Tables[table], rowMap) + } + + return rows.Err() +} + +func (s *ExportService) getScanValuesForTable(cols []string, types utils.DBSchemaTableTypes) []any { + res := make([]any, len(cols)) + for i, col := range cols { + // Store a pointer + // Note: don't create a helper function for this switch, because it would return type "any" and mess everything up + // If the column is nullable, we need a pointer to a pointer! + switch types[col].Name { + case "boolean", "bool": + var x bool + if types[col].Nullable { + res[i] = utils.Ptr(utils.Ptr(x)) + } else { + res[i] = utils.Ptr(x) + } + case "blob", "bytea", "jsonb": + // Treat jsonb columns as binary too + var x []byte + if types[col].Nullable { + res[i] = utils.Ptr(utils.Ptr(x)) + } else { + res[i] = utils.Ptr(x) + } + case "timestamp", "timestamptz", "timestamp with time zone", "datetime": + var x datatype.DateTime + if types[col].Nullable { + res[i] = utils.Ptr(utils.Ptr(x)) + } else { + res[i] = utils.Ptr(x) + } + case "integer", "int", "bigint": + var x int64 + if types[col].Nullable { + res[i] = utils.Ptr(utils.Ptr(x)) + } else { + res[i] = utils.Ptr(x) + } + default: + // Treat everything else as a string (including the "numeric" type) + var x string + if types[col].Nullable { + res[i] = utils.Ptr(utils.Ptr(x)) + } else { + res[i] = utils.Ptr(x) + } + } + } + + return res +} + +func (s *ExportService) writeExportZipStream(ctx context.Context, w io.Writer, dbData DatabaseExport) error { + zipWriter := zip.NewWriter(w) + + // Add database.json + jsonWriter, err := zipWriter.Create("database.json") + if err != nil { + return fmt.Errorf("failed to create database.json in zip: %w", err) + } + + jsonEncoder := json.NewEncoder(jsonWriter) + jsonEncoder.SetEscapeHTML(false) + + if err := jsonEncoder.Encode(dbData); err != nil { + return fmt.Errorf("failed to encode database.json: %w", err) + } + + // Add uploaded files + if err := s.addUploadsToZip(ctx, zipWriter); err != nil { + return err + } + + return zipWriter.Close() +} + +// addUploadsToZip adds all files from the storage to the ZIP archive under the "uploads/" directory +func (s *ExportService) addUploadsToZip(ctx context.Context, zipWriter *zip.Writer) error { + return s.storage.Walk(ctx, "/", func(p storage.ObjectInfo) error { + zipPath := filepath.Join("uploads", p.Path) + + w, err := zipWriter.Create(zipPath) + if err != nil { + return fmt.Errorf("failed to create zip entry for %s: %w", zipPath, err) + } + + f, _, err := s.storage.Open(ctx, p.Path) + if err != nil { + return fmt.Errorf("failed to open file %s: %w", zipPath, err) + } + defer f.Close() + + if _, err := io.Copy(w, f); err != nil { + return fmt.Errorf("failed to copy file %s into zip: %w", zipPath, err) + } + return nil + }) +} diff --git a/backend/internal/service/import_service.go b/backend/internal/service/import_service.go new file mode 100644 index 00000000..1c0ec7cf --- /dev/null +++ b/backend/internal/service/import_service.go @@ -0,0 +1,264 @@ +package service + +import ( + "archive/zip" + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "slices" + "strings" + + "gorm.io/gorm" + + datatype "github.com/pocket-id/pocket-id/backend/internal/model/types" + "github.com/pocket-id/pocket-id/backend/internal/storage" + "github.com/pocket-id/pocket-id/backend/internal/utils" +) + +// ImportService handles importing Pocket ID data from an exported ZIP archive. +type ImportService struct { + db *gorm.DB + storage storage.FileStorage +} + +type DatabaseExport struct { + Provider string `json:"provider"` + Version uint `json:"version"` + Tables map[string][]map[string]any `json:"tables"` + TableOrder []string `json:"tableOrder"` +} + +func NewImportService(db *gorm.DB, storage storage.FileStorage) *ImportService { + return &ImportService{ + db: db, + storage: storage, + } +} + +// ImportFromZip performs the full import process from the given ZIP reader. +func (s *ImportService) ImportFromZip(ctx context.Context, r *zip.Reader) error { + dbData, err := processZipDatabaseJson(r.File) + if err != nil { + return err + } + + err = s.ImportDatabase(dbData) + if err != nil { + return err + } + + err = s.importUploads(ctx, r.File) + if err != nil { + return err + } + + return nil +} + +// ImportDatabase only imports the database data from the given DatabaseExport struct. +func (s *ImportService) ImportDatabase(dbData DatabaseExport) error { + err := s.resetSchema(dbData.Version, dbData.Provider) + if err != nil { + return err + } + + err = s.insertData(dbData) + if err != nil { + return err + } + + return nil +} + +// processZipDatabaseJson extracts database.json from the ZIP archive +func processZipDatabaseJson(files []*zip.File) (dbData DatabaseExport, err error) { + for _, f := range files { + if f.Name == "database.json" { + return parseDatabaseJsonStream(f) + } + } + return dbData, errors.New("database.json not found in the ZIP file") +} + +func parseDatabaseJsonStream(f *zip.File) (dbData DatabaseExport, err error) { + rc, err := f.Open() + if err != nil { + return dbData, fmt.Errorf("failed to open database.json: %w", err) + } + defer rc.Close() + + err = json.NewDecoder(rc).Decode(&dbData) + if err != nil { + return dbData, fmt.Errorf("failed to decode database.json: %w", err) + } + + return dbData, nil +} + +// importUploads imports files from the uploads/ directory in the ZIP archive +func (s *ImportService) importUploads(ctx context.Context, files []*zip.File) error { + const maxFileSize = 50 << 20 // 50 MiB + const uploadsPrefix = "uploads/" + + for _, f := range files { + if !strings.HasPrefix(f.Name, uploadsPrefix) { + continue + } + + if f.UncompressedSize64 > maxFileSize { + return fmt.Errorf("file %s too large (%d bytes)", f.Name, f.UncompressedSize64) + } + + targetPath := strings.TrimPrefix(f.Name, uploadsPrefix) + if strings.HasSuffix(f.Name, "/") || targetPath == "" { + continue // Skip directories + } + + err := s.storage.DeleteAll(ctx, targetPath) + if err != nil { + return fmt.Errorf("failed to delete existing file %s: %w", targetPath, err) + } + + rc, err := f.Open() + if err != nil { + return err + } + + buf, err := io.ReadAll(rc) + rc.Close() + if err != nil { + return fmt.Errorf("read file %s: %w", f.Name, err) + } + + err = s.storage.Save(ctx, targetPath, bytes.NewReader(buf)) + if err != nil { + return fmt.Errorf("failed to save file %s: %w", targetPath, err) + } + } + + return nil +} + +// resetSchema drops the existing schema and migrates to the target version +func (s *ImportService) resetSchema(targetVersion uint, exportDbProvider string) error { + sqlDb, err := s.db.DB() + if err != nil { + return fmt.Errorf("failed to get sql.DB: %w", err) + } + + m, err := utils.GetEmbeddedMigrateInstance(sqlDb) + if err != nil { + return fmt.Errorf("failed to get migrate instance: %w", err) + } + + err = m.Drop() + if err != nil { + return fmt.Errorf("failed to drop existing schema: %w", err) + } + + // Needs to be called again to re-create the schema_migrations table + m, err = utils.GetEmbeddedMigrateInstance(sqlDb) + if err != nil { + return fmt.Errorf("failed to get migrate instance: %w", err) + } + + err = m.Migrate(targetVersion) + if err != nil { + return fmt.Errorf("migration failed: %w", err) + } + + return nil +} + +// insertData populates the DB with the imported data +func (s *ImportService) insertData(dbData DatabaseExport) error { + schema, err := utils.LoadDBSchemaTypes(s.db) + if err != nil { + return fmt.Errorf("failed to load schema types: %w", err) + } + + return s.db.Transaction(func(tx *gorm.DB) error { + // Iterate through all tables + // Some tables need to be processed in order + tables := make([]string, 0, len(dbData.Tables)) + tables = append(tables, dbData.TableOrder...) + + for t := range dbData.Tables { + // Skip tables already present where the order matters + // Also skip the schema_migrations table + if slices.Contains(dbData.TableOrder, t) || t == "schema_migrations" { + continue + } + tables = append(tables, t) + } + + // Insert rows + for _, table := range tables { + for _, row := range dbData.Tables[table] { + err = normalizeRowWithSchema(row, table, schema) + if err != nil { + return fmt.Errorf("failed to normalize row for table '%s': %w", table, err) + } + err = tx.Table(table).Create(row).Error + if err != nil { + return fmt.Errorf("failed inserting into table '%s': %w", table, err) + } + } + } + return nil + }) +} + +// normalizeRowWithSchema converts row values based on the DB schema +func normalizeRowWithSchema(row map[string]any, table string, schema utils.DBSchemaTypes) error { + if schema[table] == nil { + return fmt.Errorf("schema not found for table '%s'", table) + } + + for col, val := range row { + if val == nil { + // If the value is nil, skip the column + continue + } + + colType := schema[table][col] + + switch colType.Name { + case "timestamp", "timestamptz", "timestamp with time zone", "datetime": + // Dates are stored as strings + str, ok := val.(string) + if !ok { + return fmt.Errorf("value for column '%s/%s' was expected to be a string, but was '%T'", table, col, val) + } + d, err := datatype.DateTimeFromString(str) + if err != nil { + return fmt.Errorf("failed to decode value for column '%s/%s' as timestamp: %w", table, col, err) + } + row[col] = d + + case "blob", "bytea", "jsonb": + // Binary data and jsonb data is stored in the file as base64-encoded string + str, ok := val.(string) + if !ok { + return fmt.Errorf("value for column '%s/%s' was expected to be a string, but was '%T'", table, col, val) + } + b, err := base64.StdEncoding.DecodeString(str) + if err != nil { + return fmt.Errorf("failed to decode value for column '%s/%s' from base64: %w", table, col, err) + } + + // For jsonb, we additionally cast to json.RawMessage + if colType.Name == "jsonb" { + row[col] = json.RawMessage(b) + } else { + row[col] = b + } + } + } + + return nil +} diff --git a/backend/internal/service/jwt_service.go b/backend/internal/service/jwt_service.go index eb9aef11..07203a99 100644 --- a/backend/internal/service/jwt_service.go +++ b/backend/internal/service/jwt_service.go @@ -48,6 +48,7 @@ const ( ) type JwtService struct { + db *gorm.DB envConfig *common.EnvConfigSchema privateKey jwk.Key keyId string @@ -58,7 +59,6 @@ type JwtService struct { func NewJwtService(db *gorm.DB, appConfigService *AppConfigService) (*JwtService, error) { service := &JwtService{} - // Ensure keys are generated or loaded err := service.init(db, appConfigService, &common.EnvConfig) if err != nil { return nil, err @@ -70,14 +70,15 @@ func NewJwtService(db *gorm.DB, appConfigService *AppConfigService) (*JwtService func (s *JwtService) init(db *gorm.DB, appConfigService *AppConfigService, envConfig *common.EnvConfigSchema) (err error) { s.appConfigService = appConfigService s.envConfig = envConfig + s.db = db // Ensure keys are generated or loaded - return s.loadOrGenerateKey(db) + return s.LoadOrGenerateKey() } -func (s *JwtService) loadOrGenerateKey(db *gorm.DB) error { +func (s *JwtService) LoadOrGenerateKey() error { // Get the key provider - keyProvider, err := jwkutils.GetKeyProvider(db, s.envConfig, s.appConfigService.GetDbConfig().InstanceID.Value) + keyProvider, err := jwkutils.GetKeyProvider(s.db, s.envConfig, s.appConfigService.GetDbConfig().InstanceID.Value) if err != nil { return fmt.Errorf("failed to get key provider: %w", err) } diff --git a/backend/internal/utils/db_migration_util.go b/backend/internal/utils/db_migration_util.go new file mode 100644 index 00000000..cbf33253 --- /dev/null +++ b/backend/internal/utils/db_migration_util.go @@ -0,0 +1,130 @@ +package utils + +import ( + "database/sql" + "errors" + "fmt" + "log/slog" + + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database" + postgresMigrate "github.com/golang-migrate/migrate/v4/database/postgres" + sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3" + "github.com/golang-migrate/migrate/v4/source/iofs" + "github.com/pocket-id/pocket-id/backend/internal/common" + "github.com/pocket-id/pocket-id/backend/resources" +) + +// MigrateDatabase applies database migrations using embedded migration files or fetches them from GitHub if a downgrade is detected. +func MigrateDatabase(sqlDb *sql.DB) error { + m, err := GetEmbeddedMigrateInstance(sqlDb) + if err != nil { + return fmt.Errorf("failed to get migrate instance: %w", err) + } + + path := "migrations/" + string(common.EnvConfig.DbProvider) + requiredVersion, err := getRequiredMigrationVersion(path) + if err != nil { + return fmt.Errorf("failed to get last migration version: %w", err) + } + + currentVersion, _, _ := m.Version() + if currentVersion > requiredVersion { + slog.Warn("Database version is newer than the application supports, possible downgrade detected", slog.Uint64("db_version", uint64(currentVersion)), slog.Uint64("app_version", uint64(requiredVersion))) + if !common.EnvConfig.AllowDowngrade { + return fmt.Errorf("database version (%d) is newer than application version (%d), downgrades are not allowed (set ALLOW_DOWNGRADE=true to enable)", currentVersion, requiredVersion) + } + slog.Info("Fetching migrations from GitHub to handle possible downgrades") + return migrateDatabaseFromGitHub(sqlDb, requiredVersion) + } + + if err := m.Migrate(requiredVersion); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return fmt.Errorf("failed to apply embedded migrations: %w", err) + } + return nil +} + +// GetEmbeddedMigrateInstance creates a migrate.Migrate instance using embedded migration files. +func GetEmbeddedMigrateInstance(sqlDb *sql.DB) (*migrate.Migrate, error) { + path := "migrations/" + string(common.EnvConfig.DbProvider) + source, err := iofs.New(resources.FS, path) + if err != nil { + return nil, fmt.Errorf("failed to create embedded migration source: %w", err) + } + + driver, err := newMigrationDriver(sqlDb, common.EnvConfig.DbProvider) + if err != nil { + return nil, fmt.Errorf("failed to create migration driver: %w", err) + } + + m, err := migrate.NewWithInstance("iofs", source, "pocket-id", driver) + if err != nil { + return nil, fmt.Errorf("failed to create migration instance: %w", err) + } + return m, nil +} + +// newMigrationDriver creates a database.Driver instance based on the given database provider. +func newMigrationDriver(sqlDb *sql.DB, dbProvider common.DbProvider) (driver database.Driver, err error) { + switch dbProvider { + case common.DbProviderSqlite: + driver, err = sqliteMigrate.WithInstance(sqlDb, &sqliteMigrate.Config{ + NoTxWrap: true, + }) + case common.DbProviderPostgres: + driver, err = postgresMigrate.WithInstance(sqlDb, &postgresMigrate.Config{}) + default: + // Should never happen at this point + return nil, fmt.Errorf("unsupported database provider: %s", common.EnvConfig.DbProvider) + } + if err != nil { + return nil, fmt.Errorf("failed to create migration driver: %w", err) + } + + return driver, nil +} + +// migrateDatabaseFromGitHub applies database migrations fetched from GitHub to handle downgrades. +func migrateDatabaseFromGitHub(sqlDb *sql.DB, version uint) error { + srcURL := "github://pocket-id/pocket-id/backend/resources/migrations/" + string(common.EnvConfig.DbProvider) + + driver, err := newMigrationDriver(sqlDb, common.EnvConfig.DbProvider) + if err != nil { + return fmt.Errorf("failed to create migration driver: %w", err) + } + + m, err := migrate.NewWithDatabaseInstance(srcURL, "pocket-id", driver) + if err != nil { + return fmt.Errorf("failed to create GitHub migration instance: %w", err) + } + + if err := m.Migrate(version); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return fmt.Errorf("failed to apply GitHub migrations: %w", err) + } + return nil +} + +// getRequiredMigrationVersion reads the embedded migration files and returns the highest version number found. +func getRequiredMigrationVersion(path string) (uint, error) { + entries, err := resources.FS.ReadDir(path) + if err != nil { + return 0, fmt.Errorf("failed to read migration directory: %w", err) + } + + var maxVersion uint + for _, entry := range entries { + if entry.IsDir() { + continue + } + name := entry.Name() + var version uint + n, err := fmt.Sscanf(name, "%d_", &version) + if err == nil && n == 1 { + if version > maxVersion { + maxVersion = version + } + } + } + + return maxVersion, nil +} diff --git a/backend/internal/utils/db_util.go b/backend/internal/utils/db_util.go new file mode 100644 index 00000000..6e7141ec --- /dev/null +++ b/backend/internal/utils/db_util.go @@ -0,0 +1,116 @@ +package utils + +import ( + "fmt" + "strings" + + "gorm.io/gorm" +) + +// DBTableExists checks if a table exists in the database +func DBTableExists(db *gorm.DB, tableName string) (exists bool, err error) { + switch db.Name() { + case "postgres": + query := `SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = ? + )` + err = db.Raw(query, tableName).Scan(&exists).Error + if err != nil { + return false, err + } + case "sqlite": + query := `SELECT COUNT(*) > 0 FROM sqlite_master WHERE type='table' AND name=?` + err = db.Raw(query, tableName).Scan(&exists).Error + if err != nil { + return false, err + } + default: + return false, fmt.Errorf("unsupported database dialect: %s", db.Name()) + } + + return exists, nil +} + +type DBSchemaColumn struct { + Name string + Nullable bool +} +type DBSchemaTableTypes = map[string]DBSchemaColumn +type DBSchemaTypes = map[string]DBSchemaTableTypes + +// LoadDBSchemaTypes retrieves the column types for all tables in the DB +// Result is a map of "table --> column --> {name: column type name, nullable: boolean}" +func LoadDBSchemaTypes(db *gorm.DB) (result DBSchemaTypes, err error) { + result = make(DBSchemaTypes) + + switch db.Name() { + case "postgres": + var rows []struct { + TableName string + ColumnName string + DataType string + Nullable bool + } + err := db. + Raw(` + SELECT table_name, column_name, data_type, is_nullable = 'YES' AS nullable + FROM information_schema.columns + WHERE table_schema = 'public'; + `). + Scan(&rows). + Error + if err != nil { + return nil, err + } + for _, r := range rows { + t := strings.ToLower(r.DataType) + if result[r.TableName] == nil { + result[r.TableName] = make(map[string]DBSchemaColumn) + } + result[r.TableName][r.ColumnName] = DBSchemaColumn{ + Name: strings.ToLower(t), + Nullable: r.Nullable, + } + } + + case "sqlite": + var tables []string + err = db. + Raw(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';`). + Scan(&tables). + Error + if err != nil { + return nil, err + } + for _, table := range tables { + var cols []struct { + Name string + Type string + Notnull bool + } + err := db. + Raw(`PRAGMA table_info("` + table + `");`). + Scan(&cols). + Error + if err != nil { + return nil, err + } + for _, c := range cols { + if result[table] == nil { + result[table] = make(map[string]DBSchemaColumn) + } + result[table][c.Name] = DBSchemaColumn{ + Name: strings.ToLower(c.Type), + Nullable: !c.Notnull, + } + } + } + + default: + return nil, fmt.Errorf("unsupported database dialect: %s", db.Name()) + } + + return result, nil +} diff --git a/backend/internal/utils/servicerunner.go b/backend/internal/utils/servicerunner.go index a1267912..4ec99618 100644 --- a/backend/internal/utils/servicerunner.go +++ b/backend/internal/utils/servicerunner.go @@ -38,6 +38,7 @@ func (r *ServiceRunner) Run(ctx context.Context) error { // Ignore context canceled errors here as they generally indicate that the service is stopping if rErr != nil && !errors.Is(rErr, context.Canceled) { + cancel() errCh <- rErr return } diff --git a/backend/internal/utils/servicerunner_test.go b/backend/internal/utils/servicerunner_test.go index 271a4c4d..90ff7775 100644 --- a/backend/internal/utils/servicerunner_test.go +++ b/backend/internal/utils/servicerunner_test.go @@ -61,6 +61,26 @@ func TestServiceRunner_Run(t *testing.T) { require.ErrorIs(t, err, expectedErr) }) + t.Run("service error cancels others", func(t *testing.T) { + expectedErr := errors.New("boom") + errorService := func(ctx context.Context) error { + return expectedErr + } + waitingService := func(ctx context.Context) error { + <-ctx.Done() + return ctx.Err() + } + + runner := NewServiceRunner(errorService, waitingService) + + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + + err := runner.Run(ctx) + require.Error(t, err) + require.ErrorIs(t, err, expectedErr) + }) + t.Run("context canceled", func(t *testing.T) { // Create a service that waits until context is canceled waitingService := func(ctx context.Context) error { diff --git a/backend/resources/e2e-tests/database.json b/backend/resources/e2e-tests/database.json new file mode 120000 index 00000000..a5d7ad50 --- /dev/null +++ b/backend/resources/e2e-tests/database.json @@ -0,0 +1 @@ +../../../tests/database.json \ No newline at end of file diff --git a/backend/resources/files_test.go b/backend/resources/files_test.go new file mode 100644 index 00000000..122c1df8 --- /dev/null +++ b/backend/resources/files_test.go @@ -0,0 +1,72 @@ +package resources + +import ( + "embed" + "slices" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// This test is meant to enforce that for every new migration added, a file with the same migration number exists for all supported databases +// This is necessary to ensure import/export works correctly +// Note: if a migration is not needed for a database, ensure there's a file with an empty (no-op) migration (e.g. even just a comment) +func TestMigrationsMatchingVersions(t *testing.T) { + // We can ignore migrations with version below 20251115000000 + const ignoreBefore = 20251115000000 + + // Scan postgres migrations + postgresMigrations := scanMigrations(t, FS, "migrations/postgres", ignoreBefore) + + // Scan sqlite migrations + sqliteMigrations := scanMigrations(t, FS, "migrations/sqlite", ignoreBefore) + + // Sort both lists for consistent comparison + slices.Sort(postgresMigrations) + slices.Sort(sqliteMigrations) + + // Compare the lists + assert.Equal(t, postgresMigrations, sqliteMigrations, "Migration versions must match between Postgres and SQLite") +} + +// scanMigrations scans a directory for migration files and returns a list of versions +func scanMigrations(t *testing.T, fs embed.FS, dir string, ignoreBefore int64) []int64 { + t.Helper() + + entries, err := fs.ReadDir(dir) + require.NoErrorf(t, err, "Failed to read directory '%s'", dir) + + // Divide by 2 because of up and down files + versions := make([]int64, 0, len(entries)/2) + for _, entry := range entries { + if entry.IsDir() { + continue + } + + filename := entry.Name() + + // Only consider .up.sql files + if !strings.HasSuffix(filename, ".up.sql") { + continue + } + + // Extract version from filename (format: _.up.sql) + versionString, _, ok := strings.Cut(filename, "_") + require.Truef(t, ok, "Migration file has unexpected format: %s", filename) + + version, err := strconv.ParseInt(versionString, 10, 64) + require.NoErrorf(t, err, "Failed to parse version from filename '%s'", filename) + + // Exclude migrations with version below ignoreBefore + if version < ignoreBefore { + continue + } + + versions = append(versions, version) + } + + return versions +} diff --git a/backend/resources/migrations/postgres/20251117141000_export_normalization.down.sql b/backend/resources/migrations/postgres/20251117141000_export_normalization.down.sql new file mode 100644 index 00000000..d34341f0 --- /dev/null +++ b/backend/resources/migrations/postgres/20251117141000_export_normalization.down.sql @@ -0,0 +1 @@ +-- No-op in Postgres \ No newline at end of file diff --git a/backend/resources/migrations/postgres/20251117141000_export_normalization.up.sql b/backend/resources/migrations/postgres/20251117141000_export_normalization.up.sql new file mode 100644 index 00000000..d34341f0 --- /dev/null +++ b/backend/resources/migrations/postgres/20251117141000_export_normalization.up.sql @@ -0,0 +1 @@ +-- No-op in Postgres \ No newline at end of file diff --git a/backend/resources/migrations/sqlite/20251117141000_export_normalization.down.sql b/backend/resources/migrations/sqlite/20251117141000_export_normalization.down.sql new file mode 100644 index 00000000..939b3456 --- /dev/null +++ b/backend/resources/migrations/sqlite/20251117141000_export_normalization.down.sql @@ -0,0 +1,133 @@ +PRAGMA foreign_keys = OFF; + +BEGIN; + +CREATE TABLE users_old +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME, + username TEXT COLLATE NOCASE NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + first_name TEXT, + last_name TEXT NOT NULL, + display_name TEXT NOT NULL, + is_admin NUMERIC DEFAULT 0 NOT NULL, + ldap_id TEXT, + locale TEXT, + disabled NUMERIC DEFAULT 0 NOT NULL +); + +INSERT INTO users_old ( + id, + created_at, + username, + email, + first_name, + last_name, + display_name, + is_admin, + ldap_id, + locale, + disabled +) +SELECT + id, + created_at, + username, + email, + first_name, + last_name, + display_name, + CASE WHEN is_admin THEN 1 ELSE 0 END, + ldap_id, + locale, + CASE WHEN disabled THEN 1 ELSE 0 END +FROM users; + +DROP TABLE users; + +ALTER TABLE users_old RENAME TO users; + +CREATE UNIQUE INDEX users_ldap_id ON users (ldap_id); + + + +CREATE TABLE webauthn_credentials_old +( + id TEXT PRIMARY KEY, + created_at DATETIME NOT NULL, + name TEXT NOT NULL, + credential_id TEXT NOT NULL UNIQUE, + public_key BLOB NOT NULL, + attestation_type TEXT NOT NULL, + transport BLOB NOT NULL, + user_id TEXT REFERENCES users ON DELETE CASCADE, + backup_eligible NUMERIC DEFAULT 0 NOT NULL, + backup_state NUMERIC DEFAULT 0 NOT NULL +); + +INSERT INTO webauthn_credentials_old ( + id, + created_at, + name, + credential_id, + public_key, + attestation_type, + transport, + user_id, + backup_eligible, + backup_state +) +SELECT + id, + created_at, + name, + credential_id, + public_key, + attestation_type, + transport, + user_id, + CASE WHEN backup_eligible THEN 1 ELSE 0 END, + CASE WHEN backup_state THEN 1 ELSE 0 END +FROM webauthn_credentials; + +DROP TABLE webauthn_credentials; + +ALTER TABLE webauthn_credentials_old RENAME TO webauthn_credentials; + + + +CREATE TABLE webauthn_sessions_old +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME, + challenge TEXT NOT NULL UNIQUE, + expires_at DATETIME NOT NULL, + user_verification TEXT NOT NULL, + credential_params TEXT DEFAULT '[]' NOT NULL +); + +INSERT INTO webauthn_sessions_old ( + id, + created_at, + challenge, + expires_at, + user_verification, + credential_params +) +SELECT + id, + created_at, + challenge, + expires_at, + user_verification, + credential_params +FROM webauthn_sessions; + +DROP TABLE webauthn_sessions; + +ALTER TABLE webauthn_sessions_old RENAME TO webauthn_sessions; + +COMMIT; + +PRAGMA foreign_keys = ON; \ No newline at end of file diff --git a/backend/resources/migrations/sqlite/20251117141000_export_normalization.up.sql b/backend/resources/migrations/sqlite/20251117141000_export_normalization.up.sql new file mode 100644 index 00000000..b280d370 --- /dev/null +++ b/backend/resources/migrations/sqlite/20251117141000_export_normalization.up.sql @@ -0,0 +1,144 @@ +PRAGMA foreign_keys = OFF; + +BEGIN; + +-- 1. Create a new table with BOOLEAN columns +CREATE TABLE users_new +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME, + username TEXT COLLATE NOCASE NOT NULL UNIQUE, + email TEXT NOT NULL UNIQUE, + first_name TEXT, + last_name TEXT NOT NULL, + display_name TEXT NOT NULL, + is_admin BOOLEAN DEFAULT FALSE NOT NULL, + ldap_id TEXT, + locale TEXT, + disabled BOOLEAN DEFAULT FALSE NOT NULL +); + +-- 2. Copy all existing data, converting numeric bools to real booleans +INSERT INTO users_new ( + id, + created_at, + username, + email, + first_name, + last_name, + display_name, + is_admin, + ldap_id, + locale, + disabled +) +SELECT + id, + created_at, + username, + email, + first_name, + last_name, + display_name, + CASE WHEN is_admin != 0 THEN TRUE ELSE FALSE END, + ldap_id, + locale, + CASE WHEN disabled != 0 THEN TRUE ELSE FALSE END +FROM users; + +-- 3. Drop old table +DROP TABLE users; + +-- 4. Rename new table to original name +ALTER TABLE users_new RENAME TO users; + +-- 5. Recreate index +CREATE UNIQUE INDEX users_ldap_id ON users (ldap_id); + +-- 6. Create temporary table with changed credential_id type to BLOB +CREATE TABLE webauthn_credentials_dg_tmp +( + id TEXT PRIMARY KEY, + created_at DATETIME NOT NULL, + name TEXT NOT NULL, + credential_id BLOB NOT NULL UNIQUE, + public_key BLOB NOT NULL, + attestation_type TEXT NOT NULL, + transport BLOB NOT NULL, + user_id TEXT REFERENCES users ON DELETE CASCADE, + backup_eligible BOOLEAN DEFAULT FALSE NOT NULL, + backup_state BOOLEAN DEFAULT FALSE NOT NULL +); + +-- 7. Copy existing data into the temporary table +INSERT INTO webauthn_credentials_dg_tmp ( + id, + created_at, + name, + credential_id, + public_key, + attestation_type, + transport, + user_id, + backup_eligible, + backup_state +) +SELECT + id, + created_at, + name, + credential_id, + public_key, + attestation_type, + transport, + user_id, + backup_eligible, + backup_state +FROM webauthn_credentials; + +-- 8. Drop old table +DROP TABLE webauthn_credentials; + +-- 9. Rename temporary table to original name +ALTER TABLE webauthn_credentials_dg_tmp + RENAME TO webauthn_credentials; + +-- 10. Create temporary table with credential_params type changed to BLOB +CREATE TABLE webauthn_sessions_dg_tmp +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME, + challenge TEXT NOT NULL UNIQUE, + expires_at DATETIME NOT NULL, + user_verification TEXT NOT NULL, + credential_params BLOB DEFAULT '[]' NOT NULL +); + +-- 11. Copy existing data into the temporary sessions table +INSERT INTO webauthn_sessions_dg_tmp ( + id, + created_at, + challenge, + expires_at, + user_verification, + credential_params +) +SELECT + id, + created_at, + challenge, + expires_at, + user_verification, + credential_params +FROM webauthn_sessions; + +-- 12. Drop old table +DROP TABLE webauthn_sessions; + +-- 13. Rename temporary sessions table to original name +ALTER TABLE webauthn_sessions_dg_tmp + RENAME TO webauthn_sessions; + +COMMIT; + +PRAGMA foreign_keys = ON; \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d3c69ec..41efa56c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -52,7 +52,7 @@ importers: version: 13.2.2 '@tailwindcss/vite': specifier: ^4.1.17 - version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) axios: specifier: ^1.13.2 version: 1.13.2 @@ -70,10 +70,10 @@ importers: version: 1.5.4 runed: specifier: ^0.36.0 - version: 0.36.0(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(zod@4.1.13) + version: 0.36.0(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(zod@4.1.13) sveltekit-superforms: specifier: ^2.28.1 - version: 2.28.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(@types/json-schema@7.0.15)(esbuild@0.25.11)(svelte@5.44.0)(typescript@5.9.3) + version: 2.28.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(@types/json-schema@7.0.15)(esbuild@0.25.12)(svelte@5.44.1)(typescript@5.9.3) tailwind-merge: specifier: ^3.4.0 version: 3.4.0 @@ -95,16 +95,16 @@ importers: version: 3.10.0 '@lucide/svelte': specifier: ^0.554.0 - version: 0.554.0(svelte@5.44.0) + version: 0.554.0(svelte@5.44.1) '@sveltejs/adapter-static': specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))) + version: 3.0.10(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))) '@sveltejs/kit': specifier: ^2.49.0 - version: 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) '@sveltejs/vite-plugin-svelte': specifier: ^6.2.1 - version: 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + version: 6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) '@types/eslint': specifier: ^9.6.1 version: 9.6.1 @@ -116,7 +116,7 @@ importers: version: 1.5.6 bits-ui: specifier: ^2.14.4 - version: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0) + version: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1) eslint: specifier: ^9.39.1 version: 9.39.1(jiti@2.6.1) @@ -125,37 +125,37 @@ importers: version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-svelte: specifier: ^3.13.0 - version: 3.13.0(eslint@9.39.1(jiti@2.6.1))(svelte@5.44.0) + version: 3.13.0(eslint@9.39.1(jiti@2.6.1))(svelte@5.44.1) formsnap: specifier: ^2.0.1 - version: 2.0.1(svelte@5.44.0)(sveltekit-superforms@2.28.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(@types/json-schema@7.0.15)(esbuild@0.25.11)(svelte@5.44.0)(typescript@5.9.3)) + version: 2.0.1(svelte@5.44.1)(sveltekit-superforms@2.28.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(@types/json-schema@7.0.15)(esbuild@0.25.12)(svelte@5.44.1)(typescript@5.9.3)) globals: specifier: ^16.5.0 version: 16.5.0 mode-watcher: specifier: ^1.1.0 - version: 1.1.0(svelte@5.44.0) + version: 1.1.0(svelte@5.44.1) prettier: specifier: ^3.6.2 version: 3.6.2 prettier-plugin-svelte: specifier: ^3.4.0 - version: 3.4.0(prettier@3.6.2)(svelte@5.44.0) + version: 3.4.0(prettier@3.6.2)(svelte@5.44.1) prettier-plugin-tailwindcss: specifier: ^0.7.1 - version: 0.7.1(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.0))(prettier@3.6.2) + version: 0.7.1(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.1))(prettier@3.6.2) rollup: specifier: ^4.53.3 version: 4.53.3 svelte: specifier: ^5.44.0 - version: 5.44.0 + version: 5.44.1 svelte-check: specifier: ^4.3.4 - version: 4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3) + version: 4.3.4(picomatch@4.0.3)(svelte@5.44.1)(typescript@5.9.3) svelte-sonner: specifier: ^1.0.6 - version: 1.0.6(svelte@5.44.0) + version: 1.0.6(svelte@5.44.1) tailwind-variants: specifier: ^3.2.2 version: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.17) @@ -176,16 +176,23 @@ importers: version: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6) tests: + dependencies: + adm-zip: + specifier: ^0.5.16 + version: 0.5.16 devDependencies: '@playwright/test': specifier: ^1.57.0 version: 1.57.0 + '@types/adm-zip': + specifier: ^0.5.7 + version: 0.5.7 '@types/node': - specifier: ^24.10.1 - version: 24.10.1 + specifier: ^22.18.12 + version: 22.19.1 dotenv: specifier: ^17.2.3 version: 17.2.3 @@ -198,14 +205,11 @@ importers: packages: - '@ark/regex@0.0.0': - resolution: {integrity: sha512-p4vsWnd/LRGOdGQglbwOguIVhPmCAf5UzquvnDoxqhhPWTP84wWgi1INea8MgJ4SnI2gp37f13oA4Waz9vwNYg==} + '@ark/schema@0.55.0': + resolution: {integrity: sha512-IlSIc0FmLKTDGr4I/FzNHauMn0MADA6bCjT1wauu4k6MyxhC1R9gz0olNpIRvK7lGGDwtc/VO0RUDNvVQW5WFg==} - '@ark/schema@0.50.0': - resolution: {integrity: sha512-hfmP82GltBZDadIOeR3argKNlYYyB2wyzHp0eeAqAOFBQguglMV/S7Ip2q007bRtKxIMLDqFY6tfPie1dtssaQ==} - - '@ark/util@0.50.0': - resolution: {integrity: sha512-tIkgIMVRpkfXRQIEf0G2CJryZVtHVrqcWHMDa5QKo0OEEBu0tHkRSIMm4Ln8cd8Bn9TPZtvc/kE2Gma8RESPSg==} + '@ark/util@0.55.0': + resolution: {integrity: sha512-aWFNK7aqSvqFtVsl1xmbTjGbg91uqtJV7Za76YGNEwIO4qLjMfyY8flmmbhooYMuqPCO2jyxu8hve943D+w3bA==} '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} @@ -248,161 +252,161 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@emnapi/runtime@1.6.0': - resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==} + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} - '@esbuild/aix-ppc64@0.25.11': - resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.11': - resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.11': - resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.11': - resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.11': - resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.11': - resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.11': - resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.11': - resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.11': - resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.11': - resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.11': - resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.11': - resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.11': - resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.11': - resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.11': - resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.11': - resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.11': - resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.11': - resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.11': - resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.11': - resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.11': - resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.11': - resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.11': - resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.11': - resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.11': - resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.11': - resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -448,11 +452,11 @@ packages: '@exodus/schemasafe@1.3.0': resolution: {integrity: sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==} - '@finom/zod-to-json-schema@3.24.11': - resolution: {integrity: sha512-fL656yBPiWebtfGItvtXLWrFNGlF1NcDFS0WdMQXMs9LluVg0CfT5E2oXYp0pidl0vVG53XkW55ysijNkU5/hA==} + '@finom/zod-to-json-schema@3.24.12': + resolution: {integrity: sha512-mf8CyoW+dFvsvROvHIXznrYWdmlxvBJGIeQpGJaD9iBn23kSSPiC7H0YIqgziMZJDFIzL4VEFCwpcUSHmoeNVw==} deprecated: 'Use https://www.npmjs.com/package/zod-v3-to-json-schema instead. See issue comment for details: https://github.com/StefanTerdell/zod-to-json-schema/issues/178#issuecomment-3533122539' peerDependencies: - zod: ^4.0.14 + zod: ^3.25 || ^4.0.14 '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -493,124 +497,135 @@ packages: resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.4': - resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==} + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.4': - resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==} + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.3': - resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==} + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.3': - resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==} + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.3': - resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==} + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.2.3': - resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==} + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.2.3': - resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==} + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.2.3': - resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==} + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.2.3': - resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==} + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.2.3': - resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.2.3': - resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==} + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.4': - resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==} + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.4': - resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==} + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-ppc64@0.34.4': - resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==} + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - '@img/sharp-linux-s390x@0.34.4': - resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==} + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.4': - resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==} + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.4': - resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==} + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.4': - resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==} + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.4': - resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==} + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.4': - resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==} + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.4': - resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==} + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.4': - resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==} + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -657,9 +672,6 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/source-map@0.3.11': - resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -1038,8 +1050,8 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@sveltejs/acorn-typescript@1.0.6': - resolution: {integrity: sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==} + '@sveltejs/acorn-typescript@1.0.7': + resolution: {integrity: sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==} peerDependencies: acorn: ^8.9.0 @@ -1172,6 +1184,9 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@types/adm-zip@0.5.7': + resolution: {integrity: sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -1187,6 +1202,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@22.19.1': + resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} + '@types/node@24.10.1': resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} @@ -1201,8 +1219,8 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} - '@types/validator@13.15.3': - resolution: {integrity: sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==} + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} '@typeschema/class-validator@0.3.0': resolution: {integrity: sha512-OJSFeZDIQ8EK1HTljKLT5CItM2wsbgczLN8tMEfz3I1Lmhc5TBfkZ0eikFzUC16tI3d1Nag7um6TfCgp2I2Bww==} @@ -1301,6 +1319,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + adm-zip@0.5.16: + resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==} + engines: {node: '>=12.0'} + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -1338,8 +1360,11 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - arktype@2.1.23: - resolution: {integrity: sha512-tyxNWX6xJVMb2EPJJ3OjgQS1G/vIeQRrZuY4DeBNQmh8n7geS+czgbauQWB6Pr+RXiOO8ChEey44XdmxsqGmfQ==} + arkregex@0.0.3: + resolution: {integrity: sha512-bU21QJOJEFJK+BPNgv+5bVXkvRxyAvgnon75D92newgHxkBJTgiFwQxusyViYyJkETsddPlHyspshDQcCzmkNg==} + + arktype@2.1.27: + resolution: {integrity: sha512-enctOHxI4SULBv/TDtCVi5M8oLd4J5SVlPUblXDzSsOYQNMzmVbUosGBnJuZDKmFlN5Ie0/QVEuTE+Z5X1UhsQ==} array-timsort@1.0.3: resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} @@ -1396,8 +1421,8 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} - caniuse-lite@1.0.30001751: - resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} + caniuse-lite@1.0.30001757: + resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -1414,8 +1439,8 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - class-validator@0.14.2: - resolution: {integrity: sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==} + class-validator@0.14.3: + resolution: {integrity: sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==} cli-cursor@5.0.0: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} @@ -1454,9 +1479,6 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - comment-json@4.4.1: resolution: {integrity: sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==} engines: {node: '>= 6'} @@ -1509,8 +1531,8 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} - dayjs@1.11.18: - resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} debounce-fn@6.0.0: resolution: {integrity: sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==} @@ -1569,8 +1591,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.4.2: - resolution: {integrity: sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==} + devalue@5.5.0: + resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} dijkstrajs@1.0.3: resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} @@ -1606,8 +1628,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - effect@3.18.4: - resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} + effect@3.19.6: + resolution: {integrity: sha512-Eh1E/CI+xCAcMSDC5DtyE29yWJINC0zwBbwHappQPorjKyS69rCA8qzpsHpfhKnPDYgxdg8zkknii8mZ+6YMQA==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -1660,8 +1682,8 @@ packages: peerDependencies: esbuild: '*' - esbuild@0.25.11: - resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true @@ -1723,8 +1745,8 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - esrap@2.1.0: - resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==} + esrap@2.2.0: + resolution: {integrity: sha512-WBmtxe7R9C5mvL4n2le8nMUe4mD5V9oiK2vJpQ9I3y20ENPUomPcphBXE8D1x/Bm84oN1V+lOfgXxtqmxTp3Xg==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -1738,8 +1760,8 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - exsolve@1.0.7: - resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} fast-check@3.23.2: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} @@ -1803,8 +1825,8 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@4.0.4: - resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} formsnap@2.0.1: @@ -1850,8 +1872,8 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@11.0.3: - resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + glob@11.1.0: + resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==} engines: {node: 20 || >=22} hasBin: true @@ -1916,8 +1938,8 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inline-style-parser@0.2.4: - resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} @@ -1973,8 +1995,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true jsesc@3.1.0: @@ -2031,8 +2053,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - libphonenumber-js@1.12.24: - resolution: {integrity: sha512-l5IlyL9AONj4voSd7q9xkuQOL4u8Ty44puTic7J88CmdXkxfGsRfoVLXHCxppwehgpb/Chdb80FFehHqjN3ItQ==} + libphonenumber-js@1.12.29: + resolution: {integrity: sha512-P2aLrbeqHbmh8+9P35LXQfXOKc7XJ0ymUKl7tyeyQjdRNfzunXWxQXGc4yl3fUf28fqLRfPY+vIVvFXK7KEBTw==} lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} @@ -2165,16 +2187,16 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime-types@3.0.1: - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} - engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - minimatch@10.0.3: - resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + minimatch@10.1.1: + resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==} engines: {node: 20 || >=22} minimatch@3.1.2: @@ -2307,8 +2329,8 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-scurry@2.0.0: - resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + path-scurry@2.0.1: + resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==} engines: {node: 20 || >=22} pathe@2.0.3: @@ -2572,11 +2594,11 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - sharp@0.34.4: - resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} shebang-command@2.0.0: @@ -2663,8 +2685,8 @@ packages: stubborn-utils@1.0.2: resolution: {integrity: sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==} - style-to-object@1.0.11: - resolution: {integrity: sha512-5A560JmXr7wDyGLK12Nq/EYS38VkGlglVzkis1JEdbGWSnbQIEhZzTJhzURXN5/8WwwFCs/f/VVcmkTppbXLow==} + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} @@ -2727,8 +2749,8 @@ packages: peerDependencies: svelte: ^5.0.0 - svelte@5.44.0: - resolution: {integrity: sha512-R7387No2zEGw4CtYtI2rgsui6BqjFARzoZFGLiLN5OPla0Pq4Ra2WwcP/zBomP3MYalhSNvF1fzDMuU0P0zPJw==} + svelte@5.44.1: + resolution: {integrity: sha512-8VnkRXpa6tJ9IqiwKvzZBNnBy9tZg0N63duDz0EJqiozsmBEAZfHiZzWWWAneIN+cAWkK1JkafW1xIbC4YrdBA==} engines: {node: '>=18'} sveltekit-superforms@2.28.1: @@ -2764,11 +2786,6 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - terser@5.44.0: - resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} - engines: {node: '>=10'} - hasBin: true - tiny-case@1.0.3: resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} @@ -2829,8 +2846,8 @@ packages: resolution: {integrity: sha512-xxCJm+Bckc6kQBknN7i9fnP/xobQRsRQxR01CztFkp/h++yfVxUUcmMgfR2HttJx/dpWjS9ubVuyspJv24Q9DA==} engines: {node: '>=20'} - typebox@1.0.43: - resolution: {integrity: sha512-NALtTG+DfndC+48JURdOQ6y6CrCEZl9xLM+BMNwFduUIUJv6AqqVT3ZcCf+jQ9uEJlLAhLyds/gkfufkpBh+2g==} + typebox@1.0.56: + resolution: {integrity: sha512-KMd1DJnIRqLUzAicpFmGqgmt+/IePCEmT/Jtywyyyn0hK6+dupQnxm7OAIn/cL/vu22jKi1XvDjDhrpatZ46kA==} typescript-eslint@8.48.0: resolution: {integrity: sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==} @@ -2848,11 +2865,14 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - unplugin@2.3.10: - resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} uri-js@4.4.1: @@ -2876,16 +2896,16 @@ packages: typescript: optional: true - valibot@1.1.0: - resolution: {integrity: sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==} + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: typescript: optional: true - validator@13.15.15: - resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} + validator@13.15.23: + resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==} engines: {node: '>= 0.10'} vary@1.1.2: @@ -2989,11 +3009,6 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} - engines: {node: '>= 14.6'} - hasBin: true - yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -3021,17 +3036,12 @@ packages: snapshots: - '@ark/regex@0.0.0': + '@ark/schema@0.55.0': dependencies: - '@ark/util': 0.50.0 + '@ark/util': 0.55.0 optional: true - '@ark/schema@0.50.0': - dependencies: - '@ark/util': 0.50.0 - optional: true - - '@ark/util@0.50.0': + '@ark/util@0.55.0': optional: true '@babel/code-frame@7.27.1': @@ -3084,87 +3094,87 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@emnapi/runtime@1.6.0': + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.25.11': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/android-arm64@0.25.11': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm@0.25.11': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-x64@0.25.11': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.25.11': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-x64@0.25.11': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.25.11': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.25.11': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/linux-arm64@0.25.11': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm@0.25.11': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-ia32@0.25.11': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-loong64@0.25.11': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-mips64el@0.25.11': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-ppc64@0.25.11': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.25.11': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-s390x@0.25.11': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-x64@0.25.11': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.25.11': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.25.11': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.25.11': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.25.11': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.25.11': + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/sunos-x64@0.25.11': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/win32-arm64@0.25.11': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-ia32@0.25.11': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-x64@0.25.11': + '@esbuild/win32-x64@0.25.12': optional: true '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': @@ -3198,7 +3208,7 @@ snapshots: globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -3216,7 +3226,7 @@ snapshots: '@exodus/schemasafe@1.3.0': optional: true - '@finom/zod-to-json-schema@3.24.11(zod@4.1.13)': + '@finom/zod-to-json-schema@3.24.12(zod@4.1.13)': dependencies: zod: 4.1.13 optional: true @@ -3232,12 +3242,12 @@ snapshots: '@floating-ui/utils@0.2.10': {} - '@gcornut/valibot-json-schema@0.42.0(esbuild@0.25.11)(typescript@5.9.3)': + '@gcornut/valibot-json-schema@0.42.0(esbuild@0.25.12)(typescript@5.9.3)': dependencies: valibot: 0.42.1(typescript@5.9.3) optionalDependencies: '@types/json-schema': 7.0.15 - esbuild-runner: 2.2.2(esbuild@0.25.11) + esbuild-runner: 2.2.2(esbuild@0.25.12) transitivePeerDependencies: - esbuild - typescript @@ -3265,90 +3275,98 @@ snapshots: '@img/colour@1.0.0': optional: true - '@img/sharp-darwin-arm64@0.34.4': + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.3 + '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.34.4': + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.3 + '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.3': + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.2.3': + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.2.3': + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.2.3': + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.3': + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.2.3': + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.2.3': + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.3': + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.3': + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-linux-arm64@0.34.4': + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.3 + '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm@0.34.4': + '@img/sharp-linux-arm@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.3 + '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-ppc64@0.34.4': + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.3 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.34.4': + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.3 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-x64@0.34.4': + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.3 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.34.4': + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.34.4': + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-wasm32@0.34.4': + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.6.0 + '@emnapi/runtime': 1.7.1 optional: true - '@img/sharp-win32-arm64@0.34.4': + '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-ia32@0.34.4': + '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-x64@0.34.4': + '@img/sharp-win32-x64@0.34.5': optional: true '@inlang/paraglide-js@2.5.0': @@ -3358,7 +3376,7 @@ snapshots: commander: 11.1.0 consola: 3.4.0 json5: 2.2.3 - unplugin: 2.3.10 + unplugin: 2.3.11 urlpattern-polyfill: 10.1.0 transitivePeerDependencies: - babel-plugin-macros @@ -3418,12 +3436,6 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/source-map@0.3.11': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - optional: true - '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -3445,9 +3457,9 @@ snapshots: '@lix-js/server-protocol-schema@0.1.1': {} - '@lucide/svelte@0.554.0(svelte@5.44.0)': + '@lucide/svelte@0.554.0(svelte@5.44.1)': dependencies: - svelte: 5.44.0 + svelte: 5.44.1 '@next/env@16.0.1': {} @@ -3713,51 +3725,51 @@ snapshots: '@standard-schema/spec@1.0.0': {} - '@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.7(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))': dependencies: - '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) - '@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))': dependencies: '@standard-schema/spec': 1.0.0 - '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.4.2 + devalue: 5.5.0 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 mrmime: 2.0.1 sade: 1.8.1 - set-cookie-parser: 2.7.1 + set-cookie-parser: 2.7.2 sirv: 3.0.2 - svelte: 5.44.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + svelte: 5.44.1 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6) - '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) debug: 4.4.3 - svelte: 5.44.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + svelte: 5.44.1 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) debug: 4.4.3 deepmerge: 4.3.1 magic-string: 0.30.21 - svelte: 5.44.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + svelte: 5.44.1 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6) + vitefu: 1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) transitivePeerDependencies: - supports-color @@ -3830,12 +3842,16 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 - '@tailwindcss/vite@4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))': + '@tailwindcss/vite@4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6))': dependencies: '@tailwindcss/node': 4.1.17 '@tailwindcss/oxide': 4.1.17 tailwindcss: 4.1.17 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6) + + '@types/adm-zip@0.5.7': + dependencies: + '@types/node': 22.19.1 '@types/cookie@0.6.0': {} @@ -3852,6 +3868,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@22.19.1': + dependencies: + undici-types: 6.21.0 + '@types/node@24.10.1': dependencies: undici-types: 7.16.0 @@ -3868,14 +3888,14 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/validator@13.15.3': + '@types/validator@13.15.10': optional: true - '@typeschema/class-validator@0.3.0(@types/json-schema@7.0.15)(class-validator@0.14.2)': + '@typeschema/class-validator@0.3.0(@types/json-schema@7.0.15)(class-validator@0.14.3)': dependencies: '@typeschema/core': 0.14.0(@types/json-schema@7.0.15) optionalDependencies: - class-validator: 0.14.2 + class-validator: 0.14.3 transitivePeerDependencies: - '@types/json-schema' optional: true @@ -3983,13 +4003,13 @@ snapshots: '@vinejs/vine@3.0.1': dependencies: '@poppinss/macroable': 1.1.0 - '@types/validator': 13.15.3 + '@types/validator': 13.15.10 '@vinejs/compiler': 3.0.0 camelcase: 8.0.0 - dayjs: 1.11.18 + dayjs: 1.11.19 dlv: 1.1.3 normalize-url: 8.1.0 - validator: 13.15.15 + validator: 13.15.23 optional: true accepts@1.3.8: @@ -4003,6 +4023,8 @@ snapshots: acorn@8.15.0: {} + adm-zip@0.5.16: {} + ajv-formats@3.0.1(ajv@8.17.1): optionalDependencies: ajv: 8.17.1 @@ -4035,11 +4057,16 @@ snapshots: aria-query@5.3.2: {} - arktype@2.1.23: + arkregex@0.0.3: dependencies: - '@ark/regex': 0.0.0 - '@ark/schema': 0.50.0 - '@ark/util': 0.50.0 + '@ark/util': 0.55.0 + optional: true + + arktype@2.1.27: + dependencies: + '@ark/schema': 0.55.0 + '@ark/util': 0.55.0 + arkregex: 0.0.3 optional: true array-timsort@1.0.3: {} @@ -4054,7 +4081,7 @@ snapshots: axios@1.13.2: dependencies: follow-redirects: 1.15.11 - form-data: 4.0.4 + form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -4065,15 +4092,15 @@ snapshots: base64id@2.0.0: {} - bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0): + bits-ui@2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1): dependencies: '@floating-ui/core': 1.7.3 '@floating-ui/dom': 1.7.4 '@internationalized/date': 3.10.0 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0) - svelte: 5.44.0 - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0) + runed: 0.35.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1) + svelte: 5.44.1 + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1) tabbable: 6.3.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -4102,7 +4129,7 @@ snapshots: camelcase@8.0.0: optional: true - caniuse-lite@1.0.30001751: {} + caniuse-lite@1.0.30001757: {} chalk@4.1.2: dependencies: @@ -4119,11 +4146,11 @@ snapshots: dependencies: consola: 3.4.2 - class-validator@0.14.2: + class-validator@0.14.3: dependencies: - '@types/validator': 13.15.3 - libphonenumber-js: 1.12.24 - validator: 13.15.15 + '@types/validator': 13.15.10 + libphonenumber-js: 1.12.29 + validator: 13.15.23 optional: true cli-cursor@5.0.0: @@ -4156,9 +4183,6 @@ snapshots: commander@13.1.0: {} - commander@2.20.3: - optional: true - comment-json@4.4.1: dependencies: array-timsort: 1.0.3 @@ -4208,7 +4232,7 @@ snapshots: date-fns@4.1.0: {} - dayjs@1.11.18: + dayjs@1.11.19: optional: true debounce-fn@6.0.0: @@ -4239,7 +4263,7 @@ snapshots: detect-libc@2.1.2: {} - devalue@5.4.2: {} + devalue@5.5.0: {} dijkstrajs@1.0.3: {} @@ -4278,7 +4302,7 @@ snapshots: eastasianwidth@0.2.0: {} - effect@3.18.4: + effect@3.19.6: dependencies: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 @@ -4332,41 +4356,41 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - esbuild-runner@2.2.2(esbuild@0.25.11): + esbuild-runner@2.2.2(esbuild@0.25.12): dependencies: - esbuild: 0.25.11 + esbuild: 0.25.12 source-map-support: 0.5.21 tslib: 2.4.0 optional: true - esbuild@0.25.11: + esbuild@0.25.12: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.11 - '@esbuild/android-arm': 0.25.11 - '@esbuild/android-arm64': 0.25.11 - '@esbuild/android-x64': 0.25.11 - '@esbuild/darwin-arm64': 0.25.11 - '@esbuild/darwin-x64': 0.25.11 - '@esbuild/freebsd-arm64': 0.25.11 - '@esbuild/freebsd-x64': 0.25.11 - '@esbuild/linux-arm': 0.25.11 - '@esbuild/linux-arm64': 0.25.11 - '@esbuild/linux-ia32': 0.25.11 - '@esbuild/linux-loong64': 0.25.11 - '@esbuild/linux-mips64el': 0.25.11 - '@esbuild/linux-ppc64': 0.25.11 - '@esbuild/linux-riscv64': 0.25.11 - '@esbuild/linux-s390x': 0.25.11 - '@esbuild/linux-x64': 0.25.11 - '@esbuild/netbsd-arm64': 0.25.11 - '@esbuild/netbsd-x64': 0.25.11 - '@esbuild/openbsd-arm64': 0.25.11 - '@esbuild/openbsd-x64': 0.25.11 - '@esbuild/openharmony-arm64': 0.25.11 - '@esbuild/sunos-x64': 0.25.11 - '@esbuild/win32-arm64': 0.25.11 - '@esbuild/win32-ia32': 0.25.11 - '@esbuild/win32-x64': 0.25.11 + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 escape-string-regexp@4.0.0: {} @@ -4374,7 +4398,7 @@ snapshots: dependencies: eslint: 9.39.1(jiti@2.6.1) - eslint-plugin-svelte@3.13.0(eslint@9.39.1(jiti@2.6.1))(svelte@5.44.0): + eslint-plugin-svelte@3.13.0(eslint@9.39.1(jiti@2.6.1))(svelte@5.44.1): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.5 @@ -4386,9 +4410,9 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) semver: 7.7.3 - svelte-eslint-parser: 1.4.0(svelte@5.44.0) + svelte-eslint-parser: 1.4.0(svelte@5.44.1) optionalDependencies: - svelte: 5.44.0 + svelte: 5.44.1 transitivePeerDependencies: - ts-node @@ -4456,7 +4480,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.1.0: + esrap@2.2.0: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4468,7 +4492,7 @@ snapshots: esutils@2.0.3: {} - exsolve@1.0.7: {} + exsolve@1.0.8: {} fast-check@3.23.2: dependencies: @@ -4517,7 +4541,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@4.0.4: + form-data@4.0.5: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -4525,11 +4549,11 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 - formsnap@2.0.1(svelte@5.44.0)(sveltekit-superforms@2.28.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(@types/json-schema@7.0.15)(esbuild@0.25.11)(svelte@5.44.0)(typescript@5.9.3)): + formsnap@2.0.1(svelte@5.44.1)(sveltekit-superforms@2.28.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(@types/json-schema@7.0.15)(esbuild@0.25.12)(svelte@5.44.1)(typescript@5.9.3)): dependencies: - svelte: 5.44.0 - svelte-toolbelt: 0.5.0(svelte@5.44.0) - sveltekit-superforms: 2.28.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(@types/json-schema@7.0.15)(esbuild@0.25.11)(svelte@5.44.0)(typescript@5.9.3) + svelte: 5.44.1 + svelte-toolbelt: 0.5.0(svelte@5.44.1) + sveltekit-superforms: 2.28.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(@types/json-schema@7.0.15)(esbuild@0.25.12)(svelte@5.44.1)(typescript@5.9.3) fsevents@2.3.2: optional: true @@ -4569,14 +4593,14 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@11.0.3: + glob@11.1.0: dependencies: foreground-child: 3.3.1 jackspeak: 4.1.1 - minimatch: 10.0.3 + minimatch: 10.1.1 minipass: 7.1.2 package-json-from-dist: 1.0.1 - path-scurry: 2.0.0 + path-scurry: 2.0.1 globals@14.0.0: {} @@ -4628,7 +4652,7 @@ snapshots: imurmurhash@0.1.4: {} - inline-style-parser@0.2.4: {} + inline-style-parser@0.2.7: {} is-extglob@2.1.1: {} @@ -4673,7 +4697,7 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -4716,7 +4740,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.12.24: + libphonenumber-js@1.12.29: optional: true lightningcss-android-arm64@1.30.2: @@ -4814,13 +4838,13 @@ snapshots: dependencies: mime-db: 1.52.0 - mime-types@3.0.1: + mime-types@3.0.2: dependencies: mime-db: 1.54.0 mimic-function@5.0.1: {} - minimatch@10.0.3: + minimatch@10.1.1: dependencies: '@isaacs/brace-expansion': 5.0.0 @@ -4836,11 +4860,11 @@ snapshots: minipass@7.1.2: {} - mode-watcher@1.1.0(svelte@5.44.0): + mode-watcher@1.1.0(svelte@5.44.1): dependencies: - runed: 0.25.0(svelte@5.44.0) - svelte: 5.44.0 - svelte-toolbelt: 0.7.1(svelte@5.44.0) + runed: 0.25.0(svelte@5.44.1) + svelte: 5.44.1 + svelte-toolbelt: 0.7.1(svelte@5.44.1) mri@1.2.0: {} @@ -4858,7 +4882,7 @@ snapshots: dependencies: '@next/env': 16.0.1 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001751 + caniuse-lite: 1.0.30001757 postcss: 8.4.31 react: 19.2.0 react-dom: 19.2.0(react@19.2.0) @@ -4873,7 +4897,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 16.0.1 '@next/swc-win32-x64-msvc': 16.0.1 '@playwright/test': 1.57.0 - sharp: 0.34.4 + sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -4951,7 +4975,7 @@ snapshots: path-key@3.1.1: {} - path-scurry@2.0.0: + path-scurry@2.0.1: dependencies: lru-cache: 11.2.2 minipass: 7.1.2 @@ -4967,7 +4991,7 @@ snapshots: pkg-types@2.3.0: dependencies: confbox: 0.2.2 - exsolve: 1.0.7 + exsolve: 1.0.8 pathe: 2.0.3 playwright-core@1.57.0: {} @@ -5014,16 +5038,16 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.0): + prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.1): dependencies: prettier: 3.6.2 - svelte: 5.44.0 + svelte: 5.44.1 - prettier-plugin-tailwindcss@0.7.1(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.0))(prettier@3.6.2): + prettier-plugin-tailwindcss@0.7.1(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.1))(prettier@3.6.2): dependencies: prettier: 3.6.2 optionalDependencies: - prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@5.44.0) + prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@5.44.1) prettier@3.6.2: {} @@ -5063,11 +5087,11 @@ snapshots: commander: 13.1.0 conf: 15.0.2 debounce: 2.2.0 - esbuild: 0.25.11 - glob: 11.0.3 + esbuild: 0.25.12 + glob: 11.1.0 jiti: 2.4.2 log-symbols: 7.0.1 - mime-types: 3.0.1 + mime-types: 3.0.2 normalize-path: 3.0.0 nypm: 0.6.0 ora: 8.2.0 @@ -5126,38 +5150,38 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.53.3 fsevents: 2.3.3 - runed@0.23.4(svelte@5.44.0): + runed@0.23.4(svelte@5.44.1): dependencies: esm-env: 1.2.2 - svelte: 5.44.0 + svelte: 5.44.1 - runed@0.25.0(svelte@5.44.0): + runed@0.25.0(svelte@5.44.1): dependencies: esm-env: 1.2.2 - svelte: 5.44.0 + svelte: 5.44.1 - runed@0.28.0(svelte@5.44.0): + runed@0.28.0(svelte@5.44.1): dependencies: esm-env: 1.2.2 - svelte: 5.44.0 + svelte: 5.44.1 - runed@0.35.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0): + runed@0.35.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.44.0 + svelte: 5.44.1 optionalDependencies: - '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) - runed@0.36.0(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(zod@4.1.13): + runed@0.36.0(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(zod@4.1.13): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.44.0 + svelte: 5.44.1 optionalDependencies: - '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) + '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) zod: 4.1.13 sade@1.8.1: @@ -5174,36 +5198,38 @@ snapshots: set-blocking@2.0.0: {} - set-cookie-parser@2.7.1: {} + set-cookie-parser@2.7.2: {} - sharp@0.34.4: + sharp@0.34.5: dependencies: '@img/colour': 1.0.0 detect-libc: 2.1.2 semver: 7.7.3 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.4 - '@img/sharp-darwin-x64': 0.34.4 - '@img/sharp-libvips-darwin-arm64': 1.2.3 - '@img/sharp-libvips-darwin-x64': 1.2.3 - '@img/sharp-libvips-linux-arm': 1.2.3 - '@img/sharp-libvips-linux-arm64': 1.2.3 - '@img/sharp-libvips-linux-ppc64': 1.2.3 - '@img/sharp-libvips-linux-s390x': 1.2.3 - '@img/sharp-libvips-linux-x64': 1.2.3 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.3 - '@img/sharp-libvips-linuxmusl-x64': 1.2.3 - '@img/sharp-linux-arm': 0.34.4 - '@img/sharp-linux-arm64': 0.34.4 - '@img/sharp-linux-ppc64': 0.34.4 - '@img/sharp-linux-s390x': 0.34.4 - '@img/sharp-linux-x64': 0.34.4 - '@img/sharp-linuxmusl-arm64': 0.34.4 - '@img/sharp-linuxmusl-x64': 0.34.4 - '@img/sharp-wasm32': 0.34.4 - '@img/sharp-win32-arm64': 0.34.4 - '@img/sharp-win32-ia32': 0.34.4 - '@img/sharp-win32-x64': 0.34.4 + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 optional: true shebang-command@2.0.0: @@ -5306,9 +5332,9 @@ snapshots: stubborn-utils@1.0.2: {} - style-to-object@1.0.11: + style-to-object@1.0.14: dependencies: - inline-style-parser: 0.2.4 + inline-style-parser: 0.2.7 styled-jsx@5.1.6(react@19.2.0): dependencies: @@ -5322,19 +5348,19 @@ snapshots: dependencies: has-flag: 4.0.0 - svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3): + svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.44.1)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.44.0 + svelte: 5.44.1 typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.4.0(svelte@5.44.0): + svelte-eslint-parser@1.4.0(svelte@5.44.1): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -5343,74 +5369,74 @@ snapshots: postcss-scss: 4.0.9(postcss@8.5.6) postcss-selector-parser: 7.1.0 optionalDependencies: - svelte: 5.44.0 + svelte: 5.44.1 - svelte-sonner@1.0.6(svelte@5.44.0): + svelte-sonner@1.0.6(svelte@5.44.1): dependencies: - runed: 0.28.0(svelte@5.44.0) - svelte: 5.44.0 + runed: 0.28.0(svelte@5.44.1) + svelte: 5.44.1 - svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0) - style-to-object: 1.0.11 - svelte: 5.44.0 + runed: 0.35.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1) + style-to-object: 1.0.14 + svelte: 5.44.1 transitivePeerDependencies: - '@sveltejs/kit' - svelte-toolbelt@0.5.0(svelte@5.44.0): + svelte-toolbelt@0.5.0(svelte@5.44.1): dependencies: clsx: 2.1.1 - style-to-object: 1.0.11 - svelte: 5.44.0 + style-to-object: 1.0.14 + svelte: 5.44.1 - svelte-toolbelt@0.7.1(svelte@5.44.0): + svelte-toolbelt@0.7.1(svelte@5.44.1): dependencies: clsx: 2.1.1 - runed: 0.23.4(svelte@5.44.0) - style-to-object: 1.0.11 - svelte: 5.44.0 + runed: 0.23.4(svelte@5.44.1) + style-to-object: 1.0.14 + svelte: 5.44.1 - svelte@5.44.0: + svelte@5.44.1: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) '@types/estree': 1.0.8 acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.4.2 + devalue: 5.5.0 esm-env: 1.2.2 - esrap: 2.1.0 + esrap: 2.2.0 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 zimmerframe: 1.1.4 - sveltekit-superforms@2.28.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(@types/json-schema@7.0.15)(esbuild@0.25.11)(svelte@5.44.0)(typescript@5.9.3): + sveltekit-superforms@2.28.1(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(@types/json-schema@7.0.15)(esbuild@0.25.12)(svelte@5.44.1)(typescript@5.9.3): dependencies: - '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) - devalue: 5.4.2 + '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)))(svelte@5.44.1)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)) + devalue: 5.5.0 memoize-weak: 1.0.2 - svelte: 5.44.0 + svelte: 5.44.1 ts-deepmerge: 7.0.3 optionalDependencies: '@exodus/schemasafe': 1.3.0 - '@finom/zod-to-json-schema': 3.24.11(zod@4.1.13) - '@gcornut/valibot-json-schema': 0.42.0(esbuild@0.25.11)(typescript@5.9.3) - '@typeschema/class-validator': 0.3.0(@types/json-schema@7.0.15)(class-validator@0.14.2) + '@finom/zod-to-json-schema': 3.24.12(zod@4.1.13) + '@gcornut/valibot-json-schema': 0.42.0(esbuild@0.25.12)(typescript@5.9.3) + '@typeschema/class-validator': 0.3.0(@types/json-schema@7.0.15)(class-validator@0.14.3) '@vinejs/vine': 3.0.1 - arktype: 2.1.23 - class-validator: 0.14.2 - effect: 3.18.4 + arktype: 2.1.27 + class-validator: 0.14.3 + effect: 3.19.6 joi: 17.13.3 json-schema-to-ts: 3.1.1 superstruct: 2.0.2 - typebox: 1.0.43 - valibot: 1.1.0(typescript@5.9.3) + typebox: 1.0.56 + valibot: 1.2.0(typescript@5.9.3) yup: 1.7.1 zod: 4.1.13 transitivePeerDependencies: @@ -5434,14 +5460,6 @@ snapshots: tapable@2.3.0: {} - terser@5.44.0: - dependencies: - '@jridgewell/source-map': 0.3.11 - acorn: 8.15.0 - commander: 2.20.3 - source-map-support: 0.5.21 - optional: true - tiny-case@1.0.3: optional: true @@ -5479,7 +5497,7 @@ snapshots: tsx@4.20.6: dependencies: - esbuild: 0.25.11 + esbuild: 0.25.12 get-tsconfig: 4.13.0 optionalDependencies: fsevents: 2.3.3 @@ -5497,7 +5515,7 @@ snapshots: dependencies: tagged-tag: 1.0.0 - typebox@1.0.43: + typebox@1.0.56: optional: true typescript-eslint@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): @@ -5515,9 +5533,11 @@ snapshots: uint8array-extras@1.5.0: {} + undici-types@6.21.0: {} + undici-types@7.16.0: {} - unplugin@2.3.10: + unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.15.0 @@ -5539,19 +5559,19 @@ snapshots: typescript: 5.9.3 optional: true - valibot@1.1.0(typescript@5.9.3): + valibot@1.2.0(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 optional: true - validator@13.15.15: + validator@13.15.23: optional: true vary@1.1.2: {} - vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1): + vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6): dependencies: - esbuild: 0.25.11 + esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 @@ -5562,13 +5582,11 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 - terser: 5.44.0 tsx: 4.20.6 - yaml: 2.8.1 - vitefu@1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)): + vitefu@1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)): optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6) webpack-virtual-modules@0.6.2: {} @@ -5606,9 +5624,6 @@ snapshots: yaml@1.10.2: {} - yaml@2.8.1: - optional: true - yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 diff --git a/scripts/docker/entrypoint.sh b/scripts/docker/entrypoint.sh index 429211be..80817b10 100755 --- a/scripts/docker/entrypoint.sh +++ b/scripts/docker/entrypoint.sh @@ -14,19 +14,16 @@ PGID=${PGID:-1000} # Check if the group with PGID exists; if not, create it if ! getent group pocket-id-group > /dev/null 2>&1; then - echo "Creating group $PGID..." addgroup -g "$PGID" pocket-id-group fi # Check if a user with PUID exists; if not, create it if ! id -u pocket-id > /dev/null 2>&1; then if ! getent passwd "$PUID" > /dev/null 2>&1; then - echo "Creating user $PUID..." - adduser -u "$PUID" -G pocket-id-group pocket-id > /dev/null 2>&1 + adduser -uD "$PUID" -G pocket-id-group pocket-id > /dev/null 2>&1 else # If a user with the PUID already exists, use that user existing_user=$(getent passwd "$PUID" | cut -d: -f1) - echo "Using existing user: $existing_user" fi fi diff --git a/tests/package.json b/tests/package.json index 8e26a07e..8dda4277 100644 --- a/tests/package.json +++ b/tests/package.json @@ -8,9 +8,13 @@ }, "devDependencies": { "@playwright/test": "^1.57.0", - "@types/node": "^24.10.1", + "@types/adm-zip": "^0.5.7", + "@types/node": "^22.18.12", "dotenv": "^17.2.3", "jose": "^6.1.2", "prettier": "^3.6.2" + }, + "dependencies": { + "adm-zip": "^0.5.16" } } diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts index 0b0cc460..5ac10f10 100644 --- a/tests/playwright.config.ts +++ b/tests/playwright.config.ts @@ -21,11 +21,15 @@ export default defineConfig({ trace: 'on-first-retry' }, projects: [ - { name: 'setup', testMatch: /.*\.setup\.ts/ }, + { name: 'cli', testMatch: /cli\.spec\.ts/ }, + { name: 'auth-setup', testMatch: /auth\.setup\.ts/ }, { - name: 'chromium', - use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' }, - dependencies: ['setup'] + name: 'browser-chrome', + use: { ...devices['Desktop Chrome'], storageState: '.tmp/auth/user.json' }, + testIgnore: /cli\.spec\.ts/, + dependencies: ['auth-setup'] } - ] + ], + globalSetup: './specs/fixtures/global.setup.ts', + globalTeardown: './specs/fixtures/global.teardown.ts' }); diff --git a/tests/resources/export/database.json b/tests/resources/export/database.json new file mode 100644 index 00000000..1461420a --- /dev/null +++ b/tests/resources/export/database.json @@ -0,0 +1,312 @@ +{ + "provider": "sqlite", + "version": 20251117141000, + "tableOrder": ["users", "user_groups", "oidc_clients"], + "tables": { + "api_keys": [ + { + "created_at": "2025-11-25T12:39:02Z", + "description": null, + "expiration_email_sent": false, + "expires_at": "2025-12-25T12:39:02Z", + "id": "5f1fa856-c164-4295-961e-175a0d22d725", + "key": "6c34966f57ef2bb7857649aff0e7ab3ad67af93c846342ced3f5a07be8706c20", + "last_used_at": null, + "name": "Test API Key", + "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" + } + ], + "app_config_variables": [ + { + "key": "instanceId", + "value": "test-instance-id" + } + ], + "kv": [ + { + "key": "jwt_private_key.json", + "value": "7d/5hl7diJ2rnFL14hEAQf9tzpu29aqXQ8jpJ2iqqKUNFZpdOkEpud0CmRv4H3r8yyk2u/Gqqj9klSy58DJkYXGF5PAYgLyoBIb7L3JXWRbxg4cQ3QJCug13l2OTmpAKoVc+rmX8c3j3h1sNqyJ+7Ql5sS0jSeyiYgIsFNCdnK5alBDyvtcpe/QDpklmP4JCeVpvmf2rLGplk3g5UO5ydJ8UiDXxfDmi+gF6NKJvrGnnah8Ar3G/x88z+tTJtp0DIQFwxXwUM2XZqzEVGm8K2r0w5o9/Keh6bBBaiuH2C78ZOaijGV3DovhR+e9J0cYUYGwT42MZMx9fSWQ/lvWGGnf+Uq3MXJfjWSREfhkp8KTQwR9F7+dnVJWswOEk7jPR8I7hCWTMxJyvaFX3wgAXIVmhrgXZQQbYOqTt56IoqUl0xOJku8dA8opg2UcLlmmuOh6+hfkXKsiiS/H/9c1BVIGj1fCOiT6IePh4wKKSTbwJnPD5EKmdJpgTsUpjcDnXQKY4ReO0UpdRdKxwRDDLeQuG6j+ljGxR9GPudCU9Nmci6rFVI6n5LWYkQxBA1O73RpmXRZPDzntDfpXMEonkmSvOoxaCK2Id7CRKMdqvR0kEouwnhk5WSFtsfi3sA0pkXzPFxwZeWM8vFtbffZOZzXaOhxCOfcj1NClZohlZhyc4jvkxmrpY7PSaAzih0AmHI7y0LYFi6fZu/K4EheVa1+KF55nWZ8ARikHMWKAKkyExkTak7xyN884TDmzURRaPlQg4jzQte5WMNjAG/hlHibdMBNvgwiYd49ZxteJ8ABdbiXVRl+2JGbdjl2ubpQZwOn7bJKlqO56bIwsZ+e4+pXsuOGdBahkHrUjtMEmH3DZbGc6CJLbcmdhdpApLQRRcLAazxJhzAwJ47FRYsHsj57LnYNvmcKdIxw8rxCdLUuzz95uw0T3ankEO5J9sjem+HMEuKdwXK1UcuOn2rjR8Sd/BuvQmeso27dFbPXqXYNS90Ml45YyTvcKSiopD181oZR703TFUSpR7dsiqROMr+p/2jN9h6a8WbQ8xpksyclaQByY/M77AssbXnG6wfhRsntNIINCZLbBnjXOyz6ZHIC5K4tSTdcnWaiYPeRPQmnw9UUvHAcNU2yMWsy0eU377yDS0WstTxOdQutTdkczl8kv5Lo26JiEK7mSIuRK19ffF9Zz8FG8+eKv5zdyIPjyQRDYBysUoDv5huKe2eoxJu/MWS2Pql/ZtUGeD6Ozm3mCvh0vQ9ceagBkY6Ocm3du0ziAKP29Ri0mjg4DizVorbLzsh+EQH/s2Pi9MnjUZDlEmuLl2Xfp7/w4j/8u0N0tVR70VDFuGdKpTjFY3vS8EJrPtyMTM51x1D9rb8gIql8aR/rJw4YF+huxg1mv5n6+tGVqg5msbPmF12eJijP4lkmaRwIpLW5pJTtaDkUj7uOeu1mm4k+Dt5nh0/0jPHzrv6bcTCcbV7UjMHDoTXXqEpFAAJ66rHR7zdAJu+YKsnTIZyLmOpcowq7LL8G9qTvV0OSpyQWUIavRSgbDHFqEqRs+JU94jAzkq8nCY5MTd9m5sIv9InfdT3k+pwpsE/FKge8nghFLtbUrafGkzTky8SE2druvVcIvbfXMfLIKRUYjJgnWc0gQzF5J6pzXM7D2r/RG6JDzASqjlbURq6v9bhNerlOVdMujWKEEVcKWIzlbt4RkihRjM8AUqIZQOyicGQ+4yfIjAHw5viuABONYs3OIWULnFqJxdvS9rNKhfxSjIq9cfqyzevq2xrRoMXEonobh6M3bD2Vang8OAeVeD1OXWPERi4pepCYFS9RJ/Xa/UWxptsqSNuGcb3fAzQSmLpXLGdWRoKXvSe7EYgc0bGcLOjSTu5RURKo+EF9i4KT9EJauf6VXw5dTf/CCIJRXE1bWzXhSCFYntohYhX2ldOCDYpi/jFBC6Vtkw0ud3/xq8Nmhd5gUk+SpngByCZH3Pm3H+jvlbMpiqkDkm1v74hDX13Xhrcw2eWyuqKBVoRCCniUvwpYNbGvBfjC6Hcizv0Aybciwj+4nybt5EPoEUm6S6Gs7fG7QpPdvrzpAxX70MlmdkF/gwyuhbEeJhLK+WL7qAsN5CvHPzVbsIf90x+nGTtMJPgpxVr0tJMj+vprXV4WxutfARBiOnqe58MhA857sd+MzKBgKnoLOBRTiC3qc/0/ULwbG2HCCD7nmwzz7M4nUuMvo8rgS7z0BF68OClT8X3JwSXbL5Wg==" + } + ], + "oidc_authorization_codes": [ + { + "client_id": "3654a746-35d4-4321-ac61-0bdcff2b4055", + "code": "auth-code", + "code_challenge": null, + "code_challenge_method_sha256": null, + "created_at": "2025-11-25T12:39:02Z", + "expires_at": "2025-11-25T13:39:02Z", + "id": "6bdd221e-d9f7-4e3d-92c0-4be125802ba2", + "nonce": "nonce", + "scope": "openid profile", + "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" + }, + { + "client_id": "7c21a609-96b5-4011-9900-272b8d31a9d1", + "code": "federated", + "code_challenge": null, + "code_challenge_method_sha256": null, + "created_at": "2025-11-25T12:39:02Z", + "expires_at": "2025-11-25T13:39:02Z", + "id": "37e914bd-ff2c-4653-8cd8-550f0213e430", + "nonce": "nonce", + "scope": "openid profile", + "user_id": "1cd19686-f9a6-43f4-a41f-14a0bf5b4036" + } + ], + "oidc_clients": [ + { + "callback_urls": "WyJodHRwOi8vbmV4dGNsb3VkL2F1dGgvY2FsbGJhY2siXQ==", + "created_at": "2025-11-25T12:39:02Z", + "created_by_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e", + "credentials": "e30=", + "dark_image_type": null, + "id": "3654a746-35d4-4321-ac61-0bdcff2b4055", + "image_type": "png", + "is_public": false, + "launch_url": "https://nextcloud.local", + "logout_callback_urls": "WyJodHRwOi8vbmV4dGNsb3VkL2F1dGgvbG9nb3V0L2NhbGxiYWNrIl0=", + "name": "Nextcloud", + "pkce_enabled": false, + "requires_reauthentication": false, + "secret": "$2a$10$9dypwot8nGuCjT6wQWWpJOckZfRprhe2EkwpKizxS/fpVHrOLEJHC" + }, + { + "callback_urls": "WyJodHRwOi8vaW1taWNoL2F1dGgvY2FsbGJhY2siXQ==", + "created_at": "2025-11-25T12:39:02Z", + "created_by_id": "1cd19686-f9a6-43f4-a41f-14a0bf5b4036", + "credentials": "e30=", + "dark_image_type": null, + "id": "606c7782-f2b1-49e5-8ea9-26eb1b06d018", + "image_type": null, + "is_public": false, + "launch_url": null, + "logout_callback_urls": "bnVsbA==", + "name": "Immich", + "pkce_enabled": false, + "requires_reauthentication": false, + "secret": "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe" + }, + { + "callback_urls": "WyJodHRwOi8vdGFpbHNjYWxlL2F1dGgvY2FsbGJhY2siXQ==", + "created_at": "2025-11-25T12:39:02Z", + "created_by_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e", + "credentials": "e30=", + "dark_image_type": null, + "id": "7c21a609-96b5-4011-9900-272b8d31a9d1", + "image_type": null, + "is_public": false, + "launch_url": null, + "logout_callback_urls": "WyJodHRwOi8vdGFpbHNjYWxlL2F1dGgvbG9nb3V0L2NhbGxiYWNrIl0=", + "name": "Tailscale", + "pkce_enabled": false, + "requires_reauthentication": false, + "secret": "$2a$10$xcRReBsvkI1XI6FG8xu/pOgzeF00bH5Wy4d/NThwcdi3ZBpVq/B9a" + }, + { + "callback_urls": "WyJodHRwOi8vZmVkZXJhdGVkL2F1dGgvY2FsbGJhY2siXQ==", + "created_at": "2025-11-25T12:39:02Z", + "created_by_id": "1cd19686-f9a6-43f4-a41f-14a0bf5b4036", + "credentials": "eyJmZWRlcmF0ZWRJZGVudGl0aWVzIjpbeyJpc3N1ZXIiOiJodHRwczovL2V4dGVybmFsLWlkcC5sb2NhbCIsInN1YmplY3QiOiJjNDgyMzJmZi1mZjY1LTQ1ZWQtYWU5Ni03YWZhOGE5YjQ0M2IiLCJhdWRpZW5jZSI6ImFwaTovL1BvY2tldElEIiwiandrcyI6Imh0dHA6Ly9sb2NhbGhvc3Q6MTQxMS9hcGkvZXh0ZXJuYWxpZHAvandrcy5qc29uIn1dfQ==", + "dark_image_type": null, + "id": "c48232ff-ff65-45ed-ae96-7afa8a9b443b", + "image_type": null, + "is_public": false, + "launch_url": null, + "logout_callback_urls": "bnVsbA==", + "name": "Federated", + "pkce_enabled": false, + "requires_reauthentication": false, + "secret": "$2a$10$Ak.FP8riD1ssy2AGGbG.gOpnp/rBpymd74j0nxNMtW0GG1Lb4gzxe" + } + ], + "oidc_clients_allowed_user_groups": [ + { + "oidc_client_id": "606c7782-f2b1-49e5-8ea9-26eb1b06d018", + "user_group_id": "adab18bf-f89d-4087-9ee1-70ff15b48211" + } + ], + "oidc_refresh_tokens": [ + { + "client_id": "3654a746-35d4-4321-ac61-0bdcff2b4055", + "created_at": "2025-11-25T12:39:02Z", + "expires_at": "2025-11-26T12:39:02Z", + "id": "4928604e-e689-410c-9b25-5b9b6db9e46e", + "scope": "openid profile email", + "token": "fef6e2e37eb990f0bd7abd48a41d530c54b6a1f139b556e35e62475e6f4cb38d", + "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" + } + ], + "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", + "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", + "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", + "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 + } + ], + "user_authorized_oidc_clients": [ + { + "client_id": "3654a746-35d4-4321-ac61-0bdcff2b4055", + "last_used_at": "2025-08-01T13:00:00Z", + "scope": "openid profile email", + "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" + }, + { + "client_id": "7c21a609-96b5-4011-9900-272b8d31a9d1", + "last_used_at": "2025-08-10T14:00:00Z", + "scope": "openid profile email", + "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" + }, + { + "client_id": "c48232ff-ff65-45ed-ae96-7afa8a9b443b", + "last_used_at": "2025-08-12T12:00:00Z", + "scope": "openid profile email", + "user_id": "1cd19686-f9a6-43f4-a41f-14a0bf5b4036" + } + ], + "user_groups": [ + { + "created_at": "2025-11-25T12:39:02Z", + "friendly_name": "Developers", + "id": "c7ae7c01-28a3-4f3c-9572-1ee734ea8368", + "ldap_id": null, + "name": "developers" + }, + { + "created_at": "2025-11-25T12:39:02Z", + "friendly_name": "Designers", + "id": "adab18bf-f89d-4087-9ee1-70ff15b48211", + "ldap_id": null, + "name": "designers" + } + ], + "user_groups_users": [ + { + "user_group_id": "c7ae7c01-28a3-4f3c-9572-1ee734ea8368", + "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" + }, + { + "user_group_id": "c7ae7c01-28a3-4f3c-9572-1ee734ea8368", + "user_id": "1cd19686-f9a6-43f4-a41f-14a0bf5b4036" + }, + { + "user_group_id": "adab18bf-f89d-4087-9ee1-70ff15b48211", + "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" + } + ], + "users": [ + { + "created_at": "2025-11-25T12:39:02Z", + "disabled": false, + "display_name": "Tim Cook", + "email": "tim.cook@test.com", + "first_name": "Tim", + "id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e", + "is_admin": true, + "last_name": "Cook", + "ldap_id": null, + "locale": null, + "username": "tim" + }, + { + "created_at": "2025-11-25T12:39:02Z", + "disabled": false, + "display_name": "Craig Federighi", + "email": "craig.federighi@test.com", + "first_name": "Craig", + "id": "1cd19686-f9a6-43f4-a41f-14a0bf5b4036", + "is_admin": false, + "last_name": "Federighi", + "ldap_id": null, + "locale": null, + "username": "craig" + } + ], + "webauthn_credentials": [ + { + "attestation_type": "none", + "backup_eligible": false, + "backup_state": false, + "created_at": "2025-11-25T12:39:02Z", + "credential_id": "dGVzdC1jcmVkZW50aWFsLXRpbQ==", + "id": "fa7977f9-7cf8-40fa-abca-42b917b6e692", + "name": "Passkey 1", + "public_key": "pQMmIAEhWCDBw6jkpXXr0pHrtAQetxiR5cTcILG/YGDCdKrhVhNDHCJYIIu12YrF6B7Frwl3AUqEpdrYEwj3Fo3XkGgvrBIJEUmGAQI=", + "transport": "WyJpbnRlcm5hbCJd", + "user_id": "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e" + }, + { + "attestation_type": "none", + "backup_eligible": false, + "backup_state": false, + "created_at": "2025-11-25T12:39:02Z", + "credential_id": "dGVzdC1jcmVkZW50aWFsLWNyYWln", + "id": "4bcc54ef-01d1-4970-be51-669ccd8c0198", + "name": "Passkey 2", + "public_key": "pSJYIPmc+FlEB0neERqqscxKckGF8yq1AYrANiloshAUAouHAQIDJiABIVggj4qA0PrZzg8Co1C27nyUbzrp8Ewjr7eOlGI2LfrzmbI=", + "transport": "WyJpbnRlcm5hbCJd", + "user_id": "1cd19686-f9a6-43f4-a41f-14a0bf5b4036" + } + ], + "webauthn_sessions": [ + { + "challenge": "challenge", + "created_at": "2025-11-25T12:39:02Z", + "credential_params": "W3sidHlwZSI6InB1YmxpYy1rZXkiLCJhbGciOi03fSx7InR5cGUiOiJwdWJsaWMta2V5IiwiYWxnIjotMjU3fV0=", + "expires_at": "2025-11-25T13:39:02Z", + "id": "267f6907-7bc8-4ea1-9d47-c42a172dc1c7", + "user_verification": "preferred" + } + ] + } +} diff --git a/tests/resources/export/uploads/application-images b/tests/resources/export/uploads/application-images new file mode 120000 index 00000000..4fa7a7ee --- /dev/null +++ b/tests/resources/export/uploads/application-images @@ -0,0 +1 @@ +../../../../backend/resources/images/ \ No newline at end of file diff --git a/tests/resources/export/uploads/profile-pictures/defaults/CF.png b/tests/resources/export/uploads/profile-pictures/defaults/CF.png new file mode 100644 index 00000000..7c399f55 Binary files /dev/null and b/tests/resources/export/uploads/profile-pictures/defaults/CF.png differ diff --git a/tests/resources/export/uploads/profile-pictures/defaults/TC.png b/tests/resources/export/uploads/profile-pictures/defaults/TC.png new file mode 100644 index 00000000..b8192304 Binary files /dev/null and b/tests/resources/export/uploads/profile-pictures/defaults/TC.png differ diff --git a/tests/assets/clouds.jpg b/tests/resources/images/clouds.jpg similarity index 100% rename from tests/assets/clouds.jpg rename to tests/resources/images/clouds.jpg diff --git a/tests/assets/nextcloud-logo.png b/tests/resources/images/nextcloud-logo.png similarity index 100% rename from tests/assets/nextcloud-logo.png rename to tests/resources/images/nextcloud-logo.png diff --git a/tests/assets/pingvin-share-logo.png b/tests/resources/images/pingvin-share-logo.png similarity index 100% rename from tests/assets/pingvin-share-logo.png rename to tests/resources/images/pingvin-share-logo.png diff --git a/tests/assets/w3-schools-favicon.ico b/tests/resources/images/w3-schools-favicon.ico similarity index 100% rename from tests/assets/w3-schools-favicon.ico rename to tests/resources/images/w3-schools-favicon.ico diff --git a/tests/setup/docker-compose-postgres.yml b/tests/setup/docker-compose-postgres.yml index 09539b75..2c534f3d 100644 --- a/tests/setup/docker-compose-postgres.yml +++ b/tests/setup/docker-compose-postgres.yml @@ -11,7 +11,7 @@ services: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=pocket-id healthcheck: - test: ['CMD-SHELL', 'pg_isready -U postgres'] + test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 @@ -27,3 +27,6 @@ services: depends_on: postgres: condition: service_healthy + +volumes: + pocket-id-test-data: diff --git a/tests/setup/docker-compose-s3.yml b/tests/setup/docker-compose-s3.yml index 34bcb590..1fdc511d 100644 --- a/tests/setup/docker-compose-s3.yml +++ b/tests/setup/docker-compose-s3.yml @@ -35,7 +35,9 @@ services: S3_ACCESS_KEY_ID: test S3_SECRET_ACCESS_KEY: test S3_FORCE_PATH_STYLE: true - KEYS_STORAGE: database depends_on: create-bucket: condition: service_completed_successfully + +volumes: + pocket-id-test-data: diff --git a/tests/setup/docker-compose.yml b/tests/setup/docker-compose.yml index 8ac7d80f..d4e65c14 100644 --- a/tests/setup/docker-compose.yml +++ b/tests/setup/docker-compose.yml @@ -11,13 +11,18 @@ services: pocket-id: image: pocket-id:test ports: - - '1411:1411' + - "1411:1411" environment: APP_ENV: test ENCRYPTION_KEY: test-encryption-key FILE_BACKEND: ${FILE_BACKEND} + volumes: + - pocket-id-test-data:/app/data build: args: - BUILD_TAGS=e2etest context: ../.. dockerfile: docker/Dockerfile + +volumes: + pocket-id-test-data: diff --git a/tests/specs/application-configuration.spec.ts b/tests/specs/application-configuration.spec.ts index a675a60b..1f7e56f2 100644 --- a/tests/specs/application-configuration.spec.ts +++ b/tests/specs/application-configuration.spec.ts @@ -119,11 +119,11 @@ test('Update email configuration', async ({ page }) => { test('Update application images', async ({ page }) => { await page.getByRole('button', { name: 'Expand card' }).nth(4).click(); - await page.getByLabel('Favicon').setInputFiles('assets/w3-schools-favicon.ico'); - await page.getByLabel('Light Mode Logo').setInputFiles('assets/pingvin-share-logo.png'); - await page.getByLabel('Dark Mode Logo').setInputFiles('assets/nextcloud-logo.png'); - await page.getByLabel('Default Profile Picture').setInputFiles('assets/pingvin-share-logo.png'); - await page.getByLabel('Background Image').setInputFiles('assets/clouds.jpg'); + await page.getByLabel('Favicon').setInputFiles('resources/images/w3-schools-favicon.ico'); + await page.getByLabel('Light Mode Logo').setInputFiles('resources/images/pingvin-share-logo.png'); + await page.getByLabel('Dark Mode Logo').setInputFiles('resources/images/nextcloud-logo.png'); + await page.getByLabel('Default Profile Picture').setInputFiles('resources/images/pingvin-share-logo.png'); + await page.getByLabel('Background Image').setInputFiles('resources/images/clouds.jpg'); await page.getByRole('button', { name: 'Save' }).last().click(); await expect(page.locator('[data-type="success"]')).toHaveText( diff --git a/tests/specs/cli.spec.ts b/tests/specs/cli.spec.ts new file mode 100644 index 00000000..b7c8e1b9 --- /dev/null +++ b/tests/specs/cli.spec.ts @@ -0,0 +1,366 @@ +import { expect, test } from '@playwright/test'; +import AdmZip from 'adm-zip'; +import { execFileSync, ExecFileSyncOptions } from 'child_process'; +import crypto from 'crypto'; +import { users } from 'data'; +import fs from 'fs'; +import path from 'path'; +import { cleanupBackend } from 'utils/cleanup.util'; +import { pathFromRoot, tmpDir } from 'utils/fs.util'; + +const containerName = 'pocket-id'; +const setupDir = pathFromRoot('setup'); +const exampleExportPath = pathFromRoot('resources/export'); +const dockerCommandMaxBuffer = 100 * 1024 * 1024; +let mode: 'sqlite' | 'postgres' | 's3' = 'sqlite'; + +test.beforeAll(() => { + const dockerComposeLs = runDockerCommand(['compose', 'ls', '--format', 'json']); + if (dockerComposeLs.includes('postgres')) { + mode = 'postgres'; + } else if (dockerComposeLs.includes('s3')) { + mode = 's3'; + } + console.log(`Running CLI tests in ${mode.toUpperCase()} mode`); +}); + +test('Export', async ({ baseURL }) => { + // Reset the backend but with LDAP setup because the example export has no LDAP data + await cleanupBackend({ skipLdapSetup: true }); + + // Fetch the profile pictures because they get generated on demand + await Promise.all([ + fetch(`${baseURL}/api/users/${users.craig.id}/profile-picture.png`), + fetch(`${baseURL}/api/users/${users.tim.id}/profile-picture.png`) + ]); + + // Export the data from the seeded container + const exportPath = path.join(tmpDir, 'export.zip'); + const extractPath = path.join(tmpDir, 'export-extracted'); + + runExport(exportPath); + unzipExport(exportPath, extractPath); + + compareExports(exampleExportPath, extractPath); +}); + +test('Export via stdout', async ({ baseURL }) => { + await cleanupBackend({ skipLdapSetup: true }); + + await Promise.all([ + fetch(`${baseURL}/api/users/${users.craig.id}/profile-picture.png`), + fetch(`${baseURL}/api/users/${users.tim.id}/profile-picture.png`) + ]); + + const stdoutBuffer = runExportToStdout(); + const stdoutExtractPath = path.join(tmpDir, 'export-stdout-extracted'); + unzipExportBuffer(stdoutBuffer, stdoutExtractPath); + + compareExports(exampleExportPath, stdoutExtractPath); +}); + +test('Import', async () => { + // Reset the backend without seeding + await cleanupBackend({ skipSeed: true }); + + // Run the import with the example export data + const exampleExportArchivePath = path.join(tmpDir, 'example-export.zip'); + archiveExampleExport(exampleExportArchivePath); + + try { + runDockerComposeCommand(['stop', containerName]); + runImport(exampleExportArchivePath); + } finally { + runDockerComposeCommand(['up', '-d', containerName]); + } + + // Export again from the imported instance + const exportPath = path.join(tmpDir, 'export.zip'); + const exportExtracted = path.join(tmpDir, 'export-extracted'); + runExport(exportPath); + unzipExport(exportPath, exportExtracted); + + compareExports(exampleExportPath, exportExtracted); +}); + +test('Import via stdin', async () => { + await cleanupBackend({ skipSeed: true }); + + const exampleExportArchivePath = path.join(tmpDir, 'example-export-stdin.zip'); + const exampleExportBuffer = archiveExampleExport(exampleExportArchivePath); + + try { + runDockerComposeCommand(['stop', containerName]); + runImportFromStdin(exampleExportBuffer); + } finally { + runDockerComposeCommand(['up', '-d', containerName]); + } + + const exportPath = path.join(tmpDir, 'export-from-stdin.zip'); + const exportExtracted = path.join(tmpDir, 'export-from-stdin-extracted'); + runExport(exportPath); + unzipExport(exportPath, exportExtracted); + + compareExports(exampleExportPath, exportExtracted); +}); + +function compareExports(dir1: string, dir2: string): void { + const hashes1 = hashAllFiles(dir1); + const hashes2 = hashAllFiles(dir2); + + const files1 = Object.keys(hashes1).sort(); + const files2 = Object.keys(hashes2).sort(); + expect(files2).toEqual(files1); + + for (const file of files1) { + expect(hashes2[file], `${file} hash should match`).toEqual(hashes1[file]); + } + + // Compare database.json contents + const expectedData = loadJSON(path.join(dir1, 'database.json')); + const actualData = loadJSON(path.join(dir2, 'database.json')); + + // Check special fields + validateSpecialFields(actualData); + + // Normalize and compare + const normalizedExpected = normalizeJSON(expectedData); + const normalizedActual = normalizeJSON(actualData); + expect(normalizedActual).toEqual(normalizedExpected); +} + +function archiveExampleExport(outputPath: string): Buffer { + fs.rmSync(outputPath, { force: true }); + + const zip = new AdmZip(); + const files = fs.readdirSync(exampleExportPath); + for (const file of files) { + const filePath = path.join(exampleExportPath, file); + if (fs.statSync(filePath).isFile()) { + zip.addLocalFile(filePath); + } else if (fs.statSync(filePath).isDirectory()) { + zip.addLocalFolder(filePath, file); + } + } + + const buffer = zip.toBuffer(); + fs.writeFileSync(outputPath, buffer); + return buffer; +} + +// Helper to load JSON files +function loadJSON(path: string) { + return JSON.parse(fs.readFileSync(path, 'utf-8')); +} + +function normalizeJSON(obj: any): any { + if (typeof obj === 'string') { + try { + // Normalize JSON strings + const parsed = JSON.parse(atob(obj)); + return JSON.stringify(normalizeJSON(parsed)); + } catch { + return obj; + } + } + + if (Array.isArray(obj)) { + // Sort arrays to make order irrelevant + return obj + .map(normalizeJSON) + .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))); + } else if (obj && typeof obj === 'object') { + const ignoredKeys = ['id', 'created_at', 'expires_at', 'credentials', 'provider', 'version']; + + // Sort and normalize object keys, skipping ignored ones + return Object.keys(obj) + .filter((key) => !ignoredKeys.includes(key)) + .sort() + .reduce( + (acc, key) => { + acc[key] = normalizeJSON(obj[key]); + return acc; + }, + {} as Record + ); + } + + return obj; +} + +function validateSpecialFields(obj: any): void { + if (Array.isArray(obj)) { + for (const item of obj) validateSpecialFields(item); + } else if (obj && typeof obj === 'object') { + for (const [key, value] of Object.entries(obj)) { + if (key === 'id') { + expect(isUUID(value), `Expected '${value}' to be a valid UUID`).toBe(true); + } else if (key === 'created_at' || key === 'expires_at') { + expect( + isValidISODate(value), + `Expected '${key}' = ${value} to be a valid ISO 8601 date string` + ).toBe(true); + } else if (key === 'provider') { + expect( + ['postgres', 'sqlite'].includes(value as string), + `Expected 'provider' to be either 'postgres' or 'sqlite', got '${value}'` + ).toBe(true); + } else if (key === 'version') { + expect(value).toBeGreaterThanOrEqual(20251001000000); + } else { + validateSpecialFields(value); + } + } + } +} + +function isUUID(value: any): boolean { + if (typeof value !== 'string') return false; + const uuidRegex = /^[^-]{8}-[^-]{4}-[^-]{4}-[^-]{4}-[^-]{12}$/; + return uuidRegex.test(value); +} + +function isValidISODate(value: any): boolean { + const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/; + if (!isoRegex.test(value)) return false; + const date = new Date(value); + return !isNaN(date.getTime()); +} + +function runImport(pathToFile: string) { + const importContainerId = runDockerComposeCommand([ + 'run', + '-d', + '-v', + `${pathToFile}:/app/data/pocket-id-export.zip`, + containerName, + '/app/pocket-id', + 'import', + '--path', + '/app/data/pocket-id-export.zip', + '--yes' + ]); + try { + runDockerCommand(['wait', importContainerId]); + } finally { + runDockerCommand(['rm', '-f', importContainerId]); + } +} + +function runImportFromStdin(archive: Buffer): void { + runDockerComposeCommandRaw( + ['run', '--rm', '-T', containerName, '/app/pocket-id', 'import', '--yes', '--path', '-'], + { input: archive } + ); +} + +function runExport(outputFile: string): void { + const containerId = runDockerComposeCommand([ + 'run', + '-d', + containerName, + '/app/pocket-id', + 'export', + '--path', + '/app/data/pocket-id-export.zip' + ]); + + try { + // Wait until export finishes + runDockerCommand(['wait', containerId]); + runDockerCommand(['cp', `${containerId}:/app/data/pocket-id-export.zip`, outputFile]); + } finally { + runDockerCommand(['rm', '-f', containerId]); + } + + expect(fs.existsSync(outputFile)).toBe(true); +} + +function runExportToStdout(): Buffer { + const res = runDockerComposeCommandRaw([ + 'run', + '--rm', + '-T', + containerName, + '/app/pocket-id', + 'export', + '--path', + '-' + ]); + fs.writeFileSync('export-stdout.txt', res); + return res; +} + +function unzipExport(zipFile: string, destDir: string): void { + fs.rmSync(destDir, { recursive: true, force: true }); + const zip = new AdmZip(zipFile); + zip.extractAllTo(destDir, true); +} + +function unzipExportBuffer(zipBuffer: Buffer, destDir: string): void { + fs.rmSync(destDir, { recursive: true, force: true }); + const zip = new AdmZip(zipBuffer); + zip.extractAllTo(destDir, true); +} + +function hashFile(filePath: string): string { + const buffer = fs.readFileSync(filePath); + return crypto.createHash('sha256').update(buffer).digest('hex'); +} + +function getAllFiles(dir: string, root = dir): string[] { + return fs.readdirSync(dir).flatMap((entry) => { + if (['.DS_Store', 'database.json'].includes(entry)) return []; + + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + return stat.isDirectory() ? getAllFiles(fullPath, root) : [path.relative(root, fullPath)]; + }); +} + +function hashAllFiles(dir: string): Record { + const files = getAllFiles(dir); + const hashes: Record = {}; + for (const relativePath of files) { + const fullPath = path.join(dir, relativePath); + hashes[relativePath] = hashFile(fullPath); + } + return hashes; +} + +function runDockerCommand(args: string[], options?: ExecFileSyncOptions): string { + return execFileSync('docker', args, { + cwd: setupDir, + stdio: 'pipe', + maxBuffer: dockerCommandMaxBuffer, + ...options + }) + .toString() + .trim(); +} + +function runDockerComposeCommand(args: string[]): string { + return runDockerComposeCommandRaw(args).toString().trim(); +} + +function runDockerComposeCommandRaw(args: string[], options?: ExecFileSyncOptions): Buffer { + return execFileSync('docker', dockerComposeArgs(args), { + cwd: setupDir, + stdio: 'pipe', + maxBuffer: dockerCommandMaxBuffer, + ...options + }) as Buffer; +} + +function dockerComposeArgs(args: string[]): string[] { + let dockerComposeFile = 'docker-compose.yml'; + switch (mode) { + case 'postgres': + dockerComposeFile = 'docker-compose-postgres.yml'; + break; + case 's3': + dockerComposeFile = 'docker-compose-s3.yml'; + break; + } + return ['compose', '-f', dockerComposeFile, ...args]; +} diff --git a/tests/specs/auth.setup.ts b/tests/specs/fixtures/auth.setup.ts similarity index 56% rename from tests/specs/auth.setup.ts rename to tests/specs/fixtures/auth.setup.ts index 4e0e7cbc..872f8ef1 100644 --- a/tests/specs/auth.setup.ts +++ b/tests/specs/fixtures/auth.setup.ts @@ -1,8 +1,9 @@ import { test as setup } from '@playwright/test'; -import authUtil from '../utils/auth.util'; -import { cleanupBackend } from '../utils/cleanup.util'; +import { pathFromRoot } from 'utils/fs.util'; +import authUtil from '../../utils/auth.util'; +import { cleanupBackend } from '../../utils/cleanup.util'; -const authFile = './.auth/user.json'; +const authFile = pathFromRoot('.tmp/auth/user.json'); setup('authenticate', async ({ page }) => { await cleanupBackend(); diff --git a/tests/specs/fixtures/global.setup.ts b/tests/specs/fixtures/global.setup.ts new file mode 100644 index 00000000..d30850d1 --- /dev/null +++ b/tests/specs/fixtures/global.setup.ts @@ -0,0 +1,8 @@ +import fs from 'fs'; +import { tmpDir } from 'utils/fs.util'; + +async function globalSetup() { + await fs.promises.mkdir(tmpDir, { recursive: true }); +} + +export default globalSetup; diff --git a/tests/specs/fixtures/global.teardown.ts b/tests/specs/fixtures/global.teardown.ts new file mode 100644 index 00000000..8991c453 --- /dev/null +++ b/tests/specs/fixtures/global.teardown.ts @@ -0,0 +1,8 @@ +import fs from 'fs'; +import { tmpDir } from 'utils/fs.util'; + +async function globalTeardown() { + await fs.promises.rm(tmpDir, { recursive: true, force: true }); +} + +export default globalTeardown; diff --git a/tests/specs/oidc-client-settings.spec.ts b/tests/specs/oidc-client-settings.spec.ts index ed2756c3..c847733b 100644 --- a/tests/specs/oidc-client-settings.spec.ts +++ b/tests/specs/oidc-client-settings.spec.ts @@ -20,9 +20,9 @@ test.describe('Create OIDC client', () => { await page.getByTestId('callback-url-2').fill(oidcClient.secondCallbackUrl); await page.locator('[role="tab"][data-value="light-logo"]').first().click(); - await page.setInputFiles('#oidc-client-logo-light', 'assets/pingvin-share-logo.png'); + await page.setInputFiles('#oidc-client-logo-light', 'resources/images/pingvin-share-logo.png'); await page.locator('[role="tab"][data-value="dark-logo"]').first().click(); - await page.setInputFiles('#oidc-client-logo-dark', 'assets/pingvin-share-logo.png'); + await page.setInputFiles('#oidc-client-logo-dark', 'resources/images/pingvin-share-logo.png'); if (clientId) { await page.getByRole('button', { name: 'Show Advanced Options' }).click(); @@ -71,9 +71,9 @@ test('Edit OIDC client', async ({ page }) => { await page.getByLabel('Name').fill('Nextcloud updated'); await page.getByTestId('callback-url-1').first().fill('http://nextcloud-updated/auth/callback'); await page.locator('[role="tab"][data-value="light-logo"]').first().click(); - await page.setInputFiles('#oidc-client-logo-light', 'assets/nextcloud-logo.png'); + await page.setInputFiles('#oidc-client-logo-light', 'resources/images/nextcloud-logo.png'); await page.locator('[role="tab"][data-value="dark-logo"]').first().click(); - await page.setInputFiles('#oidc-client-logo-dark', 'assets/nextcloud-logo.png'); + await page.setInputFiles('#oidc-client-logo-dark', 'resources/images/nextcloud-logo.png'); await page.getByLabel('Client Launch URL').fill(oidcClient.launchURL); await page.getByRole('button', { name: 'Save' }).click(); diff --git a/tests/specs/user-signup.spec.ts b/tests/specs/user-signup.spec.ts index 9b7fb019..4644f980 100644 --- a/tests/specs/user-signup.spec.ts +++ b/tests/specs/user-signup.spec.ts @@ -25,7 +25,7 @@ test.describe('Initial User Signup', () => { }); test('Initial Signup - success flow', async ({ page }) => { - await cleanupBackend(true); + await cleanupBackend({ skipSeed: true }); await page.goto('/setup'); await page.getByLabel('First name').fill('Jane'); await page.getByLabel('Last name').fill('Smith'); diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 0b392b76..168d7167 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -1,6 +1,10 @@ { "compilerOptions": { "baseUrl": ".", - "lib": ["ES2022"] + "lib": ["ES2022"], + "esModuleInterop": true, + "module": "es2022", + "moduleResolution": "node", + "target": "es2022" } } diff --git a/tests/utils/cleanup.util.ts b/tests/utils/cleanup.util.ts index 3e82b0b7..b0cc1f28 100644 --- a/tests/utils/cleanup.util.ts +++ b/tests/utils/cleanup.util.ts @@ -1,9 +1,9 @@ import playwrightConfig from '../playwright.config'; -export async function cleanupBackend(skipSeed = false) { +export async function cleanupBackend({ skipSeed = false, skipLdapSetup = false } = {}) { const url = new URL('/api/test/reset', playwrightConfig.use!.baseURL); - if (process.env.SKIP_LDAP_TESTS === 'true' || skipSeed) { + if (process.env.SKIP_LDAP_TESTS === 'true' || skipSeed || skipLdapSetup) { url.searchParams.append('skip-ldap', 'true'); } diff --git a/tests/utils/fs.util.ts b/tests/utils/fs.util.ts new file mode 100644 index 00000000..a5fc56cf --- /dev/null +++ b/tests/utils/fs.util.ts @@ -0,0 +1,7 @@ +import path from 'path'; + +export const tmpDir = pathFromRoot('.tmp'); + +export function pathFromRoot(p: string): string { + return path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', p); +}