Merge branch 'main' into qa

This commit is contained in:
Max Peintner
2025-04-22 11:39:05 +02:00
45 changed files with 2503 additions and 542 deletions

View File

@@ -4,6 +4,10 @@ on:
push:
branches:
- main
workflow_dispatch:
permissions:
packages: write
jobs:
build:
@@ -39,7 +43,14 @@ jobs:
with:
driver-opts: 'image=moby/buildkit:v0.11.6'
- name: Login
- name: Login Public
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login Private
uses: docker/login-action@v3
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
@@ -50,9 +61,15 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DOCKER_IMAGE }}
# generate Docker tags based on the following events/attributes
tags: type=sha
images: |
ghcr.io/zitadel/login
${{ secrets.DOCKER_IMAGE }}
tags: |
type=edge
type=ref,event=branch
type=ref,event=tag
type=ref,event=pr
type=sha
- name: Install dependencies
run: pnpm install
@@ -69,8 +86,23 @@ jobs:
timeout-minutes: 10
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: true
- name: Export digest
run: |
mkdir -p /tmp/digests/app
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/app/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests
path: /tmp/digests
if-no-files-found: error
retention-days: 1

View File

@@ -2,9 +2,9 @@ name: Quality
on:
pull_request:
schedule:
# schedule:
# Every morning at 6:00 AM CET
- cron: '0 4 * * *'
# - cron: '0 4 * * *'
workflow_dispatch:
inputs:
target-env:
@@ -110,6 +110,14 @@ jobs:
run: pnpm build
if: ${{ startsWith(matrix.command, 'test:acceptance') }}
- name: Run SAML SP
run: ZITADEL_DEV_UID=root pnpm run-samlsp
if: ${{ matrix.command == 'test:acceptance' }}
- name: Run OIDC RP
run: ZITADEL_DEV_UID=root pnpm run-oidcrp
if: ${{ matrix.command == 'test:acceptance' }}
- name: Check
id: check
run: pnpm ${{ contains(matrix.command, 'test:acceptance') && 'test:acceptance' || matrix.command }}

View File

@@ -47,11 +47,8 @@ export ZITADEL_DEV_UID="$(id -u)"
# Pull images
docker compose --file ./acceptance/docker-compose.yaml pull
# Run ZITADEL and configure ./apps/login/.env.local
docker compose --file ./acceptance/docker-compose.yaml run setup
# Configure your shell to use the environment variables written to ./apps/login/.env.acceptance
export $(cat ./apps/login/.env.acceptance | xargs)
# Run ZITADEL with local notification sink and configure ./apps/login/.env.local
pnpm run-sink
```
### Developing Against Your ZITADEL Cloud Instance
@@ -79,6 +76,22 @@ pnpm dev
The application is now available at `http://localhost:3000`
### Adding applications and IDPs
```sh
# OPTIONAL Run SAML SP
pnpm run-samlsp
# OPTIONAL Run OIDC RP
pnpm run-oidcrp
# OPTIONAL Run SAML IDP
pnpm run-samlidp
# OPTIONAL Run OIDC OP
pnpm run-oidcop
```
### Testing
You can execute the following commands `pnpm test` for a single test run or `pnpm test:watch` in the following directories:

View File

@@ -1,4 +1,4 @@
FROM golang:1.19-alpine
FROM golang:1.24-alpine
RUN apk add curl jq
COPY setup.sh /setup.sh
RUN chmod +x /setup.sh

View File

@@ -1,7 +1,7 @@
services:
zitadel:
user: "${ZITADEL_DEV_UID}"
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:v2.67.2}"
image: "${ZITADEL_IMAGE:-ghcr.io/zitadel/zitadel:02617cf17fdde849378c1a6b5254bbfb2745b164}"
command: 'start-from-init --masterkey "MasterkeyNeedsToHave32Characters" --tlsMode disabled --config /zitadel.yaml --steps /zitadel.yaml'
ports:
- "8080:8080"
@@ -11,6 +11,8 @@ services:
depends_on:
db:
condition: "service_healthy"
extra_hosts:
- "localhost:host-gateway"
db:
restart: "always"
@@ -57,7 +59,7 @@ services:
condition: "service_completed_successfully"
sink:
image: golang:1.19-alpine
image: golang:1.24-alpine
container_name: sink
command: go run /sink/main.go -port '3333' -email '/email' -sms '/sms' -notification '/notification'
ports:

View File

@@ -0,0 +1,20 @@
services:
oidcop:
image: golang:1.24-alpine
container_name: oidcop
command: go run main.go
environment:
API_URL: 'http://localhost:8080'
API_DOMAIN: 'localhost:8080'
PAT_FILE: '/pat/zitadel-admin-sa.pat'
SCHEMA: 'http'
HOST: 'localhost'
PORT: "8004"
working_dir: /oidc
ports:
- 8004:8004
volumes:
- "../../pat:/pat"
- "./:/oidc"
extra_hosts:
- "localhost:host-gateway"

View File

@@ -0,0 +1,28 @@
module github.com/zitadel/typescript/acceptance/idp/oidc
go 1.24.1
require github.com/zitadel/oidc/v3 v3.37.0
require (
github.com/bmatcuk/doublestar/v4 v4.8.1 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/muhlemmer/gu v0.3.1 // indirect
github.com/muhlemmer/httpforwarded v0.1.0 // indirect
github.com/rs/cors v1.11.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/zitadel/logging v0.6.2 // indirect
github.com/zitadel/schema v1.3.1 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
)

View File

@@ -0,0 +1,71 @@
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/zitadel/logging v0.6.2 h1:MW2kDDR0ieQynPZ0KIZPrh9ote2WkxfBif5QoARDQcU=
github.com/zitadel/logging v0.6.2/go.mod h1:z6VWLWUkJpnNVDSLzrPSQSQyttysKZ6bCRongw0ROK4=
github.com/zitadel/oidc/v3 v3.37.0 h1:nYATWlnP7f18XiAbw6upUruBaqfB1kUrXrSTf1EYGO8=
github.com/zitadel/oidc/v3 v3.37.0/go.mod h1:/xDan4OUQhguJ4Ur73OOJrtugvR164OMnidXP9xfVNw=
github.com/zitadel/schema v1.3.1 h1:QT3kwiRIRXXLVAs6gCK/u044WmUVh6IlbLXUsn6yRQU=
github.com/zitadel/schema v1.3.1/go.mod h1:071u7D2LQacy1HAN+YnMd/mx1qVE2isb0Mjeqg46xnU=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

186
acceptance/idp/oidc/main.go Normal file
View File

@@ -0,0 +1,186 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/zitadel/oidc/v3/example/server/exampleop"
"github.com/zitadel/oidc/v3/example/server/storage"
)
func main() {
apiURL := os.Getenv("API_URL")
pat := readPAT(os.Getenv("PAT_FILE"))
domain := os.Getenv("API_DOMAIN")
schema := os.Getenv("SCHEMA")
host := os.Getenv("HOST")
port := os.Getenv("PORT")
logger := slog.New(
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}),
)
issuer := fmt.Sprintf("%s://%s:%s/", schema, host, port)
redirectURI := fmt.Sprintf("%s/idps/callback", apiURL)
clientID := "web"
clientSecret := "secret"
storage.RegisterClients(
storage.WebClient(clientID, clientSecret, redirectURI),
)
storage := storage.NewStorage(storage.NewUserStore(issuer))
router := exampleop.SetupServer(issuer, storage, logger, false)
server := &http.Server{
Addr: ":" + port,
Handler: router,
}
go func() {
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP server error: %v", err)
}
log.Println("Stopped serving new connections.")
}()
createZitadelResources(apiURL, pat, domain, issuer, clientID, clientSecret)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
}
func readPAT(path string) string {
f, err := os.Open(path)
if err != nil {
panic(err)
}
pat, err := io.ReadAll(f)
if err != nil {
panic(err)
}
return strings.Trim(string(pat), "\n")
}
func createZitadelResources(apiURL, pat, domain, issuer, clientID, clientSecret string) error {
idpID, err := CreateIDP(apiURL, pat, domain, issuer, clientID, clientSecret)
if err != nil {
return err
}
return ActivateIDP(apiURL, pat, domain, idpID)
}
type createIDP struct {
Name string `json:"name"`
Issuer string `json:"issuer"`
ClientId string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
Scopes []string `json:"scopes"`
ProviderOptions providerOptions `json:"providerOptions"`
IsIdTokenMapping bool `json:"isIdTokenMapping"`
UsePkce bool `json:"usePkce"`
}
type providerOptions struct {
IsLinkingAllowed bool `json:"isLinkingAllowed"`
IsCreationAllowed bool `json:"isCreationAllowed"`
IsAutoCreation bool `json:"isAutoCreation"`
IsAutoUpdate bool `json:"isAutoUpdate"`
AutoLinking string `json:"autoLinking"`
}
type idp struct {
ID string `json:"id"`
}
func CreateIDP(apiURL, pat, domain string, issuer, clientID, clientSecret string) (string, error) {
createIDP := &createIDP{
Name: "OIDC",
Issuer: issuer,
ClientId: clientID,
ClientSecret: clientSecret,
Scopes: []string{"openid", "profile", "email"},
ProviderOptions: providerOptions{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
AutoLinking: "AUTO_LINKING_OPTION_USERNAME",
},
IsIdTokenMapping: false,
UsePkce: false,
}
resp, err := doRequestWithHeaders(apiURL+"/admin/v1/idps/generic_oidc", pat, domain, createIDP)
if err != nil {
return "", err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
defer resp.Body.Close()
idp := new(idp)
if err := json.Unmarshal(data, idp); err != nil {
return "", err
}
return idp.ID, nil
}
type activateIDP struct {
IdpId string `json:"idpId"`
}
func ActivateIDP(apiURL, pat, domain string, idpID string) error {
activateIDP := &activateIDP{
IdpId: idpID,
}
_, err := doRequestWithHeaders(apiURL+"/admin/v1/policies/login/idps", pat, domain, activateIDP)
return err
}
func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
if err != nil {
return nil, err
}
values := http.Header{}
values.Add("Authorization", "Bearer "+pat)
values.Add("x-forwarded-host", domain)
values.Add("Content-Type", "application/json")
req.Header = values
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}

View File

@@ -0,0 +1,20 @@
services:
samlidp:
image: golang:1.24-alpine
container_name: samlidp
command: go run main.go
environment:
API_URL: 'http://localhost:8080'
API_DOMAIN: 'localhost:8080'
PAT_FILE: '/pat/zitadel-admin-sa.pat'
SCHEMA: 'http'
HOST: 'localhost'
PORT: "8003"
working_dir: /saml
ports:
- 8003:8003
volumes:
- "../../pat:/pat"
- "./:/saml"
extra_hosts:
- "localhost:host-gateway"

View File

@@ -0,0 +1,16 @@
module github.com/zitadel/typescript/acceptance/idp/saml
go 1.24.1
require (
github.com/crewjam/saml v0.4.14
github.com/mattermost/xml-roundtrip-validator v0.1.0
github.com/zenazn/goji v1.0.1
golang.org/x/crypto v0.36.0
)
require (
github.com/beevik/etree v1.1.0 // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/russellhaering/goxmldsig v1.3.0 // indirect
)

View File

@@ -0,0 +1,49 @@
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/russellhaering/goxmldsig v1.3.0 h1:DllIWUgMy0cRUMfGiASiYEa35nsieyD3cigIwLonTPM=
github.com/russellhaering/goxmldsig v1.3.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/zenazn/goji v1.0.1 h1:4lbD8Mx2h7IvloP7r2C0D6ltZP6Ufip8Hn0wmSK5LR8=
github.com/zenazn/goji v1.0.1/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=

328
acceptance/idp/saml/main.go Normal file
View File

