Fork of github.com/did-method-plc/did-method-plc
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge branch 'main' into bnewbold/doc-iteration

+2319 -147
+3
.gitignore
··· 14 14 \#*\# 15 15 *~ 16 16 *.swp 17 + 18 + # Don't ignore this file itself 19 + !.gitignore
+18 -17
README.md
··· 2 2 3 3 DID PLC is a self-authenticating [DID](https://www.w3.org/TR/did-core/) which is strongly-consistent, recoverable, and allows for key rotation. 4 4 5 - An example DID is: `did:plc:yk4dd2qkboz2yv6tpubpc6co` 5 + An example DID is: `did:plc:ewvi7nxzyoun6zhxrhs64oiz` 6 6 7 - Control over a `did:plc` identity rests in a set of re-configurable "rotation" keys pairs. These keys can sign update "operations" to mutate the identity (including key rotations), with each operation referencing a prior version of the identity state by hash. Each identity starts from an initial "genesis" operation, and the hash of this initial object is what defines the DID itself (that is, the DID URI "identifier" string). A central "directory" server collects and validates operations, and maintains a transparent log of operations for each DID. 7 + Control over a `did:plc` identity rests in a set of reconfigurable rotation keys pairs. These keys can sign update operations to mutate the identity (including key rotations), with each operation referencing a prior version of the identity state by hash. Each identity starts from an initial genesis operation, and the hash of this initial object is what defines the DID itself (that is, the DID URI identifier string). A central directory server collects and validates operations, and maintains a transparent log of operations for each DID. 8 + 9 + This git repository contains a TypeScript reference implementation of the method (`@did-plc/lib`) and a directory server `@did-plc/server`, both in the `package/` directory. The `go-didplc/`directory is intended to hold a golang implementation. 8 10 9 11 ## Motivation 10 12 11 - [Bluesky](https://blueskyweb.xyz/) developed DID PLC when designing the [AT Protocol](https://atproto.com) ("atproto") because we were not satisfied with any of the existing DID methods. 12 - We wanted a strongly consistent, highly available, recoverable, and cryptographically secure method with fast and cheap propagation of updates. 13 + [Bluesky PBC](https://blueskyweb.xyz/) developed DID PLC when designing the [AT Protocol](https://atproto.com) (atproto) because we were not satisfied with any of the existing DID methods. We wanted a strongly consistent, highly available, recoverable, and cryptographically secure method with fast and cheap propagation of updates. 13 14 14 - We originally titled the method "Placeholder", because we didn't want it to stick around forever in its current form. We are actively hoping to replace it with or evolve it into something less centralized - likely a permissioned DID consortium. That being said, we do intend to support `did:plc` in the current form until after any successor is deployed, with a reasonable grace period. We would also provide a migration route to allow continued use of existing `did:plc` identifiers. 15 + We originally titled the method "placeholder", because we didn't want it to stick around forever in its current form. We are actively hoping to replace it with or evolve it into something less centralized - likely a permissioned DID consortium. That being said, we do intend to support `did:plc` in the current form until after any successor is deployed, with a reasonable grace period. We would also provide a migration route to allow continued use of existing `did:plc` identifiers. 15 16 16 17 ## How it works 17 18 ··· 23 24 - `alsoKnownAs` (array of strings): priority-ordered list of URIs which indicate other names or aliases associated with the DID identifier 24 25 - `services` (map with string keys; values are maps with `type` and `endpoint` string fields): a set of service / URL mappings. the key strings should not include a `#` prefix; that will be added when rendering the DID document. 25 26 26 - Every update "operation" to the DID identifier, including the initial creation operation ("genesis" operation), contains all of the above information, except for the `did` field. The DID itself is generated from a hash of the signed genesis operation (details described below), which makes the DID entirely self-certifying. Updates after initial creation contain a pointer to the most-recent previous operation (by hash). 27 + Every update operation to the DID identifier, including the initial creation operation (the genesis operation), contains all of the above information, except for the `did` field. The DID itself is generated from a hash of the signed genesis operation (details described below), which makes the DID entirely self-certifying. Updates after initial creation contain a pointer to the most-recent previous operation (by hash). 27 28 28 - "Operations" are signed and submitted to the central PLC directory server over an un-authenticated HTTP request. The PLC server validates operations against any and all existing operations on the DID (including signature validation, recovery time windows, etc), and either rejects the operation or accepts and permanently stores the operation, along with a server-generated timestamp. 29 + Operations are signed and submitted to the central PLC directory server over an un-authenticated HTTP request. The PLC server validates operations against any and all existing operations on the DID (including signature validation, recovery time windows, etc), and either rejects the operation or accepts and permanently stores the operation, along with a server-generated timestamp. 29 30 30 - A special operation type is a "tombstone", which clears all of the data fields and permanently "de-activates" the DID. Note that the usual recovery time window applies to "tombstone" operations. 31 + A special operation type is a "tombstone", which clears all of the data fields and permanently deactivates the DID. Note that the usual recovery time window applies to tombstone operations. 31 32 32 - Note that `rotationKeys` and `verificationMethods` ("signing keys") may have public keys which are re-used across many accounts. There is not necessarily a one-to-one mapping between a DID and either "rotation" keys or "signing" keys. 33 + Note that `rotationKeys` and `verificationMethods` (signing keys) may have public keys which are re-used across many accounts. There is not necessarily a one-to-one mapping between a DID and either rotation keys or signing keys. 33 34 34 - Only `secp256k1` ("k256") and NIST P-256 ("p256") keys are currently supported, for both "rotation" and "signing" keys. 35 + Only `secp256k1` ("k256") and NIST P-256 ("p256") keys are currently supported, for both rotation and signing keys. 35 36 36 37 ### Use with AT Protocol 37 38 38 39 The following information should be included for use with atproto: 39 40 40 - - `verificationMethods`: an `atproto` entry with a "blessed" public key type, to be used as a "signing key" for authenticating updates to the account's repository. the signing key does not have any control over the DID identity unless also included in the `rotationKeys` list. best practice is to maintain separation between rotation keys and atproto signing keys 41 - - `alsoKnownAs`: should include an `at://` URI indicating a "handle" (hostname) for the account. note that the handle/DID mapping needs to be validated bi-directionally (via handle resolution), and needs to be re-verified periodically 41 + - `verificationMethods`: an `atproto` entry with a "blessed" public key type, to be used as a signing key for authenticating updates to the account's repository. The signing key does not have any control over the DID identity unless also included in the `rotationKeys` list. Best practice is to maintain separation between rotation keys and atproto signing keys. 42 + - `alsoKnownAs`: should include an `at://` URI indicating a handle (hostname) for the account. Note that the handle/DID mapping needs to be validated bi-directionally (via handle resolution), and needs to be re-verified periodically 42 43 - `services`: an `atproto_pds` entry with an `AtprotoPersonalDataServer` type and http/https URL `endpoint` indicating the account's current PDS hostname. for example, `https://pds.example.com` (no `/xrpc/` suffix needed). 43 44 44 45 ### Operation Serialization, Signing, and Validation 45 46 46 - There are a couple variations on the "operation" data object schema. The operations are also serialized both as simple JSON objects, or binary DAG-CBOR encoding for the purpose of hashing or signing. 47 + There are a couple variations on the operation data object schema. The operations are also serialized both as simple JSON objects, or binary DAG-CBOR encoding for the purpose of hashing or signing. 47 48 48 49 A regular creation or update operation contains the following fields: 49 50 ··· 52 53 - `verificationMethods` (mapping of string keys and values): as described above 53 54 - `alsoKnownAs` (array of strings): as described above 54 55 - `services` (mapping of string keys and object values): as described above 55 - - `prev` (string, nullable): a "CID" hash pointer to a previous operation if an update, or `null` for a creation. if `null`, the key should actually be part of the object, with value `null`, not simply omitted. in DAG-CBOR encoding, the CID is string-encoded, not a binary IPLD "Link" 56 + - `prev` (string, nullable): a CID hash pointer to a previous operation if an update, or `null` for a creation. If `null`, the key should actually be part of the object, with value `null`, not simply omitted. In DAG-CBOR encoding, the CID is string-encoded, not a binary IPLD "Link" 56 57 - `sig` (string): signature of the operation in `base64url` encoding 57 58 58 59 A tombstone operation contains: ··· 378 379 379 380 ## Possible Future Changes 380 381 381 - The set of allowed ("blessed") public key cryptographic algorithms (aka, curves) may expanded over time, slowly. Likewise, support for additional "blessed" CID types and parameters may be expanded over time, slowly. 382 + The set of allowed ("blessed") public key cryptographic algorithms (aka, curves) may expanded over time, slowly. Likewise, support for additional blessed CID types and parameters may be expanded over time, slowly. 382 383 383 384 The recovery time window may become configurable, within constraints, as part of the DID metadata itself. 384 385 385 386 Support for "DID Controller Delegation" could be useful (eg, in the context of atproto PDS hosts), and may be incorporated. 386 387 387 - In the context of atproto, support for multiple "handles" for the same DID is being considered, with a single "primary" handle. But no final decision has been made yet. 388 + In the context of atproto, support for multiple handles for the same DID is being considered, with a single primary handle. But no final decision has been made yet. 388 389 389 390 We welcome proposals for small additions to make `did:plc` more generic and reusable for applications other than atproto. But no promises: atproto will remain the focus for the near future. 390 391 391 - Moving governance of the `did:plc` method, and operation of registry servers, out of the sole control of Bluesky PBLLC is something we are enthusiastic about. Audit log snapshots, mirroring, and automated third-party auditing have all been considered as mechanisms to mitigate the centralized nature of the PLC server. 392 + We are enthusiastic about the prospect of moving governance of the `did:plc` method, and operation of registry servers, out of the sole control of Bluesky PBC. Audit log snapshots, mirroring, and automated third-party auditing have all been considered as mechanisms to mitigate the centralized nature of the PLC server. 392 393 393 394 The size of the `verificationMethods`, `alsoKnownAs`, and `service` mappings/arrays may be specifically constrained. And the maximum DAG-CBOR size may be constrained. 394 395
+4
go-didplc/.gitignore
··· 1 + /webplc 2 + /plan.txt 3 + !static/.well-known/ 4 + !.gitignore
+42
go-didplc/Makefile
··· 1 + 2 + SHELL = /bin/bash 3 + .SHELLFLAGS = -o pipefail -c 4 + 5 + .PHONY: help 6 + help: ## Print info about all commands 7 + @echo "Commands:" 8 + @echo 9 + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[01;32m%-20s\033[0m %s\n", $$1, $$2}' 10 + 11 + .PHONY: build 12 + build: ## Build all executables 13 + go build ./cmd/webplc 14 + 15 + .PHONY: test 16 + test: ## Run all tests 17 + go test ./... 18 + 19 + .PHONY: coverage-html 20 + coverage-html: ## Generate test coverage report and open in browser 21 + go test ./... -coverpkg=./... -coverprofile=test-coverage.out 22 + go tool cover -html=test-coverage.out 23 + 24 + .PHONY: lint 25 + lint: ## Verify code style and run static checks 26 + go vet ./... 27 + test -z $(gofmt -l ./...) 28 + 29 + .PHONY: fmt 30 + fmt: ## Run syntax re-formatting (modify in place) 31 + go fmt ./... 32 + 33 + .PHONY: check 34 + check: ## Compile everything, checking syntax (does not output binaries) 35 + go build ./... 36 + 37 + .env: 38 + if [ ! -f ".env" ]; then cp example.dev.env .env; fi 39 + 40 + .PHONY: run-dev-webplc 41 + run-dev-webplc: .env ## Runs 'bskyweb' for local dev 42 + GOLOG_LOG_LEVEL=info go run ./cmd/webplc serve --debug
+23
go-didplc/README.md
··· 1 + 2 + `go-didplc`: did:plc in golang 3 + ============================== 4 + 5 + This golang package will eventually be an implementation of the did:plc specification in golang, including at a minimum verification of DID documents from a PLC operation log. 6 + 7 + For now it primarily contains a basic website for the PLC directory, allowing lookup of individual DID documents. 8 + 9 + 10 + ## Developer Quickstart 11 + 12 + Install golang. We are generally using v1.20+. 13 + 14 + In this directory (`go-didplc/`): 15 + 16 + # re-build and run daemon 17 + go run ./cmd/webplc serve 18 + 19 + # build and output a binary 20 + go build -o webplc ./cmd/webplc/ 21 + 22 + The easiest way to configure the daemon is to copy `example.env` to `.env` and 23 + fill in auth values there.
+60
go-didplc/cmd/webplc/main.go
··· 1 + package main 2 + 3 + import ( 4 + "os" 5 + 6 + _ "github.com/joho/godotenv/autoload" 7 + 8 + logging "github.com/ipfs/go-log" 9 + "github.com/urfave/cli/v2" 10 + ) 11 + 12 + var log = logging.Logger("webplc") 13 + 14 + func init() { 15 + logging.SetAllLoggers(logging.LevelDebug) 16 + //logging.SetAllLoggers(logging.LevelWarn) 17 + } 18 + 19 + func main() { 20 + run(os.Args) 21 + } 22 + 23 + func run(args []string) { 24 + 25 + app := cli.App{ 26 + Name: "webplc", 27 + Usage: "web server for bsky.app web app (SPA)", 28 + } 29 + 30 + app.Commands = []*cli.Command{ 31 + &cli.Command{ 32 + Name: "serve", 33 + Usage: "run the web server", 34 + Action: serve, 35 + Flags: []cli.Flag{ 36 + &cli.StringFlag{ 37 + Name: "plc-host", 38 + Usage: "method, hostname, and port of PLC instance", 39 + Value: "https://plc.directory", 40 + EnvVars: []string{"ATP_PLC_HOST"}, 41 + }, 42 + &cli.StringFlag{ 43 + Name: "http-address", 44 + Usage: "Specify the local IP/port to bind to", 45 + Required: false, 46 + Value: ":8700", 47 + EnvVars: []string{"HTTP_ADDRESS"}, 48 + }, 49 + &cli.BoolFlag{ 50 + Name: "debug", 51 + Usage: "Enable debug mode", 52 + Value: false, 53 + Required: false, 54 + EnvVars: []string{"DEBUG"}, 55 + }, 56 + }, 57 + }, 58 + } 59 + app.RunAndExitOnError() 60 + }
+82
go-didplc/cmd/webplc/renderer.go
··· 1 + package main 2 + 3 + import ( 4 + "bytes" 5 + "embed" 6 + "errors" 7 + "fmt" 8 + "io" 9 + "path/filepath" 10 + 11 + "github.com/flosch/pongo2/v6" 12 + "github.com/labstack/echo/v4" 13 + ) 14 + 15 + type RendererLoader struct { 16 + prefix string 17 + fs *embed.FS 18 + } 19 + 20 + func NewRendererLoader(prefix string, fs *embed.FS) pongo2.TemplateLoader { 21 + return &RendererLoader{ 22 + prefix: prefix, 23 + fs: fs, 24 + } 25 + } 26 + func (l *RendererLoader) Abs(_, name string) string { 27 + // TODO: remove this workaround 28 + // Figure out why this method is being called 29 + // twice on template names resulting in a failure to resolve 30 + // the template name. 31 + if filepath.HasPrefix(name, l.prefix) { 32 + return name 33 + } 34 + return filepath.Join(l.prefix, name) 35 + } 36 + 37 + func (l *RendererLoader) Get(path string) (io.Reader, error) { 38 + b, err := l.fs.ReadFile(path) 39 + if err != nil { 40 + return nil, fmt.Errorf("reading template %q failed: %w", path, err) 41 + } 42 + return bytes.NewReader(b), nil 43 + } 44 + 45 + type Renderer struct { 46 + TemplateSet *pongo2.TemplateSet 47 + Debug bool 48 + } 49 + 50 + func NewRenderer(prefix string, fs *embed.FS, debug bool) *Renderer { 51 + return &Renderer{ 52 + TemplateSet: pongo2.NewSet(prefix, NewRendererLoader(prefix, fs)), 53 + Debug: debug, 54 + } 55 + } 56 + 57 + func (r Renderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error { 58 + var ctx pongo2.Context 59 + 60 + if data != nil { 61 + var ok bool 62 + ctx, ok = data.(pongo2.Context) 63 + if !ok { 64 + return errors.New("no pongo2.Context data was passed") 65 + } 66 + } 67 + 68 + var t *pongo2.Template 69 + var err error 70 + 71 + if r.Debug { 72 + t, err = pongo2.FromFile(name) 73 + } else { 74 + t, err = r.TemplateSet.FromFile(name) 75 + } 76 + 77 + if err != nil { 78 + return err 79 + } 80 + 81 + return t.ExecuteWriter(ctx, w) 82 + }
+77
go-didplc/cmd/webplc/resolve.go
··· 1 + package main 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "io" 7 + "net/http" 8 + ) 9 + 10 + type VerificationMethod struct { 11 + Id string `json:"id"` 12 + Type string `json:"type"` 13 + Controller string `json:"controller"` 14 + PublicKeyMultibase string `json:"publicKeyMultibase"` 15 + } 16 + 17 + type DidService struct { 18 + Id string `json:"id"` 19 + Type string `json:"type"` 20 + ServiceEndpoint string `json:"serviceEndpoint"` 21 + } 22 + 23 + type DidDoc struct { 24 + AlsoKnownAs []string `json:"alsoKnownAs"` 25 + VerificationMethod []VerificationMethod `json:"verificationMethod"` 26 + Service []DidService `json:"service"` 27 + } 28 + 29 + type ResolutionResult struct { 30 + Doc *DidDoc 31 + DocJson *string 32 + StatusCode int 33 + } 34 + 35 + func ResolveDidPlc(client *http.Client, plc_host, did string) (*ResolutionResult, error) { 36 + result := ResolutionResult{} 37 + res, err := client.Get(fmt.Sprintf("%s/%s", plc_host, did)) 38 + if err != nil { 39 + return nil, fmt.Errorf("error making http request: %v", err) 40 + } 41 + defer res.Body.Close() 42 + log.Debugf("PLC resolution result status=%d did=%s", res.StatusCode, did) 43 + 44 + result.StatusCode = res.StatusCode 45 + if res.StatusCode == 404 || res.StatusCode == 410 { 46 + return &result, nil 47 + } else if res.StatusCode != 200 { 48 + return &result, nil 49 + } 50 + 51 + respBytes, err := io.ReadAll(res.Body) 52 + if err != nil { 53 + return nil, fmt.Errorf("failed to read PLC result body: %v", err) 54 + } 55 + 56 + doc := DidDoc{} 57 + err = json.Unmarshal(respBytes, &doc) 58 + if err != nil { 59 + return nil, fmt.Errorf("failed to parse DID Document JSON: %v", err) 60 + } 61 + result.Doc = &doc 62 + 63 + // parse and re-serialize JSON in pretty (indent) style 64 + var data map[string]interface{} 65 + err = json.Unmarshal(respBytes, &data) 66 + if err != nil { 67 + return nil, fmt.Errorf("failed to parse DID Document JSON: %v", err) 68 + } 69 + indentJson, err := json.MarshalIndent(data, "", " ") 70 + if err != nil { 71 + return nil, fmt.Errorf("failed to parse DID Document JSON: %v", err) 72 + } 73 + s := string(indentJson) 74 + result.DocJson = &s 75 + 76 + return &result, nil 77 + }
+274
go-didplc/cmd/webplc/server.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "embed" 6 + "errors" 7 + "fmt" 8 + "io/fs" 9 + "net/http" 10 + "os" 11 + "os/signal" 12 + "strings" 13 + "syscall" 14 + "time" 15 + 16 + "github.com/flosch/pongo2/v6" 17 + "github.com/klauspost/compress/gzhttp" 18 + "github.com/klauspost/compress/gzip" 19 + "github.com/labstack/echo/v4" 20 + "github.com/labstack/echo/v4/middleware" 21 + "github.com/russross/blackfriday/v2" 22 + "github.com/urfave/cli/v2" 23 + ) 24 + 25 + //go:embed templates/* 26 + var TemplateFS embed.FS 27 + 28 + //go:embed static/* 29 + var StaticFS embed.FS 30 + 31 + //go:embed spec/v0.1/did-plc.md 32 + var specZeroOneMarkdown []byte 33 + 34 + //go:embed spec/plc-server-openapi3.yaml 35 + var apiOpenapiYaml []byte 36 + 37 + type Server struct { 38 + echo *echo.Echo 39 + httpd *http.Server 40 + client *http.Client 41 + plcHost string 42 + } 43 + 44 + func serve(cctx *cli.Context) error { 45 + debug := cctx.Bool("debug") 46 + httpAddress := cctx.String("http-address") 47 + 48 + // Echo 49 + e := echo.New() 50 + 51 + // create a new session (no auth) 52 + client := http.Client{ 53 + Transport: &http.Transport{ 54 + Proxy: http.ProxyFromEnvironment, 55 + ForceAttemptHTTP2: true, 56 + MaxIdleConns: 100, 57 + IdleConnTimeout: 90 * time.Second, 58 + TLSHandshakeTimeout: 10 * time.Second, 59 + ExpectContinueTimeout: 1 * time.Second, 60 + }, 61 + } 62 + 63 + // httpd variable 64 + var ( 65 + httpTimeout = 2 * time.Minute 66 + httpMaxHeaderBytes = 2 * (1024 * 1024) 67 + gzipMinSizeBytes = 1024 * 2 68 + gzipCompressionLevel = gzip.BestSpeed 69 + gzipExceptMIMETypes = []string{"image/png"} 70 + ) 71 + 72 + // Wrap the server handler in a gzip handler to compress larger responses. 73 + gzipHandler, err := gzhttp.NewWrapper( 74 + gzhttp.MinSize(gzipMinSizeBytes), 75 + gzhttp.CompressionLevel(gzipCompressionLevel), 76 + gzhttp.ExceptContentTypes(gzipExceptMIMETypes), 77 + ) 78 + if err != nil { 79 + return err 80 + } 81 + 82 + server := &Server{ 83 + echo: e, 84 + client: &client, 85 + plcHost: cctx.String("plc-host"), 86 + } 87 + 88 + server.httpd = &http.Server{ 89 + Handler: gzipHandler(server), 90 + Addr: httpAddress, 91 + WriteTimeout: httpTimeout, 92 + ReadTimeout: httpTimeout, 93 + MaxHeaderBytes: httpMaxHeaderBytes, 94 + } 95 + 96 + e.HideBanner = true 97 + // SECURITY: Do not modify without due consideration. 98 + e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ 99 + ContentTypeNosniff: "nosniff", 100 + XFrameOptions: "SAMEORIGIN", 101 + HSTSMaxAge: 31536000, // 365 days 102 + // TODO: 103 + // ContentSecurityPolicy 104 + // XSSProtection 105 + })) 106 + e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ 107 + // Don't log requests for static content. 108 + Skipper: func(c echo.Context) bool { 109 + return strings.HasPrefix(c.Request().URL.Path, "/static") 110 + }, 111 + })) 112 + e.Renderer = NewRenderer("templates/", &TemplateFS, debug) 113 + e.HTTPErrorHandler = server.errorHandler 114 + 115 + // redirect trailing slash to non-trailing slash. 116 + // all of our current endpoints have no trailing slash. 117 + e.Use(middleware.RemoveTrailingSlashWithConfig(middleware.TrailingSlashConfig{ 118 + RedirectCode: http.StatusFound, 119 + })) 120 + 121 + staticHandler := http.FileServer(func() http.FileSystem { 122 + if debug { 123 + log.Debugf("serving static file from the local file system") 124 + return http.FS(os.DirFS("static")) 125 + } 126 + fsys, err := fs.Sub(StaticFS, "static") 127 + if err != nil { 128 + log.Fatal(err) 129 + } 130 + return http.FS(fsys) 131 + }()) 132 + 133 + // static file routes 134 + e.GET("/robots.txt", echo.WrapHandler(staticHandler)) 135 + e.GET("/favicon.ico", echo.WrapHandler(staticHandler)) 136 + e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler))) 137 + e.GET("/.well-known/*", echo.WrapHandler(staticHandler)) 138 + e.GET("/security.txt", func(c echo.Context) error { 139 + return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt") 140 + }) 141 + 142 + // meta stuff 143 + e.GET("/_health", server.WebHealth) 144 + e.GET("/healthz", server.WebHealth) 145 + 146 + // actual pages/views 147 + e.GET("/", server.WebHome) 148 + e.GET("/resolve", server.WebResolve) 149 + e.GET("/did/:did", server.WebDid) 150 + e.GET("/spec/v0.1/did-plc", server.WebSpecZeroOne) 151 + e.GET("/api/redoc", server.WebRedoc) 152 + e.GET("/api/plc-server-openapi3.yaml", server.WebOpenapiYaml) 153 + 154 + // Start the server. 155 + log.Infof("starting server address=%s", httpAddress) 156 + go func() { 157 + if err := server.httpd.ListenAndServe(); err != nil { 158 + if !errors.Is(err, http.ErrServerClosed) { 159 + log.Errorf("HTTP server shutting down unexpectedly: %s", err) 160 + } 161 + } 162 + }() 163 + 164 + // Wait for a signal to exit. 165 + log.Info("registering OS exit signal handler") 166 + quit := make(chan struct{}) 167 + exitSignals := make(chan os.Signal, 1) 168 + signal.Notify(exitSignals, syscall.SIGINT, syscall.SIGTERM) 169 + go func() { 170 + sig := <-exitSignals 171 + log.Infof("received OS exit signal: %s", sig) 172 + 173 + // Shut down the HTTP server. 174 + if err := server.Shutdown(); err != nil { 175 + log.Errorf("HTTP server shutdown error: %s", err) 176 + } 177 + 178 + // Trigger the return that causes an exit. 179 + close(quit) 180 + }() 181 + <-quit 182 + log.Infof("graceful shutdown complete") 183 + return nil 184 + } 185 + 186 + func (srv *Server) ServeHTTP(rw http.ResponseWriter, req *http.Request) { 187 + srv.echo.ServeHTTP(rw, req) 188 + } 189 + 190 + func (srv *Server) Shutdown() error { 191 + log.Info("shutting down") 192 + 193 + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 194 + defer cancel() 195 + 196 + return srv.httpd.Shutdown(ctx) 197 + } 198 + 199 + func (srv *Server) errorHandler(err error, c echo.Context) { 200 + code := http.StatusInternalServerError 201 + errorMessage := "" 202 + if he, ok := err.(*echo.HTTPError); ok { 203 + code = he.Code 204 + if he.Message != nil { 205 + errorMessage = fmt.Sprintf("%s", he.Message) 206 + } 207 + } 208 + c.Logger().Error(err) 209 + data := pongo2.Context{ 210 + "statusCode": code, 211 + "errorMessage": errorMessage, 212 + } 213 + if err = c.Render(code, "templates/error.html", data); err != nil { 214 + c.Logger().Error(err) 215 + } 216 + } 217 + 218 + func (srv *Server) WebHome(c echo.Context) error { 219 + data := pongo2.Context{} 220 + return c.Render(http.StatusOK, "templates/home.html", data) 221 + } 222 + 223 + func (srv *Server) WebSpecZeroOne(c echo.Context) error { 224 + data := pongo2.Context{} 225 + data["html_title"] = "did:plc Specification v0.1" 226 + data["markdown_html"] = string(blackfriday.Run(specZeroOneMarkdown)) 227 + return c.Render(http.StatusOK, "templates/markdown.html", data) 228 + } 229 + 230 + func (srv *Server) WebHealth(c echo.Context) error { 231 + resp := map[string]interface{}{ 232 + "status": "ok", 233 + } 234 + return c.JSON(http.StatusOK, resp) 235 + } 236 + 237 + func (srv *Server) WebOpenapiYaml(c echo.Context) error { 238 + return c.Blob(http.StatusOK, "text/yaml", apiOpenapiYaml) 239 + } 240 + 241 + func (srv *Server) WebRedoc(c echo.Context) error { 242 + data := pongo2.Context{} 243 + return c.Render(http.StatusOK, "templates/redoc.html", data) 244 + } 245 + 246 + func (srv *Server) WebResolve(c echo.Context) error { 247 + data := pongo2.Context{} 248 + did := c.QueryParam("did") 249 + if did != "" { 250 + return c.Redirect(http.StatusMovedPermanently, "/did/"+did) 251 + } 252 + return c.Render(http.StatusOK, "templates/resolve.html", data) 253 + } 254 + 255 + func (srv *Server) WebDid(c echo.Context) error { 256 + data := pongo2.Context{} 257 + did := c.Param("did") 258 + data["did"] = did 259 + if !strings.HasPrefix(did, "did:plc:") { 260 + return echo.NewHTTPError(http.StatusBadRequest, fmt.Errorf("Not a valid DID PLC identifier: %s", did)) 261 + } 262 + res, err := ResolveDidPlc(srv.client, srv.plcHost, did) 263 + if err != nil { 264 + return err 265 + } 266 + if res.StatusCode == 404 { 267 + return echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("DID not in PLC directory: %s", did)) 268 + } 269 + if res.StatusCode == 410 { 270 + return echo.NewHTTPError(http.StatusNotFound, fmt.Errorf("DID has been permanently deleted: %s", did)) 271 + } 272 + data["result"] = res 273 + return c.Render(http.StatusOK, "templates/did.html", data) 274 + }
+458
go-didplc/cmd/webplc/spec/plc-server-openapi3.yaml
··· 1 + openapi: 3.0.0 2 + info: 3 + title: "did:plc Directory Server API" 4 + version: 0.1 5 + contact: 6 + name: "Protocol Team at Bluesky" 7 + email: "protocol@blueskyweb.xyz" 8 + url: "https://web.plc.directory" 9 + description: | 10 + DID PLC is a self-authenticating [DID](https://www.w3.org/TR/did-core/) which is strongly-consistent, recoverable, and allows for key rotation. 11 + 12 + The central directory server receives and persists self-signed operation logs for each DID, starting with a "genesis operation" which defined the DID identifier itself. This document describes the HTTP API for interacting with the directory server to resolve DID Document, fetch audit logs, and submit signed operations. 13 + 14 + The HTTP API is permissionless, but only valid (correctly signed) operations are accepted. Reasonable rate-limits are applied, but they should not interfer with account recovery in most situations. 15 + servers: 16 + - url: https://plc.directory 17 + 18 + paths: 19 + /{did}: 20 + get: 21 + description: "Resolve DID Document for the indicated DID" 22 + operationId: ResolveDid 23 + parameters: 24 + - name: did 25 + in: path 26 + required: true 27 + schema: 28 + type: string 29 + responses: 30 + '200': 31 + description: "Success, returned DID Document" 32 + content: 33 + application/did+ld+json: 34 + schema: 35 + $ref: '#/components/schemas/DidDocument' 36 + '404': 37 + $ref: '#/components/responses/404DidNotFound' 38 + '410': 39 + $ref: '#/components/responses/410DidNotAvailable' 40 + x-codeSamples: 41 + - lang: Shell 42 + label: curl 43 + source: | 44 + curl -s https://plc.directory/did:plc:pyc2ihzpelxtg4cdkfzbhcv4 | jq . 45 + - lang: Python 46 + label: Python 47 + source: | 48 + import requests 49 + 50 + did = "did:plc:pyc2ihzpelxtg4cdkfzbhcv4" 51 + resp = requests.get(f"https://plc.directory/{did}") 52 + resp.raise_for_status() 53 + print(resp.json()) 54 + post: 55 + description: "Create new PLC Operation for the indicated DID" 56 + operationId: CreatePlcOp 57 + parameters: 58 + - name: did 59 + in: path 60 + required: true 61 + schema: 62 + type: string 63 + requestBody: 64 + required: true 65 + content: 66 + application/json: 67 + schema: 68 + $ref: '#/components/schemas/Operation' 69 + responses: 70 + '200': 71 + description: "Success, operation validated and persisted" 72 + # TODO: what is returned here? 73 + '400': 74 + $ref: '#/components/responses/400BadOperation' 75 + '404': 76 + $ref: '#/components/responses/404DidNotFound' 77 + '410': 78 + $ref: '#/components/responses/410DidNotAvailable' 79 + /{did}/log: 80 + get: 81 + description: "Get Current PLC Operation Chain" 82 + operationId: GetPlcOpLog 83 + parameters: 84 + - name: did 85 + in: path 86 + required: true 87 + schema: 88 + type: string 89 + responses: 90 + '200': 91 + description: "Success, retured operation log" 92 + content: 93 + application/json: 94 + schema: 95 + type: array 96 + items: 97 + $ref: '#/components/schemas/Operation' 98 + '404': 99 + $ref: '#/components/responses/404DidNotFound' 100 + /{did}/log/audit: 101 + get: 102 + description: "Get PLC Operation Audit Log" 103 + operationId: GetPlcAuditLog 104 + parameters: 105 + - name: did 106 + in: path 107 + required: true 108 + schema: 109 + type: string 110 + responses: 111 + '200': 112 + description: "Success, retured audit log" 113 + content: 114 + application/json: 115 + schema: 116 + type: array 117 + items: 118 + $ref: '#/components/schemas/LogEntry' 119 + '404': 120 + $ref: '#/components/responses/404DidNotFound' 121 + /{did}/log/last: 122 + get: 123 + description: "Get Latest PLC Operation" 124 + operationId: GetLastOp 125 + parameters: 126 + - name: did 127 + in: path 128 + required: true 129 + schema: 130 + type: string 131 + responses: 132 + '200': 133 + description: "Success, returned latest operation" 134 + content: 135 + application/json: 136 + schema: 137 + $ref: '#/components/schemas/LogEntry' 138 + '404': 139 + $ref: '#/components/responses/404DidNotFound' 140 + /{did}/data: 141 + get: 142 + description: "Get Current PLC Data for the indicated DID" 143 + operationId: GetPlcData 144 + parameters: 145 + - name: did 146 + in: path 147 + required: true 148 + schema: 149 + type: string 150 + responses: 151 + '200': 152 + description: "Success, retured current PLC data" 153 + # TODO: basically just an op, but missing some fields? sigh. 154 + '404': 155 + $ref: '#/components/responses/404DidNotFound' 156 + '410': 157 + $ref: '#/components/responses/410DidNotAvailable' 158 + /export: 159 + get: 160 + description: "Bulk fetch PLC Operations for all DIDs, with pagination, in JSON Lines format" 161 + operationId: Export 162 + parameters: 163 + - name: count 164 + in: query 165 + schema: 166 + type: integer 167 + default: 10 168 + maximum: 1000 169 + - name: after 170 + in: query 171 + schema: 172 + type: string 173 + format: date-time 174 + description: "Return only operations after this indexed timestamp" 175 + responses: 176 + '200': 177 + description: "Success, returned batch of operations" 178 + content: 179 + application/jsonlines: 180 + description: "Newline-delimited JSON file, with a separate JSON object on each line" 181 + schema: 182 + $ref: '#/components/schemas/LogEntry' 183 + '400': 184 + $ref: '#/components/responses/400BadRequest' 185 + 186 + components: 187 + responses: 188 + 404DidNotFound: 189 + description: "DID Not Found" 190 + content: 191 + application/json: 192 + schema: 193 + type: object 194 + properties: 195 + message: 196 + type: string 197 + example: 198 + message: "DID not registered: did:plc:ewvi7nxzyoun6zhxrhs64oiz" 199 + 410DidNotAvailable: 200 + description: "DID Not Available (Tombstone)" 201 + content: 202 + application/json: 203 + schema: 204 + type: object 205 + properties: 206 + message: 207 + type: string 208 + example: 209 + message: "DID not available: did:plc:ewvi7nxzyoun6zhxrhs64oiz" 210 + 400BadOperation: 211 + description: "Invalid PLC Operation" 212 + content: 213 + application/json: 214 + schema: 215 + type: object 216 + properties: 217 + message: 218 + type: string 219 + example: 220 + message: "Invalid Signature" 221 + 400BadRequest: 222 + description: "Bad Request" 223 + content: 224 + application/json: 225 + schema: 226 + type: object 227 + properties: 228 + message: 229 + type: string 230 + example: 231 + message: "Invalid Query Parameter" 232 + schemas: 233 + Operation: 234 + oneOf: 235 + - $ref: '#/components/schemas/PlcOp' 236 + - $ref: '#/components/schemas/TombstoneOp' 237 + - $ref: '#/components/schemas/LegacyCreateOp' 238 + discriminator: 239 + propertyName: type 240 + mapping: 241 + plc_operation: '#/components/schemas/PlcOp' 242 + plc_tombstone: '#/components/schemas/TombstoneOp' 243 + create: '#/components/schemas/LegacyCreateOp' 244 + PlcOp: 245 + type: object 246 + description: "Regular PLC operation. Can be a genesis operation (create DID, no 'prev' field), or a data update." 247 + required: 248 + - type 249 + - rotationKeys 250 + - verificationMethods 251 + - alsoKnownAs 252 + - services 253 + - prev 254 + - sig 255 + properties: 256 + type: 257 + type: string 258 + rotationKeys: 259 + type: array 260 + items: 261 + type: string 262 + description: "Ordered set (no duplicates) of cryptographic public keys in did:key format" 263 + verificationMethods: 264 + type: object 265 + description: "Map (object) of application-specific cryptographic public keys in did:key format" 266 + alsoKnownAs: 267 + type: array 268 + items: 269 + type: string 270 + description: "Ordered set (no duplicates) of aliases and names for this account, in the form of URIs" 271 + services: 272 + type: object 273 + description: "Map (object) of application-specific service endpoints for this account" 274 + prev: 275 + type: string 276 + nullable: true 277 + description: "Strong reference (hash) of preceeding operation for this DID, in string CID format. Null for genesis operation" 278 + sig: 279 + type: string 280 + description: "Cryptographic signature of this object, with base64 string encoding" 281 + example: 282 + type: "plc_operation" 283 + services: 284 + atproto_pds: 285 + type: "AtprotoPersonalDataServer" 286 + endpoint: "https://bsky.social" 287 + alsoKnownAs: 288 + - "at://atproto.com" 289 + rotationKeys: 290 + - "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg" 291 + - "did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK" 292 + verificationMethods: 293 + atproto: "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 294 + 295 + TombstoneOp: 296 + type: object 297 + description: "Special operation which deactives (revokes) the DID. This is permanent once the recovery window expires." 298 + required: 299 + - type 300 + - prev 301 + - sig 302 + properties: 303 + type: 304 + type: string 305 + prev: 306 + type: string 307 + description: "Strong reference (hash) of preceeding operation for this DID, in string CID format" 308 + sig: 309 + type: string 310 + description: "Cryptographic signature of this object, with base64 string encoding" 311 + example: 312 + type: "plc_tombstone" 313 + prev: "bafyreid6awsb6lzc54zxaq2roijyvpbjp5d6mii2xyztn55yli7htyjgqy" 314 + sig: "41iJmrPRUTIi24HBduzgoavjOibAx2yFJ2p1d7zTN6ZmMgjSaTF8dJf0HtdU4EBNUBTWq33PZyh5tyb1bJq3Fw" 315 + 316 + LegacyCreateOp: 317 + type: object 318 + description: "Obsolete PLC genesis operations, which must still be supported to ensure all did:plc identifiers can be resolved correctly." 319 + required: 320 + - type 321 + - signingKey 322 + - recoveryKey 323 + - handle 324 + - service 325 + - prev 326 + - sig 327 + properties: 328 + type: 329 + type: string 330 + signingKey: 331 + type: string 332 + description: "atproto cryptographic public key in did:key format" 333 + recoveryKey: 334 + type: string 335 + description: "PLC recovery cryptographic public key in did:key format" 336 + handle: 337 + type: string 338 + description: "atproto handle as AT-URI (at://)" 339 + service: 340 + type: string 341 + description: "atproto_pds service endpoint URL" 342 + prev: 343 + type: string 344 + nullable: true 345 + description: "Strong reference (hash) of preceeding operation for this DID, in string CID format" 346 + sig: 347 + type: string 348 + description: "Cryptographic signature of this object, with base64 string encoding" 349 + example: 350 + type: "create" 351 + signingKey: "did:key:zQ3shP5TBe1sQfSttXty15FAEHV1DZgcxRZNxvEWnPfLFwLxJ" 352 + recoveryKey: "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg" 353 + handle: "first-post.bsky.social" 354 + service: "https://bsky.social" 355 + prev: null 356 + sig: "yvN4nQYWTZTDl9nKSSyC5EC3nsF5g4S56OmRg9G6_-pM6FCItV2U2u14riiMGyHiCD86l6O-1xC5MPwf8vVsRw" 357 + 358 + LogEntry: 359 + type: object 360 + required: 361 + - did 362 + - operation 363 + - cid 364 + - nullified 365 + - createdAt 366 + properties: 367 + did: 368 + type: string 369 + description: "DID that this operation applies to" 370 + operation: 371 + $ref: "#/components/schemas/Operation" 372 + cid: 373 + type: cid 374 + description: "Hash of the operation, in string CID format" 375 + nullified: 376 + type: bool 377 + description: "Whether this operation is included in the current operation chain, or has been overridden" 378 + createdAt: 379 + type: string 380 + format: date-time 381 + description: "Timestamp when this operation was received by the directory server" 382 + example: 383 + did: "did:plc:ewvi7nxzyoun6zhxrhs64oiz" 384 + operation: 385 + sig: "lza4at_jCtGo_TYgL5PC1ZNP7lhF4DV8H50LWHhvdHcB143x1wEwqZ43xvV36Pws6OOnJLJrkibEUFDFqkhIhg" 386 + prev: null 387 + type: "plc_operation" 388 + services: 389 + atproto_pds: 390 + type: "AtprotoPersonalDataServer" 391 + endpoint: "https://bsky.social" 392 + alsoKnownAs: 393 + - "at://atprotocol.bsky.social" 394 + rotationKeys: 395 + - "did:key:zQ3shhCGUqDKjStzuDxPkTxN6ujddP4RkEKJJouJGRRkaLGbg" 396 + - "did:key:zQ3shpKnbdPx3g3CmPf5cRVTPe1HtSwVn5ish3wSnDPQCbLJK" 397 + verificationMethods: 398 + atproto: "did:key:zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 399 + cid: "bafyreibfvkh3n6odvdpwj54j4xxdsgnn4zo5utbyf7z7nfbyikhtygzjcq" 400 + nullified: false 401 + createdAt: "2023-04-26T06:19:25.508Z" 402 + 403 + DidDocument: 404 + type: object 405 + required: 406 + - id 407 + properties: 408 + id: 409 + type: string 410 + example: "did:plc:ewvi7nxzyoun6zhxrhs64oiz" 411 + alsoKnownAs: 412 + type: array 413 + description: "Ordered set (no duplicates) of aliases and names for this account, in the form of URIs" 414 + items: 415 + type: string 416 + example: "at://atproto.com" 417 + verificationMethods: 418 + type: array 419 + items: 420 + type: object 421 + required: 422 + - id 423 + - type 424 + - controller 425 + - publicKeyMultibase 426 + properties: 427 + id: 428 + type: string 429 + type: 430 + type: string 431 + controller: 432 + type: string 433 + publicKeyMultibase: 434 + type: string 435 + example: 436 + id: "#atproto" 437 + type: "EcdsaSecp256k1VerificationKey2019" 438 + controller: "did:plc:ewvi7nxzyoun6zhxrhs64oiz" 439 + publicKeyMultibase: "zQYEBzXeuTM9UR3rfvNag6L3RNAs5pQZyYPsomTsgQhsxLdEgCrPTLgFna8yqCnxPpNT7DBk6Ym3dgPKNu86vt9GR" 440 + service: 441 + type: array 442 + items: 443 + type: object 444 + required: 445 + - id 446 + - type 447 + - serviceEndpoint 448 + properties: 449 + id: 450 + type: string 451 + type: 452 + type: string 453 + serviceEndpoint: 454 + type: string 455 + example: 456 + id: "#atproto_pds" 457 + type: "AtprotoPersonalDataServer" 458 + serviceEndpoint: "https://bsky.social"
+392
go-didplc/cmd/webplc/spec/v0.1/did-plc.md
··· 1 + 2 + # `did:plc` Method Specification 3 + 4 + **Version:** v0.1 (May 2023) 5 + 6 + DID PLC is a self-authenticating [DID](https://www.w3.org/TR/did-core/) which is strongly-consistent, recoverable, and allows for key rotation. 7 + 8 + An example DID is: `did:plc:ewvi7nxzyoun6zhxrhs64oiz` 9 + 10 + Control over a `did:plc` identity rests in a set of reconfigurable rotation keys pairs. These keys can sign update operations to mutate the identity (including key rotations), with each operation referencing a prior version of the identity state by hash. Each identity starts from an initial genesis operation, and the hash of this initial object is what defines the DID itself (that is, the DID URI identifier string). A central directory server collects and validates operations, and maintains a transparent log of operations for each DID. 11 + 12 + ## How it works 13 + 14 + The core data fields associated with an active `did:plc` identifier at any point in time are listed below. The encoding and structure differs somewhat from DID document formatting and semantics, but this information is sufficient to render a valid DID document. 15 + 16 + - `did` (string): the full DID identifier 17 + - `rotationKeys` (array of strings): priority-ordered list of public keys in `did:key` encoding. must include least 1 key and at most 5 keys, with no duplication. control of the DID identifier rests in these keys. not included in DID document. 18 + - `verificationMethods` (map with string keys and values): a set service / public key mappings. the values are public keys `did:key` encoding; they get re-encoded in "multibase" form when rendered in DID document. the key strings should not include a `#` prefix; that will be added when rendering the DID document. used to generate `verificationMethods` of DID document. these keys do not have control over the DID document 19 + - `alsoKnownAs` (array of strings): priority-ordered list of URIs which indicate other names or aliases associated with the DID identifier 20 + - `services` (map with string keys; values are maps with `type` and `endpoint` string fields): a set of service / URL mappings. the key strings should not include a `#` prefix; that will be added when rendering the DID document. 21 + 22 + Every update operation to the DID identifier, including the initial creation operation (the genesis operation), contains all of the above information, except for the `did` field. The DID itself is generated from a hash of the signed genesis operation (details described below), which makes the DID entirely self-certifying. Updates after initial creation contain a pointer to the most-recent previous operation (by hash). 23 + 24 + Operations are signed and submitted to the central PLC directory server over an un-authenticated HTTP request. The PLC server validates operations against any and all existing operations on the DID (including signature validation, recovery time windows, etc), and either rejects the operation or accepts and permanently stores the operation, along with a server-generated timestamp. 25 + 26 + A special operation type is a "tombstone", which clears all of the data fields and permanently deactivates the DID. Note that the usual recovery time window applies to tombstone operations. 27 + 28 + Note that `rotationKeys` and `verificationMethods` (signing keys) may have public keys which are re-used across many accounts. There is not necessarily a one-to-one mapping between a DID and either rotation keys or signing keys. 29 + 30 + Only `secp256k1` ("k256") and NIST P-256 ("p256") keys are currently supported, for both rotation and signing keys. 31 + 32 + ### Use with AT Protocol 33 + 34 + The following information should be included for use with atproto: 35 + 36 + - `verificationMethods`: an `atproto` entry with a "blessed" public key type, to be used as a signing key for authenticating updates to the account's repository. The signing key does not have any control over the DID identity unless also included in the `rotationKeys` list. Best practice is to maintain separation between rotation keys and atproto signing keys. 37 + - `alsoKnownAs`: should include an `at://` URI indicating a handle (hostname) for the account. Note that the handle/DID mapping needs to be validated bi-directionally (via handle resolution), and needs to be re-verified periodically 38 + - `services`: an `atproto_pds` entry with an `AtprotoPersonalDataServer` type and http/https URL `endpoint` indicating the account's current PDS hostname. for example, `https://pds.example.com` (no `/xrpc/` suffix needed). 39 + 40 + ### Operation Serialization, Signing, and Validation 41 + 42 + There are a couple variations on the operation data object schema. The operations are also serialized both as simple JSON objects, or binary DAG-CBOR encoding for the purpose of hashing or signing. 43 + 44 + A regular creation or update operation contains the following fields: 45 + 46 + - `type` (string): with fixed value `plc_operation` 47 + - `rotationKeys` (array of strings): as described above 48 + - `verificationMethods` (mapping of string keys and values): as described above 49 + - `alsoKnownAs` (array of strings): as described above 50 + - `services` (mapping of string keys and object values): as described above 51 + - `prev` (string, nullable): a CID hash pointer to a previous operation if an update, or `null` for a creation. If `null`, the key should actually be part of the object, with value `null`, not simply omitted. In DAG-CBOR encoding, the CID is string-encoded, not a binary IPLD "Link" 52 + - `sig` (string): signature of the operation in `base64url` encoding 53 + 54 + A tombstone operation contains: 55 + 56 + - `type` (string): with fixed value `plc_tombstone` 57 + - `prev` (string): same as above, but not nullable 58 + - `sig` (string): signature of the operation (same as above) 59 + 60 + There is also a deprecated legacy operation format, supported *only* for creation ("genesis") operations: 61 + 62 + - `type` (string): with fixed value `create` 63 + - `signingKey` (string): single `did:key` value (not an array of strings) 64 + - `recoveryKey` (string): single `did:key` value (not an array of strings); and note "recovery" terminology, not "rotation" 65 + - `handle` (string): single value, indicating atproto handle, instead of `alsoKnownAs`. bare handle, with no `at://` prefix 66 + - `service` (string): single value, http/https URL of atproto PDS 67 + - `prev` (null): always include, but always with value `null` 68 + - `sig` (string): signature of the operation (same as above) 69 + 70 + Legacy `create` operations are stored in the PLC registry and may be returned in responses, so validating software needs to support that format. Conversion of the legacy format to "regular" operation format is relatively straight-forward, but there exist many `did:plc` identifiers where the DID identifier itself is based on the hash of the old format, so they will unfortunately be around forever. 71 + 72 + The process for signing and hashing operation objects is to first encode them in the DAG-CBOR binary serialization format. [DAG-CBOR](https://ipld.io/specs/codecs/dag-cbor/spec/) is a restricted subset of the Concise Binary Object Representation (CBOR), an IETF standard (RFC 8949), with semantics and value types similar to JSON. 73 + 74 + As an anti-abuse mechanism, operations have a maximum size when encoded as DAG-CBOR. The current limit is 7500 bytes. 75 + 76 + For signatures, the object is first encoded as DAG-CBOR *without* the `sig` field at all (as opposed to a `null` value in that field). Those bytes are signed, and then the signature bytes are encoded as a string using `base64url` encoding. The `sig` value is then populated with the string. In strongly typed programming languages it is a best practice to have distinct "signed" and "unsigned" types. 77 + 78 + When working with signatures, note that ECDSA signatures are not necessarily *deterministic* or *unique*. That is, the same key signing the same bytes *might* generate the same signature every time, or it might generate a *different* signature every time, depending on the cryptographic library and configuration. In some cases it is also easy for a third party to take a valid signature and transform it in to a new, distinct signature, which also validates. Be sure to always use the "validate signature" routine from a cryptographic library, instead of re-signing bytes and directly comparing the signature bytes. 79 + 80 + For `prev` references, the SHA-256 of the previous operation's bytes are encoded as a "[CID](https://github.com/multiformats/cid)", with the following parameters: 81 + 82 + - CIDv1 83 + - `base32` multibase encoding (prefix: `b`) 84 + - `dag-cbor` multibase type (code: 0x71) 85 + - `sha-256` multihash (code: 0x12) 86 + 87 + Rotation keys are serialized as strings using [did:key](https://w3c-ccg.github.io/did-method-key/), and only `secp256k1` ("k256") and NIST P-256 ("p256") are currently supported. 88 + 89 + The signing keys (`verificationMethods`) are also serialized using `did:key` in operations. When rendered in a DID document, signing keys are represented as objects, with the actual keys in multibase encoding, as required by the DID Core specification. 90 + 91 + The DID itself is derived from the hash of the first operation in the log, called the "genesis" operation. The signed operation is encoded in DAG-CBOR; the bytes are hashed with SHA-256; the hash bytes are `base32`-encoded (not hex encoded) as a string; and that string is truncated to 24 chars to yield the "identifier" segment of the DID. 92 + 93 + In pseudo-code: 94 + `did:plc:${base32Encode(sha256(createOp)).slice(0,24)}` 95 + 96 + ### Identifier Syntax 97 + 98 + The DID PLC method name is `plc`. The identifier part is 24 characters long, including only characters from the `base32` encoding set. An example is `did:plc:yk4dd2qkboz2yv6tpubpc6co`. This means: 99 + 100 + - the overall identifier length is 32 characters 101 + - the entire identifier is lower-case (and should be normalized to lower-case) 102 + - the entire identifier is ASCII, and includes only the characters `a-z`, `0-9`, and `:` (and does not even use digits `0189`) 103 + 104 + 105 + ### Key Rotation & Account Recovery 106 + 107 + Any key specified in `rotationKeys` has the ability to sign operations for the DID document. 108 + 109 + The set of rotation keys for a DID is not included in the DID document. They are an internal detail of PLC, and are stored in the operation log. 110 + 111 + Keys are listed in the `rotationKeys` field of operations in order of descending authority. 112 + 113 + The PLC server provides a 72hr window during which a higher authority rotation key can "rewrite" history, clobbering any operations (or chain of operations) signed by a lower-authority rotation key. 114 + 115 + To do so, that key must sign a new operation that points to the CID of the last "valid" operation - ie the fork point. 116 + The PLC server will accept this recovery operation as long as: 117 + 118 + - it is submitted within 72hrs of the referenced operation 119 + - the key used for the signature has a lower index in the `rotationKeys` array than the key that signed the to-be-invalidated operation 120 + 121 + 122 + ### Privacy and Security Concerns 123 + 124 + The full history of DID operations and updates, including timestamps, is permanently publicly accessible. This is true even after DID deactivation. It is important to recognize (and communicate to account holders) that any personally identifiable information (PII) encoded in `alsoKnownAs` URIs will be publicly visible even after DID deactivation, and can not be redacted or purged. 125 + 126 + In the context of atproto, this includes the full history of handle updates and PDS locations (URLs) over time. To be explicit, it does not include any other account metadata such as email addresses or IP addresses. Handle history could potentially de-anonymize account holders if they switch handles between a known identity and an anonymous or pseudonymous identity. 127 + 128 + The PLC server does not cross-validate `alsoKnownAs` or `service` entries in operations. This means that any DID can "claim" to have any identity, or to have an active account with any service (identified by URL). This data should *not* be trusted without bi-directionally verification, for example using handle resolution. 129 + 130 + The timestamp metadata encoded in the PLC audit log could be cross-verified against network traffic or other information to de-anonymize account holders. It also makes the "identity creation date" public. 131 + 132 + If "rotation" and "signing" keys are re-used across multiple account, it could reveal non-public identity details or relationships. For example, if two individuals cross-share rotation keys as a trusted backup, that information is public. If device-local recovery or signing keys are uniquely shared by two identifiers, that would indicate that those identities may actually be the same person. 133 + 134 + 135 + #### PLC Server Trust Model 136 + 137 + The PLC server has a public endpoint to receive operation objects from any client (without authentication). The server verifies operations, orders them according to recovery rules, and makes the log of operations publicly available. 138 + 139 + The operation log is self-certifying, and contains all the information needed to construct (or verify) the the current state of the DID document. 140 + 141 + Some trust is required in the PLC server. Its attacks are limited to: 142 + 143 + - Denial of service: rejecting valid operations, or refusing to serve some information about the DID 144 + - Misordering: In the event of a fork in DID document history, the server could choose to serve the "wrong" fork 145 + 146 + 147 + ### DID Creation 148 + 149 + To summarize the process of creating a new `did:plc` identifier: 150 + 151 + - collect values for all of the core data fields, including generating new secure key pairs if necessary 152 + - construct an "unsigned" regular operation object. include a `prev` field with `null` value. do not use the deprecated/legacy operation format for new DID creations 153 + - serialize the "unsigned" operation with DAG-CBOR, and sign the resulting bytes with one of the initial `rotationKeys`. encode the signature as `base64url`, and use that to construct a "signed" operation object 154 + - serialize the "signed" operation with DAG-CBOR, take the SHA-256 hash of those bytes, and encode the hash bytes in `base32`. use the first 24 characters to generate DID value (`did:plc:<hashchars>`) 155 + - serialize the "signed" operation as simple JSON, and submit it via HTTP POST to `https://plc.directory/:did` 156 + - if the HTTP status code is successful, the DID has been registered 157 + 158 + When "signing" using a "`rotationKey`", what is meant is to sign using the private key associated the public key in the `rotationKey` list. 159 + 160 + ### DID Update 161 + 162 + To summarize the process of updating a new `did:plc` identifier: 163 + 164 + - if the current DID state isn't known, fetch the current state from `https://plc.directory/:did/data` 165 + - if the most recent valid DID operation CID (hash) isn't known, fetch the audit log from `https://plc.directory/:did/log/audit`, identify the most recent valid operation, and get the `cid` value. if this is a recovery operation, the relevant "valid" operation to fork from may not be the most recent in the audit log 166 + - collect updated values for all of the core data fields, including generating new secure key pairs if necessary (eg, key rotation) 167 + - construct an "unsigned" regular operation object. include a `prev` field with the CID (hash) of the previous valid operation 168 + - serialize the "unsigned" operation with DAG-CBOR, and sign the resulting bytes with one of the previously-existing `rotationKeys`. encode the signature as `base64url`, and use that to construct a "signed" operation object 169 + - serialize the "signed" operation as simple JSON, and submit it via HTTP POST to `https://plc.directory/:did` 170 + - if the HTTP status code is successful, the DID has been updated 171 + - the DID update may be nullified by a "rotation" operation during the recovery window (currently 72hr) 172 + 173 + ### DID Deactivation 174 + 175 + To summarize the process of de-activating an existing `did:plc` identifier: 176 + 177 + - if the most recent valid DID operation CID (hash) isn't known, fetch the audit log from `https://plc.directory/:did/log/audit`, identify the most recent valid operation, and get the `cid` value 178 + - construct an "unsigned" tombstone operation object. include a `prev` field with the CID (hash) of the previous valid operation 179 + - serialize the "unsigned" tombstone operation with DAG-CBOR, and sign the resulting bytes with one of the previously-existing `rotationKeys`. encode the signature as `base64url`, and use that to construct a "signed" tombstone operation object 180 + - serialize the "signed" tombstone operation as simple JSON, and submit it via HTTP POST to `https://plc.directory/:did` 181 + - if the HTTP status code is successful, the DID has been deactivated 182 + - the DID deactivation may be nullified by a "rotation" operation during the recovery window (currently 72hr) 183 + 184 + ### DID Resolution 185 + 186 + PLC DIDs are resolved to a DID document (JSON) by making simple HTTP GET request to the PLC server. The resolution endpoint is: `https://plc.directory/:did` 187 + 188 + The PLC-specific state data (based on the most recent operation) can be fetched as a JSON object at: `https://plc.directory/:did/data` 189 + 190 + 191 + ### Audit Logs 192 + 193 + As an additional check against abuse by the PLC server, and to promote resiliency, the set of all identifiers is enumerable, and the set of all operations for all identifiers (even "nullified" operations) can be enumerated and audited. 194 + 195 + The log of currently-valid operations for a given DID, as JSON, can be found at: `https://plc.directory/:did/log/audit` 196 + 197 + The audit history of a given DID (complete with timestamps and invalidated forked histories), as JSON, can be found at: `https://plc.directory/:did/log/audit` 198 + 199 + To fully validate a DID document against the operation log: 200 + 201 + - fetch the full audit log 202 + - for the genesis operation, validate the DID 203 + - note that the genesis operation may be in deprecated/legacy format, and should be encoded and verified in that format 204 + - see the "DID Creation" section above for details 205 + - for each operation in the log, validate signatures: 206 + - identify the set of valid `rotationKeys` at that point of time: either the initial keys for a "genesis" operation, or the keys in the `prev` operation 207 + - remove any `sig` field and serialize the "unsigned" operation with DAG-CBOR, yielding bytes 208 + - decode the `base64url` `sig` field to bytes 209 + - for each of the `rotationKeys`, attempt to verify the signature against the "unsigned" bytes 210 + - if no key matches, there has been a trust violation; the PLC server should never have accepted the operation 211 + - verify the correctness of "nullified" operations and the current active operation log using the rules around rotation keys and recovery windows 212 + 213 + The complete log of operations for all DIDs on the PLC server can be enumerated efficiently: 214 + 215 + - HTTP endpoint: `https://plc.directory/export` 216 + - output format: [JSON lines](https://jsonlines.org/) 217 + - `count` query parameter, as an integer, maximum 1000 lines per request 218 + - `after` query parameter, based on `createdAt` timestamp, for pagination 219 + 220 + 221 + ## Example 222 + 223 + ```ts 224 + // note: we use shorthand for keys for ease of reference, but consider them valid did:keys 225 + 226 + // Genesis operation 227 + const genesisOp = { 228 + type: 'plc_operation', 229 + verificationMethods: { 230 + atproto: "did:key:zSigningKey" 231 + }, 232 + rotationKeys: [ 233 + "did:key:zRecoveryKey", 234 + "did:key:zRotationKey" 235 + ], 236 + alsoKnownAs: [ 237 + "at://alice.test" 238 + ], 239 + services: { 240 + atproto_pds: { 241 + type: "AtprotoPersonalDataServer", 242 + endpoint: "https://example.test" 243 + } 244 + }, 245 + prev: null, 246 + sig: 'sig_from_did:key:zRotationKey' 247 + } 248 + 249 + // Operation to update recovery key 250 + const updateKeys = { 251 + type: 'plc_operation', 252 + verificationMethods: { 253 + atproto: "did:key:zSigningKey" 254 + }, 255 + rotationKeys: [ 256 + "did:key:zNewRecoveryKey", 257 + "did:key:zRotationKey" 258 + ], 259 + alsoKnownAs: [ 260 + "at://alice.test" 261 + ], 262 + services: { 263 + atproto_pds: { 264 + type: "AtprotoPersonalDataServer", 265 + endpoint: "https://example.test" 266 + } 267 + }, 268 + prev: CID(genesisOp), 269 + sig: 'sig_from_did:key:zRotationKey' 270 + } 271 + 272 + // Invalid operation that will be rejected 273 + // because did:key:zAttackerKey is not listed in rotationKeys 274 + const invalidUpdate = { 275 + type: 'plc_operation', 276 + verificationMethods: { 277 + atproto: "did:key:zAttackerKey" 278 + }, 279 + rotationKeys: [ 280 + "did:key:zAttackerKey" 281 + ], 282 + alsoKnownAs: [ 283 + "at://bob.test" 284 + ], 285 + services: { 286 + atproto_pds: { 287 + type: "AtprotoPersonalDataServer", 288 + endpoint: "https://example.test" 289 + } 290 + }, 291 + prev: CID(updateKeys), 292 + sig: 'sig_from_did:key:zAttackerKey' 293 + } 294 + 295 + // Valid recovery operation that "undoes" updateKeys 296 + const recoveryOp = { 297 + type: 'plc_operation', 298 + verificationMethods: { 299 + atproto: "did:key:zSigningKey" 300 + }, 301 + rotationKeys: [ 302 + "did:key:zRecoveryKey" 303 + ], 304 + alsoKnownAs: [ 305 + "at://alice.test" 306 + ], 307 + services: { 308 + atproto_pds: { 309 + type: "AtprotoPersonalDataServer", 310 + endpoint: "https://example.test" 311 + } 312 + }, 313 + prev: CID(genesisOp), 314 + sig: 'sig_from_did:key:zRecoveryKey' 315 + } 316 + ``` 317 + 318 + ## Presentation as DID Document 319 + 320 + The following data: 321 + 322 + ```ts 323 + { 324 + did: 'did:plc:7iza6de2dwap2sbkpav7c6c6', 325 + verificationMethods: { 326 + atproto: 'did:key:zDnaeh9v2RmcMo13Du2d6pjUf5bZwtauYxj3n9dYjw4EZUAR7' 327 + }, 328 + rotationKeys: [ 329 + 'did:key:zDnaedvvAsDE6H3BDdBejpx9ve2Tz95cymyCAKF66JbyMh1Lt', 330 + 'did:key:zDnaeh9v2RmcMo13Du2d6pjUf5bZwtauYxj3n9dYjw4EZUAR7' 331 + ], 332 + alsoKnownAs: [ 333 + 'at://alice.test' 334 + ], 335 + services: { 336 + atproto_pds: { 337 + type: "AtprotoPersonalDataServer", 338 + endpoint: "https://example.test" 339 + } 340 + } 341 + } 342 + ``` 343 + 344 + Will be presented as the following DID document: 345 + 346 + ```ts 347 + { 348 + '@context': [ 349 + 'https://www.w3.org/ns/did/v1', 350 + 'https://w3id.org/security/suites/ecdsa-2019/v1' 351 + ], 352 + id: 'did:plc:7iza6de2dwap2sbkpav7c6c6', 353 + alsoKnownAs: [ 'at://alice.test' ], 354 + verificationMethod: [ 355 + { 356 + id: '#atproto', 357 + type: 'EcdsaSecp256r1VerificationKey2019', 358 + controller: 'did:plc:7iza6de2dwap2sbkpav7c6c6', 359 + publicKeyMultibase: 'zSSa7w8s5aApu6td45gWTAAFkqCnaWY6ZsJ8DpyzDdYmVy4fARKqbn5F1UYBUMeVvYTBsoSoLvZnPdjd3pVHbmAHP' 360 + } 361 + ], 362 + service: [ 363 + { 364 + id: '#atproto_pds', 365 + type: 'AtprotoPersonalDataServer', 366 + serviceEndpoint: 'https://example2.com' 367 + } 368 + ] 369 + } 370 + ``` 371 + 372 + ## Possible Future Changes 373 + 374 + The set of allowed ("blessed") public key cryptographic algorithms (aka, curves) may expanded over time, slowly. Likewise, support for additional blessed CID types and parameters may be expanded over time, slowly. 375 + 376 + The recovery time window may become configurable, within constraints, as part of the DID metadata itself. 377 + 378 + Support for "DID Controller Delegation" could be useful (eg, in the context of atproto PDS hosts), and may be incorporated. 379 + 380 + In the context of atproto, support for multiple handles for the same DID is being considered, with a single primary handle. But no final decision has been made yet. 381 + 382 + We welcome proposals for small additions to make `did:plc` more generic and reusable for applications other than atproto. But no promises: atproto will remain the focus for the near future. 383 + 384 + We are enthusiastic about the prospect of moving governance of the `did:plc` method, and operation of registry servers, out of the sole control of Bluesky PBC. Audit log snapshots, mirroring, and automated third-party auditing have all been considered as mechanisms to mitigate the centralized nature of the PLC server. 385 + 386 + The size of the `verificationMethods`, `alsoKnownAs`, and `service` mappings/arrays may be specifically constrained. And the maximum DAG-CBOR size may be constrained. 387 + 388 + As an anti-abuse mechanisms, the PLC server load balancer restricts the number of HTTP requests per time window. The limits are generous, and operating large services or scraping the operation log should not run in to limits. Specific per-DID limits on operation rate may be introduced over time. For example, no more than N operations per DID per rotation key per 24 hour window. 389 + 390 + A "DID PLC history explorer" web interface would make the public nature of the DID audit log more publicly understandable. 391 + 392 + It is concievable that longer DID PLCs, with more of the SHA-256 characters, will be supported in the future. It is also concievable that a different hash algorithm would be allowed. Any such changes would allow existing DIDs in their existing syntax to continue being used.
+3
go-didplc/cmd/webplc/static/.well-known/security.txt
··· 1 + Contact: mailto:security@bsky.app 2 + Preferred-Languages: en 3 + Canonical: https://web.plc.directory/.well-known/security.txt
+5
go-didplc/cmd/webplc/static/pico.min.css
··· 1 + @charset "UTF-8";/*! 2 + * Pico CSS v1.5.10 (https://picocss.com) 3 + * Copyright 2019-2023 - Licensed under MIT 4 + */:root{--font-family:system-ui,-apple-system,"Segoe UI","Roboto","Ubuntu","Cantarell","Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--line-height:1.5;--font-weight:400;--font-size:16px;--border-radius:0.25rem;--border-width:1px;--outline-width:3px;--spacing:1rem;--typography-spacing-vertical:1.5rem;--block-spacing-vertical:calc(var(--spacing) * 2);--block-spacing-horizontal:var(--spacing);--grid-spacing-vertical:0;--grid-spacing-horizontal:var(--spacing);--form-element-spacing-vertical:0.75rem;--form-element-spacing-horizontal:1rem;--nav-element-spacing-vertical:1rem;--nav-element-spacing-horizontal:0.5rem;--nav-link-spacing-vertical:0.5rem;--nav-link-spacing-horizontal:0.5rem;--form-label-font-weight:var(--font-weight);--transition:0.2s ease-in-out;--modal-overlay-backdrop-filter:blur(0.25rem)}@media (min-width:576px){:root{--font-size:17px}}@media (min-width:768px){:root{--font-size:18px}}@media (min-width:992px){:root{--font-size:19px}}@media (min-width:1200px){:root{--font-size:20px}}@media (min-width:576px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 2.5)}}@media (min-width:768px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 3)}}@media (min-width:992px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 3.5)}}@media (min-width:1200px){body>footer,body>header,body>main,section{--block-spacing-vertical:calc(var(--spacing) * 4)}}@media (min-width:576px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.25)}}@media (min-width:768px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.5)}}@media (min-width:992px){article{--block-spacing-horizontal:calc(var(--spacing) * 1.75)}}@media (min-width:1200px){article{--block-spacing-horizontal:calc(var(--spacing) * 2)}}dialog>article{--block-spacing-vertical:calc(var(--spacing) * 2);--block-spacing-horizontal:var(--spacing)}@media (min-width:576px){dialog>article{--block-spacing-vertical:calc(var(--spacing) * 2.5);--block-spacing-horizontal:calc(var(--spacing) * 1.25)}}@media (min-width:768px){dialog>article{--block-spacing-vertical:calc(var(--spacing) * 3);--block-spacing-horizontal:calc(var(--spacing) * 1.5)}}a{--text-decoration:none}a.contrast,a.secondary{--text-decoration:underline}small{--font-size:0.875em}h1,h2,h3,h4,h5,h6{--font-weight:700}h1{--font-size:2rem;--typography-spacing-vertical:3rem}h2{--font-size:1.75rem;--typography-spacing-vertical:2.625rem}h3{--font-size:1.5rem;--typography-spacing-vertical:2.25rem}h4{--font-size:1.25rem;--typography-spacing-vertical:1.874rem}h5{--font-size:1.125rem;--typography-spacing-vertical:1.6875rem}[type=checkbox],[type=radio]{--border-width:2px}[type=checkbox][role=switch]{--border-width:3px}tfoot td,tfoot th,thead td,thead th{--border-width:3px}:not(thead,tfoot)>*>td{--font-size:0.875em}code,kbd,pre,samp{--font-family:"Menlo","Consolas","Roboto Mono","Ubuntu Monospace","Noto Mono","Oxygen Mono","Liberation Mono",monospace,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"}kbd{--font-weight:bolder}:root:not([data-theme=dark]),[data-theme=light]{--background-color:#fff;--color:hsl(205, 20%, 32%);--h1-color:hsl(205, 30%, 15%);--h2-color:#24333e;--h3-color:hsl(205, 25%, 23%);--h4-color:#374956;--h5-color:hsl(205, 20%, 32%);--h6-color:#4d606d;--muted-color:hsl(205, 10%, 50%);--muted-border-color:hsl(205, 20%, 94%);--primary:hsl(195, 85%, 41%);--primary-hover:hsl(195, 90%, 32%);--primary-focus:rgba(16, 149, 193, 0.125);--primary-inverse:#fff;--secondary:hsl(205, 15%, 41%);--secondary-hover:hsl(205, 20%, 32%);--secondary-focus:rgba(89, 107, 120, 0.125);--secondary-inverse:#fff;--contrast:hsl(205, 30%, 15%);--contrast-hover:#000;--contrast-focus:rgba(89, 107, 120, 0.125);--contrast-inverse:#fff;--mark-background-color:#fff2ca;--mark-color:#543a26;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:transparent;--form-element-border-color:hsl(205, 14%, 68%);--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:transparent;--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205, 18%, 86%);--form-element-disabled-border-color:hsl(205, 14%, 68%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#c62828;--form-element-invalid-active-border-color:#d32f2f;--form-element-invalid-focus-color:rgba(211, 47, 47, 0.125);--form-element-valid-border-color:#388e3c;--form-element-valid-active-border-color:#43a047;--form-element-valid-focus-color:rgba(67, 160, 71, 0.125);--switch-background-color:hsl(205, 16%, 77%);--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:hsl(205, 18%, 86%);--range-active-border-color:hsl(205, 16%, 77%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:#f6f8f9;--code-background-color:hsl(205, 20%, 94%);--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330, 40%, 50%);--code-property-color:hsl(185, 40%, 40%);--code-value-color:hsl(40, 20%, 50%);--code-comment-color:hsl(205, 14%, 68%);--accordion-border-color:var(--muted-border-color);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:var(--background-color);--card-border-color:var(--muted-border-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(27, 40, 50, 0.01698),0.0335rem 0.067rem 0.402rem rgba(27, 40, 50, 0.024),0.0625rem 0.125rem 0.75rem rgba(27, 40, 50, 0.03),0.1125rem 0.225rem 1.35rem rgba(27, 40, 50, 0.036),0.2085rem 0.417rem 2.502rem rgba(27, 40, 50, 0.04302),0.5rem 1rem 6rem rgba(27, 40, 50, 0.06),0 0 0 0.0625rem rgba(27, 40, 50, 0.015);--card-sectionning-background-color:#fbfbfc;--dropdown-background-color:#fbfbfc;--dropdown-border-color:#e1e6eb;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:hsl(205, 20%, 94%);--modal-overlay-background-color:rgba(213, 220, 226, 0.7);--progress-background-color:hsl(205, 18%, 86%);--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(198, 40, 40)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(65, 84, 98)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(56, 142, 60)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");color-scheme:light}@media only screen and (prefers-color-scheme:dark){:root:not([data-theme]){--background-color:#11191f;--color:hsl(205, 16%, 77%);--h1-color:hsl(205, 20%, 94%);--h2-color:#e1e6eb;--h3-color:hsl(205, 18%, 86%);--h4-color:#c8d1d8;--h5-color:hsl(205, 16%, 77%);--h6-color:#afbbc4;--muted-color:hsl(205, 10%, 50%);--muted-border-color:#1f2d38;--primary:hsl(195, 85%, 41%);--primary-hover:hsl(195, 80%, 50%);--primary-focus:rgba(16, 149, 193, 0.25);--primary-inverse:#fff;--secondary:hsl(205, 15%, 41%);--secondary-hover:hsl(205, 10%, 50%);--secondary-focus:rgba(115, 130, 140, 0.25);--secondary-inverse:#fff;--contrast:hsl(205, 20%, 94%);--contrast-hover:#fff;--contrast-focus:rgba(115, 130, 140, 0.25);--contrast-inverse:#000;--mark-background-color:#d1c284;--mark-color:#11191f;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:#11191f;--form-element-border-color:#374956;--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:var(--form-element-background-color);--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205, 25%, 23%);--form-element-disabled-border-color:hsl(205, 20%, 32%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#b71c1c;--form-element-invalid-active-border-color:#c62828;--form-element-invalid-focus-color:rgba(198, 40, 40, 0.25);--form-element-valid-border-color:#2e7d32;--form-element-valid-active-border-color:#388e3c;--form-element-valid-focus-color:rgba(56, 142, 60, 0.25);--switch-background-color:#374956;--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:#24333e;--range-active-border-color:hsl(205, 25%, 23%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:rgba(115, 130, 140, 0.05);--code-background-color:#18232c;--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330, 30%, 50%);--code-property-color:hsl(185, 30%, 50%);--code-value-color:hsl(40, 10%, 50%);--code-comment-color:#4d606d;--accordion-border-color:var(--muted-border-color);--accordion-active-summary-color:var(--primary);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:#141e26;--card-border-color:var(--card-background-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698),0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024),0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03),0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036),0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302),0.5rem 1rem 6rem rgba(0, 0, 0, 0.06),0 0 0 0.0625rem rgba(0, 0, 0, 0.015);--card-sectionning-background-color:#18232c;--dropdown-background-color:hsl(205, 30%, 15%);--dropdown-border-color:#24333e;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:rgba(36, 51, 62, 0.75);--modal-overlay-background-color:rgba(36, 51, 62, 0.8);--progress-background-color:#24333e;--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(0, 0, 0)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(183, 28, 28)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(46, 125, 50)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");color-scheme:dark}}[data-theme=dark]{--background-color:#11191f;--color:hsl(205, 16%, 77%);--h1-color:hsl(205, 20%, 94%);--h2-color:#e1e6eb;--h3-color:hsl(205, 18%, 86%);--h4-color:#c8d1d8;--h5-color:hsl(205, 16%, 77%);--h6-color:#afbbc4;--muted-color:hsl(205, 10%, 50%);--muted-border-color:#1f2d38;--primary:hsl(195, 85%, 41%);--primary-hover:hsl(195, 80%, 50%);--primary-focus:rgba(16, 149, 193, 0.25);--primary-inverse:#fff;--secondary:hsl(205, 15%, 41%);--secondary-hover:hsl(205, 10%, 50%);--secondary-focus:rgba(115, 130, 140, 0.25);--secondary-inverse:#fff;--contrast:hsl(205, 20%, 94%);--contrast-hover:#fff;--contrast-focus:rgba(115, 130, 140, 0.25);--contrast-inverse:#000;--mark-background-color:#d1c284;--mark-color:#11191f;--ins-color:#388e3c;--del-color:#c62828;--blockquote-border-color:var(--muted-border-color);--blockquote-footer-color:var(--muted-color);--button-box-shadow:0 0 0 rgba(0, 0, 0, 0);--button-hover-box-shadow:0 0 0 rgba(0, 0, 0, 0);--form-element-background-color:#11191f;--form-element-border-color:#374956;--form-element-color:var(--color);--form-element-placeholder-color:var(--muted-color);--form-element-active-background-color:var(--form-element-background-color);--form-element-active-border-color:var(--primary);--form-element-focus-color:var(--primary-focus);--form-element-disabled-background-color:hsl(205, 25%, 23%);--form-element-disabled-border-color:hsl(205, 20%, 32%);--form-element-disabled-opacity:0.5;--form-element-invalid-border-color:#b71c1c;--form-element-invalid-active-border-color:#c62828;--form-element-invalid-focus-color:rgba(198, 40, 40, 0.25);--form-element-valid-border-color:#2e7d32;--form-element-valid-active-border-color:#388e3c;--form-element-valid-focus-color:rgba(56, 142, 60, 0.25);--switch-background-color:#374956;--switch-color:var(--primary-inverse);--switch-checked-background-color:var(--primary);--range-border-color:#24333e;--range-active-border-color:hsl(205, 25%, 23%);--range-thumb-border-color:var(--background-color);--range-thumb-color:var(--secondary);--range-thumb-hover-color:var(--secondary-hover);--range-thumb-active-color:var(--primary);--table-border-color:var(--muted-border-color);--table-row-stripped-background-color:rgba(115, 130, 140, 0.05);--code-background-color:#18232c;--code-color:var(--muted-color);--code-kbd-background-color:var(--contrast);--code-kbd-color:var(--contrast-inverse);--code-tag-color:hsl(330, 30%, 50%);--code-property-color:hsl(185, 30%, 50%);--code-value-color:hsl(40, 10%, 50%);--code-comment-color:#4d606d;--accordion-border-color:var(--muted-border-color);--accordion-active-summary-color:var(--primary);--accordion-close-summary-color:var(--color);--accordion-open-summary-color:var(--muted-color);--card-background-color:#141e26;--card-border-color:var(--card-background-color);--card-box-shadow:0.0145rem 0.029rem 0.174rem rgba(0, 0, 0, 0.01698),0.0335rem 0.067rem 0.402rem rgba(0, 0, 0, 0.024),0.0625rem 0.125rem 0.75rem rgba(0, 0, 0, 0.03),0.1125rem 0.225rem 1.35rem rgba(0, 0, 0, 0.036),0.2085rem 0.417rem 2.502rem rgba(0, 0, 0, 0.04302),0.5rem 1rem 6rem rgba(0, 0, 0, 0.06),0 0 0 0.0625rem rgba(0, 0, 0, 0.015);--card-sectionning-background-color:#18232c;--dropdown-background-color:hsl(205, 30%, 15%);--dropdown-border-color:#24333e;--dropdown-box-shadow:var(--card-box-shadow);--dropdown-color:var(--color);--dropdown-hover-background-color:rgba(36, 51, 62, 0.75);--modal-overlay-background-color:rgba(36, 51, 62, 0.8);--progress-background-color:#24333e;--progress-color:var(--primary);--loading-spinner-opacity:0.5;--tooltip-background-color:var(--contrast);--tooltip-color:var(--contrast-inverse);--icon-checkbox:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-chevron-button-inverse:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(0, 0, 0)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");--icon-close:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(115, 130, 140)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='18' y1='6' x2='6' y2='18'%3E%3C/line%3E%3Cline x1='6' y1='6' x2='18' y2='18'%3E%3C/line%3E%3C/svg%3E");--icon-date:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'%3E%3C/rect%3E%3Cline x1='16' y1='2' x2='16' y2='6'%3E%3C/line%3E%3Cline x1='8' y1='2' x2='8' y2='6'%3E%3C/line%3E%3Cline x1='3' y1='10' x2='21' y2='10'%3E%3C/line%3E%3C/svg%3E");--icon-invalid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(183, 28, 28)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E");--icon-minus:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(255, 255, 255)' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='5' y1='12' x2='19' y2='12'%3E%3C/line%3E%3C/svg%3E");--icon-search:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'%3E%3C/circle%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'%3E%3C/line%3E%3C/svg%3E");--icon-time:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(162, 175, 185)' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cpolyline points='12 6 12 12 16 14'%3E%3C/polyline%3E%3C/svg%3E");--icon-valid:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='rgb(46, 125, 50)' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'%3E%3C/polyline%3E%3C/svg%3E");color-scheme:dark}[type=checkbox],[type=radio],[type=range],progress{accent-color:var(--primary)}*,::after,::before{box-sizing:border-box;background-repeat:no-repeat}::after,::before{text-decoration:inherit;vertical-align:inherit}:where(:root){-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%;-moz-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--background-color);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);line-height:var(--line-height);font-family:var(--font-family);text-rendering:optimizeLegibility;overflow-wrap:break-word;cursor:default;-moz-tab-size:4;-o-tab-size:4;tab-size:4}main{display:block}body{width:100%;margin:0}body>footer,body>header,body>main{width:100%;margin-right:auto;margin-left:auto;padding:var(--block-spacing-vertical) 0}.container,.container-fluid{width:100%;margin-right:auto;margin-left:auto;padding-right:var(--spacing);padding-left:var(--spacing)}@media (min-width:576px){.container{max-width:510px;padding-right:0;padding-left:0}}@media (min-width:768px){.container{max-width:700px}}@media (min-width:992px){.container{max-width:920px}}@media (min-width:1200px){.container{max-width:1130px}}section{margin-bottom:var(--block-spacing-vertical)}.grid{grid-column-gap:var(--grid-spacing-horizontal);grid-row-gap:var(--grid-spacing-vertical);display:grid;grid-template-columns:1fr;margin:0}@media (min-width:992px){.grid{grid-template-columns:repeat(auto-fit,minmax(0%,1fr))}}.grid>*{min-width:0}figure{display:block;margin:0;padding:0;overflow-x:auto}figure figcaption{padding:calc(var(--spacing) * .5) 0;color:var(--muted-color)}b,strong{font-weight:bolder}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}address,blockquote,dl,figure,form,ol,p,pre,table,ul{margin-top:0;margin-bottom:var(--typography-spacing-vertical);color:var(--color);font-style:normal;font-weight:var(--font-weight);font-size:var(--font-size)}[role=link],a{--color:var(--primary);--background-color:transparent;outline:0;background-color:var(--background-color);color:var(--color);-webkit-text-decoration:var(--text-decoration);text-decoration:var(--text-decoration);transition:background-color var(--transition),color var(--transition),box-shadow var(--transition),-webkit-text-decoration var(--transition);transition:background-color var(--transition),color var(--transition),text-decoration var(--transition),box-shadow var(--transition);transition:background-color var(--transition),color var(--transition),text-decoration var(--transition),box-shadow var(--transition),-webkit-text-decoration var(--transition)}[role=link]:is([aria-current],:hover,:active,:focus),a:is([aria-current],:hover,:active,:focus){--color:var(--primary-hover);--text-decoration:underline}[role=link]:focus,a:focus{--background-color:var(--primary-focus)}[role=link].secondary,a.secondary{--color:var(--secondary)}[role=link].secondary:is([aria-current],:hover,:active,:focus),a.secondary:is([aria-current],:hover,:active,:focus){--color:var(--secondary-hover)}[role=link].secondary:focus,a.secondary:focus{--background-color:var(--secondary-focus)}[role=link].contrast,a.contrast{--color:var(--contrast)}[role=link].contrast:is([aria-current],:hover,:active,:focus),a.contrast:is([aria-current],:hover,:active,:focus){--color:var(--contrast-hover)}[role=link].contrast:focus,a.contrast:focus{--background-color:var(--contrast-focus)}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:var(--typography-spacing-vertical);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);font-family:var(--font-family)}h1{--color:var(--h1-color)}h2{--color:var(--h2-color)}h3{--color:var(--h3-color)}h4{--color:var(--h4-color)}h5{--color:var(--h5-color)}h6{--color:var(--h6-color)}:where(address,blockquote,dl,figure,form,ol,p,pre,table,ul)~:is(h1,h2,h3,h4,h5,h6){margin-top:var(--typography-spacing-vertical)}.headings,hgroup{margin-bottom:var(--typography-spacing-vertical)}.headings>*,hgroup>*{margin-bottom:0}.headings>:last-child,hgroup>:last-child{--color:var(--muted-color);--font-weight:unset;font-size:1rem;font-family:unset}p{margin-bottom:var(--typography-spacing-vertical)}small{font-size:var(--font-size)}:where(dl,ol,ul){padding-right:0;padding-left:var(--spacing);-webkit-padding-start:var(--spacing);padding-inline-start:var(--spacing);-webkit-padding-end:0;padding-inline-end:0}:where(dl,ol,ul) li{margin-bottom:calc(var(--typography-spacing-vertical) * .25)}:where(dl,ol,ul) :is(dl,ol,ul){margin:0;margin-top:calc(var(--typography-spacing-vertical) * .25)}ul li{list-style:square}mark{padding:.125rem .25rem;background-color:var(--mark-background-color);color:var(--mark-color);vertical-align:baseline}blockquote{display:block;margin:var(--typography-spacing-vertical) 0;padding:var(--spacing);border-right:none;border-left:.25rem solid var(--blockquote-border-color);-webkit-border-start:0.25rem solid var(--blockquote-border-color);border-inline-start:0.25rem solid var(--blockquote-border-color);-webkit-border-end:none;border-inline-end:none}blockquote footer{margin-top:calc(var(--typography-spacing-vertical) * .5);color:var(--blockquote-footer-color)}abbr[title]{border-bottom:1px dotted;text-decoration:none;cursor:help}ins{color:var(--ins-color);text-decoration:none}del{color:var(--del-color)}::-moz-selection{background-color:var(--primary-focus)}::selection{background-color:var(--primary-focus)}:where(audio,canvas,iframe,img,svg,video){vertical-align:middle}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}:where(iframe){border-style:none}img{max-width:100%;height:auto;border-style:none}:where(svg:not([fill])){fill:currentColor}svg:not(:root){overflow:hidden}button{margin:0;overflow:visible;font-family:inherit;text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}button{display:block;width:100%;margin-bottom:var(--spacing)}[role=button]{display:inline-block;text-decoration:none}[role=button],button,input[type=button],input[type=reset],input[type=submit]{--background-color:var(--primary);--border-color:var(--primary);--color:var(--primary-inverse);--box-shadow:var(--button-box-shadow, 0 0 0 rgba(0, 0, 0, 0));padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[role=button]:is([aria-current],:hover,:active,:focus),button:is([aria-current],:hover,:active,:focus),input[type=button]:is([aria-current],:hover,:active,:focus),input[type=reset]:is([aria-current],:hover,:active,:focus),input[type=submit]:is([aria-current],:hover,:active,:focus){--background-color:var(--primary-hover);--border-color:var(--primary-hover);--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0));--color:var(--primary-inverse)}[role=button]:focus,button:focus,input[type=button]:focus,input[type=reset]:focus,input[type=submit]:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--primary-focus)}:is(button,input[type=submit],input[type=button],[role=button]).secondary,input[type=reset]{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);cursor:pointer}:is(button,input[type=submit],input[type=button],[role=button]).secondary:is([aria-current],:hover,:active,:focus),input[type=reset]:is([aria-current],:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover);--color:var(--secondary-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).secondary:focus,input[type=reset]:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--secondary-focus)}:is(button,input[type=submit],input[type=button],[role=button]).contrast{--background-color:var(--contrast);--border-color:var(--contrast);--color:var(--contrast-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).contrast:is([aria-current],:hover,:active,:focus){--background-color:var(--contrast-hover);--border-color:var(--contrast-hover);--color:var(--contrast-inverse)}:is(button,input[type=submit],input[type=button],[role=button]).contrast:focus{--box-shadow:var(--button-hover-box-shadow, 0 0 0 rgba(0, 0, 0, 0)),0 0 0 var(--outline-width) var(--contrast-focus)}:is(button,input[type=submit],input[type=button],[role=button]).outline,input[type=reset].outline{--background-color:transparent;--color:var(--primary)}:is(button,input[type=submit],input[type=button],[role=button]).outline:is([aria-current],:hover,:active,:focus),input[type=reset].outline:is([aria-current],:hover,:active,:focus){--background-color:transparent;--color:var(--primary-hover)}:is(button,input[type=submit],input[type=button],[role=button]).outline.secondary,input[type=reset].outline{--color:var(--secondary)}:is(button,input[type=submit],input[type=button],[role=button]).outline.secondary:is([aria-current],:hover,:active,:focus),input[type=reset].outline:is([aria-current],:hover,:active,:focus){--color:var(--secondary-hover)}:is(button,input[type=submit],input[type=button],[role=button]).outline.contrast{--color:var(--contrast)}:is(button,input[type=submit],input[type=button],[role=button]).outline.contrast:is([aria-current],:hover,:active,:focus){--color:var(--contrast-hover)}:where(button,[type=submit],[type=button],[type=reset],[role=button])[disabled],:where(fieldset[disabled]) :is(button,[type=submit],[type=button],[type=reset],[role=button]),a[role=button]:not([href]){opacity:.5;pointer-events:none}input,optgroup,select,textarea{margin:0;font-size:1rem;line-height:var(--line-height);font-family:inherit;letter-spacing:inherit}input{overflow:visible}select{text-transform:none}legend{max-width:100%;padding:0;color:inherit;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{padding:0}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}::-moz-focus-inner{padding:0;border-style:none}:-moz-focusring{outline:0}:-moz-ui-invalid{box-shadow:none}::-ms-expand{display:none}[type=file],[type=range]{padding:0;border-width:0}input:not([type=checkbox],[type=radio],[type=range]){height:calc(1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + var(--border-width) * 2)}fieldset{margin:0;margin-bottom:var(--spacing);padding:0;border:0}fieldset legend,label{display:block;margin-bottom:calc(var(--spacing) * .25);font-weight:var(--form-label-font-weight,var(--font-weight))}input:not([type=checkbox],[type=radio]),select,textarea{width:100%}input:not([type=checkbox],[type=radio],[type=range],[type=file]),select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal)}input,select,textarea{--background-color:var(--form-element-background-color);--border-color:var(--form-element-border-color);--color:var(--form-element-color);--box-shadow:none;border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}:where(select,textarea):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[type=checkbox],[type=radio],[readonly]):is(:active,:focus){--background-color:var(--form-element-active-background-color)}:where(select,textarea):is(:active,:focus),input:not([type=submit],[type=button],[type=reset],[role=switch],[readonly]):is(:active,:focus){--border-color:var(--form-element-active-border-color)}input:not([type=submit],[type=button],[type=reset],[type=range],[type=file],[readonly]):focus,select:focus,textarea:focus{--box-shadow:0 0 0 var(--outline-width) var(--form-element-focus-color)}:where(fieldset[disabled]) :is(input:not([type=submit],[type=button],[type=reset]),select,textarea),input:not([type=submit],[type=button],[type=reset])[disabled],select[disabled],textarea[disabled]{--background-color:var(--form-element-disabled-background-color);--border-color:var(--form-element-disabled-border-color);opacity:var(--form-element-disabled-opacity);pointer-events:none}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week])[aria-invalid]{padding-right:calc(var(--form-element-spacing-horizontal) + 1.5rem)!important;padding-left:var(--form-element-spacing-horizontal);-webkit-padding-start:var(--form-element-spacing-horizontal)!important;padding-inline-start:var(--form-element-spacing-horizontal)!important;-webkit-padding-end:calc(var(--form-element-spacing-horizontal) + 1.5rem)!important;padding-inline-end:calc(var(--form-element-spacing-horizontal) + 1.5rem)!important;background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week])[aria-invalid=false]{background-image:var(--icon-valid)}:where(input,select,textarea):not([type=checkbox],[type=radio],[type=date],[type=datetime-local],[type=month],[type=time],[type=week])[aria-invalid=true]{background-image:var(--icon-invalid)}:where(input,select,textarea)[aria-invalid=false]{--border-color:var(--form-element-valid-border-color)}:where(input,select,textarea)[aria-invalid=false]:is(:active,:focus){--border-color:var(--form-element-valid-active-border-color)!important;--box-shadow:0 0 0 var(--outline-width) var(--form-element-valid-focus-color)!important}:where(input,select,textarea)[aria-invalid=true]{--border-color:var(--form-element-invalid-border-color)}:where(input,select,textarea)[aria-invalid=true]:is(:active,:focus){--border-color:var(--form-element-invalid-active-border-color)!important;--box-shadow:0 0 0 var(--outline-width) var(--form-element-invalid-focus-color)!important}[dir=rtl] :where(input,select,textarea):not([type=checkbox],[type=radio]):is([aria-invalid],[aria-invalid=true],[aria-invalid=false]){background-position:center left .75rem}input::-webkit-input-placeholder,input::placeholder,select:invalid,textarea::-webkit-input-placeholder,textarea::placeholder{color:var(--form-element-placeholder-color);opacity:1}input:not([type=checkbox],[type=radio]),select,textarea{margin-bottom:var(--spacing)}select::-ms-expand{border:0;background-color:transparent}select:not([multiple],[size]){padding-right:calc(var(--form-element-spacing-horizontal) + 1.5rem);padding-left:var(--form-element-spacing-horizontal);-webkit-padding-start:var(--form-element-spacing-horizontal);padding-inline-start:var(--form-element-spacing-horizontal);-webkit-padding-end:calc(var(--form-element-spacing-horizontal) + 1.5rem);padding-inline-end:calc(var(--form-element-spacing-horizontal) + 1.5rem);background-image:var(--icon-chevron);background-position:center right .75rem;background-size:1rem auto;background-repeat:no-repeat}[dir=rtl] select:not([multiple],[size]){background-position:center left .75rem}:where(input,select,textarea,.grid)+small{display:block;width:100%;margin-top:calc(var(--spacing) * -.75);margin-bottom:var(--spacing);color:var(--muted-color)}label>:where(input,select,textarea){margin-top:calc(var(--spacing) * .25)}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:1.25em;height:1.25em;margin-top:-.125em;margin-right:.375em;margin-left:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:.375em;margin-inline-end:.375em;border-width:var(--border-width);font-size:inherit;vertical-align:middle;cursor:pointer}[type=checkbox]::-ms-check,[type=radio]::-ms-check{display:none}[type=checkbox]:checked,[type=checkbox]:checked:active,[type=checkbox]:checked:focus,[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--background-color:var(--primary);--border-color:var(--primary);background-image:var(--icon-checkbox);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=checkbox]~label,[type=radio]~label{display:inline-block;margin-right:.375em;margin-bottom:0;cursor:pointer}[type=checkbox]:indeterminate{--background-color:var(--primary);--border-color:var(--primary);background-image:var(--icon-minus);background-position:center;background-size:.75em auto;background-repeat:no-repeat}[type=radio]{border-radius:50%}[type=radio]:checked,[type=radio]:checked:active,[type=radio]:checked:focus{--background-color:var(--primary-inverse);border-width:.35em;background-image:none}[type=checkbox][role=switch]{--background-color:var(--switch-background-color);--border-color:var(--switch-background-color);--color:var(--switch-color);width:2.25em;height:1.25em;border:var(--border-width) solid var(--border-color);border-radius:1.25em;background-color:var(--background-color);line-height:1.25em}[type=checkbox][role=switch]:focus{--background-color:var(--switch-background-color);--border-color:var(--switch-background-color)}[type=checkbox][role=switch]:checked{--background-color:var(--switch-checked-background-color);--border-color:var(--switch-checked-background-color)}[type=checkbox][role=switch]:before{display:block;width:calc(1.25em - (var(--border-width) * 2));height:100%;border-radius:50%;background-color:var(--color);content:"";transition:margin .1s ease-in-out}[type=checkbox][role=switch]:checked{background-image:none}[type=checkbox][role=switch]:checked::before{margin-left:calc(1.125em - var(--border-width));-webkit-margin-start:calc(1.125em - var(--border-width));margin-inline-start:calc(1.125em - var(--border-width))}[type=checkbox]:checked[aria-invalid=false],[type=checkbox][aria-invalid=false],[type=checkbox][role=switch]:checked[aria-invalid=false],[type=checkbox][role=switch][aria-invalid=false],[type=radio]:checked[aria-invalid=false],[type=radio][aria-invalid=false]{--border-color:var(--form-element-valid-border-color)}[type=checkbox]:checked[aria-invalid=true],[type=checkbox][aria-invalid=true],[type=checkbox][role=switch]:checked[aria-invalid=true],[type=checkbox][role=switch][aria-invalid=true],[type=radio]:checked[aria-invalid=true],[type=radio][aria-invalid=true]{--border-color:var(--form-element-invalid-border-color)}[type=color]::-webkit-color-swatch-wrapper{padding:0}[type=color]::-moz-focus-inner{padding:0}[type=color]::-webkit-color-swatch{border:0;border-radius:calc(var(--border-radius) * .5)}[type=color]::-moz-color-swatch{border:0;border-radius:calc(var(--border-radius) * .5)}input:not([type=checkbox],[type=radio],[type=range],[type=file]):is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){--icon-position:0.75rem;--icon-width:1rem;padding-right:calc(var(--icon-width) + var(--icon-position));background-image:var(--icon-date);background-position:center right var(--icon-position);background-size:var(--icon-width) auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=time]{background-image:var(--icon-time)}[type=date]::-webkit-calendar-picker-indicator,[type=datetime-local]::-webkit-calendar-picker-indicator,[type=month]::-webkit-calendar-picker-indicator,[type=time]::-webkit-calendar-picker-indicator,[type=week]::-webkit-calendar-picker-indicator{width:var(--icon-width);margin-right:calc(var(--icon-width) * -1);margin-left:var(--icon-position);opacity:0}[dir=rtl] :is([type=date],[type=datetime-local],[type=month],[type=time],[type=week]){text-align:right}@-moz-document url-prefix(){[type=date],[type=datetime-local],[type=month],[type=time],[type=week]{padding-right:var(--form-element-spacing-horizontal)!important;background-image:none!important}}[type=file]{--color:var(--muted-color);padding:calc(var(--form-element-spacing-vertical) * .5) 0;border:0;border-radius:0;background:0 0}[type=file]::file-selector-button{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:calc(var(--spacing)/ 2);margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::file-selector-button:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=file]::-webkit-file-upload-button{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:calc(var(--spacing)/ 2);margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;-webkit-transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::-webkit-file-upload-button:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=file]::-ms-browse{--background-color:var(--secondary);--border-color:var(--secondary);--color:var(--secondary-inverse);margin-right:calc(var(--spacing)/ 2);margin-left:0;margin-inline-start:0;margin-inline-end:calc(var(--spacing)/ 2);padding:calc(var(--form-element-spacing-vertical) * .5) calc(var(--form-element-spacing-horizontal) * .5);border:var(--border-width) solid var(--border-color);border-radius:var(--border-radius);outline:0;background-color:var(--background-color);box-shadow:var(--box-shadow);color:var(--color);font-weight:var(--font-weight);font-size:1rem;line-height:var(--line-height);text-align:center;cursor:pointer;-ms-transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}[type=file]::-ms-browse:is(:hover,:active,:focus){--background-color:var(--secondary-hover);--border-color:var(--secondary-hover)}[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:1.25rem;background:0 0}[type=range]::-webkit-slider-runnable-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-webkit-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-moz-range-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-moz-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-ms-track{width:100%;height:.25rem;border-radius:var(--border-radius);background-color:var(--range-border-color);-ms-transition:background-color var(--transition),box-shadow var(--transition);transition:background-color var(--transition),box-shadow var(--transition)}[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-webkit-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]::-moz-range-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-moz-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]::-ms-thumb{-webkit-appearance:none;width:1.25rem;height:1.25rem;margin-top:-.5rem;border:2px solid var(--range-thumb-border-color);border-radius:50%;background-color:var(--range-thumb-color);cursor:pointer;-ms-transition:background-color var(--transition),transform var(--transition);transition:background-color var(--transition),transform var(--transition)}[type=range]:focus,[type=range]:hover{--range-border-color:var(--range-active-border-color);--range-thumb-color:var(--range-thumb-hover-color)}[type=range]:active{--range-thumb-color:var(--range-thumb-active-color)}[type=range]:active::-webkit-slider-thumb{transform:scale(1.25)}[type=range]:active::-moz-range-thumb{transform:scale(1.25)}[type=range]:active::-ms-thumb{transform:scale(1.25)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{-webkit-padding-start:calc(var(--form-element-spacing-horizontal) + 1.75rem);padding-inline-start:calc(var(--form-element-spacing-horizontal) + 1.75rem);border-radius:5rem;background-image:var(--icon-search);background-position:center left 1.125rem;background-size:1rem auto;background-repeat:no-repeat}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{-webkit-padding-start:calc(var(--form-element-spacing-horizontal) + 1.75rem)!important;padding-inline-start:calc(var(--form-element-spacing-horizontal) + 1.75rem)!important;background-position:center left 1.125rem,center right .75rem}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=false]{background-image:var(--icon-search),var(--icon-valid)}input:not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid=true]{background-image:var(--icon-search),var(--icon-invalid)}[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;display:none}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search]{background-position:center right 1.125rem}[dir=rtl] :where(input):not([type=checkbox],[type=radio],[type=range],[type=file])[type=search][aria-invalid]{background-position:center right 1.125rem,center left .75rem}:where(table){width:100%;border-collapse:collapse;border-spacing:0;text-indent:0}td,th{padding:calc(var(--spacing)/ 2) var(--spacing);border-bottom:var(--border-width) solid var(--table-border-color);color:var(--color);font-weight:var(--font-weight);font-size:var(--font-size);text-align:left;text-align:start}tfoot td,tfoot th{border-top:var(--border-width) solid var(--table-border-color);border-bottom:0}table[role=grid] tbody tr:nth-child(odd){background-color:var(--table-row-stripped-background-color)}code,kbd,pre,samp{font-size:.875em;font-family:var(--font-family)}pre{-ms-overflow-style:scrollbar;overflow:auto}code,kbd,pre{border-radius:var(--border-radius);background:var(--code-background-color);color:var(--code-color);font-weight:var(--font-weight);line-height:initial}code,kbd{display:inline-block;padding:.375rem .5rem}pre{display:block;margin-bottom:var(--spacing);overflow-x:auto}pre>code{display:block;padding:var(--spacing);background:0 0;font-size:14px;line-height:var(--line-height)}code b{color:var(--code-tag-color);font-weight:var(--font-weight)}code i{color:var(--code-property-color);font-style:normal}code u{color:var(--code-value-color);text-decoration:none}code em{color:var(--code-comment-color);font-style:normal}kbd{background-color:var(--code-kbd-background-color);color:var(--code-kbd-color);vertical-align:baseline}hr{height:0;border:0;border-top:1px solid var(--muted-border-color);color:inherit}[hidden],template{display:none!important}canvas{display:inline-block}details{display:block;margin-bottom:var(--spacing);padding-bottom:var(--spacing);border-bottom:var(--border-width) solid var(--accordion-border-color)}details summary{line-height:1rem;list-style-type:none;cursor:pointer;transition:color var(--transition)}details summary:not([role]){color:var(--accordion-close-summary-color)}details summary::-webkit-details-marker{display:none}details summary::marker{display:none}details summary::-moz-list-bullet{list-style-type:none}details summary::after{display:block;width:1rem;height:1rem;-webkit-margin-start:calc(var(--spacing,1rem) * 0.5);margin-inline-start:calc(var(--spacing,1rem) * .5);float:right;transform:rotate(-90deg);background-image:var(--icon-chevron);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:"";transition:transform var(--transition)}details summary:focus{outline:0}details summary:focus:not([role=button]){color:var(--accordion-active-summary-color)}details summary[role=button]{width:100%;text-align:left}details summary[role=button]::after{height:calc(1rem * var(--line-height,1.5));background-image:var(--icon-chevron-button)}details summary[role=button]:not(.outline).contrast::after{background-image:var(--icon-chevron-button-inverse)}details[open]>summary{margin-bottom:calc(var(--spacing))}details[open]>summary:not([role]):not(:focus){color:var(--accordion-open-summary-color)}details[open]>summary::after{transform:rotate(0)}[dir=rtl] details summary{text-align:right}[dir=rtl] details summary::after{float:left;background-position:left center}article{margin:var(--block-spacing-vertical) 0;padding:var(--block-spacing-vertical) var(--block-spacing-horizontal);border-radius:var(--border-radius);background:var(--card-background-color);box-shadow:var(--card-box-shadow)}article>footer,article>header{margin-right:calc(var(--block-spacing-horizontal) * -1);margin-left:calc(var(--block-spacing-horizontal) * -1);padding:calc(var(--block-spacing-vertical) * .66) var(--block-spacing-horizontal);background-color:var(--card-sectionning-background-color)}article>header{margin-top:calc(var(--block-spacing-vertical) * -1);margin-bottom:var(--block-spacing-vertical);border-bottom:var(--border-width) solid var(--card-border-color);border-top-right-radius:var(--border-radius);border-top-left-radius:var(--border-radius)}article>footer{margin-top:var(--block-spacing-vertical);margin-bottom:calc(var(--block-spacing-vertical) * -1);border-top:var(--border-width) solid var(--card-border-color);border-bottom-right-radius:var(--border-radius);border-bottom-left-radius:var(--border-radius)}:root{--scrollbar-width:0px}dialog{display:flex;z-index:999;position:fixed;top:0;right:0;bottom:0;left:0;align-items:center;justify-content:center;width:inherit;min-width:100%;height:inherit;min-height:100%;padding:var(--spacing);border:0;-webkit-backdrop-filter:var(--modal-overlay-backdrop-filter);backdrop-filter:var(--modal-overlay-backdrop-filter);background-color:var(--modal-overlay-background-color);color:var(--color)}dialog article{max-height:calc(100vh - var(--spacing) * 2);overflow:auto}@media (min-width:576px){dialog article{max-width:510px}}@media (min-width:768px){dialog article{max-width:700px}}dialog article>footer,dialog article>header{padding:calc(var(--block-spacing-vertical) * .5) var(--block-spacing-horizontal)}dialog article>header .close{margin:0;margin-left:var(--spacing);float:right}dialog article>footer{text-align:right}dialog article>footer [role=button]{margin-bottom:0}dialog article>footer [role=button]:not(:first-of-type){margin-left:calc(var(--spacing) * .5)}dialog article p:last-of-type{margin:0}dialog article .close{display:block;width:1rem;height:1rem;margin-top:calc(var(--block-spacing-vertical) * -.5);margin-bottom:var(--typography-spacing-vertical);margin-left:auto;background-image:var(--icon-close);background-position:center;background-size:auto 1rem;background-repeat:no-repeat;opacity:.5;transition:opacity var(--transition)}dialog article .close:is([aria-current],:hover,:active,:focus){opacity:1}dialog:not([open]),dialog[open=false]{display:none}.modal-is-open{padding-right:var(--scrollbar-width,0);overflow:hidden;pointer-events:none;touch-action:none}.modal-is-open dialog{pointer-events:auto}:where(.modal-is-opening,.modal-is-closing) dialog,:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-duration:.2s;animation-timing-function:ease-in-out;animation-fill-mode:both}:where(.modal-is-opening,.modal-is-closing) dialog{animation-duration:.8s;animation-name:modal-overlay}:where(.modal-is-opening,.modal-is-closing) dialog>article{animation-delay:.2s;animation-name:modal}.modal-is-closing dialog,.modal-is-closing dialog>article{animation-delay:0s;animation-direction:reverse}@keyframes modal-overlay{from{-webkit-backdrop-filter:none;backdrop-filter:none;background-color:transparent}}@keyframes modal{from{transform:translateY(-100%);opacity:0}}:where(nav li)::before{float:left;content:"​"}nav,nav ul{display:flex}nav{justify-content:space-between}nav ol,nav ul{align-items:center;margin-bottom:0;padding:0;list-style:none}nav ol:first-of-type,nav ul:first-of-type{margin-left:calc(var(--nav-element-spacing-horizontal) * -1)}nav ol:last-of-type,nav ul:last-of-type{margin-right:calc(var(--nav-element-spacing-horizontal) * -1)}nav li{display:inline-block;margin:0;padding:var(--nav-element-spacing-vertical) var(--nav-element-spacing-horizontal)}nav li>*{--spacing:0}nav :where(a,[role=link]){display:inline-block;margin:calc(var(--nav-link-spacing-vertical) * -1) calc(var(--nav-link-spacing-horizontal) * -1);padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal);border-radius:var(--border-radius);text-decoration:none}nav :where(a,[role=link]):is([aria-current],:hover,:active,:focus){text-decoration:none}nav[aria-label=breadcrumb]{align-items:center;justify-content:start}nav[aria-label=breadcrumb] ul li:not(:first-child){-webkit-margin-start:var(--nav-link-spacing-horizontal);margin-inline-start:var(--nav-link-spacing-horizontal)}nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{position:absolute;width:calc(var(--nav-link-spacing-horizontal) * 2);-webkit-margin-start:calc(var(--nav-link-spacing-horizontal)/ 2);margin-inline-start:calc(var(--nav-link-spacing-horizontal)/ 2);content:"/";color:var(--muted-color);text-align:center}nav[aria-label=breadcrumb] a[aria-current]{background-color:transparent;color:inherit;text-decoration:none;pointer-events:none}nav [role=button]{margin-right:inherit;margin-left:inherit;padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal)}aside li,aside nav,aside ol,aside ul{display:block}aside li{padding:calc(var(--nav-element-spacing-vertical) * .5) var(--nav-element-spacing-horizontal)}aside li a{display:block}aside li [role=button]{margin:inherit}[dir=rtl] nav[aria-label=breadcrumb] ul li:not(:last-child) ::after{content:"\\"}progress{display:inline-block;vertical-align:baseline}progress{-webkit-appearance:none;-moz-appearance:none;display:inline-block;appearance:none;width:100%;height:.5rem;margin-bottom:calc(var(--spacing) * .5);overflow:hidden;border:0;border-radius:var(--border-radius);background-color:var(--progress-background-color);color:var(--progress-color)}progress::-webkit-progress-bar{border-radius:var(--border-radius);background:0 0}progress[value]::-webkit-progress-value{background-color:var(--progress-color)}progress::-moz-progress-bar{background-color:var(--progress-color)}@media (prefers-reduced-motion:no-preference){progress:indeterminate{background:var(--progress-background-color) linear-gradient(to right,var(--progress-color) 30%,var(--progress-background-color) 30%) top left/150% 150% no-repeat;animation:progress-indeterminate 1s linear infinite}progress:indeterminate[value]::-webkit-progress-value{background-color:transparent}progress:indeterminate::-moz-progress-bar{background-color:transparent}}@media (prefers-reduced-motion:no-preference){[dir=rtl] progress:indeterminate{animation-direction:reverse}}@keyframes progress-indeterminate{0%{background-position:200% 0}100%{background-position:-200% 0}}details[role=list],li[role=list]{position:relative}details[role=list] summary+ul,li[role=list]>ul{display:flex;z-index:99;position:absolute;top:auto;right:0;left:0;flex-direction:column;margin:0;padding:0;border:var(--border-width) solid var(--dropdown-border-color);border-radius:var(--border-radius);border-top-right-radius:0;border-top-left-radius:0;background-color:var(--dropdown-background-color);box-shadow:var(--card-box-shadow);color:var(--dropdown-color);white-space:nowrap}details[role=list] summary+ul li,li[role=list]>ul li{width:100%;margin-bottom:0;padding:calc(var(--form-element-spacing-vertical) * .5) var(--form-element-spacing-horizontal);list-style:none}details[role=list] summary+ul li:first-of-type,li[role=list]>ul li:first-of-type{margin-top:calc(var(--form-element-spacing-vertical) * .5)}details[role=list] summary+ul li:last-of-type,li[role=list]>ul li:last-of-type{margin-bottom:calc(var(--form-element-spacing-vertical) * .5)}details[role=list] summary+ul li a,li[role=list]>ul li a{display:block;margin:calc(var(--form-element-spacing-vertical) * -.5) calc(var(--form-element-spacing-horizontal) * -1);padding:calc(var(--form-element-spacing-vertical) * .5) var(--form-element-spacing-horizontal);overflow:hidden;color:var(--dropdown-color);text-decoration:none;text-overflow:ellipsis}details[role=list] summary+ul li a:hover,li[role=list]>ul li a:hover{background-color:var(--dropdown-hover-background-color)}details[role=list] summary::after,li[role=list]>a::after{display:block;width:1rem;height:calc(1rem * var(--line-height,1.5));-webkit-margin-start:0.5rem;margin-inline-start:.5rem;float:right;transform:rotate(0);background-position:right center;background-size:1rem auto;background-repeat:no-repeat;content:""}details[role=list]{padding:0;border-bottom:none}details[role=list] summary{margin-bottom:0}details[role=list] summary:not([role]){height:calc(1rem * var(--line-height) + var(--form-element-spacing-vertical) * 2 + var(--border-width) * 2);padding:var(--form-element-spacing-vertical) var(--form-element-spacing-horizontal);border:var(--border-width) solid var(--form-element-border-color);border-radius:var(--border-radius);background-color:var(--form-element-background-color);color:var(--form-element-placeholder-color);line-height:inherit;cursor:pointer;transition:background-color var(--transition),border-color var(--transition),color var(--transition),box-shadow var(--transition)}details[role=list] summary:not([role]):active,details[role=list] summary:not([role]):focus{border-color:var(--form-element-active-border-color);background-color:var(--form-element-active-background-color)}details[role=list] summary:not([role]):focus{box-shadow:0 0 0 var(--outline-width) var(--form-element-focus-color)}details[role=list][open] summary{border-bottom-right-radius:0;border-bottom-left-radius:0}details[role=list][open] summary::before{display:block;z-index:1;position:fixed;top:0;right:0;bottom:0;left:0;background:0 0;content:"";cursor:default}nav details[role=list] summary,nav li[role=list] a{display:flex;direction:ltr}nav details[role=list] summary+ul,nav li[role=list]>ul{min-width:-moz-fit-content;min-width:fit-content;border-radius:var(--border-radius)}nav details[role=list] summary+ul li a,nav li[role=list]>ul li a{border-radius:0}nav details[role=list] summary,nav details[role=list] summary:not([role]){height:auto;padding:var(--nav-link-spacing-vertical) var(--nav-link-spacing-horizontal)}nav details[role=list][open] summary{border-radius:var(--border-radius)}nav details[role=list] summary+ul{margin-top:var(--outline-width);-webkit-margin-start:0;margin-inline-start:0}nav details[role=list] summary[role=link]{margin-bottom:calc(var(--nav-link-spacing-vertical) * -1);line-height:var(--line-height)}nav details[role=list] summary[role=link]+ul{margin-top:calc(var(--nav-link-spacing-vertical) + var(--outline-width));-webkit-margin-start:calc(var(--nav-link-spacing-horizontal) * -1);margin-inline-start:calc(var(--nav-link-spacing-horizontal) * -1)}li[role=list] a:active~ul,li[role=list] a:focus~ul,li[role=list]:hover>ul{display:flex}li[role=list]>ul{display:none;margin-top:calc(var(--nav-link-spacing-vertical) + var(--outline-width));-webkit-margin-start:calc(var(--nav-element-spacing-horizontal) - var(--nav-link-spacing-horizontal));margin-inline-start:calc(var(--nav-element-spacing-horizontal) - var(--nav-link-spacing-horizontal))}li[role=list]>a::after{background-image:var(--icon-chevron)}label>details[role=list]{margin-top:calc(var(--spacing) * .25);margin-bottom:var(--spacing)}[aria-busy=true]{cursor:progress}[aria-busy=true]:not(input,select,textarea,html)::before{display:inline-block;width:1em;height:1em;border:.1875em solid currentColor;border-radius:1em;border-right-color:transparent;content:"";vertical-align:text-bottom;vertical-align:-.125em;animation:spinner .75s linear infinite;opacity:var(--loading-spinner-opacity)}[aria-busy=true]:not(input,select,textarea,html):not(:empty)::before{margin-right:calc(var(--spacing) * .5);margin-left:0;-webkit-margin-start:0;margin-inline-start:0;-webkit-margin-end:calc(var(--spacing) * .5);margin-inline-end:calc(var(--spacing) * .5)}[aria-busy=true]:not(input,select,textarea,html):empty{text-align:center}a[aria-busy=true],button[aria-busy=true],input[type=button][aria-busy=true],input[type=reset][aria-busy=true],input[type=submit][aria-busy=true]{pointer-events:none}@keyframes spinner{to{transform:rotate(360deg)}}[data-tooltip]{position:relative}[data-tooltip]:not(a,button,input){border-bottom:1px dotted;text-decoration:none;cursor:help}[data-tooltip]::after,[data-tooltip]::before,[data-tooltip][data-placement=top]::after,[data-tooltip][data-placement=top]::before{display:block;z-index:99;position:absolute;bottom:100%;left:50%;padding:.25rem .5rem;overflow:hidden;transform:translate(-50%,-.25rem);border-radius:var(--border-radius);background:var(--tooltip-background-color);content:attr(data-tooltip);color:var(--tooltip-color);font-style:normal;font-weight:var(--font-weight);font-size:.875rem;text-decoration:none;text-overflow:ellipsis;white-space:nowrap;opacity:0;pointer-events:none}[data-tooltip]::after,[data-tooltip][data-placement=top]::after{padding:0;transform:translate(-50%,0);border-top:.3rem solid;border-right:.3rem solid transparent;border-left:.3rem solid transparent;border-radius:0;background-color:transparent;content:"";color:var(--tooltip-background-color)}[data-tooltip][data-placement=bottom]::after,[data-tooltip][data-placement=bottom]::before{top:100%;bottom:auto;transform:translate(-50%,.25rem)}[data-tooltip][data-placement=bottom]:after{transform:translate(-50%,-.3rem);border:.3rem solid transparent;border-bottom:.3rem solid}[data-tooltip][data-placement=left]::after,[data-tooltip][data-placement=left]::before{top:50%;right:100%;bottom:auto;left:auto;transform:translate(-.25rem,-50%)}[data-tooltip][data-placement=left]:after{transform:translate(.3rem,-50%);border:.3rem solid transparent;border-left:.3rem solid}[data-tooltip][data-placement=right]::after,[data-tooltip][data-placement=right]::before{top:50%;right:auto;bottom:auto;left:100%;transform:translate(.25rem,-50%)}[data-tooltip][data-placement=right]:after{transform:translate(-.3rem,-50%);border:.3rem solid transparent;border-right:.3rem solid}[data-tooltip]:focus::after,[data-tooltip]:focus::before,[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1}@media (hover:hover) and (pointer:fine){[data-tooltip]:hover::after,[data-tooltip]:hover::before,[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::after,[data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::before{animation-duration:.2s;animation-name:tooltip-slide-top}[data-tooltip]:hover::after,[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover [data-tooltip]:focus::after{animation-name:tooltip-caret-slide-top}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:focus::before,[data-tooltip][data-placement=bottom]:hover::after,[data-tooltip][data-placement=bottom]:hover::before{animation-duration:.2s;animation-name:tooltip-slide-bottom}[data-tooltip][data-placement=bottom]:focus::after,[data-tooltip][data-placement=bottom]:hover::after{animation-name:tooltip-caret-slide-bottom}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:focus::before,[data-tooltip][data-placement=left]:hover::after,[data-tooltip][data-placement=left]:hover::before{animation-duration:.2s;animation-name:tooltip-slide-left}[data-tooltip][data-placement=left]:focus::after,[data-tooltip][data-placement=left]:hover::after{animation-name:tooltip-caret-slide-left}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:focus::before,[data-tooltip][data-placement=right]:hover::after,[data-tooltip][data-placement=right]:hover::before{animation-duration:.2s;animation-name:tooltip-slide-right}[data-tooltip][data-placement=right]:focus::after,[data-tooltip][data-placement=right]:hover::after{animation-name:tooltip-caret-slide-right}}@keyframes tooltip-slide-top{from{transform:translate(-50%,.75rem);opacity:0}to{transform:translate(-50%,-.25rem);opacity:1}}@keyframes tooltip-caret-slide-top{from{opacity:0}50%{transform:translate(-50%,-.25rem);opacity:0}to{transform:translate(-50%,0);opacity:1}}@keyframes tooltip-slide-bottom{from{transform:translate(-50%,-.75rem);opacity:0}to{transform:translate(-50%,.25rem);opacity:1}}@keyframes tooltip-caret-slide-bottom{from{opacity:0}50%{transform:translate(-50%,-.5rem);opacity:0}to{transform:translate(-50%,-.3rem);opacity:1}}@keyframes tooltip-slide-left{from{transform:translate(.75rem,-50%);opacity:0}to{transform:translate(-.25rem,-50%);opacity:1}}@keyframes tooltip-caret-slide-left{from{opacity:0}50%{transform:translate(.05rem,-50%);opacity:0}to{transform:translate(.3rem,-50%);opacity:1}}@keyframes tooltip-slide-right{from{transform:translate(-.75rem,-50%);opacity:0}to{transform:translate(.25rem,-50%);opacity:1}}@keyframes tooltip-caret-slide-right{from{opacity:0}50%{transform:translate(-.05rem,-50%);opacity:0}to{transform:translate(-.3rem,-50%);opacity:1}}[aria-controls]{cursor:pointer}[aria-disabled=true],[disabled]{cursor:not-allowed}[aria-hidden=false][hidden]{display:initial}[aria-hidden=false][hidden]:not(:focus){clip:rect(0,0,0,0);position:absolute}[tabindex],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation}[dir=rtl]{direction:rtl}@media (prefers-reduced-motion:reduce){:not([aria-busy=true]),:not([aria-busy=true])::after,:not([aria-busy=true])::before{background-attachment:initial!important;animation-duration:1ms!important;animation-delay:-1ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-delay:0s!important;transition-duration:0s!important}} 5 + /*# sourceMappingURL=pico.min.css.map */
+5
go-didplc/cmd/webplc/static/robots.txt
··· 1 + # Hello Friends! 2 + 3 + # By default, may crawl anything on this domain. HTTP 429 ("backoff") status codes are used for rate-limiting. Up to a handful of concurrent requests should be ok. 4 + User-Agent: * 5 + Allow: /
+94
go-didplc/cmd/webplc/templates/base.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8"> 5 + <meta name="referrer" content="origin-when-cross-origin"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1"> 7 + <link rel="stylesheet" href="/static/pico.min.css"> 8 + <title>{%- block head_title -%}did:plc Directory{%- endblock -%}</title> 9 + <style> 10 + html { position: relative; min-height: 100%; height: auto; } 11 + body { margin-bottom: 3em; } 12 + body > nav { background-color: var(--muted-border-color); } 13 + body > footer { position: absolute; bottom: 0px; padding: 2em; background-color: var(--muted-border-color); } 14 + thead th { font-weight: bold; } 15 + main article { margin: 2.5rem 0; padding: 2rem; } 16 + 17 + /* Green Light scheme (Default) */ 18 + /* Can be forced with data-theme="light" */ 19 + [data-theme="light"], 20 + :root:not([data-theme="dark"]) { 21 + --color: black; 22 + --primary: #3949ab; 23 + --primary-hover: #3f51b5; 24 + --primary-focus: rgba(57, 73, 171, 0.125); 25 + --primary-inverse: #FFF; 26 + --code-color: var(--color); 27 + } 28 + 29 + /* Green Dark scheme (Auto) */ 30 + /* Automatically enabled if user has Dark mode enabled */ 31 + @media only screen and (prefers-color-scheme: dark) { 32 + :root:not([data-theme]) { 33 + --color: white; 34 + --primary: #3949ab; 35 + --primary-hover: #3f51b5; 36 + --primary-focus: rgba(57, 73, 171, 0.25); 37 + --primary-inverse: #FFF; 38 + --code-color: var(--color); 39 + } 40 + } 41 + 42 + /* Green Dark scheme (Forced) */ 43 + /* Enabled if forced with data-theme="dark" */ 44 + [data-theme="dark"] { 45 + --color: white; 46 + /* --muted-color: white; */ 47 + --primary: #3949ab; 48 + --primary-hover: #3f51b5; 49 + --primary-focus: rgba(57, 73, 171, 0.25); 50 + --primary-inverse: #FFF; 51 + --code-color: var(--color); 52 + } 53 + 54 + /* Green (Common styles) */ 55 + :root { 56 + --form-element-active-border-color: var(--primary); 57 + --form-element-focus-color: var(--primary-focus); 58 + --switch-color: var(--primary-inverse); 59 + --switch-checked-background-color: var(--primary); 60 + } 61 + </style> 62 + {# <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"> #} 63 + {# <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"> #} 64 + {# <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png"> #} 65 + {% block html_head_extra -%}{%- endblock %} 66 + <meta name="application-name" name="DID PLC Directory"> 67 + <meta name="generator" name="webplc"> 68 + </head> 69 + <body> 70 + {%- block body_all %} 71 + <nav class="container-fluid"> 72 + <ul> 73 + <li><a href="/"><strong>did:plc</strong></a></li> 74 + </ul> 75 + <ul> 76 + <li><a href="/resolve" >Lookup</a> </li> 77 + <li><a href="/api/redoc" >API</a> </li> 78 + <li><a href="/spec/v0.1/did-plc" >Specification</a> </li> 79 + <li><a href="https://github.com/bluesky-social/did-method-plc">Code</a></li> 80 + </ul> 81 + </nav> 82 + 83 + <main class="container"> 84 + {% block main %}{% endblock %} 85 + </main> 86 + 87 + <footer class="container-fluid"> 88 + <div class="container"> 89 + <small>Developed by <a href="https://blueskyweb.xyz">Bluesky PBC</a> for <a href="https://atproto.com">atproto</a></small> 90 + </div> 91 + </footer> 92 + {% endblock -%} 93 + </body> 94 + </html>
+88
go-didplc/cmd/webplc/templates/did.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block head_title -%} 4 + {{ did }} 5 + {%- endblock %} 6 + 7 + {% block main -%} 8 + <h3 style="font-family: monospace;">{{ did }}</h3> 9 + 10 + {% if result.StatusCode == 200 %} 11 + 12 + <article> 13 + <h4 style="margin-bottom: 1em;">Names and Aliases</h4> 14 + <div style="width: 100%; overflow-x: auto;"> 15 + <table role="grid" style="width: 100%;"> 16 + <thead> 17 + <tr> 18 + <th>URI</th> 19 + </tr> 20 + </thead> 21 + <tbody style="font-family: monospace;"> 22 + {% for uri in result.Doc.AlsoKnownAs %} 23 + <tr> 24 + <td>{{ uri }}</td> 25 + </tr> 26 + {% endfor %} 27 + </tbody> 28 + </table> 29 + </div> 30 + </article> 31 + 32 + <article> 33 + <h4 style="margin-bottom: 1em;">Services</h4> 34 + <div style="width: 100%; overflow-x: auto;"> 35 + <table role="grid" style="width: 100%;"> 36 + <thead> 37 + <tr> 38 + <th>ID</th> 39 + <th>Type</th> 40 + <th>URL</th> 41 + </tr> 42 + </thead> 43 + <tbody style="font-family: monospace;"> 44 + {% for vm in result.Doc.Service %} 45 + <tr> 46 + <td>{{ vm.Id }}</td> 47 + <td>{{ vm.Type }}</td> 48 + <td>{{ vm.ServiceEndpoint }}</td> 49 + </tr> 50 + {% endfor %} 51 + </tbody> 52 + </table> 53 + </div> 54 + </article> 55 + 56 + <article> 57 + <h4 style="margin-bottom: 1em;">Verification Methods</h4> 58 + <div style="width: 100%; overflow-x: auto;"> 59 + <table role="grid" style="width: 100%;"> 60 + <thead> 61 + <tr> 62 + <th>ID</th> 63 + <th>Type</th> 64 + <th>Public Key (multibase-encoded)</th> 65 + </tr> 66 + </thead> 67 + <tbody style="font-family: monospace;"> 68 + {% for vm in result.Doc.VerificationMethod %} 69 + <tr> 70 + <td>{{ vm.Id }}</td> 71 + <td>{{ vm.Type }}</td> 72 + <td>{{ vm.PublicKeyMultibase }}</td> 73 + </tr> 74 + {% endfor %} 75 + </tbody> 76 + </table> 77 + </div> 78 + </article> 79 + 80 + <article> 81 + <h4>DID Document JSON</h4> 82 + <pre><code> 83 + {{- result.DocJson -}} 84 + </code></pre> 85 + </article> 86 + {% endif %} 87 + 88 + {%- endblock %}
+20
go-didplc/cmd/webplc/templates/error.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block head_title %}Error {{ statusCode }} - did:plc{% endblock %} 4 + 5 + {%- block main %} 6 + {% if statusCode == 404 %} 7 + <h1>404: Not Found</h1> 8 + {% elif statusCode == 410 %} 9 + <h1>410: Gone</h1> 10 + {% elif statusCode == 400 %} 11 + <h1>400: Bad Request</h1> 12 + {% else %} 13 + <h1>{{ statusCode }}: Server Error</h1> 14 + {% endif %} 15 + {% if errorMessage %} 16 + <p>{{ errorMessage }} 17 + {% else %} 18 + <p>Sorry about that! Our <a href="https://status.bsky.app/">Status Page</a> might have more context. 19 + {% endif %} 20 + {% endblock -%}
+26
go-didplc/cmd/webplc/templates/home.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block main -%} 4 + <hgroup> 5 + <h1>DID PLC Directory</h1> 6 + <h3>A self-authenticating Decentralized Identifier (DID) system which is strongly-consistent, recoverable, and allows for key rotation</h3> 7 + </hgroup> 8 + 9 + <p>PLC is a persistent global identifier system, which allows accounts to retain relationships while changing names or migrating between service providers. It makes use of cryptography and gives individuals (or organizations) direct control and ownership over their identitifier, but does not make use of any blockchain or cryptocurrency technology, and is inexpensive enough to provide as a no-cost service.</p> 10 + 11 + <p>PLC is a method which implements the W3C Decentralized Identifier (DID) standard. This means it is interoperable and resuable by other applications and organization.</p> 12 + 13 + <p>Bluesky PBC developed DID Placeholder when designing the AT Protocol (atproto) because we were not satisfied with any of the existing DID methods. We wanted a strongly consistent, highly available, recoverable, and cryptographically secure method with fast and cheap propagation of updates.</p> 14 + 15 + <p>While PLC originally stood for "placeholder", the system has been in production use for several months, with over half a million registered accounts in the atproto network as of August 2023. While it is conceivable that the method will evolve or be replaced over time by a successor method, we feel that the current system provides value and is worth consideration as a persistent identifier for other applications.</p> 16 + 17 + <article> 18 + <h3>For Developers</h3> 19 + <p>DID resolution can be as simple as: 20 + <pre><code>curl -s "https://plc.directory/did:plc:ewvi7nxzyoun6zhxrhs64oiz" | jq .</pre></code> 21 + <p>Jump in to the <a href="/spec/v0.1/did-plc">specification</a> to understand how to verify self-certifying operation logs. 22 + </p> 23 + <p>Check out the <a href="/api/redoc">HTTP API documentation</a> (generated from an OpenAPI schema) for details on how to submit operations, fetch the current DID document, or get a paginated export of all operations for all accounts.</p> 24 + </article> 25 + 26 + {%- endblock %}
+9
go-didplc/cmd/webplc/templates/markdown.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block head_title %} 4 + {{ html_title }} 5 + {% endblock %} 6 + 7 + {% block main -%} 8 + {{ markdown_html|safe }} 9 + {%- endblock %}
+21
go-didplc/cmd/webplc/templates/redoc.html
··· 1 + <!DOCTYPE html> 2 + <html> 3 + <head> 4 + <title>Redoc</title> 5 + <!-- needed for adaptive design --> 6 + <meta charset="utf-8"/> 7 + <meta name="viewport" content="width=device-width, initial-scale=1"> 8 + <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet"> 9 + <!-- Redoc doesn't change outer page styles --> 10 + <style> 11 + body { 12 + margin: 0; 13 + padding: 0; 14 + } 15 + </style> 16 + </head> 17 + <body> 18 + <redoc spec-url='/api/plc-server-openapi3.yaml'></redoc> 19 + <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"> </script> 20 + </body> 21 + </html>
+13
go-didplc/cmd/webplc/templates/resolve.html
··· 1 + {% extends "base.html" %} 2 + 3 + {% block head_title %} 4 + Resolve did:plc 5 + {% endblock %} 6 + 7 + {% block main -%} 8 + <h2>Resolve a did:plc Identifier</h2> 9 + <form action="/resolve" method="get"> 10 + <input type="search" name="did" placeholder="did:plc: [...]" style="width: 100%;"> 11 + </form> 12 + <p>Example: <a href="/did/did:plc:ewvi7nxzyoun6zhxrhs64oiz"><code>did:plc:ewvi7nxzyoun6zhxrhs64oiz</code></a> 13 + {%- endblock %}
+3
go-didplc/example.dev.env
··· 1 + ATP_PLC_HOST='http://localhost:2582' 2 + DEBUG='true' 3 + GOLOG_LOG_LEVEL=debug
+4
go-didplc/example.env
··· 1 + #ATP_PLC_HOST='https://plc.directory' 2 + #HTTP_ADDRESS=':8700' 3 + #DEBUG='false' 4 + #GOLOG_LOG_LEVEL=warn
+35
go-didplc/go.mod
··· 1 + module github.com/bluesky-social/did-method-plc/webplc 2 + 3 + go 1.20 4 + 5 + require ( 6 + github.com/flosch/pongo2/v6 v6.0.0 7 + github.com/ipfs/go-log v1.0.5 8 + github.com/joho/godotenv v1.5.1 9 + github.com/klauspost/compress v1.16.7 10 + github.com/labstack/echo/v4 v4.11.1 11 + github.com/urfave/cli/v2 v2.25.7 12 + ) 13 + 14 + require ( 15 + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 16 + github.com/gogo/protobuf v1.3.2 // indirect 17 + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 18 + github.com/ipfs/go-log/v2 v2.1.3 // indirect 19 + github.com/labstack/gommon v0.4.0 // indirect 20 + github.com/mattn/go-colorable v0.1.13 // indirect 21 + github.com/mattn/go-isatty v0.0.19 // indirect 22 + github.com/opentracing/opentracing-go v1.2.0 // indirect 23 + github.com/russross/blackfriday/v2 v2.1.0 // indirect 24 + github.com/valyala/bytebufferpool v1.0.0 // indirect 25 + github.com/valyala/fasttemplate v1.2.2 // indirect 26 + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 27 + go.uber.org/atomic v1.7.0 // indirect 28 + go.uber.org/multierr v1.6.0 // indirect 29 + go.uber.org/zap v1.16.0 // indirect 30 + golang.org/x/crypto v0.11.0 // indirect 31 + golang.org/x/net v0.12.0 // indirect 32 + golang.org/x/sys v0.10.0 // indirect 33 + golang.org/x/text v0.11.0 // indirect 34 + golang.org/x/time v0.3.0 // indirect 35 + )
+136
go-didplc/go.sum
··· 1 + github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 + github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 3 + github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= 4 + github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 5 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 + github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU= 9 + github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU= 10 + github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 11 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 12 + github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 13 + github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 14 + github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 15 + github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 16 + github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 17 + github.com/ipfs/go-log/v2 v2.1.3 h1:1iS3IU7aXRlbgUpN8yTTpJ53NXYjAe37vcI5+5nYrzk= 18 + github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 19 + github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 20 + github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 21 + github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 22 + github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 23 + github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= 24 + github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 25 + github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 26 + github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 27 + github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 28 + github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 29 + github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 30 + github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= 31 + github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= 32 + github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= 33 + github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 34 + github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 35 + github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 36 + github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 37 + github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 38 + github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 39 + github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 40 + github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 41 + github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 42 + github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 43 + github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 44 + github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 45 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 46 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 47 + github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 48 + github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 49 + github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 50 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 51 + github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 52 + github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 53 + github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 54 + github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 55 + github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= 56 + github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= 57 + github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 58 + github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 59 + github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 60 + github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 61 + github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 62 + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= 63 + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 64 + github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 65 + github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 66 + go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 67 + go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 68 + go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 69 + go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 70 + go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 71 + go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 72 + go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 73 + go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM= 74 + go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= 75 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 76 + golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 77 + golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 78 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 79 + golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= 80 + golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 81 + golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 82 + golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 83 + golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 84 + golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 85 + golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 86 + golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= 87 + golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 88 + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 89 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 90 + golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 91 + golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 92 + golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= 93 + golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 94 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 96 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 98 + golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 + golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 102 + golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 + golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 104 + golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 + golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 106 + golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 107 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 108 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 109 + golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 110 + golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 111 + golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 112 + golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 113 + golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 114 + golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 115 + golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 116 + golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 117 + golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 118 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 119 + golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 120 + golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 121 + golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= 122 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 123 + golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 124 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 125 + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 126 + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 127 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 128 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 129 + gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 130 + gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 131 + gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 132 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 133 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 134 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 135 + honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 136 + honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+1
go-didplc/static
··· 1 + cmd/webplc/static/
+1
go-didplc/templates
··· 1 + cmd/webplc/templates/
+1 -1
package.json
··· 2 2 "name": "did-method-plc", 3 3 "version": "0.0.1", 4 4 "repository": "git@github.com:bluesky-social/did-method-plc.git", 5 - "author": "Bluesky PBLLC <hello@blueskyweb.xyz>", 5 + "author": "Bluesky PBC <hello@blueskyweb.xyz>", 6 6 "license": "MIT", 7 7 "private": true, 8 8 "engines": {
+50 -1
packages/lib/README.md
··· 1 - # DID PLC Library 1 + 2 + @did-plc/lib - DID PLC Typescript Client Library 3 + ================================================ 4 + 5 + [![NPM](https://img.shields.io/npm/v/@did-plc/lib)](https://www.npmjs.com/package/@did-plc/lib) 6 + [![Github CI Status](https://github.com/bluesky-social/did-method-plc/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/did-method-plc/actions/workflows/repo.yaml) 7 + 8 + This library provides both a simple client for the PLC directory, and an implementation of the PLC method itself (using a cryptographically signed operation log). 9 + 10 + ## Client Usage 11 + 12 + Fetching account data from directory: 13 + 14 + ```typescript 15 + import * as plc from '@did-plc/lib' 16 + 17 + client = new plc.Client('https://plc.directory') 18 + 19 + let exampleDid = 'did:plc:yk4dd2qkboz2yv6tpubpc6co' 20 + 21 + // current account data, in terse object format 22 + const data = await client.getDocumentData(exampleDid) 23 + 24 + // or, the full DID Document 25 + const didDoc = await client.getDocument(exampleDid) 26 + ``` 27 + 28 + Registering a new DID PLC: 29 + 30 + ```typescript 31 + import { Secp256k1Keypair } from '@atproto/crypto' 32 + import * as plc from '@did-plc/lib' 33 + 34 + // please test against a sandbox or local development server 35 + client = new plc.Client('http://localhost:2582') 36 + 37 + let signingKey = await Secp256k1Keypair.create() 38 + let rotationKey = await Secp256k1Keypair.create() 39 + 40 + did = await client.createDid({ 41 + signingKey: signingKey.did(), 42 + handle: 'handle.example.com', 43 + pds: 'https://pds.example.com', 44 + rotationKeys: [rotationKey.did()], 45 + signer: rotationKey, 46 + }) 47 + ``` 2 48 49 + ## License 50 + 51 + MIT / Apache 2.0 dual-licensed.
+16 -4
packages/lib/package.json
··· 1 1 { 2 2 "name": "@did-plc/lib", 3 3 "version": "0.0.4", 4 - "main": "src/index.ts", 5 4 "license": "MIT", 5 + "description": "DID PLC Typescript Client Library", 6 + "keywords": [ 7 + "did-plc", 8 + "did", 9 + "atproto" 10 + ], 11 + "homepage": "https://web.plc.directory", 12 + "repository": { 13 + "type": "git", 14 + "url": "https://github.com/bluesky-social/did-method-plc", 15 + "directory": "packages/lib" 16 + }, 17 + "main": "dist/index.js", 6 18 "scripts": { 7 19 "test": "jest", 8 20 "prettier": "prettier --check src/", ··· 19 31 "postpublish": "npm run update-main-to-src" 20 32 }, 21 33 "dependencies": { 22 - "@atproto/common": "0.1.1", 23 - "@atproto/crypto": "0.1.0", 34 + "@atproto/common": "0.3.0", 35 + "@atproto/crypto": "0.2.2", 24 36 "@ipld/dag-cbor": "^7.0.3", 25 37 "axios": "^1.3.4", 26 38 "multiformats": "^9.6.4", 27 39 "uint8arrays": "3.0.0", 28 - "zod": "^3.14.2" 40 + "zod": "^3.21.4" 29 41 }, 30 42 "devDependencies": { 31 43 "eslint-plugin-prettier": "^4.2.1"
-8
packages/lib/src/data.ts
··· 3 3 import * as t from './types' 4 4 import { 5 5 assureValidCreationOp, 6 - assureValidOp, 7 6 assureValidSig, 8 7 normalizeOp, 9 8 } from './operations' ··· 18 17 ops: t.IndexedOperation[], 19 18 proposed: t.CompatibleOpOrTombstone, 20 19 ): Promise<{ nullified: CID[]; prev: CID | null }> => { 21 - if (check.is(proposed, t.def.createOpV1)) { 22 - const normalized = normalizeOp(proposed) 23 - await assureValidOp(normalized) 24 - } else { 25 - await assureValidOp(proposed) 26 - } 27 - 28 20 // special case if account creation 29 21 if (ops.length === 0) { 30 22 await assureValidCreationOp(did, proposed)
+2 -2
packages/lib/src/document.ts
··· 1 1 import * as crypto from '@atproto/crypto' 2 2 import * as t from './types' 3 3 import { UnsupportedKeyError } from './error' 4 - import { ParsedDidKey } from '@atproto/crypto' 4 + import { ParsedMultikey } from '@atproto/crypto' 5 5 6 6 export const formatDidDoc = (data: t.DocumentData): t.DidDocument => { 7 7 const context = [ ··· 61 61 } 62 62 63 63 const formatKeyAndContext = (key: string): KeyAndContext => { 64 - let keyInfo: ParsedDidKey 64 + let keyInfo: ParsedMultikey 65 65 try { 66 66 keyInfo = crypto.parseDidKey(key) 67 67 } catch (err) {
+1 -25
packages/lib/src/operations.ts
··· 1 1 import * as cbor from '@ipld/dag-cbor' 2 2 import { CID } from 'multiformats/cid' 3 3 import * as uint8arrays from 'uint8arrays' 4 - import { Keypair, parseDidKey, sha256, verifySignature } from '@atproto/crypto' 4 + import { Keypair, sha256, verifySignature } from '@atproto/crypto' 5 5 import { check, cidForCbor } from '@atproto/common' 6 6 import * as t from './types' 7 7 import { ··· 9 9 ImproperOperationError, 10 10 InvalidSignatureError, 11 11 MisorderedOperationError, 12 - UnsupportedKeyError, 13 12 } from './error' 14 13 15 14 export const didForCreateOp = async (op: t.CompatibleOp) => { ··· 240 239 // Verifying operations/signatures 241 240 // --------------------------- 242 241 243 - export const assureValidOp = async (op: t.OpOrTombstone) => { 244 - if (check.is(op, t.def.tombstone)) { 245 - return true 246 - } 247 - // ensure we support the op's keys 248 - const keys = [...Object.values(op.verificationMethods), ...op.rotationKeys] 249 - await Promise.all( 250 - keys.map(async (k) => { 251 - try { 252 - parseDidKey(k) 253 - } catch (err) { 254 - throw new UnsupportedKeyError(k, err) 255 - } 256 - }), 257 - ) 258 - if (op.rotationKeys.length > 5) { 259 - throw new ImproperOperationError('too many rotation keys', op) 260 - } else if (op.rotationKeys.length < 1) { 261 - throw new ImproperOperationError('need at least one rotation key', op) 262 - } 263 - } 264 - 265 242 export const assureValidCreationOp = async ( 266 243 did: string, 267 244 op: t.CompatibleOpOrTombstone, ··· 270 247 throw new MisorderedOperationError() 271 248 } 272 249 const normalized = normalizeOp(op) 273 - await assureValidOp(normalized) 274 250 await assureValidSig(normalized.rotationKeys, op) 275 251 const expectedDid = await didForCreateOp(op) 276 252 if (expectedDid !== did) {
+18 -14
packages/lib/src/types.ts
··· 34 34 const createOpV1 = unsignedCreateOpV1.extend({ sig: z.string() }) 35 35 export type CreateOpV1 = z.infer<typeof createOpV1> 36 36 37 - const unsignedOperation = z.object({ 38 - type: z.literal('plc_operation'), 39 - rotationKeys: z.array(z.string()), 40 - verificationMethods: z.record(z.string()), 41 - alsoKnownAs: z.array(z.string()), 42 - services: z.record(service), 43 - prev: z.string().nullable(), 44 - }) 37 + const unsignedOperation = z 38 + .object({ 39 + type: z.literal('plc_operation'), 40 + rotationKeys: z.array(z.string()), 41 + verificationMethods: z.record(z.string()), 42 + alsoKnownAs: z.array(z.string()), 43 + services: z.record(service), 44 + prev: z.string().nullable(), 45 + }) 46 + .strict() 45 47 export type UnsignedOperation = z.infer<typeof unsignedOperation> 46 - const operation = unsignedOperation.extend({ sig: z.string() }) 48 + const operation = unsignedOperation.extend({ sig: z.string() }).strict() 47 49 export type Operation = z.infer<typeof operation> 48 50 49 - const unsignedTombstone = z.object({ 50 - type: z.literal('plc_tombstone'), 51 - prev: z.string(), 52 - }) 51 + const unsignedTombstone = z 52 + .object({ 53 + type: z.literal('plc_tombstone'), 54 + prev: z.string(), 55 + }) 56 + .strict() 53 57 export type UnsignedTombstone = z.infer<typeof unsignedTombstone> 54 - const tombstone = unsignedTombstone.extend({ sig: z.string() }) 58 + const tombstone = unsignedTombstone.extend({ sig: z.string() }).strict() 55 59 export type Tombstone = z.infer<typeof tombstone> 56 60 57 61 const opOrTombstone = z.union([operation, tombstone])
+3 -3
packages/lib/tests/data.test.ts
··· 1 1 import { check, cidForCbor } from '@atproto/common' 2 - import { EcdsaKeypair, Secp256k1Keypair } from '@atproto/crypto' 2 + import { P256Keypair, Secp256k1Keypair } from '@atproto/crypto' 3 3 import { 4 4 GenesisHashError, 5 5 ImproperOperationError, ··· 15 15 16 16 let signingKey: Secp256k1Keypair 17 17 let rotationKey1: Secp256k1Keypair 18 - let rotationKey2: EcdsaKeypair 18 + let rotationKey2: P256Keypair 19 19 let did: string 20 20 let handle = 'at://alice.example.com' 21 21 let atpPds = 'https://example.com' ··· 25 25 beforeAll(async () => { 26 26 signingKey = await Secp256k1Keypair.create() 27 27 rotationKey1 = await Secp256k1Keypair.create() 28 - rotationKey2 = await EcdsaKeypair.create() 28 + rotationKey2 = await P256Keypair.create() 29 29 }) 30 30 31 31 const lastOp = () => {
+3 -3
packages/lib/tests/document.test.ts
··· 1 - import { EcdsaKeypair, parseDidKey, Secp256k1Keypair } from '@atproto/crypto' 1 + import { P256Keypair, parseDidKey, Secp256k1Keypair } from '@atproto/crypto' 2 2 import * as document from '../src/document' 3 3 import * as t from '../src/types' 4 4 5 5 describe('document', () => { 6 6 it('formats a valid DID document', async () => { 7 7 const atprotoKey = await Secp256k1Keypair.create() 8 - const otherKey = await EcdsaKeypair.create() 8 + const otherKey = await P256Keypair.create() 9 9 const rotate1 = await Secp256k1Keypair.create() 10 - const rotate2 = await EcdsaKeypair.create() 10 + const rotate2 = await P256Keypair.create() 11 11 const alsoKnownAs = ['at://alice.test', 'https://bob.test'] 12 12 const atpPds = 'https://example.com' 13 13 const otherService = 'https://other.com'
+5 -5
packages/lib/tests/recovery.test.ts
··· 1 1 import { cidForCbor, DAY, HOUR } from '@atproto/common' 2 - import { EcdsaKeypair, Keypair, Secp256k1Keypair } from '@atproto/crypto' 2 + import { P256Keypair, Keypair, Secp256k1Keypair } from '@atproto/crypto' 3 3 import { CID } from 'multiformats/cid' 4 4 import { InvalidSignatureError, LateRecoveryError } from '../src' 5 5 import * as data from '../src/data' ··· 9 9 describe('plc recovery', () => { 10 10 let signingKey: Secp256k1Keypair 11 11 let rotationKey1: Secp256k1Keypair 12 - let rotationKey2: EcdsaKeypair 13 - let rotationKey3: EcdsaKeypair 12 + let rotationKey2: P256Keypair 13 + let rotationKey3: P256Keypair 14 14 let did: string 15 15 const handle = 'alice.example.com' 16 16 const atpPds = 'https://example.com' ··· 22 22 beforeAll(async () => { 23 23 signingKey = await Secp256k1Keypair.create() 24 24 rotationKey1 = await Secp256k1Keypair.create() 25 - rotationKey2 = await EcdsaKeypair.create() 26 - rotationKey3 = await EcdsaKeypair.create() 25 + rotationKey2 = await P256Keypair.create() 26 + rotationKey3 = await P256Keypair.create() 27 27 }) 28 28 29 29 const formatIndexed = async (
+11 -1
packages/server/README.md
··· 1 - # DID PLC Server 1 + 2 + @did-plc/server - DID PLC Directory Service 3 + =========================================== 4 + 5 + Reference implementation of the PLC DID method, in Typescript. 6 + 7 + This is the software that runs the <https://plc.directory> service. 8 + 9 + ## License 10 + 11 + MIT / Apache 2.0 dual-licensed.
+15 -3
packages/server/package.json
··· 1 1 { 2 2 "name": "@did-plc/server", 3 3 "version": "0.0.1", 4 - "main": "dist/index.js", 5 4 "license": "MIT", 5 + "description": "DID PLC Directory Service", 6 + "keywords": [ 7 + "did-plc", 8 + "did", 9 + "atproto" 10 + ], 11 + "homepage": "https://web.plc.directory", 12 + "repository": { 13 + "type": "git", 14 + "url": "https://github.com/bluesky-social/did-method-plc", 15 + "directory": "packages/server" 16 + }, 17 + "main": "dist/index.js", 6 18 "scripts": { 7 19 "start": "node dist/bin.js", 8 20 "test": "./pg/with-test-db.sh jest", ··· 23 35 "postpublish": "npm run update-main-to-src" 24 36 }, 25 37 "dependencies": { 26 - "@atproto/common": "0.1.1", 27 - "@atproto/crypto": "0.1.0", 38 + "@atproto/common": "0.3.0", 39 + "@atproto/crypto": "0.2.2", 28 40 "@did-plc/lib": "*", 29 41 "axios": "^1.3.4", 30 42 "cors": "^2.8.5",
+147
packages/server/src/constraints.ts
··· 1 + import { DAY, HOUR, cborEncode, check } from '@atproto/common' 2 + import * as plc from '@did-plc/lib' 3 + import { ServerError } from './error' 4 + import { parseDidKey } from '@atproto/crypto' 5 + 6 + const MAX_OP_BYTES = 4000 7 + const MAX_AKA_ENTRIES = 10 8 + const MAX_AKA_LENGTH = 256 9 + const MAX_ROTATION_ENTRIES = 10 10 + const MAX_SERVICE_ENTRIES = 10 11 + const MAX_SERVICE_TYPE_LENGTH = 256 12 + const MAX_SERVICE_ENDPOINT_LENGTH = 512 13 + const MAX_ID_LENGTH = 32 14 + 15 + export function assertValidIncomingOp( 16 + op: unknown, 17 + ): asserts op is plc.OpOrTombstone { 18 + const byteLength = cborEncode(op).byteLength 19 + if (byteLength > MAX_OP_BYTES) { 20 + throw new ServerError( 21 + 400, 22 + `Operation too large (${MAX_OP_BYTES} bytes maximum in cbor encoding)`, 23 + ) 24 + } 25 + if (!check.is(op, plc.def.opOrTombstone)) { 26 + throw new ServerError(400, `Not a valid operation: ${JSON.stringify(op)}`) 27 + } 28 + if (op.type === 'plc_tombstone') { 29 + return 30 + } 31 + if (op.alsoKnownAs.length > MAX_AKA_ENTRIES) { 32 + throw new ServerError( 33 + 400, 34 + `To many alsoKnownAs entries (max ${MAX_AKA_ENTRIES})`, 35 + ) 36 + } 37 + const akaDupe: Record<string, boolean> = {} 38 + for (const aka of op.alsoKnownAs) { 39 + if (aka.length > MAX_AKA_LENGTH) { 40 + throw new ServerError( 41 + 400, 42 + `alsoKnownAs entry too long (max ${MAX_AKA_LENGTH}): ${aka}`, 43 + ) 44 + } 45 + if (akaDupe[aka]) { 46 + throw new ServerError(400, `duplicate alsoKnownAs entry: ${aka}`) 47 + } else { 48 + akaDupe[aka] = true 49 + } 50 + } 51 + if (op.rotationKeys.length > MAX_ROTATION_ENTRIES) { 52 + throw new ServerError( 53 + 400, 54 + `Too many rotationKey entries (max ${MAX_ROTATION_ENTRIES})`, 55 + ) 56 + } 57 + for (const key of op.rotationKeys) { 58 + try { 59 + parseDidKey(key) 60 + } catch (err) { 61 + throw new ServerError(400, `Invalid rotationKey: ${key}`) 62 + } 63 + } 64 + const serviceEntries = Object.entries(op.services) 65 + if (serviceEntries.length > MAX_SERVICE_ENTRIES) { 66 + throw new ServerError( 67 + 400, 68 + `To many service entries (max ${MAX_SERVICE_ENTRIES})`, 69 + ) 70 + } 71 + for (const [id, service] of serviceEntries) { 72 + if (id.length > MAX_ID_LENGTH) { 73 + throw new ServerError( 74 + 400, 75 + `Service id too long (max ${MAX_ID_LENGTH}): ${id}`, 76 + ) 77 + } 78 + if (service.type.length > MAX_SERVICE_TYPE_LENGTH) { 79 + throw new ServerError( 80 + 400, 81 + `Service type too long (max ${MAX_SERVICE_TYPE_LENGTH})`, 82 + ) 83 + } 84 + if (service.endpoint.length > MAX_SERVICE_ENDPOINT_LENGTH) { 85 + throw new ServerError( 86 + 400, 87 + `Service endpoint too long (max ${MAX_SERVICE_ENDPOINT_LENGTH})`, 88 + ) 89 + } 90 + } 91 + const verifyMethods = Object.entries(op.verificationMethods) 92 + for (const [id, key] of verifyMethods) { 93 + if (id.length > MAX_ID_LENGTH) { 94 + throw new ServerError( 95 + 400, 96 + `Verification Method id too long (max ${MAX_ID_LENGTH}): ${id}`, 97 + ) 98 + } 99 + try { 100 + parseDidKey(key) 101 + } catch (err) { 102 + throw new ServerError(400, `Invalid verificationMethod key: ${key}`) 103 + } 104 + } 105 + } 106 + 107 + const HOUR_LIMIT = 10 108 + const DAY_LIMIT = 30 109 + const WEEK_LIMIT = 100 110 + 111 + export const enforceOpsRateLimit = (ops: plc.IndexedOperation[]) => { 112 + const hourAgo = new Date(Date.now() - HOUR) 113 + const dayAgo = new Date(Date.now() - DAY) 114 + const weekAgo = new Date(Date.now() - DAY * 7) 115 + let withinHour = 0 116 + let withinDay = 0 117 + let withinWeek = 0 118 + for (const op of ops) { 119 + if (op.createdAt > weekAgo) { 120 + withinWeek++ 121 + if (withinWeek >= WEEK_LIMIT) { 122 + throw new ServerError( 123 + 400, 124 + `To many operations within last week (max ${WEEK_LIMIT})`, 125 + ) 126 + } 127 + } 128 + if (op.createdAt > dayAgo) { 129 + withinDay++ 130 + if (withinDay >= DAY_LIMIT) { 131 + throw new ServerError( 132 + 400, 133 + `To many operations within last day (max ${DAY_LIMIT})`, 134 + ) 135 + } 136 + } 137 + if (op.createdAt > hourAgo) { 138 + withinHour++ 139 + if (withinHour >= HOUR_LIMIT) { 140 + throw new ServerError( 141 + 400, 142 + `To many operations within last hour (max ${HOUR_LIMIT})`, 143 + ) 144 + } 145 + } 146 + } 147 + }
+6
packages/server/src/db/index.ts
··· 7 7 import * as migrations from '../migrations' 8 8 import { DatabaseSchema, PlcDatabase } from './types' 9 9 import MockDatabase from './mock' 10 + import { enforceOpsRateLimit } from '../constraints' 10 11 11 12 export * from './mock' 12 13 export * from './types' ··· 99 100 const ops = await this.indexedOpsForDid(did) 100 101 // throws if invalid 101 102 const { nullified, prev } = await plc.assureValidNextOp(did, ops, proposed) 103 + // do not enforce rate limits on recovery operations to prevent DDOS by a bad actor 104 + if (nullified.length === 0) { 105 + enforceOpsRateLimit(ops) 106 + } 107 + 102 108 const cid = await cidForCbor(proposed) 103 109 104 110 await this.db.transaction().execute(async (tx) => {
+4 -10
packages/server/src/routes.ts
··· 1 1 import express from 'express' 2 - import { cborEncode, check } from '@atproto/common' 3 2 import * as plc from '@did-plc/lib' 4 3 import { ServerError } from './error' 5 4 import { AppContext } from './context' 5 + import { assertValidIncomingOp } from './constraints' 6 6 7 7 export const createRouter = (ctx: AppContext): express.Router => { 8 8 const router = express.Router() 9 9 10 10 router.get('/', async function (req, res) { 11 - // HTTP temporary redirect to project git repo 12 - res.redirect(302, 'https://github.com/bluesky-social/did-method-plc') 11 + // HTTP temporary redirect to project homepage 12 + res.redirect(302, 'https://web.plc.directory') 13 13 }) 14 14 15 15 router.get('/_health', async function (req, res) { ··· 114 114 router.post('/:did', async function (req, res) { 115 115 const { did } = req.params 116 116 const op = req.body 117 - const byteLength = cborEncode(op).byteLength 118 - if (byteLength > 7500) { 119 - throw new ServerError(400, 'Operation too large') 120 - } 121 - if (!check.is(op, plc.def.compatibleOpOrTombstone)) { 122 - throw new ServerError(400, `Not a valid operation: ${JSON.stringify(op)}`) 123 - } 117 + assertValidIncomingOp(op) 124 118 await ctx.db.validateAndAddOp(did, op) 125 119 res.sendStatus(200) 126 120 })
+19 -18
packages/server/tests/server.test.ts
··· 1 - import { EcdsaKeypair } from '@atproto/crypto' 1 + import { P256Keypair } from '@atproto/crypto' 2 2 import * as plc from '@did-plc/lib' 3 3 import { CloseFn, runTestServer } from './_util' 4 4 import { check } from '@atproto/common' ··· 13 13 let db: Database 14 14 let client: plc.Client 15 15 16 - let signingKey: EcdsaKeypair 17 - let rotationKey1: EcdsaKeypair 18 - let rotationKey2: EcdsaKeypair 16 + let signingKey: P256Keypair 17 + let rotationKey1: P256Keypair 18 + let rotationKey2: P256Keypair 19 19 20 20 let did: string 21 21 ··· 27 27 db = server.db 28 28 close = server.close 29 29 client = new plc.Client(server.url) 30 - signingKey = await EcdsaKeypair.create() 31 - rotationKey1 = await EcdsaKeypair.create() 32 - rotationKey2 = await EcdsaKeypair.create() 30 + signingKey = await P256Keypair.create() 31 + rotationKey1 = await P256Keypair.create() 32 + rotationKey2 = await P256Keypair.create() 33 33 }) 34 34 35 35 afterAll(async () => { ··· 70 70 }) 71 71 72 72 it('can perform some updates', async () => { 73 - const newRotationKey = await EcdsaKeypair.create() 74 - signingKey = await EcdsaKeypair.create() 73 + const newRotationKey = await P256Keypair.create() 74 + signingKey = await P256Keypair.create() 75 75 handle = 'at://ali.example2.com' 76 76 atpPds = 'https://example2.com' 77 77 ··· 111 111 }) 112 112 113 113 it('rejects on bad updates', async () => { 114 - const newKey = await EcdsaKeypair.create() 114 + const newKey = await P256Keypair.create() 115 115 const operation = client.updateAtprotoKey(did, newKey, newKey.did()) 116 116 await expect(operation).rejects.toThrow() 117 117 }) 118 118 119 119 it('allows for recovery through a forked history', async () => { 120 - const attackerKey = await EcdsaKeypair.create() 120 + const attackerKey = await P256Keypair.create() 121 121 await client.updateRotationKeys(did, rotationKey2, [attackerKey.did()]) 122 122 123 - const newKey = await EcdsaKeypair.create() 123 + const newKey = await P256Keypair.create() 124 124 const ops = await client.getOperationLog(did) 125 125 const forkPoint = ops.at(-2) 126 126 if (!check.is(forkPoint, plc.def.operation)) { ··· 158 158 159 159 it('handles concurrent requests to many docs', async () => { 160 160 const COUNT = 20 161 - const keys: EcdsaKeypair[] = [] 161 + const keys: P256Keypair[] = [] 162 162 for (let i = 0; i < COUNT; i++) { 163 - keys.push(await EcdsaKeypair.create()) 163 + keys.push(await P256Keypair.create()) 164 164 } 165 165 await Promise.all( 166 166 keys.map(async (key, index) => { ··· 177 177 178 178 it('resolves races into a coherent history with no forks', async () => { 179 179 const COUNT = 20 180 - const keys: EcdsaKeypair[] = [] 180 + const keys: P256Keypair[] = [] 181 181 for (let i = 0; i < COUNT; i++) { 182 - keys.push(await EcdsaKeypair.create()) 182 + keys.push(await P256Keypair.create()) 183 183 } 184 184 // const prev = await client.getPrev(did) 185 185 ··· 220 220 } 221 221 }) 222 222 223 - it('still allows create v1s', async () => { 223 + it('disallows create v1s', async () => { 224 224 const createV1 = await plc.deprecatedSignCreate( 225 225 { 226 226 type: 'create', ··· 233 233 signingKey, 234 234 ) 235 235 const did = await didForCreateOp(createV1) 236 - await client.sendOperation(did, createV1 as any) 236 + const attempt = client.sendOperation(did, createV1 as any) 237 + await expect(attempt).rejects.toThrow() 237 238 }) 238 239 239 240 it('healthcheck succeeds when database is available.', async () => {
+121 -32
yarn.lock
··· 10 10 "@jridgewell/gen-mapping" "^0.1.0" 11 11 "@jridgewell/trace-mapping" "^0.3.9" 12 12 13 - "@atproto/common@0.1.1": 14 - version "0.1.1" 15 - resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.1.tgz#ec33a3b4995c91d3ad2e90fc4cdbc65284ceff84" 16 - integrity sha512-GYwot5wF/z8iYGSPjrLHuratLc0CVgovmwfJss7+BUOB6y2/Vw8+1Vw0n9DDI0gb5vmx3UI8z0uJgC8aa8yuJg== 13 + "@atproto/common-web@*": 14 + version "0.2.0" 15 + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.2.0.tgz#8420da28e89dac64ad2a11b6ff10a4023c67151f" 16 + integrity sha512-ugKrT8CWf6PDsZ29VOhOCs5K4z9BAFIV7SxPXA+MHC7pINqQ+wyjIq+DtUI8kmUSce1dTqc/lMN1akf/W5whVQ== 17 + dependencies: 18 + graphemer "^1.4.0" 19 + multiformats "^9.6.4" 20 + uint8arrays "3.0.0" 21 + zod "^3.21.4" 22 + 23 + "@atproto/common@0.3.0": 24 + version "0.3.0" 25 + resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.3.0.tgz#6ddb0a9bedbf9058353a241b056ae83539da3539" 26 + integrity sha512-R5d50Da63wQdAYaHe+rne5nM/rSYIWBaDZtVKpluysG86U1rRIgrG4qEQ/tNDt6WzYcxKXkX4EOeVvGtav2twg== 17 27 dependencies: 28 + "@atproto/common-web" "*" 18 29 "@ipld/dag-cbor" "^7.0.3" 30 + cbor-x "^1.5.1" 19 31 multiformats "^9.6.4" 20 32 pino "^8.6.1" 21 - zod "^3.14.2" 22 33 23 - "@atproto/crypto@0.1.0": 24 - version "0.1.0" 25 - resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.1.0.tgz#bc73a479f9dbe06fa025301c182d7f7ab01bc568" 26 - integrity sha512-9xgFEPtsCiJEPt9o3HtJT30IdFTGw5cQRSJVIy5CFhqBA4vDLcdXiRDLCjkzHEVbtNCsHUW6CrlfOgbeLPcmcg== 34 + "@atproto/crypto@0.2.2": 35 + version "0.2.2" 36 + resolved "https://registry.yarnpkg.com/@atproto/crypto/-/crypto-0.2.2.tgz#9832dda885512a36401d24f95990489f521593ef" 37 + integrity sha512-yepwM6pLPw/bT7Nl0nfDw251yVDpuhc0llOgD8YdCapUAH7pIn4dBcMgXiA9UzQaHA7OC9ByO5IdGPrMN/DmZw== 27 38 dependencies: 28 - "@noble/secp256k1" "^1.7.0" 29 - big-integer "^1.6.51" 30 - multiformats "^9.6.4" 31 - one-webcrypto "^1.0.3" 39 + "@noble/curves" "^1.1.0" 40 + "@noble/hashes" "^1.3.1" 32 41 uint8arrays "3.0.0" 33 42 34 43 "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": ··· 949 958 version "0.2.3" 950 959 resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" 951 960 integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== 961 + 962 + "@cbor-extract/cbor-extract-darwin-arm64@2.1.1": 963 + version "2.1.1" 964 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz#5721f6dd3feae0b96d23122853ce977e0671b7a6" 965 + integrity sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA== 966 + 967 + "@cbor-extract/cbor-extract-darwin-x64@2.1.1": 968 + version "2.1.1" 969 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.1.1.tgz#c25e7d0133950d87d101d7b3afafea8d50d83f5f" 970 + integrity sha512-h6KFOzqk8jXTvkOftyRIWGrd7sKQzQv2jVdTL9nKSf3D2drCvQB/LHUxAOpPXo3pv2clDtKs3xnHalpEh3rDsw== 971 + 972 + "@cbor-extract/cbor-extract-linux-arm64@2.1.1": 973 + version "2.1.1" 974 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.1.1.tgz#48f78e7d8f0fcc84ed074b6bfa6d15dd83187c63" 975 + integrity sha512-SxAaRcYf8S0QHaMc7gvRSiTSr7nUYMqbUdErBEu+HYA4Q6UNydx1VwFE68hGcp1qvxcy9yT5U7gA+a5XikfwSQ== 976 + 977 + "@cbor-extract/cbor-extract-linux-arm@2.1.1": 978 + version "2.1.1" 979 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.1.1.tgz#7507d346389cb682e44fab8fae9534edd52e2e41" 980 + integrity sha512-ds0uikdcIGUjPyraV4oJqyVE5gl/qYBpa/Wnh6l6xLE2lj/hwnjT2XcZCChdXwW/YFZ1LUHs6waoYN8PmK0nKQ== 981 + 982 + "@cbor-extract/cbor-extract-linux-x64@2.1.1": 983 + version "2.1.1" 984 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.1.1.tgz#b7c1d2be61c58ec18d58afbad52411ded63cd4cd" 985 + integrity sha512-GVK+8fNIE9lJQHAlhOROYiI0Yd4bAZ4u++C2ZjlkS3YmO6hi+FUxe6Dqm+OKWTcMpL/l71N6CQAmaRcb4zyJuA== 986 + 987 + "@cbor-extract/cbor-extract-win32-x64@2.1.1": 988 + version "2.1.1" 989 + resolved "https://registry.yarnpkg.com/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.1.1.tgz#21b11a1a3f18c3e7d62fd5f87438b7ed2c64c1f7" 990 + integrity sha512-2Niq1C41dCRIDeD8LddiH+mxGlO7HJ612Ll3D/E73ZWBmycued+8ghTr/Ho3CMOWPUEr08XtyBMVXAjqF+TcKw== 952 991 953 992 "@cspotcode/source-map-support@^0.8.0": 954 993 version "0.8.1" ··· 1947 1986 npmlog "^4.1.2" 1948 1987 write-file-atomic "^3.0.3" 1949 1988 1950 - "@noble/secp256k1@^1.7.0": 1951 - version "1.7.0" 1952 - resolved "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.0.tgz" 1953 - integrity sha512-kbacwGSsH/CTout0ZnZWxnW1B+jH/7r/WAAKLBtrRJ/+CUH7lgmQzl3GTrQua3SGKWNSDsS6lmjnDpIJ5Dxyaw== 1989 + "@noble/curves@^1.1.0": 1990 + version "1.2.0" 1991 + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" 1992 + integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== 1993 + dependencies: 1994 + "@noble/hashes" "1.3.2" 1995 + 1996 + "@noble/hashes@1.3.2", "@noble/hashes@^1.3.1": 1997 + version "1.3.2" 1998 + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" 1999 + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== 1954 2000 1955 2001 "@nodelib/fs.scandir@2.1.5": 1956 2002 version "2.1.5" ··· 2781 2827 resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz" 2782 2828 integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== 2783 2829 2784 - big-integer@^1.6.51: 2785 - version "1.6.51" 2786 - resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz" 2787 - integrity sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg== 2788 - 2789 2830 body-parser@1.20.1: 2790 2831 version "1.20.1" 2791 2832 resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" ··· 2959 3000 resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" 2960 3001 integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== 2961 3002 3003 + cbor-extract@^2.1.1: 3004 + version "2.1.1" 3005 + resolved "https://registry.yarnpkg.com/cbor-extract/-/cbor-extract-2.1.1.tgz#f154b31529fdb6b7c70fb3ca448f44eda96a1b42" 3006 + integrity sha512-1UX977+L+zOJHsp0mWFG13GLwO6ucKgSmSW6JTl8B9GUvACvHeIVpFqhU92299Z6PfD09aTXDell5p+lp1rUFA== 3007 + dependencies: 3008 + node-gyp-build-optional-packages "5.0.3" 3009 + optionalDependencies: 3010 + "@cbor-extract/cbor-extract-darwin-arm64" "2.1.1" 3011 + "@cbor-extract/cbor-extract-darwin-x64" "2.1.1" 3012 + "@cbor-extract/cbor-extract-linux-arm" "2.1.1" 3013 + "@cbor-extract/cbor-extract-linux-arm64" "2.1.1" 3014 + "@cbor-extract/cbor-extract-linux-x64" "2.1.1" 3015 + "@cbor-extract/cbor-extract-win32-x64" "2.1.1" 3016 + 3017 + cbor-x@^1.5.1: 3018 + version "1.5.4" 3019 + resolved "https://registry.yarnpkg.com/cbor-x/-/cbor-x-1.5.4.tgz#8f0754fa8589cbd7339b613b2b5717d133508e98" 3020 + integrity sha512-PVKILDn+Rf6MRhhcyzGXi5eizn1i0i3F8Fe6UMMxXBnWkalq9+C5+VTmlIjAYM4iF2IYF2N+zToqAfYOp+3rfw== 3021 + optionalDependencies: 3022 + cbor-extract "^2.1.1" 3023 + 2962 3024 cborg@^1.6.0: 2963 3025 version "1.9.5" 2964 3026 resolved "https://registry.npmjs.org/cborg/-/cborg-1.9.5.tgz" ··· 3514 3576 integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== 3515 3577 dependencies: 3516 3578 is-obj "^2.0.0" 3579 + 3580 + dotenv@^16.0.0: 3581 + version "16.3.1" 3582 + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" 3583 + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== 3517 3584 3518 3585 dotenv@^16.0.3: 3519 3586 version "16.0.3" ··· 4552 4619 resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" 4553 4620 integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== 4554 4621 4622 + graphemer@^1.4.0: 4623 + version "1.4.0" 4624 + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" 4625 + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== 4626 + 4555 4627 handlebars@^4.7.7: 4556 4628 version "4.7.7" 4557 4629 resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz" ··· 6094 6166 dependencies: 6095 6167 whatwg-url "^5.0.0" 6096 6168 6169 + node-gyp-build-optional-packages@5.0.3: 6170 + version "5.0.3" 6171 + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" 6172 + integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA== 6173 + 6097 6174 node-gyp@^5.0.2: 6098 6175 version "5.1.1" 6099 6176 resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.1.tgz" ··· 6365 6442 integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 6366 6443 dependencies: 6367 6444 wrappy "1" 6368 - 6369 - one-webcrypto@^1.0.3: 6370 - version "1.0.3" 6371 - resolved "https://registry.npmjs.org/one-webcrypto/-/one-webcrypto-1.0.3.tgz" 6372 - integrity sha512-fu9ywBVBPx0gS9K0etIROTiCkvI5S1TDjFsYFb3rC1ewFxeOqsbzq7aIMBHsYfrTHBcGXJaONXXjTl8B01cW1Q== 6373 6445 6374 6446 onetime@^5.1.0, onetime@^5.1.2: 6375 6447 version "5.1.2" ··· 6782 6854 resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.0.0.tgz" 6783 6855 integrity sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ== 6784 6856 6785 - pino@^8.0.0, pino@^8.11.0, pino@^8.6.1: 6857 + pino@^8.0.0, pino@^8.11.0: 6786 6858 version "8.11.0" 6787 6859 resolved "https://registry.yarnpkg.com/pino/-/pino-8.11.0.tgz#2a91f454106b13e708a66c74ebc1c2ab7ab38498" 6788 6860 integrity sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg== ··· 6799 6871 sonic-boom "^3.1.0" 6800 6872 thread-stream "^2.0.0" 6801 6873 6874 + pino@^8.6.1: 6875 + version "8.15.0" 6876 + resolved "https://registry.yarnpkg.com/pino/-/pino-8.15.0.tgz#67c61d5e397bf297e5a0433976a7f7b8aa6f876b" 6877 + integrity sha512-olUADJByk4twxccmAxb1RiGKOSvddHugCV3wkqjyv+3Sooa2KLrmXrKEWOKi0XPCLasRR5jBXxioE1jxUa4KzQ== 6878 + dependencies: 6879 + atomic-sleep "^1.0.0" 6880 + fast-redact "^3.1.1" 6881 + on-exit-leak-free "^2.1.0" 6882 + pino-abstract-transport v1.0.0 6883 + pino-std-serializers "^6.0.0" 6884 + process-warning "^2.0.0" 6885 + quick-format-unescaped "^4.0.3" 6886 + real-require "^0.2.0" 6887 + safe-stable-stringify "^2.3.1" 6888 + sonic-boom "^3.1.0" 6889 + thread-stream "^2.0.0" 6890 + 6802 6891 pirates@^4.0.4: 6803 6892 version "4.0.5" 6804 6893 resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz" ··· 8541 8630 resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" 8542 8631 integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== 8543 8632 8544 - zod@^3.14.2: 8545 - version "3.19.1" 8546 - resolved "https://registry.npmjs.org/zod/-/zod-3.19.1.tgz" 8547 - integrity sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA== 8633 + zod@^3.21.4: 8634 + version "3.22.2" 8635 + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.2.tgz#3add8c682b7077c05ac6f979fea6998b573e157b" 8636 + integrity sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==