@@ -0,0 +1,328 @@
package main
import (
"bytes"
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"encoding/xml"
"errors"
"io"
"log"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"github.com/crewjam/saml"
"github.com/crewjam/saml/logger"
"github.com/crewjam/saml/samlidp"
xrv "github.com/mattermost/xml-roundtrip-validator"
"github.com/zenazn/goji"
"github.com/zenazn/goji/bind"
"github.com/zenazn/goji/web"
"golang.org/x/crypto/bcrypt"
)
var key = func() crypto.PrivateKey {
b, _ := pem.Decode([]byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0OhbMuizgtbFOfwbK7aURuXhZx6VRuAs3nNibiuifwCGz6u9
yy7bOR0P+zqN0YkjxaokqFgra7rXKCdeABmoLqCC0U+cGmLNwPOOA0PaD5q5xKhQ
4Me3rt/R9C4Ca6k3/OnkxnKwnogcsmdgs2l8liT3qVHP04Oc7Uymq2v09bGb6nPu
fOrkXS9F6mSClxHG/q59AGOWsXK1xzIRV1eu8W2SNdyeFVU1JHiQe444xLoPul5t
InWasKayFsPlJfWNc8EoU8COjNhfo/GovFTHVjh9oUR/gwEFVwifIHihRE0Hazn2
EQSLaOr2LM0TsRsQroFjmwSGgI+X2bfbMTqWOQIDAQABAoIBAFWZwDTeESBdrLcT
zHZe++cJLxE4AObn2LrWANEv5AeySYsyzjRBYObIN9IzrgTb8uJ900N/zVr5VkxH
xUa5PKbOcowd2NMfBTw5EEnaNbILLm+coHdanrNzVu59I9TFpAFoPavrNt/e2hNo
NMGPSdOkFi81LLl4xoadz/WR6O/7N2famM+0u7C2uBe+TrVwHyuqboYoidJDhO8M
w4WlY9QgAUhkPyzZqrl+VfF1aDTGVf4LJgaVevfFCas8Ws6DQX5q4QdIoV6/0vXi
B1M+aTnWjHuiIzjBMWhcYW2+I5zfwNWRXaxdlrYXRukGSdnyO+DH/FhHePJgmlkj
NInADDkCgYEA6MEQFOFSCc/ELXYWgStsrtIlJUcsLdLBsy1ocyQa2lkVUw58TouW
RciE6TjW9rp31pfQUnO2l6zOUC6LT9Jvlb9PSsyW+rvjtKB5PjJI6W0hjX41wEO6
fshFELMJd9W+Ezao2AsP2hZJ8McCF8no9e00+G4xTAyxHsNI2AFTCQcCgYEA5cWZ
JwNb4t7YeEajPt9xuYNUOQpjvQn1aGOV7KcwTx5ELP/Hzi723BxHs7GSdrLkkDmi
Gpb+mfL4wxCt0fK0i8GFQsRn5eusyq9hLqP/bmjpHoXe/1uajFbE1fZQR+2LX05N
3ATlKaH2hdfCJedFa4wf43+cl6Yhp6ZA0Yet1r8CgYEAwiu1j8W9G+RRA5/8/DtO
yrUTOfsbFws4fpLGDTA0mq0whf6Soy/96C90+d9qLaC3srUpnG9eB0CpSOjbXXbv
kdxseLkexwOR3bD2FHX8r4dUM2bzznZyEaxfOaQypN8SV5ME3l60Fbr8ajqLO288
wlTmGM5Mn+YCqOg/T7wjGmcCgYBpzNfdl/VafOROVbBbhgXWtzsz3K3aYNiIjbp+
MunStIwN8GUvcn6nEbqOaoiXcX4/TtpuxfJMLw4OvAJdtxUdeSmEee2heCijV6g3
ErrOOy6EqH3rNWHvlxChuP50cFQJuYOueO6QggyCyruSOnDDuc0BM0SGq6+5g5s7
H++S/wKBgQDIkqBtFr9UEf8d6JpkxS0RXDlhSMjkXmkQeKGFzdoJcYVFIwq8jTNB
nJrVIGs3GcBkqGic+i7rTO1YPkquv4dUuiIn+vKZVoO6b54f+oPBXd4S0BnuEqFE
rdKNuCZhiaE2XD9L/O9KP1fh5bfEcKwazQ23EvpJHBMm8BGC+/YZNw==
-----END RSA PRIVATE KEY-----`))
k, _ := x509.ParsePKCS1PrivateKey(b.Bytes)
return k
}()
var cert = func() *x509.Certificate {
b, _ := pem.Decode([]byte(`-----BEGIN CERTIFICATE-----
MIIDBzCCAe+gAwIBAgIJAPr/Mrlc8EGhMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV
BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xNTEyMjgxOTE5NDVaFw0yNTEyMjUxOTE5
NDVaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBANDoWzLos4LWxTn8Gyu2lEbl4WcelUbgLN5zYm4ron8A
hs+rvcsu2zkdD/s6jdGJI8WqJKhYK2u61ygnXgAZqC6ggtFPnBpizcDzjgND2g+a
ucSoUODHt67f0fQuAmupN/zp5MZysJ6IHLJnYLNpfJYk96lRz9ODnO1Mpqtr9PWx
m+pz7nzq5F0vRepkgpcRxv6ufQBjlrFytccyEVdXrvFtkjXcnhVVNSR4kHuOOMS6
D7pebSJ1mrCmshbD5SX1jXPBKFPAjozYX6PxqLxUx1Y4faFEf4MBBVcInyB4oURN
B2s59hEEi2jq9izNE7EbEK6BY5sEhoCPl9m32zE6ljkCAwEAAaNQME4wHQYDVR0O
BBYEFB9ZklC1Ork2zl56zg08ei7ss/+iMB8GA1UdIwQYMBaAFB9ZklC1Ork2zl56
zg08ei7ss/+iMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAAVoTSQ5
pAirw8OR9FZ1bRSuTDhY9uxzl/OL7lUmsv2cMNeCB3BRZqm3mFt+cwN8GsH6f3uv
NONIhgFpTGN5LEcXQz89zJEzB+qaHqmbFpHQl/sx2B8ezNgT/882H2IH00dXESEf
y/+1gHg2pxjGnhRBN6el/gSaDiySIMKbilDrffuvxiCfbpPN0NRRiPJhd2ay9KuL
/RxQRl1gl9cHaWiouWWba1bSBb2ZPhv2rPMUsFo98ntkGCObDX6Y1SpkqmoTbrsb
GFsTG2DLxnvr4GdN1BSr0Uu/KV3adj47WkXVPeMYQti/bQmxQB8tRFhrw80qakTL
UzreO96WzlBBMtY=
-----END CERTIFICATE-----`))
c, _ := x509.ParseCertificate(b.Bytes)
return c
}()
// Example from https://github.com/crewjam/saml/blob/main/example/idp/idp.go
func main() {
apiURL := os.Getenv("API_URL")
pat := readPAT(os.Getenv("PAT_FILE"))
domain := os.Getenv("API_DOMAIN")
schema := os.Getenv("SCHEMA")
host := os.Getenv("HOST")
port := os.Getenv("PORT")
baseURL, err := url.Parse(schema + "://" + host + ":" + port)
if err != nil {
panic(err)
}
idpServer, err := samlidp.New(samlidp.Options{
URL: *baseURL,
Logger: logger.DefaultLogger,
Key: key,
Certificate: cert,
Store: &samlidp.MemoryStore{},
})
if err != nil {
panic(err)
}
metadata, err := xml.MarshalIndent(idpServer.IDP.Metadata(), "", " ")
if err != nil {
panic(err)
}
idpID, err := createZitadelResources(apiURL, pat, domain, metadata)
if err != nil {
panic(err)
}
lis := bind.Socket(":" + baseURL.Port())
goji.Handle("/*", idpServer)
go func() {
goji.ServeListener(lis)
}()
addService(idpServer, apiURL+"/idps/"+idpID+"/saml/metadata")
addUsers(idpServer)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
if err := lis.Close(); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
}
func readPAT(path string) string {
f, err := os.Open(path)
if err != nil {
panic(err)
}
pat, err := io.ReadAll(f)
if err != nil {
panic(err)
}
return strings.Trim(string(pat), "\n")
}
func addService(idpServer *samlidp.Server, spURLStr string) {
metadataResp, err := http.Get(spURLStr)
if err != nil {
panic(err)
}
defer metadataResp.Body.Close()
idpServer.HandlePutService(
web.C{URLParams: map[string]string{"id": spURLStr}},
httptest.NewRecorder(),
httptest.NewRequest(http.MethodPost, spURLStr, metadataResp.Body),
)
}
func getSPMetadata(r io.Reader) (spMetadata *saml.EntityDescriptor, err error) {
var data []byte
if data, err = io.ReadAll(r); err != nil {
return nil, err
}
spMetadata = &saml.EntityDescriptor{}
if err := xrv.Validate(bytes.NewBuffer(data)); err != nil {
return nil, err
}
if err := xml.Unmarshal(data, &spMetadata); err != nil {
if err.Error() == "expected element type <EntityDescriptor> but have <EntitiesDescriptor>" {
entities := &saml.EntitiesDescriptor{}
if err := xml.Unmarshal(data, &entities); err != nil {
return nil, err
}
for _, e := range entities.EntityDescriptors {
if len(e.SPSSODescriptors) > 0 {
return &e, nil
}
}
// there were no SPSSODescriptors in the response
return nil, errors.New("metadata contained no service provider metadata")
}
return nil, err
}
return spMetadata, nil
}
func addUsers(idpServer *samlidp.Server) {
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("hunter2"), bcrypt.DefaultCost)
err := idpServer.Store.Put("/users/alice", samlidp.User{Name: "alice",
HashedPassword: hashedPassword,
Groups: []string{"Administrators", "Users"},
Email: "alice@example.com",
CommonName: "Alice Smith",
Surname: "Smith",
GivenName: "Alice",
})
if err != nil {
panic(err)
}
err = idpServer.Store.Put("/users/bob", samlidp.User{
Name: "bob",
HashedPassword: hashedPassword,
Groups: []string{"Users"},
Email: "bob@example.com",
CommonName: "Bob Smith",
Surname: "Smith",
GivenName: "Bob",
})
if err != nil {
panic(err)
}
}
func createZitadelResources(apiURL, pat, domain string, metadata []byte) (string, error) {
idpID, err := CreateIDP(apiURL, pat, domain, metadata)
if err != nil {
return "", err
}
return idpID, ActivateIDP(apiURL, pat, domain, idpID)
}
type createIDP struct {
Name string `json:"name"`
MetadataXml string `json:"metadataXml"`
Binding string `json:"binding"`
WithSignedRequest bool `json:"withSignedRequest"`
ProviderOptions providerOptions `json:"providerOptions"`
NameIdFormat string `json:"nameIdFormat"`
}
type providerOptions struct {
IsLinkingAllowed bool `json:"isLinkingAllowed"`
IsCreationAllowed bool `json:"isCreationAllowed"`
IsAutoCreation bool `json:"isAutoCreation"`
IsAutoUpdate bool `json:"isAutoUpdate"`
AutoLinking string `json:"autoLinking"`
}
type idp struct {
ID string `json:"id"`
}
func CreateIDP(apiURL, pat, domain string, idpMetadata []byte) (string, error) {
encoded := make([]byte, base64.URLEncoding.EncodedLen(len(idpMetadata)))
base64.URLEncoding.Encode(encoded, idpMetadata)
createIDP := &createIDP{
Name: "CREWJAM",
MetadataXml: string(encoded),
Binding: "SAML_BINDING_REDIRECT",
WithSignedRequest: false,
ProviderOptions: providerOptions{
IsLinkingAllowed: true,
IsCreationAllowed: true,
IsAutoCreation: true,
IsAutoUpdate: true,
AutoLinking: "AUTO_LINKING_OPTION_USERNAME",
},
NameIdFormat: "SAML_NAME_ID_FORMAT_PERSISTENT",
}
resp, err := doRequestWithHeaders(apiURL+"/admin/v1/idps/saml", pat, domain, createIDP)
if err != nil {
return "", err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
defer resp.Body.Close()
idp := new(idp)
if err := json.Unmarshal(data, idp); err != nil {
return "", err
}
return idp.ID, nil
}
type activateIDP struct {
IdpId string `json:"idpId"`
}
func ActivateIDP(apiURL, pat, domain string, idpID string) error {
activateIDP := &activateIDP{
IdpId: idpID,
}
_, err := doRequestWithHeaders(apiURL+"/admin/v1/policies/login/idps", pat, domain, activateIDP)
return err
}
func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
if err != nil {
return nil, err
}
values := http.Header{}
values.Add("Authorization", "Bearer "+pat)
values.Add("x-forwarded-host", domain)
values.Add("Content-Type", "application/json")
req.Header = values
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}

View File

@@ -0,0 +1,22 @@
services:
oidcrp:
image: golang:1.24-alpine
container_name: oidcrp
command: go run main.go
environment:
API_URL: 'http://localhost:8080'
API_DOMAIN: 'localhost:8080'
PAT_FILE: '/pat/zitadel-admin-sa.pat'
LOGIN_URL: 'http://localhost:3000'
ISSUER: 'http://localhost:3000'
HOST: 'http://localhost'
PORT: '8000'
SCOPES: 'openid profile email'
working_dir: /oidc
ports:
- 8000:8000
volumes:
- "../pat:/pat"
- "./:/oidc"
extra_hosts:
- "localhost:host-gateway"

26
acceptance/oidc/go.mod Normal file
View File

@@ -0,0 +1,26 @@
module github.com/zitadel/typescript/acceptance/oidc
go 1.24.1
require (
github.com/google/uuid v1.6.0
github.com/sirupsen/logrus v1.9.3
github.com/zitadel/logging v0.6.1
github.com/zitadel/oidc/v3 v3.36.1
)
require (
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/muhlemmer/gu v0.3.1 // indirect
github.com/zitadel/schema v1.3.0 // indirect
go.opentelemetry.io/otel v1.29.0 // indirect
go.opentelemetry.io/otel/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/oauth2 v0.28.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
)

67
acceptance/oidc/go.sum Normal file
View File

@@ -0,0 +1,67 @@
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/jeremija/gosubmit v0.2.8 h1:mmSITBz9JxVtu8eqbN+zmmwX7Ij2RidQxhcwRVI4wqA=
github.com/jeremija/gosubmit v0.2.8/go.mod h1:Ui+HS073lCFREXBbdfrJzMB57OI/bdxTiLtrDHHhFPI=
github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM=
github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM=
github.com/muhlemmer/httpforwarded v0.1.0 h1:x4DLrzXdliq8mprgUMR0olDvHGkou5BJsK/vWUetyzY=
github.com/muhlemmer/httpforwarded v0.1.0/go.mod h1:yo9czKedo2pdZhoXe+yDkGVbU0TJ0q9oQ90BVoDEtw0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/zitadel/logging v0.6.1 h1:Vyzk1rl9Kq9RCevcpX6ujUaTYFX43aa4LkvV1TvUk+Y=
github.com/zitadel/logging v0.6.1/go.mod h1:Y4CyAXHpl3Mig6JOszcV5Rqqsojj+3n7y2F591Mp/ow=
github.com/zitadel/oidc/v3 v3.36.1 h1:1AT1NqKKEqAwx4GmKJZ9fYkWH2WIn/VKMfQ46nBtRf0=
github.com/zitadel/oidc/v3 v3.36.1/go.mod h1:dApGZLvWZTHRuxmcbQlW5d2XVjVYR3vGOdq536igmTs=
github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0=
github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc=
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

313
acceptance/oidc/main.go Normal file
View File

@@ -0,0 +1,313 @@
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v3/pkg/client/rp"
httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc"
)
var (
callbackPath = "/auth/callback"
key = []byte("test1234test1234")
)
func main() {
apiURL := os.Getenv("API_URL")
pat := readPAT(os.Getenv("PAT_FILE"))
domain := os.Getenv("API_DOMAIN")
loginURL := os.Getenv("LOGIN_URL")
issuer := os.Getenv("ISSUER")
host := os.Getenv("HOST")
port := os.Getenv("PORT")
scopeList := strings.Split(os.Getenv("SCOPES"), " ")
redirectURI := fmt.Sprintf("%v:%v%v", host, port, callbackPath)
cookieHandler := httphelper.NewCookieHandler(key, key, httphelper.WithUnsecure())
clientID, clientSecret, err := createZitadelResources(apiURL, pat, domain, redirectURI, loginURL)
if err != nil {
panic(err)
}
logger := slog.New(
slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
}),
)
client := &http.Client{
Timeout: time.Minute,
}
// enable outgoing request logging
logging.EnableHTTPClient(client,
logging.WithClientGroup("client"),
)
options := []rp.Option{
rp.WithCookieHandler(cookieHandler),
rp.WithVerifierOpts(rp.WithIssuedAtOffset(5 * time.Second)),
rp.WithHTTPClient(client),
rp.WithLogger(logger),
rp.WithSigningAlgsFromDiscovery(),
}
if clientSecret == "" {
options = append(options, rp.WithPKCE(cookieHandler))
}
// One can add a logger to the context,
// pre-defining log attributes as required.
ctx := logging.ToContext(context.TODO(), logger)
provider, err := rp.NewRelyingPartyOIDC(ctx, issuer, clientID, clientSecret, redirectURI, scopeList, options...)
if err != nil {
logrus.Fatalf("error creating provider %s", err.Error())
}
// generate some state (representing the state of the user in your application,
// e.g. the page where he was before sending him to login
state := func() string {
return uuid.New().String()
}
urlOptions := []rp.URLParamOpt{
rp.WithPromptURLParam("Welcome back!"),
}
// register the AuthURLHandler at your preferred path.
// the AuthURLHandler creates the auth request and redirects the user to the auth server.
// including state handling with secure cookie and the possibility to use PKCE.
// Prompts can optionally be set to inform the server of
// any messages that need to be prompted back to the user.
http.Handle("/login", rp.AuthURLHandler(
state,
provider,
urlOptions...,
))
// for demonstration purposes the returned userinfo response is written as JSON object onto response
marshalUserinfo := func(w http.ResponseWriter, r *http.Request, tokens *oidc.Tokens[*oidc.IDTokenClaims], state string, rp rp.RelyingParty, info *oidc.UserInfo) {
fmt.Println("access token", tokens.AccessToken)
fmt.Println("refresh token", tokens.RefreshToken)
fmt.Println("id token", tokens.IDToken)
data, err := json.Marshal(info)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("content-type", "application/json")
w.Write(data)
}
// register the CodeExchangeHandler at the callbackPath
// the CodeExchangeHandler handles the auth response, creates the token request and calls the callback function
// with the returned tokens from the token endpoint
// in this example the callback function itself is wrapped by the UserinfoCallback which
// will call the Userinfo endpoint, check the sub and pass the info into the callback function
http.Handle(callbackPath, rp.CodeExchangeHandler(rp.UserinfoCallback(marshalUserinfo), provider))
// if you would use the callback without calling the userinfo endpoint, simply switch the callback handler for:
//
// http.Handle(callbackPath, rp.CodeExchangeHandler(marshalToken, provider))
// simple counter for request IDs
var counter atomic.Int64
// enable incomming request logging
mw := logging.Middleware(
logging.WithLogger(logger),
logging.WithGroup("server"),
logging.WithIDFunc(func() slog.Attr {
return slog.Int64("id", counter.Add(1))
}),
)
server := &http.Server{
Addr: ":" + port,
Handler: mw(http.DefaultServeMux),
}
go func() {
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP server error: %v", err)
}
log.Println("Stopped serving new connections.")
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
}
func readPAT(path string) string {
f, err := os.Open(path)
if err != nil {
panic(err)
}
pat, err := io.ReadAll(f)
if err != nil {
panic(err)
}
return strings.Trim(string(pat), "\n")
}
func createZitadelResources(apiURL, pat, domain, redirectURI, loginURL string) (string, string, error) {
projectID, err := CreateProject(apiURL, pat, domain)
if err != nil {
return "", "", err
}
return CreateApp(apiURL, pat, domain, projectID, redirectURI, loginURL)
}
type project struct {
ID string `json:"id"`
}
type createProject struct {
Name string `json:"name"`
ProjectRoleAssertion bool `json:"projectRoleAssertion"`
ProjectRoleCheck bool `json:"projectRoleCheck"`
HasProjectCheck bool `json:"hasProjectCheck"`
PrivateLabelingSetting string `json:"privateLabelingSetting"`
}
func CreateProject(apiURL, pat, domain string) (string, error) {
createProject := &createProject{
Name: "OIDC",
ProjectRoleAssertion: false,
ProjectRoleCheck: false,
HasProjectCheck: false,
PrivateLabelingSetting: "PRIVATE_LABELING_SETTING_UNSPECIFIED",
}
resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects", pat, domain, createProject)
if err != nil {
return "", err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
defer resp.Body.Close()
p := new(project)
if err := json.Unmarshal(data, p); err != nil {
return "", err
}
fmt.Printf("projectID: %+v\n", p.ID)
return p.ID, nil
}
type createApp struct {
Name string `json:"name"`
RedirectUris []string `json:"redirectUris"`
ResponseTypes []string `json:"responseTypes"`
GrantTypes []string `json:"grantTypes"`
AppType string `json:"appType"`
AuthMethodType string `json:"authMethodType"`
PostLogoutRedirectUris []string `json:"postLogoutRedirectUris"`
Version string `json:"version"`
DevMode bool `json:"devMode"`
AccessTokenType string `json:"accessTokenType"`
AccessTokenRoleAssertion bool `json:"accessTokenRoleAssertion"`
IdTokenRoleAssertion bool `json:"idTokenRoleAssertion"`
IdTokenUserinfoAssertion bool `json:"idTokenUserinfoAssertion"`
ClockSkew string `json:"clockSkew"`
AdditionalOrigins []string `json:"additionalOrigins"`
SkipNativeAppSuccessPage bool `json:"skipNativeAppSuccessPage"`
BackChannelLogoutUri []string `json:"backChannelLogoutUri"`
LoginVersion version `json:"loginVersion"`
}
type version struct {
LoginV2 loginV2 `json:"loginV2"`
}
type loginV2 struct {
BaseUri string `json:"baseUri"`
}
type app struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
}
func CreateApp(apiURL, pat, domain, projectID string, redirectURI, loginURL string) (string, string, error) {
createApp := &createApp{
Name: "OIDC",
RedirectUris: []string{redirectURI},
ResponseTypes: []string{"OIDC_RESPONSE_TYPE_CODE"},
GrantTypes: []string{"OIDC_GRANT_TYPE_AUTHORIZATION_CODE"},
AppType: "OIDC_APP_TYPE_WEB",
AuthMethodType: "OIDC_AUTH_METHOD_TYPE_BASIC",
Version: "OIDC_VERSION_1_0",
DevMode: true,
AccessTokenType: "OIDC_TOKEN_TYPE_BEARER",
AccessTokenRoleAssertion: true,
IdTokenRoleAssertion: true,
IdTokenUserinfoAssertion: true,
ClockSkew: "1s",
SkipNativeAppSuccessPage: true,
LoginVersion: version{
LoginV2: loginV2{
BaseUri: loginURL,
},
},
}
resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects/"+projectID+"/apps/oidc", pat, domain, createApp)
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", err
}
defer resp.Body.Close()
a := new(app)
if err := json.Unmarshal(data, a); err != nil {
return "", "", err
}
return a.ClientID, a.ClientSecret, err
}
func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
if err != nil {
return nil, err
}
values := http.Header{}
values.Add("Authorization", "Bearer "+pat)
values.Add("x-forwarded-host", domain)
values.Add("Content-Type", "application/json")
req.Header = values
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}

View File

@@ -0,0 +1,22 @@
services:
samlsp:
image: golang:1.24-alpine
container_name: samlsp
command: go run main.go
environment:
API_URL: 'http://localhost:8080'
API_DOMAIN: 'localhost:8080'
PAT_FILE: '/pat/zitadel-admin-sa.pat'
LOGIN_URL: 'http://localhost:3000'
IDP_URL: 'http://localhost:3000/saml/v2/metadata'
HOST: 'http://localhost'
PORT: '8001'
working_dir: /saml
ports:
- 8001:8001
volumes:
- "../pat:/pat"
- "./:/saml"
extra_hosts:
- "localhost:host-gateway"

18
acceptance/saml/go.mod Normal file
View File

@@ -0,0 +1,18 @@
module github.com/zitadel/typescript/acceptance/saml
go 1.24.0
require github.com/crewjam/saml v0.4.14
require (
github.com/beevik/etree v1.5.0 // indirect
github.com/crewjam/httperr v0.2.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/russellhaering/goxmldsig v1.5.0 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
)

38
acceptance/saml/go.sum Normal file
View File

@@ -0,0 +1,38 @@
github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs=
github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
github.com/crewjam/httperr v0.2.0 h1:b2BfXR8U3AlIHwNeFFvZ+BV1LFvKLlzMjzaTnZMybNo=
github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3pglZ5oH4=
github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russellhaering/goxmldsig v1.5.0 h1:AU2UkkYIUOTyZRbe08XMThaOCelArgvNfYapcmSjBNw=
github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvDP6tE0nTaeUnjXDmk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=

266
acceptance/saml/main.go Normal file
View File

@@ -0,0 +1,266 @@
package main
import (
"bytes"
"context"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/crewjam/saml/samlsp"
)
var keyPair = func() tls.Certificate {
cert := []byte(`-----BEGIN CERTIFICATE-----
MIIDITCCAgmgAwIBAgIUKjAUmxsHO44X+/TKBNciPgNl1GEwDQYJKoZIhvcNAQEL
BQAwIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1wbGUuY29tMB4XDTI0MTIxOTEz
Mzc1MVoXDTI1MTIxOTEzMzc1MVowIDEeMBwGA1UEAwwVbXlzZXJ2aWNlLmV4YW1w
bGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0QYuJsayILRI
hVT7G1DlitVSXnt1iw3gEXJZfe81Egz06fUbvXF6Yo1LJmwYpqe/rm+hf4FNUb8e
2O+LH2FieA9FkVe4P2gKOzw87A/KxvpV8stgNgl4LlqRCokbc1AzeE/NiLr5TcTD
RXm3DUcYxXxinprtDu2jftFysaOZmNAukvE/iL6qS3X6ggVEDDM7tY9n5FV2eJ4E
p0ImKfypi2aZYROxOK+v5x9ryFRMl4y07lMDvmtcV45uXYmfGNCgG9PNf91Kk/mh
JxEQbxycJwFoSi9XWljR8ahPdO11LXG7Dsj/RVbY8k2LdKNstl6Ae3aCpbe9u2Pj
vxYs1bVJuQIDAQABo1MwUTAdBgNVHQ4EFgQU+mRVN5HYJWgnpopReaLhf2cMcoYw
HwYDVR0jBBgwFoAU+mRVN5HYJWgnpopReaLhf2cMcoYwDwYDVR0TAQH/BAUwAwEB
/zANBgkqhkiG9w0BAQsFAAOCAQEABJpHVuc9tGhD04infRVlofvqXIUizTlOrjZX
vozW9pIhSWEHX8o+sJP8AMZLnrsdq+bm0HE0HvgYrw7Lb8pd4FpR46TkFHjeukoj
izqfgckjIBl2nwPGlynbKA0/U/rTCSxVt7XiAn+lgYUGIpOzNdk06/hRMitrMNB7
t2C97NseVC4b1ZgyFrozsefCfUmD8IJF0+XJ4Wzmsh0jRrI8koCtVmPYnKn6vw1b
cZprg/97CWHYrsavd406wOB60CMtYl83Q16ucOF1dretDFqJC5kY+aFLvuqfag2+
kIaoPV1MnGsxveQyyHdOsEatS5XOv/1OWcmnvePDPxcvb9jCcw==
-----END CERTIFICATE-----
`)
key := []byte(`-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDRBi4mxrIgtEiF
VPsbUOWK1VJee3WLDeARcll97zUSDPTp9Ru9cXpijUsmbBimp7+ub6F/gU1Rvx7Y
74sfYWJ4D0WRV7g/aAo7PDzsD8rG+lXyy2A2CXguWpEKiRtzUDN4T82IuvlNxMNF
ebcNRxjFfGKemu0O7aN+0XKxo5mY0C6S8T+IvqpLdfqCBUQMMzu1j2fkVXZ4ngSn
QiYp/KmLZplhE7E4r6/nH2vIVEyXjLTuUwO+a1xXjm5diZ8Y0KAb081/3UqT+aEn
ERBvHJwnAWhKL1daWNHxqE907XUtcbsOyP9FVtjyTYt0o2y2XoB7doKlt727Y+O/
FizVtUm5AgMBAAECggEACak+l5f6Onj+u5vrjc4JyAaXW6ra6loSM9g8Uu3sHukW
plwoA7Pzp0u20CAxrP1Gpqw984/hSCCcb0Q2ItWMWLaC/YZni5W2WFnOyo3pzlPa
hmH4UNMT+ReCSfF/oW8w69QLcNEMjhfEu0i2iWBygIlA4SoRwC2Db6yEX7nLMwUB
6AICid9hfeACNRz/nq5ytdcHdmcB7Ptgb9jLiXr6RZw26g5AsRPHU3LdcyZAOXjP
aUHriHuHQFKAVkoEUxslvCB6ePCTCpB0bSAuzQbeGoY8fmvmNSCvJ1vrH5hiSUYp
Axtl5iNgFl5o9obb0eBYlY9x3pMSz0twdbCwfR7HAQKBgQDtWhmFm0NaJALoY+tq
lIIC0EOMSrcRIlgeXr6+g8womuDOMi5m/Nr5Mqt4mPOdP4HytrQb+a/ZmEm17KHh
mQb1vwH8ffirCBHbPNC1vwSNoxDKv9E6OysWlKiOzxPFSVZr3dKl2EMX6qi17n0l
LBrGXXaNPgYiHSmwBA5CZvvouQKBgQDhclGJfZfuoubQkUuz8yOA2uxalh/iUmQ/
G8ac6/w7dmnL9pXehqCWh06SeC3ZvW7yrf7IIGx4sTJji2FzQ+8Ta6pPELMyBEXr
1VirIFrlNVMlMQEbZcbzdzEhchM1RUpZJtl3b4amvH21UcRB69d9klcDRisKoFRm
k0P9QLHpAQKBgQDh5J9nphZa4u0ViYtTW1XFIbs3+R/0IbCl7tww67TRbF3KQL4i
7EHna88ALumkXf3qJvKRsXgoaqS0jSqgUAjst8ZHLQkOldaQxneIkezedDSWEisp
9YgTrJYjnHefiyXB8VL63jE0wPOiewEF8Mzmv6sFz+L8cq7rQ2Di16qmmQKBgQDH
bvCwVxkrMpJK2O2GH8U9fOzu6bUE6eviY/jb4mp8U7EdjGJhuuieoM2iBoxQ/SID
rmYftYcfcWlo4+juJZ99p5W+YcCTs3IDQPUyVOnzr6uA0Avxp6RKxhsBQj+5tTUj
Dpn77P3JzB7MYqvhwPcdD3LH46+5s8FWCFpx02RPAQKBgARbngtggfifatcsMC7n
lSv/FVLH7LYQAHdoW/EH5Be7FeeP+eQvGXwh1dgl+u0VZO8FvI8RwFganpBRR2Nc
ZSBRIb0fSUlTvIsckSWjpEvUJUomJXyi4PIZAfNvd9/u1uLInQiCDtObwb6hnLTU
FHHEZ+dR4eMaJp6PhNm8hu2O
-----END PRIVATE KEY-----
`)
kp, err := tls.X509KeyPair(cert, key)
if err != nil {
panic(err)
}
kp.Leaf, err = x509.ParseCertificate(kp.Certificate[0])
if err != nil {
panic(err)
}
return kp
}()
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", samlsp.AttributeFromContext(r.Context(), "UserName"))
}
func main() {
apiURL := os.Getenv("API_URL")
pat := readPAT(os.Getenv("PAT_FILE"))
domain := os.Getenv("API_DOMAIN")
loginURL := os.Getenv("LOGIN_URL")
idpURL := os.Getenv("IDP_URL")
host := os.Getenv("HOST")
port := os.Getenv("PORT")
idpMetadataURL, err := url.Parse(idpURL)
if err != nil {
panic(err)
}
idpMetadata, err := samlsp.FetchMetadata(context.Background(), http.DefaultClient,
*idpMetadataURL)
if err != nil {
panic(err)
}
fmt.Printf("idpMetadata: %+v\n", idpMetadata)
rootURL, err := url.Parse(host + ":" + port)
if err != nil {
panic(err)
}
samlSP, err := samlsp.New(samlsp.Options{
URL: *rootURL,
Key: keyPair.PrivateKey.(*rsa.PrivateKey),
Certificate: keyPair.Leaf,
IDPMetadata: idpMetadata,
})
if err != nil {
panic(err)
}
server := &http.Server{
Addr: ":" + port,
}
app := http.HandlerFunc(hello)
http.Handle("/hello", samlSP.RequireAccount(app))
http.Handle("/saml/", samlSP)
go func() {
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP server error: %v", err)
}
log.Println("Stopped serving new connections.")
}()
metadata, err := xml.MarshalIndent(samlSP.ServiceProvider.Metadata(), "", " ")
if err != nil {
panic(err)
}
if err := createZitadelResources(apiURL, pat, domain, metadata, loginURL); err != nil {
panic(err)
}
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
}
func readPAT(path string) string {
f, err := os.Open(path)
if err != nil {
panic(err)
}
pat, err := io.ReadAll(f)
if err != nil {
panic(err)
}
return strings.Trim(string(pat), "\n")
}
func createZitadelResources(apiURL, pat, domain string, metadata []byte, loginURL string) error {
projectID, err := CreateProject(apiURL, pat, domain)
if err != nil {
return err
}
return CreateApp(apiURL, pat, domain, projectID, metadata, loginURL)
}
type project struct {
ID string `json:"id"`
}
type createProject struct {
Name string `json:"name"`
ProjectRoleAssertion bool `json:"projectRoleAssertion"`
ProjectRoleCheck bool `json:"projectRoleCheck"`
HasProjectCheck bool `json:"hasProjectCheck"`
PrivateLabelingSetting string `json:"privateLabelingSetting"`
}
func CreateProject(apiURL, pat, domain string) (string, error) {
createProject := &createProject{
Name: "SAML",
ProjectRoleAssertion: false,
ProjectRoleCheck: false,
HasProjectCheck: false,
PrivateLabelingSetting: "PRIVATE_LABELING_SETTING_UNSPECIFIED",
}
resp, err := doRequestWithHeaders(apiURL+"/management/v1/projects", pat, domain, createProject)
if err != nil {
return "", err
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
defer resp.Body.Close()
p := new(project)
if err := json.Unmarshal(data, p); err != nil {
return "", err
}
return p.ID, nil
}
type createApp struct {
Name string `json:"name"`
MetadataXml string `json:"metadataXml"`
LoginVersion version `json:"loginVersion"`
}
type version struct {
LoginV2 loginV2 `json:"loginV2"`
}
type loginV2 struct {
BaseUri string `json:"baseUri"`
}
func CreateApp(apiURL, pat, domain, projectID string, spMetadata []byte, loginURL string) error {
encoded := make([]byte, base64.URLEncoding.EncodedLen(len(spMetadata)))
base64.URLEncoding.Encode(encoded, spMetadata)
createApp := &createApp{
Name: "SAML",
MetadataXml: string(encoded),
LoginVersion: version{
LoginV2: loginV2{
BaseUri: loginURL,
},
},
}
_, err := doRequestWithHeaders(apiURL+"/management/v1/projects/"+projectID+"/apps/saml", pat, domain, createApp)
return err
}
func doRequestWithHeaders(apiURL, pat, domain string, body any) (*http.Response, error) {
data, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPost, apiURL, io.NopCloser(bytes.NewReader(data)))
if err != nil {
return nil, err
}
values := http.Header{}
values.Add("Authorization", "Bearer "+pat)
values.Add("x-forwarded-host", domain)
values.Add("Content-Type", "application/json")
req.Header = values
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}

View File

@@ -17,6 +17,40 @@ if [ -z "${PAT}" ]; then
PAT=$(cat ${PAT_FILE})
fi
#################################################################
# ServiceAccount as Login Client
#################################################################
SERVICEACCOUNT_RESPONSE=$(curl -s --request POST \
--url "${ZITADEL_API_INTERNAL_URL}/management/v1/users/machine" \
--header "Authorization: Bearer ${PAT}" \
--header "Host: ${ZITADEL_API_DOMAIN}" \
--header "Content-Type: application/json" \
-d "{\"userName\": \"login\", \"name\": \"Login v2\", \"description\": \"Serviceaccount for Login v2\", \"accessTokenType\": \"ACCESS_TOKEN_TYPE_BEARER\"}")
echo "Received ServiceAccount response: ${SERVICEACCOUNT_RESPONSE}"
SERVICEACCOUNT_ID=$(echo ${SERVICEACCOUNT_RESPONSE} | jq -r '. | .userId')
echo "Received ServiceAccount ID: ${SERVICEACCOUNT_ID}"
MEMBER_RESPONSE=$(curl -s --request POST \
--url "${ZITADEL_API_INTERNAL_URL}/admin/v1/members" \
--header "Authorization: Bearer ${PAT}" \
--header "Host: ${ZITADEL_API_DOMAIN}" \
--header "Content-Type: application/json" \
-d "{\"userId\": \"${SERVICEACCOUNT_ID}\", \"roles\": [\"IAM_LOGIN_CLIENT\"]}")
echo "Received Member response: ${MEMBER_RESPONSE}"
SA_PAT_RESPONSE=$(curl -s --request POST \
--url "${ZITADEL_API_INTERNAL_URL}/management/v1/users/${SERVICEACCOUNT_ID}/pats" \
--header "Authorization: Bearer ${PAT}" \
--header "Host: ${ZITADEL_API_DOMAIN}" \
--header "Content-Type: application/json" \
-d "{\"expirationDate\": \"2519-04-01T08:45:00.000000Z\"}")
echo "Received Member response: ${MEMBER_RESPONSE}"
SA_PAT=$(echo ${SA_PAT_RESPONSE} | jq -r '. | .token')
echo "Received ServiceAccount Token: ${SA_PAT}"
#################################################################
# Environment files
#################################################################
@@ -27,7 +61,8 @@ WRITE_TEST_ENVIRONMENT_FILE=${WRITE_TEST_ENVIRONMENT_FILE:-$(dirname "$0")/../ac
echo "Writing environment file to ${WRITE_TEST_ENVIRONMENT_FILE} when done."
echo "ZITADEL_API_URL=${ZITADEL_API_URL}
ZITADEL_SERVICE_USER_TOKEN=${PAT}
ZITADEL_SERVICE_USER_TOKEN=${SA_PAT}
ZITADEL_ADMIN_TOKEN=${PAT}
SINK_NOTIFICATION_URL=${SINK_NOTIFICATION_URL}
EMAIL_VERIFICATION=true
DEBUG=true"| tee "${WRITE_ENVIRONMENT_FILE}" "${WRITE_TEST_ENVIRONMENT_FILE}" > /dev/null

View File

@@ -1,3 +1,3 @@
module github.com/zitadel/typescript/acceptance/sink
go 1.22.6
go 1.24.0

View File

@@ -29,7 +29,7 @@ export async function passwordScreen(page: Page, password: string) {
export async function passwordScreenExpect(page: Page, password: string) {
await expect(page.getByTestId(passwordField)).toHaveValue(password);
await expect(page.getByTestId("error").locator("div")).toContainText("Could not verify password");
await expect(page.getByTestId("error").locator("div")).toContainText("Failed to authenticate.");
}
export async function changePasswordScreenExpect(

View File

@@ -0,0 +1,5 @@
import { Page } from "@playwright/test";
export async function selectNewAccount(page: Page) {
await page.getByRole("link", { name: "Add another account" }).click();
}

View File

@@ -53,7 +53,7 @@ async function deleteCall(url: string) {
try {
const response = await axios.delete(url, {
headers: {
Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`,
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
},
});
@@ -87,7 +87,7 @@ async function listCall(url: string, data: any): Promise<any> {
const response = await axios.post(url, data, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`,
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
},
});
@@ -123,7 +123,7 @@ async function pushCall(url: string, data: any) {
const response = await axios.post(url, data, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.ZITADEL_SERVICE_USER_TOKEN}`,
Authorization: `Bearer ${process.env.ZITADEL_ADMIN_TOKEN}`,
},
});

196
apps/login/locales/pl.json Normal file
View File

@@ -0,0 +1,196 @@
{
"common": {
"back": "Powrót"
},
"accounts": {
"title": "Konta",
"description": "Wybierz konto, którego chcesz użyć.",
"addAnother": "Dodaj kolejne konto",
"noResults": "Nie znaleziono kont"
},
"loginname": {
"title": "Witamy ponownie!",
"description": "Wprowadź dane logowania.",
"register": "Zarejestruj nowego użytkownika"
},
"password": {
"verify": {
"title": "Hasło",
"description": "Wprowadź swoje hasło.",
"resetPassword": "Zresetuj hasło",
"submit": "Kontynuuj"
},
"set": {
"title": "Ustaw hasło",
"description": "Ustaw hasło dla swojego konta",
"codeSent": "Kod został wysłany na twój adres e-mail.",
"noCodeReceived": "Nie otrzymałeś kodu?",
"resend": "Wyślij kod ponownie",
"submit": "Kontynuuj"
},
"change": {
"title": "Zmień hasło",
"description": "Ustaw nowe hasło dla swojego konta",
"submit": "Kontynuuj"
}
},
"idp": {
"title": "Zaloguj się za pomocą SSO",
"description": "Wybierz jednego z poniższych dostawców, aby się zalogować",
"signInWithApple": "Zaloguj się przez Apple",
"signInWithGoogle": "Zaloguj się przez Google",
"signInWithAzureAD": "Zaloguj się przez AzureAD",
"signInWithGithub": "Zaloguj się przez GitHub",
"signInWithGitlab": "Zaloguj się przez GitLab",
"loginSuccess": {
"title": "Logowanie udane",
"description": "Zostałeś pomyślnie zalogowany!"
},
"linkingSuccess": {
"title": "Konto powiązane",
"description": "Pomyślnie powiązałeś swoje konto!"
},
"registerSuccess": {
"title": "Rejestracja udana",
"description": "Pomyślnie się zarejestrowałeś!"
},
"loginError": {
"title": "Logowanie nieudane",
"description": "Wystąpił błąd podczas próby logowania."
},
"linkingError": {
"title": "Powiązanie konta nie powiodło się",
"description": "Wystąpił błąd podczas próby powiązania konta."
}
},
"mfa": {
"verify": {
"title": "Zweryfikuj swoją tożsamość",
"description": "Wybierz jeden z poniższych sposobów weryfikacji.",
"noResults": "Nie znaleziono dostępnych metod uwierzytelniania dwuskładnikowego."
},
"set": {
"title": "Skonfiguruj uwierzytelnianie dwuskładnikowe",
"description": "Wybierz jedną z poniższych metod drugiego czynnika.",
"skip": "Pomiń"
}
},
"otp": {
"verify": {
"title": "Zweryfikuj uwierzytelnianie dwuskładnikowe",
"totpDescription": "Wprowadź kod z aplikacji uwierzytelniającej.",
"smsDescription": "Wprowadź kod otrzymany SMS-em.",
"emailDescription": "Wprowadź kod otrzymany e-mailem.",
"noCodeReceived": "Nie otrzymałeś kodu?",
"resendCode": "Wyślij kod ponownie",
"submit": "Kontynuuj"
},
"set": {
"title": "Skonfiguruj uwierzytelnianie dwuskładnikowe",
"totpDescription": "Zeskanuj kod QR za pomocą aplikacji uwierzytelniającej.",
"smsDescription": "Wprowadź swój numer telefonu, aby otrzymać kod SMS-em.",
"emailDescription": "Wprowadź swój adres e-mail, aby otrzymać kod e-mailem.",
"totpRegisterDescription": "Zeskanuj kod QR lub otwórz adres URL ręcznie.",
"submit": "Kontynuuj"
}
},
"passkey": {
"verify": {
"title": "Uwierzytelnij się za pomocą klucza dostępu",
"description": "Twoje urządzenie poprosi o użycie odcisku palca, rozpoznawania twarzy lub blokady ekranu.",
"usePassword": "Użyj hasła",
"submit": "Kontynuuj"
},
"set": {
"title": "Skonfiguruj klucz dostępu",
"description": "Twoje urządzenie poprosi o użycie odcisku palca, rozpoznawania twarzy lub blokady ekranu.",
"info": {
"description": "Klucz dostępu to metoda uwierzytelniania na urządzeniu, wykorzystująca np. odcisk palca, Apple FaceID lub podobne rozwiązania.",
"link": "Uwierzytelnianie bez hasła"
},
"skip": "Pomiń",
"submit": "Kontynuuj"
}
},
"u2f": {
"verify": {
"title": "Zweryfikuj uwierzytelnianie dwuskładnikowe",
"description": "Zweryfikuj swoje konto za pomocą urządzenia."
},
"set": {
"title": "Skonfiguruj uwierzytelnianie dwuskładnikowe",
"description": "Skonfiguruj urządzenie jako dodatkowy czynnik uwierzytelniania.",
"submit": "Kontynuuj"
}
},
"register": {
"methods": {
"passkey": "Klucz dostępu",
"password": "Hasło"
},
"disabled": {
"title": "Rejestracja wyłączona",
"description": "Rejestracja jest wyłączona. Skontaktuj się z administratorem."
},
"missingdata": {
"title": "Brak danych",
"description": "Podaj e-mail, imię i nazwisko, aby się zarejestrować."
},
"title": "Rejestracja",
"description": "Utwórz konto ZITADEL.",
"selectMethod": "Wybierz metodę uwierzytelniania, której chcesz użyć",
"agreeTo": "Aby się zarejestrować, musisz zaakceptować warunki korzystania",
"termsOfService": "Regulamin",
"privacyPolicy": "Polityka prywatności",
"submit": "Kontynuuj",
"password": {
"title": "Ustaw hasło",
"description": "Ustaw hasło dla swojego konta",
"submit": "Kontynuuj"
}
},
"invite": {
"title": "Zaproś użytkownika",
"description": "Podaj adres e-mail oraz imię i nazwisko użytkownika, którego chcesz zaprosić.",
"info": "Użytkownik otrzyma e-mail z dalszymi instrukcjami.",
"notAllowed": "Twoje ustawienia nie pozwalają na zapraszanie użytkowników.",
"submit": "Kontynuuj",
"success": {
"title": "Użytkownik zaproszony",
"description": "E-mail został pomyślnie wysłany.",
"verified": "Użytkownik został zaproszony i już zweryfikował swój e-mail.",
"notVerifiedYet": "Użytkownik został zaproszony. Otrzyma e-mail z dalszymi instrukcjami.",
"submit": "Zaproś kolejnego użytkownika"
}
},
"signedin": {
"title": "Witaj {user}!",
"description": "Jesteś zalogowany.",
"continue": "Kontynuuj"
},
"verify": {
"userIdMissing": "Nie podano identyfikatora użytkownika!",
"success": "Użytkownik został pomyślnie zweryfikowany.",
"setupAuthenticator": "Skonfiguruj uwierzytelnianie",
"verify": {
"title": "Zweryfikuj użytkownika",
"description": "Wprowadź kod z wiadomości weryfikacyjnej.",
"noCodeReceived": "Nie otrzymałeś kodu?",
"resendCode": "Wyślij kod ponownie",
"submit": "Kontynuuj"
}
},
"authenticator": {
"title": "Wybierz metodę uwierzytelniania",
"description": "Wybierz metodę, której chcesz użyć do uwierzytelnienia.",
"noMethodsAvailable": "Brak dostępnych metod uwierzytelniania",
"allSetup": "Już skonfigurowałeś metodę uwierzytelniania!",
"linkWithIDP": "lub połącz z dostawcą tożsamości"
},
"error": {
"unknownContext": "Nie udało się pobrać kontekstu użytkownika. Upewnij się, że najpierw wprowadziłeś nazwę użytkownika lub podałeś login jako parametr wyszukiwania.",
"sessionExpired": "Twoja sesja wygasła. Zaloguj się ponownie.",
"failedLoading": "Nie udało się załadować danych. Spróbuj ponownie.",
"tryagain": "Spróbuj ponownie"
}
}

196
apps/login/locales/ru.json Normal file
View File

@@ -0,0 +1,196 @@
{
"common": {
"back": "Назад"
},
"accounts": {
"title": "Аккаунты",
"description": "Выберите аккаунт, который хотите использовать.",
"addAnother": "Добавить другой аккаунт",
"noResults": "Аккаунты не найдены"
},
"loginname": {
"title": "С возвращением!",
"description": "Введите свои данные для входа.",
"register": "Зарегистрировать нового пользователя"
},
"password": {
"verify": {
"title": "Пароль",
"description": "Введите ваш пароль.",
"resetPassword": "Сбросить пароль",
"submit": "Продолжить"
},
"set": {
"title": "Установить пароль",
"description": "Установите пароль для вашего аккаунта",
"codeSent": "Код отправлен на ваш адрес электронной почты.",
"noCodeReceived": "Не получили код?",
"resend": "Отправить код повторно",
"submit": "Продолжить"
},
"change": {
"title": "Изменить пароль",
"description": "Установите пароль для вашего аккаунта",
"submit": "Продолжить"
}
},
"idp": {
"title": "Войти через SSO",
"description": "Выберите одного из провайдеров для входа",
"signInWithApple": "Войти через Apple",
"signInWithGoogle": "Войти через Google",
"signInWithAzureAD": "Войти через AzureAD",
"signInWithGithub": "Войти через GitHub",
"signInWithGitlab": "Войти через GitLab",
"loginSuccess": {
"title": "Вход выполнен успешно",
"description": "Вы успешно вошли в систему!"
},
"linkingSuccess": {
"title": "Аккаунт привязан",
"description": "Аккаунт успешно привязан!"
},
"registerSuccess": {
"title": "Регистрация завершена",
"description": "Вы успешно зарегистрировались!"
},
"loginError": {
"title": "Ошибка входа",
"description": "Произошла ошибка при попытке входа."
},
"linkingError": {
"title": "Ошибка привязки аккаунта",
"description": "Произошла ошибка при попытке привязать аккаунт."
}
},
"mfa": {
"verify": {
"title": "Подтвердите вашу личность",
"description": "Выберите один из следующих факторов.",
"noResults": "Нет доступных методов двухфакторной аутентификации"
},
"set": {
"title": "Настройка двухфакторной аутентификации",
"description": "Выберите один из следующих методов.",
"skip": "Пропустить"
}
},
"otp": {
"verify": {
"title": "Подтверждение 2FA",
"totpDescription": "Введите код из приложения-аутентификатора.",
"smsDescription": "Введите код, полученный по SMS.",
"emailDescription": "Введите код, полученный по email.",
"noCodeReceived": "Не получили код?",
"resendCode": "Отправить код повторно",
"submit": "Продолжить"
},
"set": {
"title": "Настройка двухфакторной аутентификации",
"totpDescription": "Отсканируйте QR-код в приложении-аутентификаторе.",
"smsDescription": "Введите номер телефона для получения кода по SMS.",
"emailDescription": "Введите email для получения кода.",
"totpRegisterDescription": "Отсканируйте QR-код или перейдите по ссылке вручную.",
"submit": "Продолжить"
}
},
"passkey": {
"verify": {
"title": "Аутентификация с помощью пасскей",
"description": "Устройство запросит отпечаток пальца, лицо или экранный замок",
"usePassword": "Использовать пароль",
"submit": "Продолжить"
},
"set": {
"title": "Настройка пасскей",
"description": "Устройство запросит отпечаток пальца, лицо или экранный замок",
"info": {
"description": "Пасскей — метод аутентификации через устройство (отпечаток пальца, Apple FaceID и аналоги).",
"link": "Аутентификация без пароля"
},
"skip": "Пропустить",
"submit": "Продолжить"
}
},
"u2f": {
"verify": {
"title": "Подтверждение 2FA",
"description": "Подтвердите аккаунт с помощью устройства."
},
"set": {
"title": "Настройка двухфакторной аутентификации",
"description": "Настройте устройство как второй фактор.",
"submit": "Продолжить"
}
},
"register": {
"methods": {
"passkey": "Пасскей",
"password": "Пароль"
},
"disabled": {
"title": "Регистрация отключена",
"description": "Регистрация недоступна. Обратитесь к администратору."
},
"missingdata": {
"title": "Недостаточно данных",
"description": "Укажите email, имя и фамилию для регистрации."
},
"title": "Регистрация",
"description": "Создайте свой аккаунт ZITADEL.",
"selectMethod": "Выберите метод аутентификации",
"agreeTo": "Для регистрации необходимо принять условия:",
"termsOfService": "Условия использования",
"privacyPolicy": "Политика конфиденциальности",
"submit": "Продолжить",
"password": {
"title": "Установить пароль",
"description": "Установите пароль для вашего аккаунта",
"submit": "Продолжить"
}
},
"invite": {
"title": "Пригласить пользователя",
"description": "Укажите email и имя пользователя для приглашения.",
"info": "Пользователь получит email с инструкциями.",
"notAllowed": "Ваши настройки не позволяют приглашать пользователей.",
"submit": "Продолжить",
"success": {
"title": "Пользователь приглашён",
"description": "Письмо успешно отправлено.",
"verified": "Пользователь приглашён и уже подтвердил email.",
"notVerifiedYet": "Пользователь приглашён. Он получит email с инструкциями.",
"submit": "Пригласить другого пользователя"
}
},
"signedin": {
"title": "Добро пожаловать, {user}!",
"description": "Вы вошли в систему.",
"continue": "Продолжить"
},
"verify": {
"userIdMissing": "Не указан userId!",
"success": "Пользователь успешно подтверждён.",
"setupAuthenticator": "Настроить аутентификатор",
"verify": {
"title": "Подтверждение пользователя",
"description": "Введите код из письма подтверждения.",
"noCodeReceived": "Не получили код?",
"resendCode": "Отправить код повторно",
"submit": "Продолжить"
}
},
"authenticator": {
"title": "Выбор метода аутентификации",
"description": "Выберите предпочитаемый метод аутентификации",
"noMethodsAvailable": "Нет доступных методов аутентификации",
"allSetup": "Аутентификатор уже настроен!",
"linkWithIDP": "или привязать через Identity Provider"
},
"error": {
"unknownContext": "Не удалось получить контекст пользователя. Укажите имя пользователя или loginName в параметрах поиска.",
"sessionExpired": "Ваша сессия истекла. Войдите снова.",
"failedLoading": "Ошибка загрузки данных. Попробуйте ещё раз.",
"tryagain": "Попробовать снова"
}
}

View File

@@ -46,8 +46,9 @@
"copy-to-clipboard": "^3.3.3",
"deepmerge": "^4.3.1",
"jose": "^5.3.0",
"lucide-react": "0.469.0",
"moment": "^2.29.4",
"next": "15.2.0-canary.33",
"next": "15.3.1-canary.9",
"next-intl": "^3.25.1",
"next-themes": "^0.2.1",
"nice-grpc": "2.0.1",

View File

@@ -157,17 +157,20 @@ export default async function Page(props: {
></ChooseAuthenticatorToSetup>
)}
<div className="py-3 flex flex-col">
<p className="ztdl-p text-center">{t("linkWithIDP")}</p>
</div>
{loginSettings?.allowExternalIdp && identityProviders && (
<SignInWithIdp
identityProviders={identityProviders}
requestId={requestId}
organization={sessionWithData.factors?.user?.organizationId}
linkOnly={true} // tell the callback function to just link the IDP and not login, to get an error when user is already available
></SignInWithIdp>
<>
{identityProviders.length && (
<div className="py-3 flex flex-col">
<p className="ztdl-p text-center">{t("linkWithIDP")}</p>
</div>
)}
<SignInWithIdp
identityProviders={identityProviders}
requestId={requestId}
organization={sessionWithData.factors?.user?.organizationId}
linkOnly={true} // tell the callback function to just link the IDP and not login, to get an error when user is already available
></SignInWithIdp>
</>
)}
<div className="mt-8 flex w-full flex-row items-center">

View File

@@ -4,7 +4,7 @@ import { linkingFailed } from "@/components/idps/pages/linking-failed";
import { linkingSuccess } from "@/components/idps/pages/linking-success";
import { loginFailed } from "@/components/idps/pages/login-failed";
import { loginSuccess } from "@/components/idps/pages/login-success";
import { idpTypeToIdentityProviderType, PROVIDER_MAPPING } from "@/lib/idp";
import { idpTypeToIdentityProviderType } from "@/lib/idp";
import { getServiceUrlFromHeaders } from "@/lib/service";
import {
addHuman,
@@ -19,10 +19,7 @@ import {
import { create } from "@zitadel/client";
import { AutoLinkingOption } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
import { OrganizationSchema } from "@zitadel/proto/zitadel/object/v2/object_pb";
import {
AddHumanUserRequest,
AddHumanUserRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { AddHumanUserRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
@@ -58,6 +55,7 @@ export default async function Page(props: {
});
const { idpInformation, userId } = intent;
let { addHumanUser } = intent;
// sign in user. If user should be linked continue
if (userId && !link) {
@@ -124,7 +122,7 @@ export default async function Page(props: {
// search for potential user via username, then link
if (options?.isLinkingAllowed) {
let foundUser;
const email = PROVIDER_MAPPING[providerType](idpInformation).email?.email;
const email = addHumanUser?.email?.email;
if (options.autoLinking === AutoLinkingOption.EMAIL && email) {
foundUser = await listUsers({ serviceUrl, email }).then((response) => {
@@ -180,16 +178,14 @@ export default async function Page(props: {
if (options?.isCreationAllowed && options.isAutoCreation) {
let orgToRegisterOn: string | undefined = organization;
let userData: AddHumanUserRequest =
PROVIDER_MAPPING[providerType](idpInformation);
let newUser;
if (
!orgToRegisterOn &&
userData.username && // username or email?
ORG_SUFFIX_REGEX.test(userData.username)
addHumanUser?.username && // username or email?
ORG_SUFFIX_REGEX.test(addHumanUser.username)
) {
const matched = ORG_SUFFIX_REGEX.exec(userData.username);
const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username);
const suffix = matched?.[1] ?? "";
// this just returns orgs where the suffix is set as primary domain
@@ -209,21 +205,21 @@ export default async function Page(props: {
}
}
if (orgToRegisterOn) {
if (addHumanUser && orgToRegisterOn) {
const organizationSchema = create(OrganizationSchema, {
org: { case: "orgId", value: orgToRegisterOn },
});
userData = create(AddHumanUserRequestSchema, {
...userData,
const addHumanUserWithOrganization = create(AddHumanUserRequestSchema, {
...addHumanUser,
organization: organizationSchema,
});
}
const newUser = await addHuman({
serviceUrl,
request: userData,
});
newUser = await addHuman({
serviceUrl,
request: addHumanUserWithOrganization,
});
}
if (newUser) {
return (

View File

@@ -0,0 +1,43 @@
"use client";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
export default function SamlPost() {
const searchParams = useSearchParams();
const url = searchParams.get("url");
const relayState = searchParams.get("RelayState");
const samlResponse = searchParams.get("SAMLResponse");
useEffect(() => {
// Automatically submit the form after rendering
const form = document.getElementById("samlForm") as HTMLFormElement;
if (form) {
form.submit();
}
}, []);
if (!url || !relayState || !samlResponse) {
return (
<p className="text-center">Missing required parameters for SAML POST.</p>
);
}
return (
<html lang="en">
<head>
<meta charSet="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Redirecting...</title>
</head>
<body>
<form id="samlForm" action={url} method="POST">
<input type="hidden" name="RelayState" value={relayState} />
<input type="hidden" name="SAMLResponse" value={samlResponse} />
</form>
<p>Redirecting...</p>
</body>
</html>
);
}

View File

@@ -473,40 +473,16 @@ export async function GET(request: NextRequest) {
if (url && binding.case === "redirect") {
return NextResponse.redirect(url);
} else if (url && binding.case === "post") {
// Create form data after SAML standard
const formData = {
RelayState: binding.value.relayState,
SAMLResponse: binding.value.samlResponse,
};
const redirectUrl = constructUrl(request, "/saml-post");
// Convert form data to URL-encoded string
const formBody = Object.entries(formData)
.map(
([key, value]) =>
encodeURIComponent(key) + "=" + encodeURIComponent(value),
)
.join("&");
redirectUrl.searchParams.set("url", url);
redirectUrl.searchParams.set("RelayState", binding.value.relayState);
redirectUrl.searchParams.set(
"SAMLResponse",
binding.value.samlResponse,
);
// Make a POST request to the external URL with the form data
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formBody,
});
// Handle the response from the external URL
if (response.ok) {
return NextResponse.json({
message: "SAML request completed successfully",
});
} else {
return NextResponse.json(
{ error: "Failed to complete SAML request" },
{ status: response.status },
);
}
return NextResponse.redirect(redirectUrl.toString());
} else {
console.log(
"could not create response, redirect user to choose other account",

View File

@@ -1,7 +1,9 @@
"use client";
import { clsx } from "clsx";
import { Loader2Icon } from "lucide-react";
import { ButtonHTMLAttributes, DetailedHTMLProps, forwardRef } from "react";
import { useFormStatus } from "react-dom";
export type SignInWithIdentityProviderProps = DetailedHTMLProps<
ButtonHTMLAttributes<HTMLButtonElement>,
@@ -15,15 +17,25 @@ export const BaseButton = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
>(function BaseButton(props, ref) {
const formStatus = useFormStatus();
return (
<button
{...props}
type="button"
type="submit"
ref={ref}
disabled={formStatus.pending}
className={clsx(
"transition-all cursor-pointer flex flex-row items-center bg-background-light-400 text-text-light-500 dark:bg-background-dark-500 dark:text-text-dark-500 border border-divider-light hover:border-black dark:border-divider-dark hover:dark:border-white focus:border-primary-light-500 focus:dark:border-primary-dark-500 outline-none rounded-md px-4 text-sm",
"flex-1 transition-all cursor-pointer flex flex-row items-center bg-background-light-400 text-text-light-500 dark:bg-background-dark-500 dark:text-text-dark-500 border border-divider-light hover:border-black dark:border-divider-dark hover:dark:border-white focus:border-primary-light-500 focus:dark:border-primary-dark-500 outline-none rounded-md px-4 text-sm",
props.className,
)}
/>
>
<div className="flex-1 justify-between flex items-center gap-4">
<div className="flex-1 flex flex-row items-center">
{props.children}
</div>
{formStatus.pending && <Loader2Icon className="w-4 h-4 animate-spin" />}
</div>
</button>
);
});

View File

@@ -4,6 +4,39 @@ import { useTranslations } from "next-intl";
import { forwardRef } from "react";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
function GitHubLogo() {
return (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 1024 1024"
className="h-8 w-8 hidden dark:block"
>
<path
fill="#fafafa"
fillRule="evenodd"
d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28 43.52 0 87.04 5.76 128 17.28 97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0z"
clipRule="evenodd"
></path>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 1024 1024"
className="h-8 w-8 block dark:hidden"
>
<path
fill="#1B1F23"
fillRule="evenodd"
d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28 43.52 0 87.04 5.76 128 17.28 97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0z"
clipRule="evenodd"
></path>
</svg>
</>
);
}
export const SignInWithGithub = forwardRef<
HTMLButtonElement,
SignInWithIdentityProviderProps
@@ -14,32 +47,7 @@ export const SignInWithGithub = forwardRef<
return (
<BaseButton {...restProps} ref={ref}>
<div className="mx-2 my-2 flex items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 1024 1024"
className="h-8 w-8 hidden dark:block"
>
<path
fill="#fafafa"
fillRule="evenodd"
d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28 43.52 0 87.04 5.76 128 17.28 97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0z"
clipRule="evenodd"
></path>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 1024 1024"
className="h-8 w-8 block dark:hidden"
>
<path
fill="#1B1F23"
fillRule="evenodd"
d="M512 0C229.12 0 0 229.12 0 512c0 226.56 146.56 417.92 350.08 485.76 25.6 4.48 35.2-10.88 35.2-24.32 0-12.16-.64-52.48-.64-95.36-128.64 23.68-161.92-31.36-172.16-60.16-5.76-14.72-30.72-60.16-52.48-72.32-17.92-9.6-43.52-33.28-.64-33.92 40.32-.64 69.12 37.12 78.72 52.48 46.08 77.44 119.68 55.68 149.12 42.24 4.48-33.28 17.92-55.68 32.64-68.48-113.92-12.8-232.96-56.96-232.96-252.8 0-55.68 19.84-101.76 52.48-137.6-5.12-12.8-23.04-65.28 5.12-135.68 0 0 42.88-13.44 140.8 52.48 40.96-11.52 84.48-17.28 128-17.28 43.52 0 87.04 5.76 128 17.28 97.92-66.56 140.8-52.48 140.8-52.48 28.16 70.4 10.24 122.88 5.12 135.68 32.64 35.84 52.48 81.28 52.48 137.6 0 196.48-119.68 240-233.6 252.8 18.56 16 34.56 46.72 34.56 94.72 0 68.48-.64 123.52-.64 140.8 0 13.44 9.6 29.44 35.2 24.32C877.44 929.92 1024 737.92 1024 512 1024 229.12 794.88 0 512 0z"
clipRule="evenodd"
></path>
</svg>
<GitHubLogo />
</div>
{children ? (
children

View File

@@ -6,6 +6,7 @@ import { XCircleIcon } from "@heroicons/react/24/outline";
import { Timestamp, timestampDate } from "@zitadel/client";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import moment from "moment";
import { useLocale } from "next-intl";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { Avatar } from "./avatar";
@@ -37,6 +38,9 @@ export function SessionItem({
reload: () => void;
requestId?: string;
}) {
const currentLocale = useLocale();
moment.locale(currentLocale === "zh" ? "zh-cn" : currentLocale);
const [loading, setLoading] = useState<boolean>(false);
async function clearSession(id: string) {

View File

@@ -1,13 +1,12 @@
"use client";
import { idpTypeToSlug } from "@/lib/idp";
import { startIDPFlow } from "@/lib/server/idp";
import { redirectToIdp } from "@/lib/server/idp";
import {
IdentityProvider,
IdentityProviderType,
} from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { useRouter } from "next/navigation";
import { ReactNode, useCallback, useState } from "react";
import { ReactNode, useActionState } from "react";
import { Alert } from "./alert";
import { SignInWithIdentityProviderProps } from "./idps/base-button";
import { SignInWithApple } from "./idps/sign-in-with-apple";
@@ -31,45 +30,10 @@ export function SignInWithIdp({
organization,
linkOnly,
}: Readonly<SignInWithIDPProps>) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const [state, action, _isPending] = useActionState(redirectToIdp, {});
const startFlow = useCallback(
async (idpId: string, provider: string) => {
setLoading(true);
const params = new URLSearchParams();
if (linkOnly) params.set("link", "true");
if (requestId) params.set("requestId", requestId);
if (organization) params.set("organization", organization);
try {
const response = await startIDPFlow({
idpId,
successUrl: `/idp/${provider}/success?` + params.toString(),
failureUrl: `/idp/${provider}/failure?` + params.toString(),
});
if (response && "error" in response && response?.error) {
setError(response.error);
return;
}
if (response && "redirect" in response && response?.redirect) {
return router.push(response.redirect);
}
} catch {
setError("Could not start IDP flow");
} finally {
setLoading(false);
}
},
[requestId, organization, linkOnly, router],
);
const renderIDPButton = (idp: IdentityProvider) => {
const renderIDPButton = (idp: IdentityProvider, index: number) => {
const { id, name, type } = idp;
const onClick = () => startFlow(id, idpTypeToSlug(type));
const components: Partial<
Record<
@@ -88,20 +52,32 @@ export function SignInWithIdp({
),
[IdentityProviderType.GITLAB]: SignInWithGitlab,
[IdentityProviderType.GITLAB_SELF_HOSTED]: SignInWithGitlab,
[IdentityProviderType.SAML]: SignInWithGeneric,
};
const Component = components[type];
return Component ? (
<Component key={id} name={name} onClick={onClick} />
<form action={action} className="flex" key={`idp-${index}`}>
<input type="hidden" name="id" value={id} />
<input type="hidden" name="provider" value={idpTypeToSlug(type)} />
<input type="hidden" name="requestId" value={requestId} />
<input type="hidden" name="organization" value={organization} />
<input
type="hidden"
name="linkOnly"
value={linkOnly ? "true" : "false"}
/>
<Component key={id} name={name} />
</form>
) : null;
};
return (
<div className="flex flex-col w-full space-y-2 text-sm">
{identityProviders?.map(renderIDPButton)}
{error && (
{state?.error && (
<div className="py-4">
<Alert>{error}</Alert>
<Alert>{state?.error}</Alert>
</div>
)}
</div>

View File

@@ -20,10 +20,18 @@ export const LANGS: Lang[] = [
name: "Español",
code: "es",
},
{
name: "Polski",
code: "pl",
},
{
name: "简体中文",
code: "zh",
},
{
name: "Русский",
code: "ru",
},
];
export const LANGUAGE_COOKIE_NAME = "NEXT_LOCALE";

View File

@@ -1,11 +1,5 @@
import { create } from "@zitadel/client";
import { IDPType } from "@zitadel/proto/zitadel/idp/v2/idp_pb";
import { IdentityProviderType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { IDPInformation } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import {
AddHumanUserRequest,
AddHumanUserRequestSchema,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
// This maps the IdentityProviderType to a slug which is used in the /success and /failure routes
export function idpTypeToSlug(idpType: IdentityProviderType) {
@@ -74,189 +68,3 @@ export function idpTypeToIdentityProviderType(
throw new Error("Unknown identity provider type");
}
}
// this maps the IDPInformation to the AddHumanUserRequest which is used when creating a user or linking a user (email)
// TODO: extend this object from a other file which can be overwritten by customers like map = { ...PROVIDER_MAPPING, ...customerMap }
export type OIDC_USER = {
User: {
email: string;
name?: string;
given_name?: string;
family_name?: string;
};
};
const GITLAB_MAPPING = (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
name: string;
email: string;
email_verified: boolean;
};
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.email,
verification: { case: "isVerified", value: rawInfo.email_verified },
},
profile: {
displayName: rawInfo.name || idp.userName || "",
givenName: "",
familyName: "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
};
const OIDC_MAPPING = (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as OIDC_USER;
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.User?.email,
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.User?.name ?? "",
givenName: rawInfo.User?.given_name ?? "",
familyName: rawInfo.User?.family_name ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
};
const GITHUB_MAPPING = (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
email: string;
name: string;
};
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.email,
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.name ?? "",
givenName: rawInfo.name ?? "",
familyName: rawInfo.name ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
};
export const PROVIDER_MAPPING: {
[provider: number]: (rI: IDPInformation) => AddHumanUserRequest;
} = {
[IdentityProviderType.GOOGLE]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as OIDC_USER;
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.User?.email,
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.User?.name ?? "",
givenName: rawInfo.User?.given_name ?? "",
familyName: rawInfo.User?.family_name ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
},
[IdentityProviderType.GITLAB]: GITLAB_MAPPING,
[IdentityProviderType.GITLAB_SELF_HOSTED]: GITLAB_MAPPING,
[IdentityProviderType.OIDC]: OIDC_MAPPING,
// check
[IdentityProviderType.OAUTH]: OIDC_MAPPING,
[IdentityProviderType.AZURE_AD]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
jobTitle: string;
mail: string;
mobilePhone: string;
preferredLanguage: string;
id: string;
displayName?: string;
givenName?: string;
surname?: string;
officeLocation?: string;
userPrincipalName: string;
};
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.mail || rawInfo.userPrincipalName || "",
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.displayName ?? "",
givenName: rawInfo.givenName ?? "",
familyName: rawInfo.surname ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
},
[IdentityProviderType.GITHUB]: GITHUB_MAPPING,
[IdentityProviderType.GITHUB_ES]: GITHUB_MAPPING,
[IdentityProviderType.APPLE]: (idp: IDPInformation) => {
const rawInfo = idp.rawInformation as {
name?: string;
firstName?: string;
lastName?: string;
email?: string;
};
return create(AddHumanUserRequestSchema, {
username: idp.userName,
email: {
email: rawInfo.email ?? "",
verification: { case: "isVerified", value: true },
},
profile: {
displayName: rawInfo.name ?? "",
givenName: rawInfo.firstName ?? "",
familyName: rawInfo.lastName ?? "",
},
idpLinks: [
{
idpId: idp.idpId,
userId: idp.userId,
userName: idp.userName,
},
],
});
},
};

View File

@@ -6,11 +6,45 @@ import {
startIdentityProviderFlow,
} from "@/lib/zitadel";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { getNextUrl } from "../client";
import { getServiceUrlFromHeaders } from "../service";
import { checkEmailVerification } from "../verify-helper";
import { createSessionForIdpAndUpdateCookie } from "./cookie";
export type RedirectToIdpState = { error?: string | null } | undefined;
export async function redirectToIdp(
prevState: RedirectToIdpState,
formData: FormData,
): Promise<RedirectToIdpState> {
const params = new URLSearchParams();
const linkOnly = formData.get("linkOnly") === "true";
const requestId = formData.get("requestId") as string;
const organization = formData.get("organization") as string;
const idpId = formData.get("id") as string;
const provider = formData.get("provider") as string;
if (linkOnly) params.set("link", "true");
if (requestId) params.set("requestId", requestId);
if (organization) params.set("organization", organization);
const response = await startIDPFlow({
idpId,
successUrl: `/idp/${provider}/success?` + params.toString(),
failureUrl: `/idp/${provider}/failure?` + params.toString(),
});
if (response && "error" in response && response?.error) {
return { error: response.error };
}
if (response && "redirect" in response && response?.redirect) {
redirect(response.redirect);
}
}
export type StartIDPFlowCommand = {
idpId: string;
successUrl: string;

View File

@@ -915,27 +915,6 @@ export async function startIdentityProviderFlow({
});
}
export async function retrieveIdentityProviderInformation({
serviceUrl,
idpIntentId,
idpIntentToken,
}: {
serviceUrl: string;
idpIntentId: string;
idpIntentToken: string;
}) {
const userService: Client<typeof UserService> = await createServiceForHost(
UserService,
serviceUrl,
);
return userService.retrieveIdentityProviderIntent({
idpIntentId,
idpIntentToken,
});
}
export async function getAuthRequest({
serviceUrl,
authRequestId,

View File

@@ -22,7 +22,9 @@ export async function middleware(request: NextRequest) {
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
const instanceHost = `${serviceUrl}`.replace("https://", "");
const instanceHost = `${serviceUrl}`
.replace("https://", "")
.replace("http://", "");
const requestHeaders = new Headers(request.headers);

View File

@@ -26,6 +26,10 @@
"release": "turbo run build --filter=login^... && changeset publish",
"run-zitadel": "docker compose -f ./acceptance/docker-compose.yaml run setup",
"run-sink": "docker compose -f ./acceptance/docker-compose.yaml up -d sink",
"run-samlsp": "docker compose -f ./acceptance/saml/docker-compose.yaml up -d",
"run-samlidp": "docker compose -f ./acceptance/idp/saml/docker-compose.yaml up -d",
"run-oidcrp": "docker compose -f ./acceptance/oidc/docker-compose.yaml up -d",
"run-oidcop": "docker compose -f ./acceptance/idp/oidc/docker-compose.yaml up -d",
"stop": "docker compose -f ./acceptance/docker-compose.yaml stop"
},
"pnpm": {
@@ -43,7 +47,7 @@
"@types/node": "^20.17.17",
"@vitejs/plugin-react": "^4.3.3",
"@zitadel/prettier-config": "workspace:*",
"axios": "^1.7.7",
"axios": "^1.8.2",
"dotenv": "^16.4.5",
"eslint": "8.57.1",
"@zitadel/eslint-config": "workspace:*",

View File

@@ -14,7 +14,7 @@
],
"sideEffects": false,
"scripts": {
"generate": "buf generate https://github.com/zitadel/zitadel.git#tag=v2.71.1 --path ./proto/zitadel",
"generate": "buf generate https://github.com/zitadel/zitadel.git --path ./proto/zitadel",
"clean": "rm -rf zitadel .turbo node_modules google protoc-gen-openapiv2 validate"
},
"dependencies": {

470
pnpm-lock.yaml generated
View File

@@ -42,8 +42,8 @@ importers:
specifier: workspace:*
version: link:packages/zitadel-prettier-config
axios:
specifier: ^1.7.7
version: 1.7.7(debug@4.3.7)
specifier: ^1.8.2
version: 1.8.3(debug@4.3.7)
dotenv:
specifier: ^16.4.5
version: 16.4.5
@@ -85,7 +85,7 @@ importers:
version: 0.5.7(tailwindcss@3.4.14)
'@vercel/analytics':
specifier: ^1.2.2
version: 1.3.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0)
version: 1.3.1(next@15.3.1-canary.9(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0)
'@zitadel/client':
specifier: workspace:*
version: link:../../packages/zitadel-client
@@ -104,18 +104,21 @@ importers:
jose:
specifier: ^5.3.0
version: 5.8.0
lucide-react:
specifier: 0.469.0
version: 0.469.0(react@19.0.0)
moment:
specifier: ^2.29.4
version: 2.30.1
next:
specifier: 15.2.0-canary.33
version: 15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7)
specifier: 15.3.1-canary.9
version: 15.3.1-canary.9(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7)
next-intl:
specifier: ^3.25.1
version: 3.25.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0)
version: 3.25.1(next@15.3.1-canary.9(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0)
next-themes:
specifier: ^0.2.1
version: 0.2.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
version: 0.2.1(next@15.3.1-canary.9(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
nice-grpc:
specifier: 2.0.1
version: 2.0.1
@@ -405,6 +408,10 @@ packages:
resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==}
engines: {node: '>=6.9.0'}
'@babel/runtime@7.26.10':
resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==}
engines: {node: '>=6.9.0'}
'@babel/template@7.25.9':
resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==}
engines: {node: '>=6.9.0'}
@@ -600,8 +607,8 @@ packages:
'@cypress/xvfb@1.2.4':
resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==}
'@emnapi/runtime@1.3.1':
resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==}
'@emnapi/runtime@1.4.3':
resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
'@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
@@ -916,6 +923,7 @@ packages:
'@faker-js/faker@9.2.0':
resolution: {integrity: sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg==}
engines: {node: '>=18.0.0', npm: '>=9.0.0'}
deprecated: Please update to a newer version
'@floating-ui/core@1.6.8':
resolution: {integrity: sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==}
@@ -996,107 +1004,112 @@ packages:
resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
deprecated: Use @eslint/object-schema instead
'@img/sharp-darwin-arm64@0.33.5':
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
'@img/sharp-darwin-arm64@0.34.1':
resolution: {integrity: sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.33.5':
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
'@img/sharp-darwin-x64@0.34.1':
resolution: {integrity: sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.0.4':
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
'@img/sharp-libvips-darwin-arm64@1.1.0':
resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.0.4':
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
'@img/sharp-libvips-darwin-x64@1.1.0':
resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.0.4':
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
'@img/sharp-libvips-linux-arm64@1.1.0':
resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-arm@1.0.5':
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
'@img/sharp-libvips-linux-arm@1.1.0':
resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==}
cpu: [arm]
os: [linux]
'@img/sharp-libvips-linux-s390x@1.0.4':
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
'@img/sharp-libvips-linux-ppc64@1.1.0':
resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==}
cpu: [ppc64]
os: [linux]
'@img/sharp-libvips-linux-s390x@1.1.0':
resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
cpu: [s390x]
os: [linux]
'@img/sharp-libvips-linux-x64@1.0.4':
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
'@img/sharp-libvips-linux-x64@1.1.0':
resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@0.33.5':
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
'@img/sharp-linux-arm64@0.34.1':
resolution: {integrity: sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-arm@0.33.5':
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
'@img/sharp-linux-arm@0.34.1':
resolution: {integrity: sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
'@img/sharp-linux-s390x@0.33.5':
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
'@img/sharp-linux-s390x@0.34.1':
resolution: {integrity: sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
'@img/sharp-linux-x64@0.33.5':
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
'@img/sharp-linux-x64@0.34.1':
resolution: {integrity: sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.33.5':
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
'@img/sharp-linuxmusl-arm64@0.34.1':
resolution: {integrity: sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.33.5':
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
'@img/sharp-linuxmusl-x64@0.34.1':
resolution: {integrity: sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-wasm32@0.33.5':
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
'@img/sharp-wasm32@0.34.1':
resolution: {integrity: sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [wasm32]
'@img/sharp-win32-ia32@0.33.5':
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
'@img/sharp-win32-ia32@0.34.1':
resolution: {integrity: sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ia32]
os: [win32]
'@img/sharp-win32-x64@0.33.5':
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
'@img/sharp-win32-x64@0.34.1':
resolution: {integrity: sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
@@ -1136,56 +1149,56 @@ packages:
resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==}
hasBin: true
'@next/env@15.2.0-canary.33':
resolution: {integrity: sha512-y3EPM+JYKU8t2K+i6bc0QrotEZVGpqu9eVjprj4cfS8QZyZcL54s+W9aGB0TBuGavU9tQdZ50W186+toeMV+hw==}
'@next/env@15.3.1-canary.9':
resolution: {integrity: sha512-ahnXk9D1SECEeq6KhZBAkhuijmvONJqXoAmcmyVcVekq+u5I7LAQe8A7AFTSU0d5jItwJ+bfnpA6ZDGaXVG2CQ==}
'@next/eslint-plugin-next@14.2.18':
resolution: {integrity: sha512-KyYTbZ3GQwWOjX3Vi1YcQbekyGP0gdammb7pbmmi25HBUCINzDReyrzCMOJIeZisK1Q3U6DT5Rlc4nm2/pQeXA==}
'@next/swc-darwin-arm64@15.2.0-canary.33':
resolution: {integrity: sha512-+fCdK2KmR6lWoCTk1fSd5pvbiLZHfZF+D/Xdz3xrXw+pbnBtXWLKQrPT0bCtDseMxD31qcOywq5mAApvI3EGpA==}
'@next/swc-darwin-arm64@15.3.1-canary.9':
resolution: {integrity: sha512-/44Wi2KxNg1pz/Q+jNs+m0ze0Lzp7Ian1lFO22B2UcAdgPaThBp/ItBro5G39Oge6n0O0xpp2+HS3YbydJ5lOg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@15.2.0-canary.33':
resolution: {integrity: sha512-GrrU+tSmeBRow+7bnn7i5M96g3tc28hPH5t5Y65qUXGmmrZwGZN1e1d+8QbXPdAGkvjEPcOkUNQuQVpp1qpYPA==}
'@next/swc-darwin-x64@15.3.1-canary.9':
resolution: {integrity: sha512-jl7BJS/lysYlUa7rYPYKM6udKuXEUoI3e31g+8wMFbLmevivnnowzPc7yCJ3MAQmW0J6jv9U9obo+a2n5wafsQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@15.2.0-canary.33':
resolution: {integrity: sha512-8RnGxnUpASHoUf6aHUifmZom5b4Ow5nTdCib/CNYXZ6VLuL5ocvmr+DXs/SKzi9h8OHR7JkLwKXHCcF8WyscSg==}
'@next/swc-linux-arm64-gnu@15.3.1-canary.9':
resolution: {integrity: sha512-7Lj6VFzrkO82ojfTBlxIyCMKylgpCpCflekU5sJgD0ooRGcWlWSEVCiUcOSvMPeHW7TiJqTTB2NVzuHhY+yLhA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@15.2.0-canary.33':
resolution: {integrity: sha512-COyE0LzMuLBZSR+Z/TOGilyJPdwSU588Vt0+o8GoECkoDEnjyuO2s2nHa2kDAcEfUEPkhlo0tErU3mF+8AVOTQ==}
'@next/swc-linux-arm64-musl@15.3.1-canary.9':
resolution: {integrity: sha512-iqZSNVVD+PDASb64m/WD0nCRNXXd3eLIwC/U1YjfYNWwW2I0OJL1bLX0kTp57wQadYMqyv52a96yEaeen+47AQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@15.2.0-canary.33':
resolution: {integrity: sha512-3Y9lqJs+ftU9jgbLdCtvAvF8MNJsJYGMH7icb8QMs1+yOyHHbmwkZoElKdjwfUWzQ2sX28ywp73GWq4HbrsoUg==}
'@next/swc-linux-x64-gnu@15.3.1-canary.9':
resolution: {integrity: sha512-CLlCDftzkvzx+kqrp6J/lY+K7x9cNFlAwpCUJZxrzX+m5TDwgMLD045z4+Jwj+3gLF3Q+6ubPSZly0q5EwpfRQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@15.2.0-canary.33':
resolution: {integrity: sha512-FS9iA+RkZlhdWGQEKtsplVBXIYZJUn5nsRB+1UY46b3uaL6dDypu13ODaSwYuAwXGgkrZBVF9AFO3y4biBnPlA==}
'@next/swc-linux-x64-musl@15.3.1-canary.9':
resolution: {integrity: sha512-9CL3IMmfabYNHjr3Z9hw0jKVe/HBd/WhOmWvF4xtbtVHZuQcz74O2O77BPNHe8NEuhG11WiFhJsnbkSbkFJ+sQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@15.2.0-canary.33':
resolution: {integrity: sha512-Ji9CtBbUx06qvvN/rPohJN2FEFGsUv26F50f2nMRYRwrq3POXDjloGOiRocrjU0ty/cUzCz71qTUfKdmv/ajmg==}
'@next/swc-win32-arm64-msvc@15.3.1-canary.9':
resolution: {integrity: sha512-sITL9Fru9NB00sXC/+gDKpxojAPlVYIwzI1bKzugSgLwnDyydbSnqMgmea1NOr/7nFbeBnWDXpYHgq8T4VX2Bg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@15.2.0-canary.33':
resolution: {integrity: sha512-hjdbGnkwIZ8zN2vlS6lNsEJO37HRtcEGimzfkruBMsi/DwJBqkJvZbNC/XCJy3HFcU58igncqV52p1IPjmAJAw==}
'@next/swc-win32-x64-msvc@15.3.1-canary.9':
resolution: {integrity: sha512-ickRm1FKSMHaELoN//K1CMEMSTJpclg4KgaAqmv0fDnOvBLsCw/reAoZrT9N+Bt2ToDP4jbwEEhSOmH8uQ1lQg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -1479,9 +1492,6 @@ packages:
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
'@swc/helpers@0.5.13':
resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==}
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
@@ -1899,8 +1909,8 @@ packages:
resolution: {integrity: sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==}
engines: {node: '>=4'}
axios@1.7.7:
resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
axios@1.8.3:
resolution: {integrity: sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==}
axobject-query@3.1.1:
resolution: {integrity: sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg==}
@@ -1972,6 +1982,10 @@ packages:
resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==}
engines: {node: '>=6'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
call-bind@1.0.7:
resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
engines: {node: '>= 0.4'}
@@ -2316,6 +2330,10 @@ packages:
dprint-node@1.0.8:
resolution: {integrity: sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
@@ -2372,6 +2390,10 @@ packages:
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
engines: {node: '>= 0.4'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
@@ -2387,10 +2409,18 @@ packages:
resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.0.3:
resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==}
engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
es-shim-unscopables@1.0.2:
resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==}
@@ -2673,8 +2703,8 @@ packages:
flatted@3.3.1:
resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==}
follow-redirects@1.15.6:
resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
@@ -2696,6 +2726,10 @@ packages:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
form-data@4.0.2:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'}
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
@@ -2762,6 +2796,14 @@ packages:
resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
engines: {node: '>= 0.4'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
get-stream@5.2.0:
resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==}
engines: {node: '>=8'}
@@ -2838,6 +2880,10 @@ packages:
gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
@@ -2870,6 +2916,10 @@ packages:
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
engines: {node: '>= 0.4'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
@@ -3339,6 +3389,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react@0.469.0:
resolution: {integrity: sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
@@ -3362,6 +3417,10 @@ packages:
map-stream@0.1.0:
resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
meow@13.2.0:
resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==}
engines: {node: '>=18'}
@@ -3474,8 +3533,8 @@ packages:
react: '*'
react-dom: '*'
next@15.2.0-canary.33:
resolution: {integrity: sha512-WF8QLeYkakuYwksdWY/F+Bi8tNJfIbiSYk9hCmldn9sNp1lU3lqI1hrW1ynbcMSaXC+qQEr7yol2OdvVZ4nZYQ==}
next@15.3.1-canary.9:
resolution: {integrity: sha512-aCGJOPF7+e3uElbeOLc5BMhlhQ0Q0J9EAOZv47lkm/VsRgjVWh98yYy4KZg7H11YONQX7zEUa2nuD4IVbG/1KQ==}
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
hasBin: true
peerDependencies:
@@ -4110,6 +4169,11 @@ packages:
engines: {node: '>=10'}
hasBin: true
semver@7.7.1:
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
engines: {node: '>=10'}
hasBin: true
server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
@@ -4124,8 +4188,8 @@ packages:
resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
engines: {node: '>= 0.4'}
sharp@0.33.5:
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
sharp@0.34.1:
resolution: {integrity: sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
shebang-command@1.2.0:
@@ -4980,6 +5044,10 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
'@babel/runtime@7.26.10':
dependencies:
regenerator-runtime: 0.14.1
'@babel/template@7.25.9':
dependencies:
'@babel/code-frame': 7.26.2
@@ -5258,7 +5326,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@emnapi/runtime@1.3.1':
'@emnapi/runtime@1.4.3':
dependencies:
tslib: 2.8.1
optional: true
@@ -5533,79 +5601,82 @@ snapshots:
'@humanwhocodes/object-schema@2.0.3': {}
'@img/sharp-darwin-arm64@0.33.5':
'@img/sharp-darwin-arm64@0.34.1':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.0.4
'@img/sharp-libvips-darwin-arm64': 1.1.0
optional: true
'@img/sharp-darwin-x64@0.33.5':
'@img/sharp-darwin-x64@0.34.1':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.0.4
'@img/sharp-libvips-darwin-x64': 1.1.0
optional: true
'@img/sharp-libvips-darwin-arm64@1.0.4':
'@img/sharp-libvips-darwin-arm64@1.1.0':
optional: true
'@img/sharp-libvips-darwin-x64@1.0.4':
'@img/sharp-libvips-darwin-x64@1.1.0':
optional: true
'@img/sharp-libvips-linux-arm64@1.0.4':
'@img/sharp-libvips-linux-arm64@1.1.0':
optional: true
'@img/sharp-libvips-linux-arm@1.0.5':
'@img/sharp-libvips-linux-arm@1.1.0':
optional: true
'@img/sharp-libvips-linux-s390x@1.0.4':
'@img/sharp-libvips-linux-ppc64@1.1.0':
optional: true
'@img/sharp-libvips-linux-x64@1.0.4':
'@img/sharp-libvips-linux-s390x@1.1.0':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
'@img/sharp-libvips-linux-x64@1.1.0':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
optional: true
'@img/sharp-linux-arm64@0.33.5':
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
optional: true
'@img/sharp-linux-arm64@0.34.1':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.0.4
'@img/sharp-libvips-linux-arm64': 1.1.0
optional: true
'@img/sharp-linux-arm@0.33.5':
'@img/sharp-linux-arm@0.34.1':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.0.5
'@img/sharp-libvips-linux-arm': 1.1.0
optional: true
'@img/sharp-linux-s390x@0.33.5':
'@img/sharp-linux-s390x@0.34.1':
optionalDependencies:
'@img/sharp-libvips-linux-s390x': 1.0.4
'@img/sharp-libvips-linux-s390x': 1.1.0
optional: true
'@img/sharp-linux-x64@0.33.5':
'@img/sharp-linux-x64@0.34.1':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.0.4
'@img/sharp-libvips-linux-x64': 1.1.0
optional: true
'@img/sharp-linuxmusl-arm64@0.33.5':
'@img/sharp-linuxmusl-arm64@0.34.1':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
'@img/sharp-libvips-linuxmusl-arm64': 1.1.0
optional: true
'@img/sharp-linuxmusl-x64@0.33.5':
'@img/sharp-linuxmusl-x64@0.34.1':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
'@img/sharp-libvips-linuxmusl-x64': 1.1.0
optional: true
'@img/sharp-wasm32@0.33.5':
'@img/sharp-wasm32@0.34.1':
dependencies:
'@emnapi/runtime': 1.3.1
'@emnapi/runtime': 1.4.3
optional: true
'@img/sharp-win32-ia32@0.33.5':
'@img/sharp-win32-ia32@0.34.1':
optional: true
'@img/sharp-win32-x64@0.33.5':
'@img/sharp-win32-x64@0.34.1':
optional: true
'@isaacs/cliui@8.0.2':
@@ -5667,34 +5738,34 @@ snapshots:
- encoding
- supports-color
'@next/env@15.2.0-canary.33': {}
'@next/env@15.3.1-canary.9': {}
'@next/eslint-plugin-next@14.2.18':
dependencies:
glob: 10.3.10
'@next/swc-darwin-arm64@15.2.0-canary.33':
'@next/swc-darwin-arm64@15.3.1-canary.9':
optional: true
'@next/swc-darwin-x64@15.2.0-canary.33':
'@next/swc-darwin-x64@15.3.1-canary.9':
optional: true
'@next/swc-linux-arm64-gnu@15.2.0-canary.33':
'@next/swc-linux-arm64-gnu@15.3.1-canary.9':
optional: true
'@next/swc-linux-arm64-musl@15.2.0-canary.33':
'@next/swc-linux-arm64-musl@15.3.1-canary.9':
optional: true
'@next/swc-linux-x64-gnu@15.2.0-canary.33':
'@next/swc-linux-x64-gnu@15.3.1-canary.9':
optional: true
'@next/swc-linux-x64-musl@15.2.0-canary.33':
'@next/swc-linux-x64-musl@15.3.1-canary.9':
optional: true
'@next/swc-win32-arm64-msvc@15.2.0-canary.33':
'@next/swc-win32-arm64-msvc@15.3.1-canary.9':
optional: true
'@next/swc-win32-x64-msvc@15.2.0-canary.33':
'@next/swc-win32-x64-msvc@15.3.1-canary.9':
optional: true
'@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1':
@@ -5836,7 +5907,7 @@ snapshots:
'@react-aria/ssr@3.9.6(react@19.0.0)':
dependencies:
'@swc/helpers': 0.5.5
'@swc/helpers': 0.5.15
react: 19.0.0
'@react-aria/utils@3.25.3(react@19.0.0)':
@@ -5844,13 +5915,13 @@ snapshots:
'@react-aria/ssr': 3.9.6(react@19.0.0)
'@react-stately/utils': 3.10.4(react@19.0.0)
'@react-types/shared': 3.25.0(react@19.0.0)
'@swc/helpers': 0.5.5
'@swc/helpers': 0.5.15
clsx: 2.1.1
react: 19.0.0
'@react-stately/utils@3.10.4(react@19.0.0)':
dependencies:
'@swc/helpers': 0.5.13
'@swc/helpers': 0.5.15
react: 19.0.0
'@react-types/shared@3.25.0(react@19.0.0)':
@@ -5925,10 +5996,6 @@ snapshots:
'@swc/counter@0.1.3': {}
'@swc/helpers@0.5.13':
dependencies:
tslib: 2.8.1
'@swc/helpers@0.5.15':
dependencies:
tslib: 2.8.1
@@ -5959,7 +6026,7 @@ snapshots:
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.26.2
'@babel/runtime': 7.26.0
'@babel/runtime': 7.26.10
'@types/aria-query': 5.0.4
aria-query: 5.3.0
chalk: 4.1.2
@@ -6154,11 +6221,11 @@ snapshots:
'@ungap/structured-clone@1.2.0': {}
'@vercel/analytics@1.3.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0)':
'@vercel/analytics@1.3.1(next@15.3.1-canary.9(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0)':
dependencies:
server-only: 0.0.1
optionalDependencies:
next: 15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7)
next: 15.3.1-canary.9(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7)
react: 19.0.0
'@vercel/git-hooks@1.0.0': {}
@@ -6407,10 +6474,10 @@ snapshots:
axe-core@4.10.0: {}
axios@1.7.7(debug@4.3.7):
axios@1.8.3(debug@4.3.7):
dependencies:
follow-redirects: 1.15.6(debug@4.3.7)
form-data: 4.0.0
follow-redirects: 1.15.9(debug@4.3.7)
form-data: 4.0.2
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
@@ -6484,12 +6551,17 @@ snapshots:
cachedir@2.4.0: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
call-bind@1.0.7:
dependencies:
es-define-property: 1.0.0
es-errors: 1.3.0
function-bind: 1.1.2
get-intrinsic: 1.2.4
get-intrinsic: 1.3.0
set-function-length: 1.2.2
callsites@3.1.0: {}
@@ -6866,6 +6938,12 @@ snapshots:
dependencies:
detect-libc: 1.0.3
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
duplexer@0.1.2: {}
eastasianwidth@0.2.0: {}
@@ -6961,13 +7039,15 @@ snapshots:
dependencies:
get-intrinsic: 1.2.4
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-get-iterator@1.1.3:
dependencies:
call-bind: 1.0.7
get-intrinsic: 1.2.4
has-symbols: 1.0.3
get-intrinsic: 1.3.0
has-symbols: 1.1.0
is-arguments: 1.1.1
is-map: 2.0.3
is-set: 2.0.3
@@ -6996,12 +7076,23 @@ snapshots:
dependencies:
es-errors: 1.3.0
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
es-set-tostringtag@2.0.3:
dependencies:
get-intrinsic: 1.2.4
has-tostringtag: 1.0.2
hasown: 2.0.2
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
es-shim-unscopables@1.0.2:
dependencies:
hasown: 2.0.2
@@ -7437,7 +7528,7 @@ snapshots:
flatted@3.3.1: {}
follow-redirects@1.15.6(debug@4.3.7):
follow-redirects@1.15.9(debug@4.3.7):
optionalDependencies:
debug: 4.3.7(supports-color@5.5.0)
@@ -7458,6 +7549,13 @@ snapshots:
combined-stream: 1.0.8
mime-types: 2.1.35
form-data@4.0.2:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
mime-types: 2.1.35
fraction.js@4.3.7: {}
from@0.1.7: {}
@@ -7530,6 +7628,24 @@ snapshots:
has-symbols: 1.0.3
hasown: 2.0.2
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
get-stream@5.2.0:
dependencies:
pump: 3.0.0
@@ -7629,6 +7745,8 @@ snapshots:
dependencies:
get-intrinsic: 1.2.4
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
graphemer@1.4.0: {}
@@ -7654,9 +7772,11 @@ snapshots:
has-symbols@1.0.3: {}
has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.0.3
has-symbols: 1.1.0
has-unicode@2.0.1: {}
@@ -7887,7 +8007,7 @@ snapshots:
is-weakset@2.0.3:
dependencies:
call-bind: 1.0.7
get-intrinsic: 1.2.4
get-intrinsic: 1.3.0
is-windows@1.0.2: {}
@@ -8135,6 +8255,10 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-react@0.469.0(react@19.0.0):
dependencies:
react: 19.0.0
lz-string@1.5.0: {}
magic-string@0.30.12:
@@ -8154,6 +8278,8 @@ snapshots:
map-stream@0.1.0: {}
math-intrinsics@1.1.0: {}
meow@13.2.0: {}
merge-stream@2.0.0: {}
@@ -8224,23 +8350,23 @@ snapshots:
negotiator@1.0.0: {}
next-intl@3.25.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0):
next-intl@3.25.1(next@15.3.1-canary.9(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react@19.0.0):
dependencies:
'@formatjs/intl-localematcher': 0.5.4
negotiator: 1.0.0
next: 15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7)
next: 15.3.1-canary.9(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7)
react: 19.0.0
use-intl: 3.25.1(react@19.0.0)
next-themes@0.2.1(next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
next-themes@0.2.1(next@15.3.1-canary.9(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7))(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
next: 15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7)
next: 15.3.1-canary.9(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7)
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
next@15.2.0-canary.33(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7):
next@15.3.1-canary.9(@babel/core@7.26.0)(@playwright/test@1.48.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.80.7):
dependencies:
'@next/env': 15.2.0-canary.33
'@next/env': 15.3.1-canary.9
'@swc/counter': 0.1.3
'@swc/helpers': 0.5.15
busboy: 1.6.0
@@ -8250,17 +8376,17 @@ snapshots:
react-dom: 19.0.0(react@19.0.0)
styled-jsx: 5.1.6(@babel/core@7.26.0)(react@19.0.0)
optionalDependencies:
'@next/swc-darwin-arm64': 15.2.0-canary.33
'@next/swc-darwin-x64': 15.2.0-canary.33
'@next/swc-linux-arm64-gnu': 15.2.0-canary.33
'@next/swc-linux-arm64-musl': 15.2.0-canary.33
'@next/swc-linux-x64-gnu': 15.2.0-canary.33
'@next/swc-linux-x64-musl': 15.2.0-canary.33
'@next/swc-win32-arm64-msvc': 15.2.0-canary.33
'@next/swc-win32-x64-msvc': 15.2.0-canary.33
'@next/swc-darwin-arm64': 15.3.1-canary.9
'@next/swc-darwin-x64': 15.3.1-canary.9
'@next/swc-linux-arm64-gnu': 15.3.1-canary.9
'@next/swc-linux-arm64-musl': 15.3.1-canary.9
'@next/swc-linux-x64-gnu': 15.3.1-canary.9
'@next/swc-linux-x64-musl': 15.3.1-canary.9
'@next/swc-win32-arm64-msvc': 15.3.1-canary.9
'@next/swc-win32-x64-msvc': 15.3.1-canary.9
'@playwright/test': 1.48.2
sass: 1.80.7
sharp: 0.33.5
sharp: 0.34.1
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -8800,6 +8926,9 @@ snapshots:
semver@7.6.3: {}
semver@7.7.1:
optional: true
server-only@0.0.1: {}
set-blocking@2.0.0: {}
@@ -8820,31 +8949,32 @@ snapshots:
functions-have-names: 1.2.3
has-property-descriptors: 1.0.2
sharp@0.33.5:
sharp@0.34.1:
dependencies:
color: 4.2.3
detect-libc: 2.0.3
semver: 7.6.3
semver: 7.7.1
optionalDependencies:
'@img/sharp-darwin-arm64': 0.33.5
'@img/sharp-darwin-x64': 0.33.5
'@img/sharp-libvips-darwin-arm64': 1.0.4
'@img/sharp-libvips-darwin-x64': 1.0.4
'@img/sharp-libvips-linux-arm': 1.0.5
'@img/sharp-libvips-linux-arm64': 1.0.4
'@img/sharp-libvips-linux-s390x': 1.0.4
'@img/sharp-libvips-linux-x64': 1.0.4
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
'@img/sharp-linux-arm': 0.33.5
'@img/sharp-linux-arm64': 0.33.5
'@img/sharp-linux-s390x': 0.33.5
'@img/sharp-linux-x64': 0.33.5
'@img/sharp-linuxmusl-arm64': 0.33.5
'@img/sharp-linuxmusl-x64': 0.33.5
'@img/sharp-wasm32': 0.33.5
'@img/sharp-win32-ia32': 0.33.5
'@img/sharp-win32-x64': 0.33.5
'@img/sharp-darwin-arm64': 0.34.1
'@img/sharp-darwin-x64': 0.34.1
'@img/sharp-libvips-darwin-arm64': 1.1.0
'@img/sharp-libvips-darwin-x64': 1.1.0
'@img/sharp-libvips-linux-arm': 1.1.0
'@img/sharp-libvips-linux-arm64': 1.1.0
'@img/sharp-libvips-linux-ppc64': 1.1.0
'@img/sharp-libvips-linux-s390x': 1.1.0
'@img/sharp-libvips-linux-x64': 1.1.0
'@img/sharp-libvips-linuxmusl-arm64': 1.1.0
'@img/sharp-libvips-linuxmusl-x64': 1.1.0
'@img/sharp-linux-arm': 0.34.1
'@img/sharp-linux-arm64': 0.34.1
'@img/sharp-linux-s390x': 0.34.1
'@img/sharp-linux-x64': 0.34.1
'@img/sharp-linuxmusl-arm64': 0.34.1
'@img/sharp-linuxmusl-x64': 0.34.1
'@img/sharp-wasm32': 0.34.1
'@img/sharp-win32-ia32': 0.34.1
'@img/sharp-win32-x64': 0.34.1
optional: true
shebang-command@1.2.0:
@@ -9480,7 +9610,7 @@ snapshots:
wait-on@8.0.1(debug@4.3.7):
dependencies:
axios: 1.7.7(debug@4.3.7)
axios: 1.8.3(debug@4.3.7)
joi: 17.13.3
lodash: 4.17.21
minimist: 1.2.8