diff --git a/CLAUDE.md b/CLAUDE.md index 5eacba9..d933677 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -210,6 +210,8 @@ func NewConfigCmd() *cobra.Command { - The CLI provides commands to create, list, and revoke API keys - Keys are generated server-side and can be used for programmatic access - Keys are stored in the system keyring alongside tokens +- Keys are created with `JsonAPI` scope and always carry a grant (spaces × read/read-write); the CLI never creates an unrestricted key. Grant logic lives in `core/apikeygrant.go` +- `CreateAPIKey` probes the server for grant support and reads the key back, revoking it if the stored access differs from the request ### Testing Strategy - **Unit Tests**: Test individual functions and logic in isolation diff --git a/Makefile b/Makefile index f96d819..3093375 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ TANTIVY_ASSET = $(TANTIVY_ASSET_$(GOOS)_$(GOARCH)) TANTIVY_URL = https://github.com/anyproto/tantivy-go/releases/download/$(TANTIVY_VERSION)/$(TANTIVY_ASSET).tar.gz CGO_LDFLAGS := -L$(TANTIVY_LIB_PATH) -GOLANGCI_LINT_VERSION := v2.7.2 +GOLANGCI_LINT_VERSION := v2.12.2 ##@ Build diff --git a/README.md b/README.md index 70fbcd5..52f63ff 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ anytype space join anytype space list # Create an API key for programmatic access -anytype auth apikey create "my-bot-api-key" +anytype auth apikey create "my-bot-api-key" --all-spaces --read-write ``` Once running, the API is available at `http://127.0.0.1:31012`. Use your API key to authenticate requests to the endpoints described on the [Developer Portal](https://developers.anytype.io). See [Network Configuration](#network-configuration) for remote access options. @@ -128,7 +128,19 @@ By default, the server binds to `127.0.0.1` (localhost only) on ports 31010-3101 | 31012 | API | HTTP API server endpoint ⭐ | -You can change the API listen address using `--listen-address` (e.g., `--listen-address 0.0.0.0:31012`). For remote access, you can also use a reverse proxy, SSH tunnel, or Docker port mapping to expose the local ports. +You can change the JSON API listen address with `--listen-address` on `serve`, `service install`, `auth login` or `auth create` (e.g., `--listen-address 0.0.0.0:31012`). The address is remembered, including across logout, so later commands use it without the flag. The JSON API starts once an account is logged in. `serve` prints the address it will use at startup, and `anytype auth status` and the login commands print it too. For remote access, you can also use a reverse proxy, SSH tunnel, or Docker port mapping to expose the local ports. + +The server only accepts requests whose `Host` is `localhost` or an IP address, and browser requests from local origins. If you reach it by another hostname (for example through a reverse proxy) or from a web page on another origin, allow them explicitly in the server's environment: + +| Variable | Applies to | Value | +| --- | --- | --- | +| `ANYTYPE_API_ALLOWED_HOSTS` | HTTP API (31012) | Comma-separated hostnames, e.g. `anytype.example.com` | +| `ANYTYPE_API_ALLOWED_ORIGINS` | HTTP API (31012) | Comma-separated exact origins, e.g. `https://app.example.com` | +| `ANYTYPE_GRPCWEB_ALLOWED_HOSTS` | gRPC-Web (31011) | Comma-separated hostnames | +| `ANYTYPE_GRPCWEB_ALLOWED_ORIGINS` | gRPC-Web (31011) | Comma-separated exact origins | +| `ANYTYPE_GRPCWEB_ENABLE_WEBSOCKETS` | gRPC-Web (31011) | `1` to enable the WebSocket transport (off by default) | + +Set them where `anytype serve` runs: in your shell, your Docker/Compose environment, or the user service's definition. **Security note**: Always keep your API keys safe. If ports are exposed externally, third parties with your API key could gain unauthorized access to the spaces your headless instance has access to. @@ -155,8 +167,9 @@ anytype auth logout Manage API keys for programmatic access: ```bash -# Create a new API key -anytype auth apikey create +# Create a new API key: choose its spaces and whether it can write +anytype auth apikey create --space [--space ...] --read-only +anytype auth apikey create --all-spaces --read-write # List all API keys anytype auth apikey list @@ -165,6 +178,20 @@ anytype auth apikey list anytype auth apikey revoke ``` +Every key is limited to the spaces and permission you choose; there is no default. Use `anytype space list` to find space names and Ids. A key limited to specific spaces, or a read-only key, works with the JSON API v2 only; an `--all-spaces --read-write` key works with v1 and v2. + +#### Upgrading to the JSON API v2 + +Keys created by earlier CLI versions keep working with the JSON API v1, but the v2 API rejects them. To move an integration to v2: + +1. Create a new key with the access it needs, using **the same name** as the old key. On v2, a key can only delete objects created under its name. +2. Check that the integration works with the new key, then switch it over. +3. Revoke the old key with `anytype auth apikey revoke `. + +Keep the old key if the integration calls the gRPC API directly: new keys work only with the JSON API. + +After updating the CLI, restart the service (`anytype service restart`) so it runs the new version; `apikey create` refuses to create keys on an older running server. Keys created by this version don't work if you downgrade to an earlier one. + ### Space Management Work with Anytype spaces: diff --git a/cmd/auth/apikey/create/create.go b/cmd/auth/apikey/create/create.go index 3e9e55a..66631e4 100644 --- a/cmd/auth/apikey/create/create.go +++ b/cmd/auth/apikey/create/create.go @@ -1,34 +1,118 @@ package create import ( + "errors" + "fmt" + "strings" + "github.com/spf13/cobra" "github.com/anyproto/anytype-cli/cmd/cmdutil" "github.com/anyproto/anytype-cli/core" + "github.com/anyproto/anytype-cli/core/config" "github.com/anyproto/anytype-cli/core/output" ) +// Server calls, replaceable in tests. +var ( + listSpaces = core.ListSpaces + techSpaceId = config.GetTechSpaceIdFromConfig + createAPIKey = core.CreateAPIKey +) + func NewCreateCmd() *cobra.Command { + var flags core.GrantFlags + cmd := &cobra.Command{ Use: "create ", Short: "Create a new API key", - Long: "Create a new API key for programmatic access to Anytype", - Args: cmdutil.ExactArgs(1, "cannot create API key: name argument required"), + Long: `Create a new API key for programmatic access to Anytype. + +You must choose which spaces the key can access and whether it can write: + --space (repeatable) or --all-spaces + --read-only or --read-write + +Keys limited to specific spaces, or read-only keys, work with the JSON API v2 only.`, + Example: ` anytype auth apikey create my-app --space "Personal" --read-only + anytype auth apikey create my-app --all-spaces --read-write`, + Args: cmdutil.ExactArgs(1, "cannot create API key: name argument required"), RunE: func(cmd *cobra.Command, args []string) error { name := args[0] - resp, err := core.CreateAPIKey(name) + if err := core.ValidateAPIKeyName(name); err != nil { + return output.Error("Failed to create API key: %w", err) + } + if err := core.ValidateGrantFlags(flags); err != nil { + if errors.Is(err, core.ErrSpaceChoiceRequired) { + return output.Error("Failed to create API key: %w%s", err, spaceChoices()) + } + return output.Error("Failed to create API key: %w", err) + } + + var resolved []core.ResolvedSpace + if len(flags.Spaces) > 0 { + spaces, err := listSpaces() + if err != nil { + return output.Error("Failed to list spaces: %w", err) + } + techId, err := techSpaceId() + if err != nil { + return output.Error("Failed to read tech space Id: %w", err) + } + resolved, err = core.ResolveSpaces(flags.Spaces, spaces, techId) + if err != nil { + return output.Error("Failed to create API key: %w", err) + } + for _, space := range resolved { + if space.IsTech { + output.Warning("The key can access the tech space, which holds account internals; writing to it can break your account") + } + } + } + + grant, err := core.BuildGrant(flags, resolved) + if err != nil { + return output.Error("Failed to create API key: %w", err) + } + + created, err := createAPIKey(name, grant) if err != nil { return output.Error("Failed to create API key: %w", err) } output.Success("API key created successfully") output.Info("Name: %s", name) - output.Info("Key: %s", resp.AppKey) + output.Info("Key: %s", created.Key) + output.Info("Access: %s", core.DescribeGrant(created.App.Grant, resolved)) + if core.GrantWorksOnV1(created.App.Grant) { + output.Info("Works with: JSON API v1 and v2") + } else { + output.Info("Works with: JSON API v2 only") + } return nil }, } + cmd.Flags().StringArrayVar(&flags.Spaces, "space", nil, "Space the key can access, by Id or exact name (repeatable)") + cmd.Flags().BoolVar(&flags.AllSpaces, "all-spaces", false, "Let the key access all spaces, including ones created later") + cmd.Flags().BoolVar(&flags.ReadOnly, "read-only", false, "Let the key read but not change data") + cmd.Flags().BoolVar(&flags.ReadWrite, "read-write", false, "Let the key read and change data") + return cmd } + +// spaceChoices lists the user's spaces to help pick --space values. It is best +// effort: without a running server the error is returned without the list. +func spaceChoices() string { + spaces, err := listSpaces() + if err != nil || len(spaces) == 0 { + return "" + } + var b strings.Builder + b.WriteString("\n\nYour spaces:") + for _, space := range spaces { + fmt.Fprintf(&b, "\n %s (%s)", space.Name, space.SpaceId) + } + return b.String() +} diff --git a/cmd/auth/apikey/create/create_test.go b/cmd/auth/apikey/create/create_test.go new file mode 100644 index 0000000..db51c3d --- /dev/null +++ b/cmd/auth/apikey/create/create_test.go @@ -0,0 +1,122 @@ +package create + +import ( + "errors" + "io" + "strings" + "testing" + + "github.com/anyproto/anytype-cli/core" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" +) + +// stubServer replaces the server calls and records whether a key was created. +func stubServer(t *testing.T, spaces []core.SpaceListItem) *bool { + t.Helper() + created := false + origList, origTech, origCreate := listSpaces, techSpaceId, createAPIKey + listSpaces = func() ([]core.SpaceListItem, error) { return spaces, nil } + techSpaceId = func() (string, error) { return "bafyreitech.tech", nil } + createAPIKey = func(name string, grant *model.AccountAuthAppGrant) (*core.CreatedAPIKey, error) { + created = true + return &core.CreatedAPIKey{Key: "secret", App: &model.AccountAuthAppInfo{AppName: name, Scope: model.AccountAuth_JsonAPI, Grant: grant}}, nil + } + t.Cleanup(func() { listSpaces, techSpaceId, createAPIKey = origList, origTech, origCreate }) + return &created +} + +func runCreate(args ...string) error { + cmd := NewCreateCmd() + cmd.SetArgs(args) + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) + return cmd.Execute() +} + +func TestCreateCommandFlags(t *testing.T) { + cmd := NewCreateCmd() + for _, name := range []string{"space", "all-spaces", "read-only", "read-write"} { + if cmd.Flag(name) == nil { + t.Errorf("flag --%s not found", name) + } + } +} + +func TestCreateRequiresExplicitChoices(t *testing.T) { + spaces := []core.SpaceListItem{{SpaceId: "bafyreia.one", Name: "Personal"}} + + tests := []struct { + name string + args []string + wantErr error + wantMsg string + }{ + {"no space choice lists the spaces", []string{"my-app", "--read-only"}, core.ErrSpaceChoiceRequired, "Personal (bafyreia.one)"}, + {"no permission choice", []string{"my-app", "--all-spaces"}, core.ErrPermChoiceRequired, ""}, + {"no choices at all", []string{"my-app"}, core.ErrSpaceChoiceRequired, ""}, + {"conflicting space flags", []string{"my-app", "--all-spaces", "--space", "Personal", "--read-only"}, core.ErrConflictingSpaceFlags, ""}, + {"conflicting permission flags", []string{"my-app", "--all-spaces", "--read-only", "--read-write"}, core.ErrConflictingPermFlags, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + created := stubServer(t, spaces) + + err := runCreate(tt.args...) + + if !errors.Is(err, tt.wantErr) { + t.Fatalf("error = %v, want %v", err, tt.wantErr) + } + if tt.wantMsg != "" && !strings.Contains(err.Error(), tt.wantMsg) { + t.Errorf("error = %q, want it to contain %q", err, tt.wantMsg) + } + if *created { + t.Error("no key may be created without both choices") + } + }) + } +} + +func TestCreateRejectsInvalidName(t *testing.T) { + created := stubServer(t, nil) + + err := runCreate(strings.Repeat("a", 129), "--all-spaces", "--read-only") + + if err == nil { + t.Fatal("expected an error for a name over 128 bytes") + } + if *created { + t.Error("no key may be created with an invalid name") + } +} + +func TestCreateRejectsUnknownSpace(t *testing.T) { + created := stubServer(t, []core.SpaceListItem{{SpaceId: "bafyreia.one", Name: "Personal"}}) + + err := runCreate("my-app", "--space", "Nope", "--read-only") + + if err == nil || !strings.Contains(err.Error(), `"Nope" not found`) { + t.Fatalf("error = %v, want unknown space error", err) + } + if *created { + t.Error("no key may be created for an unknown space") + } +} + +func TestCreateSendsResolvedGrant(t *testing.T) { + stubServer(t, []core.SpaceListItem{{SpaceId: "bafyreia.one", Name: "Personal"}}) + var sent *model.AccountAuthAppGrant + createAPIKey = func(name string, grant *model.AccountAuthAppGrant) (*core.CreatedAPIKey, error) { + sent = grant + return &core.CreatedAPIKey{Key: "secret", App: &model.AccountAuthAppInfo{AppName: name, Grant: grant}}, nil + } + + err := runCreate("my-app", "--space", "Personal", "--read-write") + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if sent == nil || len(sent.SpaceIds) != 1 || sent.SpaceIds[0] != "bafyreia.one" || sent.Perm != model.AccountAuthAppGrant_ReadWrite { + t.Errorf("sent grant = %+v, want Personal read-write", sent) + } +} diff --git a/cmd/auth/create/create.go b/cmd/auth/create/create.go index af9d307..39e5433 100644 --- a/cmd/auth/create/create.go +++ b/cmd/auth/create/create.go @@ -26,10 +26,16 @@ func NewCreateCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { name := args[0] - accountKey, accountId, savedToKeyring, err := core.CreateWallet(name, rootPath, listenAddress, networkConfigPath) + apiAddr, explicit := cmdutil.APIListenAddr(cmd, listenAddress) + accountKey, accountId, savedToKeyring, err := core.CreateWallet(name, rootPath, apiAddr, networkConfigPath) if err != nil { return output.Error("Failed to create account: %w", err) } + if explicit { + if err := config.SetApiListenAddrToConfig(apiAddr); err != nil { + output.Warning("Failed to remember the JSON API address: %v", err) + } + } output.Success("Bot account created successfully!") @@ -70,13 +76,14 @@ func NewCreateCmd() *cobra.Command { } else { output.Success("Account key saved to config file.") } + output.Banner("JSON API listening on " + config.APIURL(apiAddr)) return nil }, } cmd.Flags().StringVar(&rootPath, "root-path", "", "Root path for account data") - cmd.Flags().StringVar(&listenAddress, "listen-address", config.DefaultAPIAddress, "API listen address in `host:port` format") + cmdutil.AddListenAddressFlag(cmd, &listenAddress) cmd.Flags().StringVar(&networkConfigPath, "network-config", "", "Path to custom network configuration YAML (for self-hosted)") return cmd diff --git a/cmd/auth/login/login.go b/cmd/auth/login/login.go index b8e69b1..8443902 100644 --- a/cmd/auth/login/login.go +++ b/cmd/auth/login/login.go @@ -3,6 +3,7 @@ package login import ( "github.com/spf13/cobra" + "github.com/anyproto/anytype-cli/cmd/cmdutil" "github.com/anyproto/anytype-cli/core" "github.com/anyproto/anytype-cli/core/config" "github.com/anyproto/anytype-cli/core/output" @@ -19,10 +20,17 @@ func NewLoginCmd() *cobra.Command { Short: "Log in to your bot account", Long: "Authenticate using your account key to access your Anytype bot account and stored data. Use --network-config for self-hosted networks.", RunE: func(cmd *cobra.Command, args []string) error { - if err := core.Login(accountKey, rootPath, listenAddress, networkConfigPath); err != nil { + apiAddr, explicit := cmdutil.APIListenAddr(cmd, listenAddress) + if err := core.Login(accountKey, rootPath, apiAddr, networkConfigPath); err != nil { return output.Error("Failed to log in: %w", err) } + if explicit { + if err := config.SetApiListenAddrToConfig(apiAddr); err != nil { + output.Warning("Failed to remember the JSON API address: %v", err) + } + } output.Success("Successfully logged in") + output.Banner("JSON API listening on " + config.APIURL(apiAddr)) return nil }, @@ -30,7 +38,7 @@ func NewLoginCmd() *cobra.Command { cmd.Flags().StringVar(&accountKey, "account-key", "", "Account key for authentication") cmd.Flags().StringVar(&rootPath, "path", "", "Root path for account data") - cmd.Flags().StringVar(&listenAddress, "listen-address", config.DefaultAPIAddress, "API listen address in `host:port` format") + cmdutil.AddListenAddressFlag(cmd, &listenAddress) cmd.Flags().StringVar(&networkConfigPath, "network-config", "", "Path to custom network configuration YAML (for self-hosted)") return cmd diff --git a/cmd/auth/status/status.go b/cmd/auth/status/status.go index 45af1e2..73cb604 100644 --- a/cmd/auth/status/status.go +++ b/cmd/auth/status/status.go @@ -88,6 +88,11 @@ func NewStatusCmd() *cobra.Command { } output.Print(" - Active session: \033[1m%v\033[0m", isLoggedIn) + if isLoggedIn { + storedAddr, _ := config.GetApiListenAddrFromConfig() + apiAddr := config.ResolveAPIListenAddr("", false, storedAddr) + output.Print(" - JSON API: \033[1m%s\033[0m", config.APIURL(apiAddr)) + } if hasAccountKey { if len(accountKey) > 8 { diff --git a/cmd/cmdutil/listenaddr.go b/cmd/cmdutil/listenaddr.go new file mode 100644 index 0000000..a6119d4 --- /dev/null +++ b/cmd/cmdutil/listenaddr.go @@ -0,0 +1,26 @@ +package cmdutil + +import ( + "github.com/spf13/cobra" + + "github.com/anyproto/anytype-cli/core/config" +) + +const listenAddressFlag = "listen-address" + +// storedAPIListenAddr reads the saved JSON API address; replaceable in tests. +var storedAPIListenAddr = config.GetApiListenAddrFromConfig + +// AddListenAddressFlag registers --listen-address for the JSON API. +func AddListenAddressFlag(cmd *cobra.Command, target *string) { + cmd.Flags().StringVar(target, listenAddressFlag, config.DefaultAPIAddress, + "JSON API listen address in `host:port` format (remembered for later commands)") +} + +// APIListenAddr resolves the JSON API address for a command and reports +// whether the user passed --listen-address explicitly. +func APIListenAddr(cmd *cobra.Command, flagValue string) (string, bool) { + changed := cmd.Flags().Changed(listenAddressFlag) + stored, _ := storedAPIListenAddr() + return config.ResolveAPIListenAddr(flagValue, changed, stored), changed +} diff --git a/cmd/cmdutil/listenaddr_test.go b/cmd/cmdutil/listenaddr_test.go new file mode 100644 index 0000000..2c8a59b --- /dev/null +++ b/cmd/cmdutil/listenaddr_test.go @@ -0,0 +1,44 @@ +package cmdutil + +import ( + "testing" + + "github.com/spf13/cobra" + + "github.com/anyproto/anytype-cli/core/config" +) + +func TestAPIListenAddr(t *testing.T) { + tests := []struct { + name string + args []string + stored string + want string + changed bool + }{ + {"flag given", []string{"--listen-address", "0.0.0.0:5000"}, "127.0.0.1:4000", "0.0.0.0:5000", true}, + {"flag omitted uses stored", nil, "127.0.0.1:4000", "127.0.0.1:4000", false}, + {"flag omitted, nothing stored", nil, "", config.DefaultAPIAddress, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + orig := storedAPIListenAddr + storedAPIListenAddr = func() (string, error) { return tt.stored, nil } + t.Cleanup(func() { storedAPIListenAddr = orig }) + + var flag string + cmd := &cobra.Command{Use: "x", RunE: func(*cobra.Command, []string) error { return nil }} + AddListenAddressFlag(cmd, &flag) + cmd.SetArgs(tt.args) + if err := cmd.Execute(); err != nil { + t.Fatalf("Execute: %v", err) + } + + got, changed := APIListenAddr(cmd, flag) + if got != tt.want || changed != tt.changed { + t.Errorf("APIListenAddr() = (%q, %v), want (%q, %v)", got, changed, tt.want, tt.changed) + } + }) + } +} diff --git a/cmd/serve/serve.go b/cmd/serve/serve.go index 79767f8..c07f538 100644 --- a/cmd/serve/serve.go +++ b/cmd/serve/serve.go @@ -6,6 +6,7 @@ import ( "github.com/kardianos/service" "github.com/spf13/cobra" + "github.com/anyproto/anytype-cli/cmd/cmdutil" "github.com/anyproto/anytype-cli/core/config" "github.com/anyproto/anytype-cli/core/output" "github.com/anyproto/anytype-cli/core/serviceprogram" @@ -26,7 +27,7 @@ func NewServeCmd() *cobra.Command { RunE: runServer, } - cmd.Flags().StringVar(&listenAddress, "listen-address", config.DefaultAPIAddress, "API listen address in `host:port` format") + cmdutil.AddListenAddressFlag(cmd, &listenAddress) cmd.Flags().BoolVarP(&quietMode, "quiet", "q", false, "Suppress most output (only errors)") cmd.Flags().BoolVarP(&verboseMode, "verbose", "v", false, "Show detailed output (debug level)") cmd.MarkFlagsMutuallyExclusive("quiet", "verbose") @@ -50,7 +51,18 @@ func runServer(cmd *cobra.Command, args []string) error { Description: "Anytype", } - prg := serviceprogram.New(listenAddress) + apiAddr, explicit := cmdutil.APIListenAddr(cmd, listenAddress) + + prg := serviceprogram.New(apiAddr) + if explicit { + // Remember the address only once this server is actually up, so a + // failed start (e.g. ports taken by another server) changes nothing. + prg.OnStarted = func() { + if err := config.SetApiListenAddrToConfig(apiAddr); err != nil { + output.Warning("Failed to remember the JSON API address: %v", err) + } + } + } s, err := service.New(prg, svcConfig) if err != nil { diff --git a/cmd/serve/serve_test.go b/cmd/serve/serve_test.go index b9eb457..e314090 100644 --- a/cmd/serve/serve_test.go +++ b/cmd/serve/serve_test.go @@ -32,8 +32,8 @@ func TestServeCmd_ListenAddressFlag(t *testing.T) { t.Errorf("listen-address default = %v, want %v", flag.DefValue, config.DefaultAPIAddress) } - if flag.Usage != "API listen address in `host:port` format" { - t.Errorf("listen-address usage = %v, want 'API listen address in `host:port` format'", flag.Usage) + if flag.Usage != "JSON API listen address in `host:port` format (remembered for later commands)" { + t.Errorf("listen-address usage = %v, want %q", flag.Usage, "JSON API listen address in `host:port` format (remembered for later commands)") } } diff --git a/cmd/service/install/install.go b/cmd/service/install/install.go index 8a5b8f4..1c2c04c 100644 --- a/cmd/service/install/install.go +++ b/cmd/service/install/install.go @@ -3,6 +3,7 @@ package install import ( "github.com/spf13/cobra" + "github.com/anyproto/anytype-cli/cmd/cmdutil" "github.com/anyproto/anytype-cli/core/config" "github.com/anyproto/anytype-cli/core/output" "github.com/anyproto/anytype-cli/core/serviceprogram" @@ -15,7 +16,9 @@ func NewInstallCmd() *cobra.Command { Use: "install", Short: "Install as a user service", RunE: func(cmd *cobra.Command, args []string) error { - s, err := serviceprogram.GetServiceWithAddress(listenAddress) + apiAddr, explicit := cmdutil.APIListenAddr(cmd, listenAddress) + + s, err := serviceprogram.GetServiceWithAddress(apiAddr) if err != nil { return output.Error("Failed to create service: %w", err) } @@ -25,10 +28,14 @@ func NewInstallCmd() *cobra.Command { return output.Error("Failed to install service: %w", err) } - output.Success("anytype service installed successfully") - if listenAddress != config.DefaultAPIAddress { - output.Info("API will listen on %s", listenAddress) + if explicit { + if err := config.SetApiListenAddrToConfig(apiAddr); err != nil { + output.Warning("Failed to remember the JSON API address: %v", err) + } } + + output.Success("anytype service installed successfully") + output.Banner("JSON API: "+config.APIURL(apiAddr), "starts when an account is logged in") output.Print("\nTo manage the service:") output.Print(" Start: anytype service start") output.Print(" Stop: anytype service stop") @@ -39,7 +46,7 @@ func NewInstallCmd() *cobra.Command { }, } - cmd.Flags().StringVar(&listenAddress, "listen-address", config.DefaultAPIAddress, "API listen address in `host:port` format") + cmdutil.AddListenAddressFlag(cmd, &listenAddress) return cmd } diff --git a/cmd/service/install/install_test.go b/cmd/service/install/install_test.go index 5239133..6449152 100644 --- a/cmd/service/install/install_test.go +++ b/cmd/service/install/install_test.go @@ -31,8 +31,8 @@ func TestInstallCmd_ListenAddressFlag(t *testing.T) { t.Errorf("listen-address default = %v, want %v", flag.DefValue, config.DefaultAPIAddress) } - if flag.Usage != "API listen address in `host:port` format" { - t.Errorf("listen-address usage = %v, want 'API listen address in `host:port` format'", flag.Usage) + if flag.Usage != "JSON API listen address in `host:port` format (remembered for later commands)" { + t.Errorf("listen-address usage = %v, want %q", flag.Usage, "JSON API listen address in `host:port` format (remembered for later commands)") } } diff --git a/cmd/shell/shell.go b/cmd/shell/shell.go index 27160fe..4f7035a 100644 --- a/cmd/shell/shell.go +++ b/cmd/shell/shell.go @@ -4,9 +4,11 @@ import ( "errors" "io" "strings" + "unicode" "github.com/chzyer/readline" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/anyproto/anytype-cli/core/output" "github.com/anyproto/anytype-cli/core/updatecheck" @@ -62,17 +64,107 @@ func runShell(rootCmd *cobra.Command) error { continue } - args := strings.Split(line, " ") - if args[0] == "shell" { + if err := executeLine(rootCmd, line); errors.Is(err, errAlreadyInShell) { output.Warning("Already in shell mode. Type 'exit' or 'quit' to leave.") - continue + } else if err != nil { + output.Warning("Command error: %v", err) } - rootCmd.SetArgs(args) + } +} - if err := rootCmd.Execute(); err != nil { - output.Warning("Command error: %v", err) +var errAlreadyInShell = errors.New("already in shell mode") + +// executeLine runs one shell line against the shared command tree. Flag values +// are reset first: cobra keeps them between executions, so an earlier +// command's --space or --read-only would otherwise leak into the next one. +func executeLine(rootCmd *cobra.Command, line string) error { + args, err := splitLine(line) + if err != nil { + return err + } + if len(args) == 0 { + return nil + } + if args[0] == "shell" { + return errAlreadyInShell + } + + resetFlags(rootCmd) + rootCmd.SetArgs(args) + return rootCmd.Execute() +} + +func resetFlags(cmd *cobra.Command) { + reset := func(flag *pflag.Flag) { + if slice, ok := flag.Value.(pflag.SliceValue); ok { + _ = slice.Replace(nil) + } else { + _ = flag.Value.Set(flag.DefValue) } + flag.Changed = false + } + cmd.Flags().VisitAll(reset) + cmd.PersistentFlags().VisitAll(reset) + for _, sub := range cmd.Commands() { + resetFlags(sub) + } +} + +// splitLine splits a shell line into arguments. Whitespace separates +// arguments; single quotes keep text literally; double quotes keep whitespace +// and allow \" and \\ escapes. +func splitLine(line string) ([]string, error) { + var args []string + var current strings.Builder + inArg := false + var quote rune + escaped := false + + for _, r := range line { + switch { + case escaped: + if r != '"' && r != '\\' { + current.WriteRune('\\') + } + current.WriteRune(r) + escaped = false + case quote == '\'': + if r == '\'' { + quote = 0 + } else { + current.WriteRune(r) + } + case quote == '"': + switch r { + case '"': + quote = 0 + case '\\': + escaped = true + default: + current.WriteRune(r) + } + case r == '\'' || r == '"': + quote = r + inArg = true + case unicode.IsSpace(r): + if inArg { + args = append(args, current.String()) + current.Reset() + inArg = false + } + default: + current.WriteRune(r) + inArg = true + } + } + + if quote != 0 || escaped { + return nil, errors.New("unterminated quote") + } + if inArg { + args = append(args, current.String()) } + return args, nil } func buildCompleter(rootCmd *cobra.Command) *readline.PrefixCompleter { diff --git a/cmd/shell/shell_test.go b/cmd/shell/shell_test.go new file mode 100644 index 0000000..96600a2 --- /dev/null +++ b/cmd/shell/shell_test.go @@ -0,0 +1,100 @@ +package shell + +import ( + "reflect" + "testing" + + "github.com/spf13/cobra" +) + +// newTestRoot builds root → group → leaf, where leaf records the flag values it +// ran with. +func newTestRoot(got *[]string, gotBool *bool) *cobra.Command { + root := &cobra.Command{Use: "root", SilenceErrors: true, SilenceUsage: true} + group := &cobra.Command{Use: "group"} + var spaces []string + var readOnly bool + leaf := &cobra.Command{ + Use: "leaf", + RunE: func(cmd *cobra.Command, args []string) error { + *got = append([]string(nil), spaces...) + *gotBool = readOnly + return nil + }, + } + leaf.Flags().StringArrayVar(&spaces, "space", nil, "") + leaf.Flags().BoolVar(&readOnly, "read-only", false, "") + group.AddCommand(leaf) + root.AddCommand(group) + return root +} + +func TestExecuteLineDoesNotCarryFlagsBetweenCommands(t *testing.T) { + var got []string + var gotBool bool + root := newTestRoot(&got, &gotBool) + + if err := executeLine(root, "group leaf --space A --read-only"); err != nil { + t.Fatalf("first command: %v", err) + } + if err := executeLine(root, "group leaf --space B"); err != nil { + t.Fatalf("second command: %v", err) + } + + if !reflect.DeepEqual(got, []string{"B"}) { + t.Errorf("second command saw --space %v, want [B]", got) + } + if gotBool { + t.Error("second command saw --read-only from the first command") + } +} + +func TestExecuteLineWithoutFlagsSeesDefaults(t *testing.T) { + var got []string + var gotBool bool + root := newTestRoot(&got, &gotBool) + + if err := executeLine(root, "group leaf --space A --read-only"); err != nil { + t.Fatalf("first command: %v", err) + } + if err := executeLine(root, "group leaf"); err != nil { + t.Fatalf("second command: %v", err) + } + + if len(got) != 0 || gotBool { + t.Errorf("second command saw --space %v --read-only=%v, want defaults", got, gotBool) + } +} + +func TestSplitLine(t *testing.T) { + tests := []struct { + name string + line string + want []string + wantErr bool + }{ + {"plain words", "auth apikey list", []string{"auth", "apikey", "list"}, false}, + {"repeated spaces", "space list", []string{"space", "list"}, false}, + {"double quotes keep spaces", `auth apikey create bot --space "My Team"`, []string{"auth", "apikey", "create", "bot", "--space", "My Team"}, false}, + {"single quotes keep spaces", `create 'my bot'`, []string{"create", "my bot"}, false}, + {"quotes are removed", `--space "Personal"`, []string{"--space", "Personal"}, false}, + {"escaped quote inside double quotes", `create "say \"hi\""`, []string{"create", `say "hi"`}, false}, + {"other backslashes are kept", `--path "C:\dir"`, []string{"--path", `C:\dir`}, false}, + {"quotes join with adjacent text", `--name=my" "bot`, []string{"--name=my bot"}, false}, + {"empty quoted argument", `create ""`, []string{"create", ""}, false}, + {"tabs separate", "space\tlist", []string{"space", "list"}, false}, + {"unterminated quote", `create "my bot`, nil, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := splitLine(tt.line) + if (err != nil) != tt.wantErr { + t.Fatalf("splitLine(%q) error = %v, wantErr %v", tt.line, err, tt.wantErr) + } + if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { + t.Errorf("splitLine(%q) = %q, want %q", tt.line, got, tt.want) + } + }) + } +} diff --git a/core/apikey.go b/core/apikey.go index c52cc2c..43f1c6f 100644 --- a/core/apikey.go +++ b/core/apikey.go @@ -2,36 +2,140 @@ package core import ( "context" + "errors" "fmt" - "github.com/anyproto/anytype-heart/pkg/lib/pb/model" + "slices" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/anyproto/anytype-heart/pb" "github.com/anyproto/anytype-heart/pb/service" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" ) -// CreateAPIKey creates a new API key for local app access -func CreateAPIKey(name string) (*pb.RpcAccountLocalLinkCreateAppResponse, error) { - var resp *pb.RpcAccountLocalLinkCreateAppResponse +// ErrServerTooOld means the running server cannot store API key grants, so a +// key created on it would silently get unrestricted access. +var ErrServerTooOld = errors.New("the running Anytype service is older than this CLI and cannot restrict API keys; restart it with: anytype service restart") + +// CreatedAPIKey is a newly created key: the secret and the key as the server +// stored it. +type CreatedAPIKey struct { + Key string + App *model.AccountAuthAppInfo +} +// CreateAPIKey creates a JSON API key limited to the given grant. +func CreateAPIKey(name string, grant *model.AccountAuthAppGrant) (*CreatedAPIKey, error) { + var created *CreatedAPIKey err := GRPCCall(func(ctx context.Context, client service.ClientCommandsClient) error { var err error - resp, err = client.AccountLocalLinkCreateApp(ctx, &pb.RpcAccountLocalLinkCreateAppRequest{ - App: &model.AccountAuthAppInfo{ - AppName: name, - }, - }) - if err != nil { - return fmt.Errorf("failed to create API key: %w", err) - } + created, err = createAPIKey(ctx, client, name, grant) + return err + }) + return created, err +} - if resp.Error != nil && resp.Error.Code != pb.RpcAccountLocalLinkCreateAppResponseError_NULL { - return fmt.Errorf("API error: %s", resp.Error.Description) - } +func createAPIKey(ctx context.Context, client service.ClientCommandsClient, name string, grant *model.AccountAuthAppGrant) (*CreatedAPIKey, error) { + if grant == nil { + return nil, errors.New("an API key must be limited to a grant") + } + if err := ensureGrantSupport(ctx, client); err != nil { + return nil, err + } - return nil + resp, err := client.AccountLocalLinkCreateApp(ctx, &pb.RpcAccountLocalLinkCreateAppRequest{ + App: &model.AccountAuthAppInfo{ + AppName: name, + Scope: model.AccountAuth_JsonAPI, + Grant: grant, + }, }) + if err != nil { + return nil, fmt.Errorf("failed to create API key: %w", err) + } + if resp.Error != nil && resp.Error.Code != pb.RpcAccountLocalLinkCreateAppResponseError_NULL { + return nil, fmt.Errorf("API error: %s", resp.Error.Description) + } - return resp, err + // Never trust the server to have applied what was asked: read the key back + // and revoke it if its access differs from the request or can't be checked. + stored, err := findAppByKey(ctx, client, resp.AppKey) + if err != nil { + return nil, removeUnverifiedKey(ctx, client, name, resp.AppKey, fmt.Errorf("could not verify the new API key: %v", err)) + } + if stored.Scope != model.AccountAuth_JsonAPI || !grantsEqual(stored.Grant, grant) { + return nil, removeUnverifiedKey(ctx, client, name, resp.AppKey, fmt.Errorf("the server did not store the requested access: %w", ErrServerTooOld)) + } + + return &CreatedAPIKey{Key: resp.AppKey, App: stored}, nil +} + +// cleanupTimeout bounds the revocation of a key that failed verification. It +// starts fresh because the create call may have used up the caller's deadline. +const cleanupTimeout = 10 * time.Second + +// removeUnverifiedKey revokes a key whose access could not be confirmed and +// returns the error to report. Errors from here on are formatted with %v, not +// wrapped: GRPCCall rewrites any error carrying an Unavailable status into +// "anytype is not running", which would hide that a key was left behind. +func removeUnverifiedKey(ctx context.Context, client service.ClientCommandsClient, name, key string, cause error) error { + ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), cleanupTimeout) + defer cancel() + + app, err := findAppByKey(ctx, client, key) + if err != nil { + return fmt.Errorf("%w; the key %q may still exist and could not be revoked (%v): check 'anytype auth apikey list' and revoke it", cause, name, err) + } + if err := revokeAPIKey(ctx, client, app.AppHash); err != nil { + return fmt.Errorf("%w; revoking the key failed (%v): revoke it with 'anytype auth apikey revoke %s'", cause, err, app.AppHash) + } + return fmt.Errorf("%w; the key was revoked", cause) +} + +// ensureGrantSupport probes for AccountLocalLinkUpdateApp, which arrived with +// grants. An empty app hash makes a capable server answer BAD_INPUT without +// changing anything; an older server does not know the method. +func ensureGrantSupport(ctx context.Context, client service.ClientCommandsClient) error { + _, err := client.AccountLocalLinkUpdateApp(ctx, &pb.RpcAccountLocalLinkUpdateAppRequest{}) + if status.Code(err) == codes.Unimplemented { + return ErrServerTooOld + } + if err != nil { + return fmt.Errorf("failed to check server capabilities: %w", err) + } + return nil +} + +func findAppByKey(ctx context.Context, client service.ClientCommandsClient, key string) (*model.AccountAuthAppInfo, error) { + resp, err := client.AccountLocalLinkListApps(ctx, &pb.RpcAccountLocalLinkListAppsRequest{}) + if err != nil { + return nil, err + } + if resp.Error != nil && resp.Error.Code != pb.RpcAccountLocalLinkListAppsResponseError_NULL { + return nil, fmt.Errorf("API error: %s", resp.Error.Description) + } + for _, app := range resp.App { + if app.AppKey == key { + return app, nil + } + } + return nil, errors.New("the new key is not in the server's key list") +} + +// grantsEqual compares grants by meaning: space order is irrelevant. +func grantsEqual(a, b *model.AccountAuthAppGrant) bool { + if a == nil || b == nil { + return a == nil && b == nil + } + if a.AllSpaces != b.AllSpaces || a.Perm != b.Perm { + return false + } + as, bs := slices.Clone(a.SpaceIds), slices.Clone(b.SpaceIds) + slices.Sort(as) + slices.Sort(bs) + return slices.Equal(as, bs) } // ListAPIKeys lists all API keys @@ -58,17 +162,21 @@ func ListAPIKeys() (*pb.RpcAccountLocalLinkListAppsResponse, error) { // RevokeAPIKey revokes an API key by appId func RevokeAPIKey(appId string) error { return GRPCCall(func(ctx context.Context, client service.ClientCommandsClient) error { - resp, err := client.AccountLocalLinkRevokeApp(ctx, &pb.RpcAccountLocalLinkRevokeAppRequest{ - AppHash: appId, - }) - if err != nil { - return fmt.Errorf("failed to revoke API key: %w", err) - } - - if resp.Error != nil && resp.Error.Code != pb.RpcAccountLocalLinkRevokeAppResponseError_NULL { - return fmt.Errorf("API error: %s", resp.Error.Description) - } + return revokeAPIKey(ctx, client, appId) + }) +} - return nil +func revokeAPIKey(ctx context.Context, client service.ClientCommandsClient, appId string) error { + resp, err := client.AccountLocalLinkRevokeApp(ctx, &pb.RpcAccountLocalLinkRevokeAppRequest{ + AppHash: appId, }) + if err != nil { + return fmt.Errorf("failed to revoke API key: %w", err) + } + + if resp.Error != nil && resp.Error.Code != pb.RpcAccountLocalLinkRevokeAppResponseError_NULL { + return fmt.Errorf("API error: %s", resp.Error.Description) + } + + return nil } diff --git a/core/apikey_test.go b/core/apikey_test.go new file mode 100644 index 0000000..867a617 --- /dev/null +++ b/core/apikey_test.go @@ -0,0 +1,241 @@ +package core + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/anyproto/anytype-heart/pb" + "github.com/anyproto/anytype-heart/pb/service" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// fakeAppLinkClient is an in-memory app link store. It embeds the client +// interface so only the methods under test need implementing. +type fakeAppLinkClient struct { + service.ClientCommandsClient + + // oldServer makes UpdateApp unknown, like anytype-heart before v0.51.3. + oldServer bool + // dropGrant makes CreateApp ignore the grant, like an old server would. + dropGrant bool + + // cancelOnCreate is called right after CreateApp, to expire the caller's + // context the way a slow create uses up GRPCCall's deadline. + cancelOnCreate func() + // listFailures makes that many ListApps calls fail as unavailable. + listFailures int + // revokeErr makes RevokeApp fail. + revokeErr error + + apps []*model.AccountAuthAppInfo + createCalls []*pb.RpcAccountLocalLinkCreateAppRequest + revoked []string +} + +func (f *fakeAppLinkClient) AccountLocalLinkUpdateApp(_ context.Context, in *pb.RpcAccountLocalLinkUpdateAppRequest, _ ...grpc.CallOption) (*pb.RpcAccountLocalLinkUpdateAppResponse, error) { + if f.oldServer { + return nil, status.Error(codes.Unimplemented, "unknown method AccountLocalLinkUpdateApp") + } + if in.AppHash == "" { + return &pb.RpcAccountLocalLinkUpdateAppResponse{Error: &pb.RpcAccountLocalLinkUpdateAppResponseError{ + Code: pb.RpcAccountLocalLinkUpdateAppResponseError_BAD_INPUT, Description: "app hash is required", + }}, nil + } + return &pb.RpcAccountLocalLinkUpdateAppResponse{Error: &pb.RpcAccountLocalLinkUpdateAppResponseError{}}, nil +} + +func (f *fakeAppLinkClient) AccountLocalLinkCreateApp(_ context.Context, in *pb.RpcAccountLocalLinkCreateAppRequest, _ ...grpc.CallOption) (*pb.RpcAccountLocalLinkCreateAppResponse, error) { + f.createCalls = append(f.createCalls, in) + key := "key-" + in.App.AppName + stored := &model.AccountAuthAppInfo{ + AppHash: "hash-" + in.App.AppName, + AppName: in.App.AppName, + AppKey: key, + Scope: in.App.Scope, + ExpireAt: in.App.ExpireAt, + Grant: in.App.Grant, + } + if f.dropGrant { + stored.Grant = nil + } + f.apps = append(f.apps, stored) + if f.cancelOnCreate != nil { + f.cancelOnCreate() + } + return &pb.RpcAccountLocalLinkCreateAppResponse{Error: &pb.RpcAccountLocalLinkCreateAppResponseError{}, AppKey: key}, nil +} + +func (f *fakeAppLinkClient) AccountLocalLinkListApps(ctx context.Context, _ *pb.RpcAccountLocalLinkListAppsRequest, _ ...grpc.CallOption) (*pb.RpcAccountLocalLinkListAppsResponse, error) { + if err := ctx.Err(); err != nil { + return nil, status.Error(codes.DeadlineExceeded, err.Error()) + } + if f.listFailures > 0 { + f.listFailures-- + return nil, status.Error(codes.Unavailable, "connection lost") + } + return &pb.RpcAccountLocalLinkListAppsResponse{Error: &pb.RpcAccountLocalLinkListAppsResponseError{}, App: f.apps}, nil +} + +func (f *fakeAppLinkClient) AccountLocalLinkRevokeApp(ctx context.Context, in *pb.RpcAccountLocalLinkRevokeAppRequest, _ ...grpc.CallOption) (*pb.RpcAccountLocalLinkRevokeAppResponse, error) { + if err := ctx.Err(); err != nil { + return nil, status.Error(codes.DeadlineExceeded, err.Error()) + } + if f.revokeErr != nil { + return nil, f.revokeErr + } + f.revoked = append(f.revoked, in.AppHash) + return &pb.RpcAccountLocalLinkRevokeAppResponse{Error: &pb.RpcAccountLocalLinkRevokeAppResponseError{}}, nil +} + +var testGrant = &model.AccountAuthAppGrant{SpaceIds: []string{"bafyreia.one"}, Perm: model.AccountAuthAppGrant_Read} + +func TestCreateAPIKeySendsJsonAPIScopeAndGrant(t *testing.T) { + client := &fakeAppLinkClient{} + + created, err := createAPIKey(context.Background(), client, "my-app", testGrant) + + if err != nil { + t.Fatalf("createAPIKey() unexpected error: %v", err) + } + if len(client.createCalls) != 1 { + t.Fatalf("CreateApp called %d times, want 1", len(client.createCalls)) + } + sent := client.createCalls[0].App + if sent.Scope != model.AccountAuth_JsonAPI { + t.Errorf("scope = %v, want JsonAPI", sent.Scope) + } + if sent.Grant != testGrant { + t.Errorf("grant = %+v, want %+v", sent.Grant, testGrant) + } + if created.Key != "key-my-app" || created.App.AppHash != "hash-my-app" { + t.Errorf("created = %+v, want key and stored app", created) + } + if len(client.revoked) != 0 { + t.Errorf("revoked %v, want nothing revoked", client.revoked) + } +} + +func TestCreateAPIKeyRejectsNilGrant(t *testing.T) { + client := &fakeAppLinkClient{} + + _, err := createAPIKey(context.Background(), client, "my-app", nil) + + if err == nil { + t.Fatal("createAPIKey() with nil grant succeeded, want an error") + } + if len(client.createCalls) != 0 { + t.Error("CreateApp must not be called without a grant") + } +} + +func TestCreateAPIKeyRefusesOldServer(t *testing.T) { + client := &fakeAppLinkClient{oldServer: true} + + _, err := createAPIKey(context.Background(), client, "my-app", testGrant) + + if !errors.Is(err, ErrServerTooOld) { + t.Fatalf("createAPIKey() error = %v, want ErrServerTooOld", err) + } + if len(client.createCalls) != 0 { + t.Error("CreateApp must not be called on a server that cannot store grants") + } +} + +func TestCreateAPIKeyRevokesKeyWhenServerDropsGrant(t *testing.T) { + client := &fakeAppLinkClient{dropGrant: true} + + _, err := createAPIKey(context.Background(), client, "my-app", testGrant) + + if err == nil { + t.Fatal("createAPIKey() succeeded although the server dropped the grant") + } + if len(client.revoked) != 1 || client.revoked[0] != "hash-my-app" { + t.Errorf("revoked = %v, want the new key revoked", client.revoked) + } +} + +func TestCreateAPIKeyRevokesKeyWhenVerificationFails(t *testing.T) { + client := &fakeAppLinkClient{listFailures: 1} + + _, err := createAPIKey(context.Background(), client, "my-app", testGrant) + + if err == nil { + t.Fatal("createAPIKey() succeeded although the key could not be verified") + } + if len(client.revoked) != 1 || client.revoked[0] != "hash-my-app" { + t.Errorf("revoked = %v, want the unverified key revoked", client.revoked) + } +} + +func TestCreateAPIKeyCleansUpWithFreshDeadline(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + client := &fakeAppLinkClient{cancelOnCreate: cancel} + + _, err := createAPIKey(ctx, client, "my-app", testGrant) + + if err == nil { + t.Fatal("createAPIKey() succeeded although verification ran out of time") + } + if len(client.revoked) != 1 { + t.Errorf("revoked = %v, want cleanup to run despite the expired context", client.revoked) + } +} + +func TestCreateAPIKeyReportsKeyThatCouldNotBeCleanedUp(t *testing.T) { + tests := []struct { + name string + client *fakeAppLinkClient + wantText string + }{ + {"key cannot be found again", &fakeAppLinkClient{listFailures: 2}, `"my-app"`}, + {"revoke fails", &fakeAppLinkClient{dropGrant: true, revokeErr: status.Error(codes.Unavailable, "connection lost")}, "hash-my-app"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := createAPIKey(context.Background(), tt.client, "my-app", testGrant) + + if err == nil { + t.Fatal("createAPIKey() succeeded, want an error") + } + if !strings.Contains(err.Error(), tt.wantText) || !strings.Contains(err.Error(), "revoke") { + t.Errorf("error = %q, want it to name %s and say to revoke it", err, tt.wantText) + } + // GRPCCall rewrites errors carrying an Unavailable status into + // "anytype is not running", which would hide the leftover key. + if _, isStatus := status.FromError(err); isStatus { + t.Errorf("error %q carries a gRPC status and would be rewritten by GRPCCall", err) + } + }) + } +} + +func TestGrantsEqual(t *testing.T) { + tests := []struct { + name string + a, b *model.AccountAuthAppGrant + want bool + }{ + {"both nil", nil, nil, true}, + {"one nil", testGrant, nil, false}, + {"same spaces and perm", testGrant, &model.AccountAuthAppGrant{SpaceIds: []string{"bafyreia.one"}, Perm: model.AccountAuthAppGrant_Read}, true}, + {"space order does not matter", &model.AccountAuthAppGrant{SpaceIds: []string{"a", "b"}}, &model.AccountAuthAppGrant{SpaceIds: []string{"b", "a"}}, true}, + {"different perm", testGrant, &model.AccountAuthAppGrant{SpaceIds: []string{"bafyreia.one"}, Perm: model.AccountAuthAppGrant_ReadWrite}, false}, + {"different spaces", testGrant, &model.AccountAuthAppGrant{SpaceIds: []string{"bafyreib.two"}}, false}, + {"all spaces vs list", &model.AccountAuthAppGrant{AllSpaces: true}, &model.AccountAuthAppGrant{SpaceIds: []string{"a"}}, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := grantsEqual(tt.a, tt.b); got != tt.want { + t.Errorf("grantsEqual() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/core/apikeygrant.go b/core/apikeygrant.go new file mode 100644 index 0000000..699f7ba --- /dev/null +++ b/core/apikeygrant.go @@ -0,0 +1,192 @@ +package core + +import ( + "errors" + "fmt" + "strings" + + "github.com/anyproto/anytype-heart/core/domain" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" +) + +var ( + ErrSpaceChoiceRequired = errors.New("choose which spaces the key can access: --space (repeatable) or --all-spaces") + ErrPermChoiceRequired = errors.New("choose the key's permission: --read-only or --read-write") + ErrConflictingSpaceFlags = errors.New("--space and --all-spaces cannot be combined") + ErrConflictingPermFlags = errors.New("--read-only and --read-write cannot be combined") +) + +// GrantFlags is the user's access choice for an API key. Both choices are +// required: the CLI never picks spaces or a permission on the user's behalf. +type GrantFlags struct { + Spaces []string + AllSpaces bool + ReadOnly bool + ReadWrite bool +} + +// ResolvedSpace is a space argument resolved to a full space Id. +type ResolvedSpace struct { + Id string + Name string + IsTech bool +} + +// ValidateGrantFlags checks that exactly one space choice and exactly one +// permission choice were made. +func ValidateGrantFlags(flags GrantFlags) error { + hasSpaces := len(flags.Spaces) > 0 + if hasSpaces && flags.AllSpaces { + return ErrConflictingSpaceFlags + } + if !hasSpaces && !flags.AllSpaces { + return ErrSpaceChoiceRequired + } + if flags.ReadOnly && flags.ReadWrite { + return ErrConflictingPermFlags + } + if !flags.ReadOnly && !flags.ReadWrite { + return ErrPermChoiceRequired + } + return nil +} + +// ResolveSpaces resolves each argument to a full space Id. An exact Id wins over +// a name; a name must match exactly one space. The tech space is only reachable +// by its explicit Id. Duplicates are dropped, keeping first-appearance order. +func ResolveSpaces(args []string, spaces []SpaceListItem, techSpaceId string) ([]ResolvedSpace, error) { + var resolved []ResolvedSpace + seen := make(map[string]struct{}) + + for _, arg := range args { + if strings.TrimSpace(arg) == "" { + return nil, errors.New("empty space argument") + } + + space, err := resolveSpace(arg, spaces, techSpaceId) + if err != nil { + return nil, err + } + if _, ok := seen[space.Id]; ok { + continue + } + seen[space.Id] = struct{}{} + resolved = append(resolved, space) + } + + return resolved, nil +} + +func resolveSpace(arg string, spaces []SpaceListItem, techSpaceId string) (ResolvedSpace, error) { + if techSpaceId != "" && arg == techSpaceId { + return ResolvedSpace{Id: techSpaceId, IsTech: true}, nil + } + for _, space := range spaces { + if space.SpaceId == arg { + return ResolvedSpace{Id: space.SpaceId, Name: space.Name}, nil + } + } + + var matches []SpaceListItem + for _, space := range spaces { + if space.Name == arg { + matches = append(matches, space) + } + } + switch len(matches) { + case 0: + return ResolvedSpace{}, fmt.Errorf("space %q not found; run 'anytype space list' to see your spaces", arg) + case 1: + return ResolvedSpace{Id: matches[0].SpaceId, Name: matches[0].Name}, nil + default: + candidates := make([]string, len(matches)) + for i, m := range matches { + candidates[i] = fmt.Sprintf("%s (%s)", m.Name, m.SpaceId) + } + return ResolvedSpace{}, fmt.Errorf("space name %q is ambiguous, pass the space Id instead: %s", arg, strings.Join(candidates, ", ")) + } +} + +// BuildGrant turns validated flags and resolved spaces into the grant sent to +// the server. It never returns a nil or empty grant: a key without a grant would +// be unrestricted. +func BuildGrant(flags GrantFlags, resolved []ResolvedSpace) (*model.AccountAuthAppGrant, error) { + if err := ValidateGrantFlags(flags); err != nil { + return nil, err + } + + perm := model.AccountAuthAppGrant_Read + if flags.ReadWrite { + perm = model.AccountAuthAppGrant_ReadWrite + } + + if flags.AllSpaces { + return &model.AccountAuthAppGrant{AllSpaces: true, Perm: perm}, nil + } + if len(resolved) == 0 { + return nil, ErrSpaceChoiceRequired + } + + spaceIds := make([]string, len(resolved)) + for i, space := range resolved { + spaceIds[i] = space.Id + } + return &model.AccountAuthAppGrant{SpaceIds: spaceIds, Perm: perm}, nil +} + +// ValidateAPIKeyName mirrors the server's rule: the name is required and at most +// domain.MaxIntegrationNameLen bytes. It is never truncated, because on API v2 +// the name decides which objects the key may delete. +func ValidateAPIKeyName(name string) error { + if strings.TrimSpace(name) == "" { + return errors.New("API key name is required") + } + if len(name) > domain.MaxIntegrationNameLen { + return fmt.Errorf("API key name is %d bytes, the maximum is %d", len(name), domain.MaxIntegrationNameLen) + } + return nil +} + +// GrantWorksOnV1 reports whether the JSON API v1 accepts a key with this grant. +// v1 cannot enforce space grants, so it only admits keys that grant no less +// than an unrestricted key: no grant, or all spaces with read-write. +func GrantWorksOnV1(grant *model.AccountAuthAppGrant) bool { + return grant == nil || (grant.AllSpaces && grant.Perm == model.AccountAuthAppGrant_ReadWrite) +} + +// DescribeGrant renders a grant for people, naming spaces where resolved names +// are known. +func DescribeGrant(grant *model.AccountAuthAppGrant, resolved []ResolvedSpace) string { + if grant == nil { + return "unrestricted" + } + perm := "read-only" + if grant.Perm == model.AccountAuthAppGrant_ReadWrite { + perm = "read-write" + } + if grant.AllSpaces { + return perm + ", all spaces" + } + + byId := make(map[string]ResolvedSpace, len(resolved)) + for _, space := range resolved { + byId[space.Id] = space + } + names := make([]string, len(grant.SpaceIds)) + for i, id := range grant.SpaceIds { + space, ok := byId[id] + switch { + case ok && space.IsTech: + names[i] = fmt.Sprintf("tech space (%s)", id) + case ok && space.Name != "": + names[i] = fmt.Sprintf("%s (%s)", space.Name, id) + default: + names[i] = id + } + } + noun := "spaces" + if len(names) == 1 { + noun = "space" + } + return fmt.Sprintf("%s, %d %s: %s", perm, len(names), noun, strings.Join(names, ", ")) +} diff --git a/core/apikeygrant_test.go b/core/apikeygrant_test.go new file mode 100644 index 0000000..5c16d8e --- /dev/null +++ b/core/apikeygrant_test.go @@ -0,0 +1,261 @@ +package core + +import ( + "errors" + "reflect" + "strings" + "testing" + + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" +) + +const testTechSpaceId = "bafyreitech.tech" + +var testSpaces = []SpaceListItem{ + {SpaceId: "bafyreia.one", Name: "Personal"}, + {SpaceId: "bafyreib.two", Name: "Team"}, + {SpaceId: "bafyreic.three", Name: "Team"}, + {SpaceId: "bafyreid.four", Name: "Work"}, +} + +func TestValidateGrantFlags(t *testing.T) { + tests := []struct { + name string + flags GrantFlags + wantErr error + }{ + {"spaces and read-only", GrantFlags{Spaces: []string{"Work"}, ReadOnly: true}, nil}, + {"all spaces and read-write", GrantFlags{AllSpaces: true, ReadWrite: true}, nil}, + {"no space choice", GrantFlags{ReadOnly: true}, ErrSpaceChoiceRequired}, + {"no permission choice", GrantFlags{AllSpaces: true}, ErrPermChoiceRequired}, + {"nothing chosen reports spaces first", GrantFlags{}, ErrSpaceChoiceRequired}, + {"spaces and all spaces together", GrantFlags{Spaces: []string{"Work"}, AllSpaces: true, ReadOnly: true}, ErrConflictingSpaceFlags}, + {"read-only and read-write together", GrantFlags{AllSpaces: true, ReadOnly: true, ReadWrite: true}, ErrConflictingPermFlags}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateGrantFlags(tt.flags) + if !errors.Is(err, tt.wantErr) { + t.Errorf("ValidateGrantFlags() error = %v, want %v", err, tt.wantErr) + } + }) + } +} + +func TestResolveSpaces(t *testing.T) { + tests := []struct { + name string + args []string + want []ResolvedSpace + wantErrText string + }{ + { + name: "by exact id", + args: []string{"bafyreid.four"}, + want: []ResolvedSpace{{Id: "bafyreid.four", Name: "Work"}}, + }, + { + name: "by unique name", + args: []string{"Personal"}, + want: []ResolvedSpace{{Id: "bafyreia.one", Name: "Personal"}}, + }, + { + name: "id and name of the same space are deduplicated", + args: []string{"Work", "bafyreid.four", "Work"}, + want: []ResolvedSpace{{Id: "bafyreid.four", Name: "Work"}}, + }, + { + name: "order of first appearance is kept", + args: []string{"Work", "Personal"}, + want: []ResolvedSpace{{Id: "bafyreid.four", Name: "Work"}, {Id: "bafyreia.one", Name: "Personal"}}, + }, + { + name: "tech space by explicit id", + args: []string{testTechSpaceId}, + want: []ResolvedSpace{{Id: testTechSpaceId, IsTech: true}}, + }, + { + name: "ambiguous name lists candidates", + args: []string{"Team"}, + wantErrText: "bafyreic.three", + }, + { + name: "unknown name or id", + args: []string{"Nope"}, + wantErrText: `space "Nope" not found`, + }, + { + name: "name match is exact, not case-insensitive", + args: []string{"work"}, + wantErrText: `space "work" not found`, + }, + { + name: "empty argument", + args: []string{""}, + wantErrText: "empty space", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ResolveSpaces(tt.args, testSpaces, testTechSpaceId) + if tt.wantErrText != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErrText) { + t.Fatalf("ResolveSpaces() error = %v, want it to contain %q", err, tt.wantErrText) + } + return + } + if err != nil { + t.Fatalf("ResolveSpaces() unexpected error: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ResolveSpaces() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestBuildGrant(t *testing.T) { + resolved := []ResolvedSpace{{Id: "bafyreia.one", Name: "Personal"}, {Id: "bafyreid.four", Name: "Work"}} + + tests := []struct { + name string + flags GrantFlags + resolved []ResolvedSpace + want *model.AccountAuthAppGrant + }{ + { + name: "listed spaces, read-only", + flags: GrantFlags{Spaces: []string{"Personal", "Work"}, ReadOnly: true}, + resolved: resolved, + want: &model.AccountAuthAppGrant{SpaceIds: []string{"bafyreia.one", "bafyreid.four"}, Perm: model.AccountAuthAppGrant_Read}, + }, + { + name: "listed spaces, read-write", + flags: GrantFlags{Spaces: []string{"Personal", "Work"}, ReadWrite: true}, + resolved: resolved, + want: &model.AccountAuthAppGrant{SpaceIds: []string{"bafyreia.one", "bafyreid.four"}, Perm: model.AccountAuthAppGrant_ReadWrite}, + }, + { + name: "all spaces, read-write", + flags: GrantFlags{AllSpaces: true, ReadWrite: true}, + want: &model.AccountAuthAppGrant{AllSpaces: true, Perm: model.AccountAuthAppGrant_ReadWrite}, + }, + { + name: "all spaces, read-only", + flags: GrantFlags{AllSpaces: true, ReadOnly: true}, + want: &model.AccountAuthAppGrant{AllSpaces: true, Perm: model.AccountAuthAppGrant_Read}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := BuildGrant(tt.flags, tt.resolved) + if err != nil { + t.Fatalf("BuildGrant() unexpected error: %v", err) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("BuildGrant() = %+v, want %+v", got, tt.want) + } + }) + } +} + +func TestBuildGrantNeverReturnsNilOrEmpty(t *testing.T) { + tests := []struct { + name string + flags GrantFlags + resolved []ResolvedSpace + }{ + {"no choices", GrantFlags{}, nil}, + {"space flag but nothing resolved", GrantFlags{Spaces: []string{"x"}, ReadOnly: true}, nil}, + {"no permission", GrantFlags{AllSpaces: true}, nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := BuildGrant(tt.flags, tt.resolved) + if err == nil { + t.Fatalf("BuildGrant() = %+v, want an error", got) + } + if got != nil { + t.Errorf("BuildGrant() returned a grant alongside error: %+v", got) + } + }) + } +} + +func TestValidateAPIKeyName(t *testing.T) { + tests := []struct { + name string + keyName string + wantErr bool + }{ + {"normal name", "my-integration", false}, + {"exactly 128 bytes", strings.Repeat("a", 128), false}, + {"empty", "", true}, + {"whitespace only", " ", true}, + {"129 bytes", strings.Repeat("a", 129), true}, + {"multibyte over the byte limit", strings.Repeat("é", 65), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateAPIKeyName(tt.keyName) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateAPIKeyName(%q) error = %v, wantErr %v", tt.keyName, err, tt.wantErr) + } + }) + } +} + +func TestGrantWorksOnV1(t *testing.T) { + tests := []struct { + name string + grant *model.AccountAuthAppGrant + want bool + }{ + {"all spaces read-write", &model.AccountAuthAppGrant{AllSpaces: true, Perm: model.AccountAuthAppGrant_ReadWrite}, true}, + {"all spaces read-only", &model.AccountAuthAppGrant{AllSpaces: true, Perm: model.AccountAuthAppGrant_Read}, false}, + {"listed spaces read-write", &model.AccountAuthAppGrant{SpaceIds: []string{"a"}, Perm: model.AccountAuthAppGrant_ReadWrite}, false}, + {"listed spaces read-only", &model.AccountAuthAppGrant{SpaceIds: []string{"a"}}, false}, + {"no grant", nil, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GrantWorksOnV1(tt.grant); got != tt.want { + t.Errorf("GrantWorksOnV1() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDescribeGrant(t *testing.T) { + tests := []struct { + name string + grant *model.AccountAuthAppGrant + resolved []ResolvedSpace + want string + }{ + {"all spaces read-write", &model.AccountAuthAppGrant{AllSpaces: true, Perm: model.AccountAuthAppGrant_ReadWrite}, nil, "read-write, all spaces"}, + {"all spaces read-only", &model.AccountAuthAppGrant{AllSpaces: true}, nil, "read-only, all spaces"}, + { + "named spaces", + &model.AccountAuthAppGrant{SpaceIds: []string{"bafyreia.one", "bafyreitech.tech"}}, + []ResolvedSpace{{Id: "bafyreia.one", Name: "Personal"}, {Id: "bafyreitech.tech", IsTech: true}}, + "read-only, 2 spaces: Personal (bafyreia.one), tech space (bafyreitech.tech)", + }, + {"spaces without names", &model.AccountAuthAppGrant{SpaceIds: []string{"bafyreia.one"}, Perm: model.AccountAuthAppGrant_ReadWrite}, nil, "read-write, 1 space: bafyreia.one"}, + {"no grant", nil, nil, "unrestricted"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := DescribeGrant(tt.grant, tt.resolved); got != tt.want { + t.Errorf("DescribeGrant() = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/core/auth.go b/core/auth.go index e0c19ea..9e50a39 100644 --- a/core/auth.go +++ b/core/auth.go @@ -223,7 +223,10 @@ func Logout() error { } configMgr := config.GetConfigManager() - if err := configMgr.Delete(); err != nil { + if err := configMgr.Load(); err != nil { + output.Warning("Failed to read config: %v", err) + } + if err := configMgr.ClearAccount(); err != nil { output.Warning("Failed to clear config: %v", err) } diff --git a/core/config/config.go b/core/config/config.go index 2db19e1..bf23fc5 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -17,6 +17,9 @@ type Config struct { // WARNING: This is insecure and should only be used on headless servers AccountKey string `json:"accountKey,omitempty"` SessionToken string `json:"sessionToken,omitempty"` + // ApiListenAddr is the JSON API address last chosen with --listen-address. + // It is a server setting, so it survives logout. + ApiListenAddr string `json:"apiListenAddr,omitempty"` } var ( @@ -152,6 +155,31 @@ func (cm *ConfigManager) SetNetworkId(networkId string) error { return cm.Save() } +func (cm *ConfigManager) SetApiListenAddr(addr string) error { + cm.mu.Lock() + cm.config.ApiListenAddr = addr + cm.mu.Unlock() + + return cm.Save() +} + +// ClearAccount removes everything tied to the logged-in account and keeps +// server settings. The file is deleted when no settings remain. +func (cm *ConfigManager) ClearAccount() error { + cm.mu.Lock() + keep := &Config{ApiListenAddr: cm.config.ApiListenAddr} + cm.mu.Unlock() + + if keep.ApiListenAddr == "" { + return cm.Delete() + } + + cm.mu.Lock() + cm.config = keep + cm.mu.Unlock() + return cm.Save() +} + func (cm *ConfigManager) Reset() error { cm.mu.Lock() cm.config = &Config{} diff --git a/core/config/config_helper.go b/core/config/config_helper.go index ff093e6..6c3758a 100644 --- a/core/config/config_helper.go +++ b/core/config/config_helper.go @@ -162,3 +162,21 @@ func ReadNetworkIdFromYAML(path string) (string, error) { return cfg.NetworkId, nil } + +func GetApiListenAddrFromConfig() (string, error) { + configMgr := GetConfigManager() + if err := configMgr.Load(); err != nil { + return "", fmt.Errorf("failed to load config: %w", err) + } + + return configMgr.Get().ApiListenAddr, nil +} + +func SetApiListenAddrToConfig(addr string) error { + configMgr := GetConfigManager() + if err := configMgr.Load(); err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + return configMgr.SetApiListenAddr(addr) +} diff --git a/core/config/config_test.go b/core/config/config_test.go index 5fbaea6..3223091 100644 --- a/core/config/config_test.go +++ b/core/config/config_test.go @@ -94,3 +94,65 @@ func TestGetConfigManager(t *testing.T) { t.Error("GetConfigManager should initialize with a valid file path") } } + +func newTestConfigManager(t *testing.T, cfg *Config) *ConfigManager { + t.Helper() + return &ConfigManager{config: cfg, filePath: filepath.Join(t.TempDir(), "config.json")} +} + +func TestApiListenAddrPersists(t *testing.T) { + cm := newTestConfigManager(t, &Config{}) + + if err := cm.SetApiListenAddr("0.0.0.0:4000"); err != nil { + t.Fatalf("SetApiListenAddr: %v", err) + } + + cm2 := &ConfigManager{config: &Config{}, filePath: cm.filePath} + if err := cm2.Load(); err != nil { + t.Fatalf("Load: %v", err) + } + if got := cm2.Get().ApiListenAddr; got != "0.0.0.0:4000" { + t.Errorf("ApiListenAddr = %q, want %q", got, "0.0.0.0:4000") + } +} + +func TestClearAccountKeepsApiListenAddr(t *testing.T) { + cm := newTestConfigManager(t, &Config{ + AccountId: "acc", + TechSpaceId: "tech", + AccountKey: "key", + SessionToken: "token", + ApiListenAddr: "0.0.0.0:4000", + }) + if err := cm.Save(); err != nil { + t.Fatalf("Save: %v", err) + } + + if err := cm.ClearAccount(); err != nil { + t.Fatalf("ClearAccount: %v", err) + } + + cm2 := &ConfigManager{config: &Config{}, filePath: cm.filePath} + if err := cm2.Load(); err != nil { + t.Fatalf("Load: %v", err) + } + want := Config{ApiListenAddr: "0.0.0.0:4000"} + if got := *cm2.Get(); got != want { + t.Errorf("config after ClearAccount = %+v, want %+v", got, want) + } +} + +func TestClearAccountRemovesFileWithoutSettings(t *testing.T) { + cm := newTestConfigManager(t, &Config{AccountId: "acc", SessionToken: "token"}) + if err := cm.Save(); err != nil { + t.Fatalf("Save: %v", err) + } + + if err := cm.ClearAccount(); err != nil { + t.Fatalf("ClearAccount: %v", err) + } + + if _, err := os.Stat(cm.filePath); !os.IsNotExist(err) { + t.Errorf("config file still exists after clearing an account with no settings to keep (err = %v)", err) + } +} diff --git a/core/config/listenaddr.go b/core/config/listenaddr.go new file mode 100644 index 0000000..0cc1127 --- /dev/null +++ b/core/config/listenaddr.go @@ -0,0 +1,20 @@ +package config + +// ResolveAPIListenAddr picks the JSON API address: an explicit --listen-address +// wins, then the address saved from an earlier explicit choice, then the +// default. The JSON API starts when an account logs in, on the address that +// login carries, so every command that logs in must agree on it. +func ResolveAPIListenAddr(flag string, flagSet bool, stored string) string { + if flagSet { + return flag + } + if stored != "" { + return stored + } + return DefaultAPIAddress +} + +// APIURL is the base URL of the JSON API listening on addr. +func APIURL(addr string) string { + return "http://" + addr +} diff --git a/core/config/listenaddr_test.go b/core/config/listenaddr_test.go new file mode 100644 index 0000000..8346235 --- /dev/null +++ b/core/config/listenaddr_test.go @@ -0,0 +1,43 @@ +package config + +import "testing" + +func TestResolveAPIListenAddr(t *testing.T) { + tests := []struct { + name string + flag string + flagSet bool + stored string + want string + }{ + {"explicit flag wins over stored", "0.0.0.0:5000", true, "127.0.0.1:4000", "0.0.0.0:5000"}, + {"explicit flag equal to default still wins", DefaultAPIAddress, true, "127.0.0.1:4000", DefaultAPIAddress}, + {"stored used when flag not set", DefaultAPIAddress, false, "127.0.0.1:4000", "127.0.0.1:4000"}, + {"default when nothing stored", DefaultAPIAddress, false, "", DefaultAPIAddress}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ResolveAPIListenAddr(tt.flag, tt.flagSet, tt.stored); got != tt.want { + t.Errorf("ResolveAPIListenAddr() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestAPIURL(t *testing.T) { + tests := []struct { + addr string + want string + }{ + {"127.0.0.1:31012", "http://127.0.0.1:31012"}, + {"0.0.0.0:31012", "http://0.0.0.0:31012"}, + {"[::1]:31012", "http://[::1]:31012"}, + } + + for _, tt := range tests { + if got := APIURL(tt.addr); got != tt.want { + t.Errorf("APIURL(%q) = %q, want %q", tt.addr, got, tt.want) + } + } +} diff --git a/core/grpcserver/loglevel_test.go b/core/grpcserver/loglevel_test.go new file mode 100644 index 0000000..63b921d --- /dev/null +++ b/core/grpcserver/loglevel_test.go @@ -0,0 +1,63 @@ +//go:build !nogrpcserver + +package grpcserver + +import ( + "io" + "os" + "strings" + "testing" + + "github.com/anyproto/anytype-heart/pkg/lib/logging" +) + +// captureStderr returns what fn wrote to stderr. zap opens its "stderr" sink +// when the config is applied, so fn must apply it after the swap. +func captureStderr(t *testing.T, fn func()) string { + t.Helper() + old := os.Stderr + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("pipe: %v", err) + } + os.Stderr = w + fn() + w.Close() + os.Stderr = old + out, _ := io.ReadAll(r) + return string(out) +} + +func TestApplyLogLevelFiltersBeforeLogin(t *testing.T) { + tests := []struct { + name string + env string + wantInfo bool + wantError bool + }{ + {"default is ERROR", "", false, true}, + {"quiet mode", "*=FATAL", false, false}, + {"verbose mode", "*=DEBUG", true, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("ANYTYPE_LOG_LEVEL", tt.env) + log := logging.Logger("anytype-cli-loglevel-test") + + out := captureStderr(t, func() { + applyLogLevel() + log.Info("INFO-LINE") + log.Error("ERROR-LINE") + _ = log.Sync() + }) + + if got := strings.Contains(out, "INFO-LINE"); got != tt.wantInfo { + t.Errorf("info shown = %v, want %v (output %q)", got, tt.wantInfo, out) + } + if got := strings.Contains(out, "ERROR-LINE"); got != tt.wantError { + t.Errorf("error shown = %v, want %v (output %q)", got, tt.wantError, out) + } + }) + } +} diff --git a/core/grpcserver/proxy.go b/core/grpcserver/proxy.go new file mode 100644 index 0000000..df8e99e --- /dev/null +++ b/core/grpcserver/proxy.go @@ -0,0 +1,90 @@ +//go:build !nogrpcserver + +package grpcserver + +import ( + "context" + "net/http" + "os" + + "github.com/anyproto/anytype-heart/util/localorigin" + "github.com/improbable-eng/grpc-web/go/grpcweb" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// Mirrors anytype-heart's cmd/grpcserver/proxy.go, which the CLI cannot import. + +// envAllowedOrigins adds comma-separated exact origins to the gRPC-Web allowlist. +const envAllowedOrigins = "ANYTYPE_GRPCWEB_ALLOWED_ORIGINS" + +// envAllowedHosts adds comma-separated Host header values to the allowlist, for +// servers bound to a routable interface and reached by name. +const envAllowedHosts = "ANYTYPE_GRPCWEB_ALLOWED_HOSTS" + +// envEnableWebsockets re-enables the grpc-websockets transport, which is still +// origin-checked when on. +const envEnableWebsockets = "ANYTYPE_GRPCWEB_ENABLE_WEBSOCKETS" + +// newOriginPolicy builds the allowlist guarding the gRPC-Web proxy: loopback +// origins, the desktop app's file:// renderer and the Webclipper extension, +// plus whatever the operator configured. +func newOriginPolicy(allowedOrigins, allowedHosts string) *localorigin.Policy { + return localorigin.New(allowedOrigins, + localorigin.AllowFileOrigin(), + localorigin.AllowHosts(allowedHosts), + localorigin.AllowWebclipperExtension(), + ) +} + +// websocketsEnabled reports whether the grpc-websockets transport is on. It is +// off by default: WebSocket handshakes skip the CORS preflight, so it would let +// any site reach the RPC surface directly. +func websocketsEnabled() bool { + return os.Getenv(envEnableWebsockets) == "1" +} + +func wrapOptions(policy *localorigin.Policy, withWebsockets bool) []grpcweb.Option { + opts := []grpcweb.Option{grpcweb.WithOriginFunc(policy.AllowOrigin)} + if withWebsockets { + opts = append(opts, + grpcweb.WithWebsockets(true), + grpcweb.WithWebsocketOriginFunc(policy.AllowRequest), + ) + } + return opts +} + +// newProxyHandler serves gRPC-Web only to callers the policy trusts. grpcweb's +// CORS layer does not reject a disallowed origin on a non-preflight request (it +// only omits the Access-Control-* headers and dispatches anyway), so the +// request is rejected here before it reaches the handler. +func newProxyHandler(webrpc *grpcweb.WrappedGrpcServer, policy *localorigin.Policy, withWebsockets bool) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + isWebsocket := webrpc.IsGrpcWebSocketRequest(r) + if isWebsocket && !withWebsockets { + http.Error(w, "grpc-websockets transport is disabled", http.StatusForbidden) + return + } + if !webrpc.IsGrpcWebRequest(r) && !webrpc.IsAcceptableGrpcCorsRequest(r) && !isWebsocket { + return + } + if !policy.AllowRequest(r) { + log.Warnf("rejected grpc-web request from untrusted origin %q (host %q)", r.Header.Get("Origin"), r.Host) + http.Error(w, "forbidden origin", http.StatusForbidden) + return + } + webrpc.ServeHTTP(w, r) + } +} + +// originInterceptor carries the Origin forwarded by the gRPC-Web proxy into the +// request context, so localorigin.OriginFromContext works on this transport. +func originInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if md, ok := metadata.FromIncomingContext(ctx); ok { + ctx = localorigin.WithOrigin(ctx, localorigin.OriginFromMetadata(md)) + } + return handler(ctx, req) + } +} diff --git a/core/grpcserver/proxy_test.go b/core/grpcserver/proxy_test.go new file mode 100644 index 0000000..6c2aa5b --- /dev/null +++ b/core/grpcserver/proxy_test.go @@ -0,0 +1,231 @@ +//go:build !nogrpcserver + +package grpcserver + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + + "github.com/anyproto/anytype-heart/util/localorigin" + "github.com/improbable-eng/grpc-web/go/grpcweb" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +const proxyHost = "127.0.0.1:31011" + +const probedMethod = "/anytype.ClientCommands/AccountLocalLinkNewChallenge" + +// newTestProxy wires the real grpcweb wrapper, with the options production +// uses, in front of a sentinel that records whether the RPC was dispatched. +func newTestProxy(t *testing.T, policy *localorigin.Policy, withWebsockets bool) (http.HandlerFunc, *atomic.Bool) { + t.Helper() + + var dispatched atomic.Bool + sentinel := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + dispatched.Store(true) + w.WriteHeader(http.StatusOK) + }) + + // WrapServer registers the real gRPC methods; WrapHandler does not, so + // register the probed endpoint to keep the CORS preflight check faithful. + opts := append(wrapOptions(policy, withWebsockets), grpcweb.WithEndpointsFunc(func() []string { + return []string{probedMethod} + })) + webrpc := grpcweb.WrapHandler(sentinel, opts...) + return newProxyHandler(webrpc, policy, withWebsockets), &dispatched +} + +func newGrpcWebRequest(host, origin string) *http.Request { + r := httptest.NewRequest(http.MethodPost, probedMethod, strings.NewReader("")) + r.Host = host + r.Header.Set("Content-Type", "application/grpc-web+proto") + r.Header.Set("X-Grpc-Web", "1") + if origin != "" { + r.Header.Set("Origin", origin) + } + return r +} + +func newWebsocketRequest(host, origin string) *http.Request { + r := httptest.NewRequest(http.MethodGet, probedMethod, nil) + r.Host = host + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-Websocket-Protocol", "grpc-websockets") + r.Header.Set("Sec-Websocket-Version", "13") + r.Header.Set("Sec-Websocket-Key", "dGhlIHNhbXBsZSBub25jZQ==") + if origin != "" { + r.Header.Set("Origin", origin) + } + return r +} + +func TestProxyHandlerRejectsUntrustedOriginBeforeDispatch(t *testing.T) { + tests := []struct { + name string + host string + origin string + }{ + {"malicious site", proxyHost, "https://evil.com"}, + {"malicious site on the proxy port", proxyHost, "https://evil.com:31011"}, + {"sandboxed iframe or data url", proxyHost, "null"}, + {"non-loopback lan origin", proxyHost, "http://192.168.1.5:3030"}, + {"dns rebinding", "evil.com:31011", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler, dispatched := newTestProxy(t, newOriginPolicy("", ""), false) + w := httptest.NewRecorder() + + handler(w, newGrpcWebRequest(tt.host, tt.origin)) + + if w.Code != http.StatusForbidden { + t.Errorf("status = %d, want %d", w.Code, http.StatusForbidden) + } + if dispatched.Load() { + t.Error("the rpc must not reach the handler") + } + }) + } +} + +func TestProxyHandlerAllowsTrustedCallers(t *testing.T) { + tests := []struct { + name string + origin string + headers map[string]string + }{ + {"packaged electron renderer", "", map[string]string{"Sec-Fetch-Site": "cross-site", "Sec-Fetch-Mode": "cors"}}, + {"native client", "", nil}, + {"web build on a loopback origin", "http://127.0.0.1:3030", nil}, + {"electron dev renderer", "http://localhost:8080", nil}, + {"webclipper extension", "chrome-extension://jbnammhjiplhpjfncnlejjjejghimdkf", nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler, dispatched := newTestProxy(t, newOriginPolicy("", ""), false) + w := httptest.NewRecorder() + req := newGrpcWebRequest(proxyHost, tt.origin) + for k, v := range tt.headers { + req.Header.Set(k, v) + } + + handler(w, req) + + if w.Code == http.StatusForbidden { + t.Errorf("status = %d, want not forbidden", w.Code) + } + if !dispatched.Load() { + t.Error("the rpc should reach the handler") + } + }) + } +} + +func TestProxyHandlerHonorsConfiguredAllowlists(t *testing.T) { + t.Run("an allowed host is accepted", func(t *testing.T) { + handler, dispatched := newTestProxy(t, newOriginPolicy("", "anytype.example.com"), false) + w := httptest.NewRecorder() + + handler(w, newGrpcWebRequest("anytype.example.com:31011", "")) + + if w.Code == http.StatusForbidden || !dispatched.Load() { + t.Errorf("status = %d, dispatched = %v; want the allowed host to pass", w.Code, dispatched.Load()) + } + }) + + t.Run("an allowed origin is accepted", func(t *testing.T) { + handler, dispatched := newTestProxy(t, newOriginPolicy("http://192.168.1.5:3030", ""), false) + w := httptest.NewRecorder() + + handler(w, newGrpcWebRequest(proxyHost, "http://192.168.1.5:3030")) + + if w.Code == http.StatusForbidden || !dispatched.Load() { + t.Errorf("status = %d, dispatched = %v; want the allowed origin to pass", w.Code, dispatched.Load()) + } + }) +} + +func TestProxyHandlerWebsockets(t *testing.T) { + t.Run("disabled by default, whatever the origin", func(t *testing.T) { + for _, origin := range []string{"https://evil.com", "null", "file://", "http://127.0.0.1:3030", ""} { + handler, dispatched := newTestProxy(t, newOriginPolicy("", ""), false) + w := httptest.NewRecorder() + + handler(w, newWebsocketRequest(proxyHost, origin)) + + if w.Code != http.StatusForbidden { + t.Errorf("origin %q: status = %d, want %d", origin, w.Code, http.StatusForbidden) + } + if dispatched.Load() { + t.Errorf("origin %q: the rpc must not reach the handler", origin) + } + } + }) + + t.Run("when enabled, an untrusted origin is still refused", func(t *testing.T) { + for _, origin := range []string{"https://evil.com", "null", "http://192.168.1.5:3030"} { + handler, dispatched := newTestProxy(t, newOriginPolicy("", ""), true) + w := httptest.NewRecorder() + + handler(w, newWebsocketRequest(proxyHost, origin)) + + if w.Code != http.StatusForbidden { + t.Errorf("origin %q: status = %d, want %d", origin, w.Code, http.StatusForbidden) + } + if dispatched.Load() { + t.Errorf("origin %q: the rpc must not reach the handler", origin) + } + } + }) +} + +func TestWebsocketsEnabledReadsEnv(t *testing.T) { + t.Setenv(envEnableWebsockets, "") + if websocketsEnabled() { + t.Error("websockets must be off by default") + } + t.Setenv(envEnableWebsockets, "1") + if !websocketsEnabled() { + t.Errorf("websockets must be on when %s=1", envEnableWebsockets) + } +} + +func TestOriginInterceptorCarriesOriginIntoContext(t *testing.T) { + tests := []struct { + name string + md metadata.MD + want string + }{ + {"webclipper extension origin", metadata.Pairs("origin", "chrome-extension://jbnammhjiplhpjfncnlejjjejghimdkf"), "chrome-extension://jbnammhjiplhpjfncnlejjjejghimdkf"}, + {"loopback page origin", metadata.Pairs("origin", "http://localhost:3000"), "http://localhost:3000"}, + {"native caller sends none", metadata.MD{}, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got string + handler := func(ctx context.Context, _ interface{}) (interface{}, error) { + got = localorigin.OriginFromContext(ctx) + return nil, nil + } + ctx := metadata.NewIncomingContext(context.Background(), tt.md) + + _, err := originInterceptor()(ctx, nil, &grpc.UnaryServerInfo{FullMethod: probedMethod}, handler) + + if err != nil { + t.Fatalf("interceptor returned error: %v", err) + } + if got != tt.want { + t.Errorf("origin = %q, want %q", got, tt.want) + } + }) + } +} diff --git a/core/grpcserver/server.go b/core/grpcserver/server.go index 0aafc65..1d58242 100644 --- a/core/grpcserver/server.go +++ b/core/grpcserver/server.go @@ -47,9 +47,7 @@ func NewServer() *Server { func (s *Server) Start(grpcAddr, grpcWebAddr string) error { app.StartWarningAfter = time.Second * 5 - if os.Getenv("ANYTYPE_LOG_LEVEL") == "" { - os.Setenv("ANYTYPE_LOG_LEVEL", "ERROR") - } + applyLogLevel() metrics.Service.InitWithKeys(metrics.DefaultInHouseKey) @@ -91,6 +89,7 @@ func (s *Server) Start(grpcAddr, grpcWebAddr string) error { unaryInterceptors = append(unaryInterceptors, grpcprocess.ProcessInfoInterceptor( "/anytype.ClientCommands/AccountLocalLinkNewChallenge", )) + unaryInterceptors = append(unaryInterceptors, originInterceptor()) s.grpcServer = grpc.NewServer( grpc.MaxRecvMsgSize(20*1024*1024), @@ -103,15 +102,12 @@ func (s *Server) Start(grpcAddr, grpcWebAddr string) error { grpc_prometheus.EnableHandlingTimeHistogram() } - webrpc := grpcweb.WrapServer( - s.grpcServer, - grpcweb.WithOriginFunc(func(origin string) bool { return true }), - grpcweb.WithWebsockets(true), - grpcweb.WithWebsocketOriginFunc(func(req *http.Request) bool { return true }), - ) + originPolicy := newOriginPolicy(os.Getenv(envAllowedOrigins), os.Getenv(envAllowedHosts)) + withWebsockets := websocketsEnabled() + webrpc := grpcweb.WrapServer(s.grpcServer, wrapOptions(originPolicy, withWebsockets)...) s.webServer = &http.Server{ - Handler: webrpc, + Handler: newProxyHandler(webrpc, originPolicy, withWebsockets), ReadHeaderTimeout: 30 * time.Second, } @@ -158,3 +154,17 @@ func (s *Server) Stop() error { log.Info("Servers stopped") return nil } + +// defaultLogLevel applies when ANYTYPE_LOG_LEVEL is unset. +const defaultLogLevel = "ERROR" + +// applyLogLevel applies ANYTYPE_LOG_LEVEL to heart's loggers right away. +// heart applies it itself only when an account logs in (InitialSetParameters), +// so without this everything logged before login uses the logger's built-in +// DEBUG default. +func applyLogLevel() { + if os.Getenv("ANYTYPE_LOG_LEVEL") == "" { + os.Setenv("ANYTYPE_LOG_LEVEL", defaultLogLevel) + } + logging.SetLogLevels(os.Getenv("ANYTYPE_LOG_LEVEL")) +} diff --git a/core/output/output.go b/core/output/output.go index de9b394..a950f6d 100644 --- a/core/output/output.go +++ b/core/output/output.go @@ -3,6 +3,8 @@ package output import ( "fmt" "os" + "strings" + "unicode/utf8" ) func Success(format string, args ...interface{}) { @@ -28,3 +30,27 @@ func Debug(format string, args ...interface{}) { func Print(format string, args ...interface{}) { fmt.Fprintf(os.Stdout, format+"\n", args...) } + +// Banner prints lines inside a box, for information that must stand out in +// busy output, such as where the JSON API listens. +func Banner(lines ...string) { + fmt.Fprint(os.Stdout, FormatBanner(lines...)) +} + +// FormatBanner draws lines inside a rounded box. +func FormatBanner(lines ...string) string { + width := 0 + for _, line := range lines { + width = max(width, utf8.RuneCountInString(line)) + } + inner := width + 3 // two spaces before the text, one after + + var b strings.Builder + b.WriteString("╭" + strings.Repeat("─", inner) + "╮\n") + for _, line := range lines { + pad := width - utf8.RuneCountInString(line) + b.WriteString("│ " + line + strings.Repeat(" ", pad) + " │\n") + } + b.WriteString("╰" + strings.Repeat("─", inner) + "╯\n") + return b.String() +} diff --git a/core/output/output_test.go b/core/output/output_test.go index c70f8c5..1749de8 100644 --- a/core/output/output_test.go +++ b/core/output/output_test.go @@ -87,3 +87,27 @@ func TestPrint(t *testing.T) { t.Errorf("Print() output = %v, want 'test 123 message'", output) } } + +func TestFormatBanner(t *testing.T) { + got := FormatBanner("JSON API: http://127.0.0.1:31012", "starts when an account is logged in") + want := strings.Join([]string{ + "╭──────────────────────────────────────╮", + "│ JSON API: http://127.0.0.1:31012 │", + "│ starts when an account is logged in │", + "╰──────────────────────────────────────╯", + }, "\n") + "\n" + if got != want { + t.Errorf("FormatBanner() =\n%s\nwant\n%s", got, want) + } +} + +func TestFormatBannerCountsRunesNotBytes(t *testing.T) { + got := FormatBanner("café") + lines := strings.Split(strings.TrimSuffix(got, "\n"), "\n") + width := len([]rune(lines[0])) + for i, line := range lines { + if n := len([]rune(line)); n != width { + t.Errorf("line %d has %d runes, want %d: %q", i, n, width, line) + } + } +} diff --git a/core/serviceprogram/serviceprogram.go b/core/serviceprogram/serviceprogram.go index 4aba1cd..276f3bb 100644 --- a/core/serviceprogram/serviceprogram.go +++ b/core/serviceprogram/serviceprogram.go @@ -63,6 +63,16 @@ type Program struct { startErr error startCh chan struct{} apiListenAddr string + + // OnStarted, if set, runs once the gRPC servers are listening, before + // Start returns. It does not run when the server fails to start. + OnStarted func() + + // startServer starts the gRPC servers; replaceable in tests. + startServer func(grpcAddr, grpcWebAddr string) error + // hasStoredAccount reports whether auto-login has a key to use; + // replaceable in tests. + hasStoredAccount func() bool } func New(apiListenAddr string) *Program { @@ -75,6 +85,15 @@ func New(apiListenAddr string) *Program { func (p *Program) Start(s service.Service) error { p.ctx, p.cancel = context.WithCancel(context.Background()) p.server = grpcserver.NewServer() + if p.startServer == nil { + p.startServer = p.server.Start + } + if p.hasStoredAccount == nil { + p.hasStoredAccount = func() bool { + key, _, err := core.GetStoredAccountKey() + return err == nil && key != "" + } + } p.wg.Add(1) go p.run() @@ -93,6 +112,13 @@ func (p *Program) Start(s service.Service) error { return fmt.Errorf("timeout waiting for server to start") } + // The JSON API starts when an account logs in: show where it will be and, + // if there is nothing to auto-login with, how to log in. + output.Banner(startupBanner(config.APIURL(p.apiListenAddr), p.hasStoredAccount())...) + + if p.OnStarted != nil { + p.OnStarted() + } return nil } @@ -115,7 +141,7 @@ func (p *Program) run() { defer p.wg.Done() defer close(p.startCh) - if err := p.server.Start(config.DefaultGRPCAddress, config.DefaultGRPCWebAddress); err != nil { + if err := p.startServer(config.DefaultGRPCAddress, config.DefaultGRPCWebAddress); err != nil { p.startErr = err return } @@ -123,8 +149,12 @@ func (p *Program) run() { // Signal successful start p.startCh <- struct{}{} - // Wait a moment for server to be ready - time.Sleep(2 * time.Second) + // Wait a moment for server to be ready; skip auto-login if stopped meanwhile + select { + case <-time.After(2 * time.Second): + case <-p.ctx.Done(): + return + } go p.attemptAutoLogin() @@ -150,9 +180,33 @@ func (p *Program) attemptAutoLogin() { continue } output.Info("Failed to auto-login with account key after %d attempts: %v", maxRetries, err) + output.Banner(autoLoginFailedBanner()...) } else { output.Success("Successfully logged in using stored account key") return } } } + +// startupBanner is shown once the server is up. With a stored account key the +// server logs in by itself, so the address is enough; without one, the JSON +// API won't start until someone logs in. +func startupBanner(url string, hasStoredAccount bool) []string { + if hasStoredAccount { + return []string{"JSON API: " + url} + } + return []string{ + "JSON API: " + url + " (starts after login)", + "", + "Not logged in. In another terminal, run one of:", + " anytype auth login # existing bot account", + " anytype auth create # new bot account", + } +} + +func autoLoginFailedBanner() []string { + return []string{ + "Auto-login failed, so the JSON API is not running.", + "In another terminal, run: anytype auth login", + } +} diff --git a/core/serviceprogram/serviceprogram_test.go b/core/serviceprogram/serviceprogram_test.go index 1d7f317..8820b0d 100644 --- a/core/serviceprogram/serviceprogram_test.go +++ b/core/serviceprogram/serviceprogram_test.go @@ -1,9 +1,14 @@ package serviceprogram import ( + "errors" + "io" + "os" + "strings" "testing" "github.com/anyproto/anytype-cli/core/config" + "github.com/anyproto/anytype-cli/core/output" ) func TestNew(t *testing.T) { @@ -92,3 +97,117 @@ func TestGetServiceWithAddress(t *testing.T) { }) } } + +func TestStartCallsOnStartedOnlyAfterServerStarts(t *testing.T) { + tests := []struct { + name string + startErr error + wantStarted bool + }{ + {"server starts", nil, true}, + {"server fails to listen", errors.New("bind: address already in use"), false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := New(config.DefaultAPIAddress) + p.startServer = func(grpcAddr, grpcWebAddr string) error { return tt.startErr } + p.hasStoredAccount = func() bool { return true } + started := false + p.OnStarted = func() { started = true } + + err := p.Start(nil) + if p.cancel != nil { + p.cancel() + } + p.wg.Wait() + + if (err != nil) != (tt.startErr != nil) { + t.Fatalf("Start() error = %v, want error %v", err, tt.startErr != nil) + } + if started != tt.wantStarted { + t.Errorf("OnStarted called = %v, want %v", started, tt.wantStarted) + } + }) + } +} + +// captureStdout returns what fn wrote to stdout. +func captureStdout(t *testing.T, fn func()) string { + t.Helper() + old := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("pipe: %v", err) + } + os.Stdout = w + fn() + w.Close() + os.Stdout = old + out, _ := io.ReadAll(r) + return string(out) +} + +func TestStartupBanner(t *testing.T) { + const url = "http://127.0.0.1:4000" + + withAccount := startupBanner(url, true) + if len(withAccount) != 1 || withAccount[0] != "JSON API: "+url { + t.Errorf("with stored account = %q, want just the address", withAccount) + } + + without := strings.Join(startupBanner(url, false), "\n") + for _, want := range []string{"JSON API: " + url, "anytype auth login", "anytype auth create "} { + if !strings.Contains(without, want) { + t.Errorf("without stored account = %q, want it to contain %q", without, want) + } + } +} + +func TestAutoLoginFailedBanner(t *testing.T) { + got := strings.Join(autoLoginFailedBanner(), "\n") + for _, want := range []string{"not running", "anytype auth login"} { + if !strings.Contains(got, want) { + t.Errorf("banner = %q, want it to contain %q", got, want) + } + } +} + +func TestStartPrintsStartupBanner(t *testing.T) { + tests := []struct { + name string + startErr error + hasAccount bool + want string + }{ + {"stored account", nil, true, output.FormatBanner(startupBanner("http://127.0.0.1:4000", true)...)}, + {"no stored account", nil, false, output.FormatBanner(startupBanner("http://127.0.0.1:4000", false)...)}, + {"server fails to listen", errors.New("bind: address already in use"), true, ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := New("127.0.0.1:4000") + p.startServer = func(grpcAddr, grpcWebAddr string) error { return tt.startErr } + p.hasStoredAccount = func() bool { return tt.hasAccount } + + out := captureStdout(t, func() { + _ = p.Start(nil) + if p.cancel != nil { + p.cancel() + } + p.wg.Wait() + }) + + if tt.want == "" { + if strings.Contains(out, "JSON API") { + t.Errorf("output %q mentions the JSON API although the server failed to start", out) + } + return + } + if !strings.Contains(out, tt.want) { + t.Errorf("output %q does not contain banner %q", out, tt.want) + } + }) + } +} diff --git a/docs/plans/apiv2-granular-api-keys.md b/docs/plans/apiv2-granular-api-keys.md new file mode 100644 index 0000000..9296a0c --- /dev/null +++ b/docs/plans/apiv2-granular-api-keys.md @@ -0,0 +1,516 @@ +# Plan: support API v2 and granular API key access (anytype-heart v0.51.3) + +Revision 2. It includes the findings of a three-lens review (correctness, +security, UX/rollout) against the heart `v0.51.3` tag. + +## Background + +anytype-heart v0.51.3 ships the JSON API v2 (`/v2/*`) and per-key access +control for API keys. + +**What changed in heart (v0.50.4 → v0.51.3):** + +- `model.Account.Auth.AppInfo` gained **one** field, `grant` + (`AppGrant`). `expireAt`, `scope` and `isActive` already existed. What's + new is that heart now validates `expireAt` when a key is created (must be + 0 or a future unix time in **seconds**) and checks it on every request. +- `AppGrant` = `spaceIds` **or** `allSpaces`, plus `perm` + (`Read` = 0, `ReadWrite` = 1). Because `Read` is the zero value, **a grant + without a `perm` is read-only**. +- `AccountLocalLinkCreateApp` checks that the name is set and at most 128 + bytes, that the scope is `Limited` or `JsonAPI` (never `Full`), that + `expireAt` is valid, and that the grant has the right shape. Only + `JsonAPI` keys can have a grant. +- New `AccountLocalLinkUpdateApp(appHash, grant)` replaces the **whole** + grant, and sending no grant removes it. It can't change scope, expiry or + name. It rejects a grant on a key that isn't `JsonAPI`. + Heart evicts cached HTTP sessions, so the change applies from the next + request. +- New `AccountLocalLinkApproveChallenge`, which only the desktop UI can + call (`Full` scope). The **gRPC-session** pairing flow is deprecated, but + JSON API auth still uses the same challenge flow. +- Key format: v0.51.3 creates prefixed `JsonAPI` keys and saves keys that + carry a grant in a v2 file format. **v0.50.4 can read neither.** + +**Which keys each API accepts:** + +| Key | `/v1` | `/v2` | direct gRPC | +|---|---|---|---| +| `Limited` (what the CLI creates today) | ✅ | ❌ 403 "create a new api key with JsonAPI scope" | ✅ `limitedScopeMethods` allowlist (ObjectSearch, ListenSessionEvents, …) | +| `JsonAPI`, no grant | ✅ | ✅ (heart calls it "legacy", suggests reissuing) | ❌ | +| `JsonAPI`, `allSpaces` + `ReadWrite` | ✅ (tech space **included**) | ✅ (tech space **excluded**) | ❌ | +| `JsonAPI`, any other grant | ❌ | ✅ within the grant | ❌ | + +On `/v1`, only keys with no grant or `allSpaces` + `ReadWrite` get through +(`ensureUngrantedKey` / `ApiGrant.IsUnrestricted`). An `allSpaces` grant +never covers the tech space on `/v2`; the tech space has to be listed +explicitly. + +**Other changes in v0.51.3 that affect the CLI:** + +- **Go 1.26.5** is required (`go.mod`). The CLI is on 1.25.7, and CI, + release and CodeQL use `go-version: "1.25"`. +- The HTTP API (`/v1` and `/v2`) now checks Host and Origin. Non-localhost + hostnames need `ANYTYPE_API_ALLOWED_HOSTS`, and browser origins need + `ANYTYPE_API_ALLOWED_ORIGINS`. This affects the remote and reverse-proxy + setups the README recommends. +- Heart's own gRPC-Web proxy (`cmd/grpcserver/proxy.go`) rejects requests + from untrusted origins/hosts before they reach any handler, and turns + WebSockets off by default (`ANYTYPE_GRPCWEB_ALLOWED_ORIGINS`, + `ANYTYPE_GRPCWEB_ALLOWED_HOSTS`, `ANYTYPE_GRPCWEB_ENABLE_WEBSOCKETS`). + The CLI's `core/grpcserver/server.go` accepts every origin and allows + WebSockets. +- The tantivy version stays at v1.0.6, so no change is needed there. + +## Goals + +1. Keys created by the CLI work on `/v2`. +2. Users can create keys limited to chosen spaces, read-only access, and an + expiry date. The CLI never creates a key with more access than the user + asked for. +3. Users can see and change a key's access. Widening access requires + explicit confirmation. +4. Existing setups keep working after upgrading, or there's a documented + migration: `/v1` keys, gRPC integrations, remote/proxy deployments. + +## Non-goals + +- A CLI client for the `/v2` REST endpoints. +- Pairing approval on a headless server (see Phase 6). Headless users create + keys directly. + +## Key decisions + +- **D1 — No default access: the user must choose spaces and permission + (option C).** `apikey create` needs: + - exactly one of `--space ` (repeatable) or `--all-spaces`, and + - exactly one of `--read-only` or `--read-write`. + + If either choice is missing, it creates nothing and prints what's needed, + with the user's spaces (name + ID) for `--space`. The CLI never picks which + spaces a key can reach or whether it can write. + - This is a **breaking change** for scripts that run + `apikey create ` without flags. They fail loudly instead of + getting a key with broad access. The release notes say so and show the + `--all-spaces --read-write` equivalent of today's behaviour. + - Rejected: A (no grant by default, which heart treats as legacy) and B + (`allSpaces` by default, which gives broad access without an explicit + decision). +- **D1a — The CLI never creates or sets a key with no grant.** Every key it + creates, and every `update` it sends, carries a grant. There's no + `--unrestricted` flag. The only thing a no-grant key adds over + `--all-spaces` is `/v2` access to the tech space, and that stays possible by listing it + explicitly with `--space `. Existing no-grant keys (made by + other clients) are shown and can be narrowed, but never re-created or + widened back to no grant. +- **D2 — Never trust the server to apply what was asked.** After any create + or update, read the key back with `ListApps` and compare. If the result + doesn't match, revoke (for create) and fail. Together with a server + version check beforehand, this closes the gap where a new CLI talks to an + older running service (see Phase 2). +- **D3 — No secrets in listings.** `list`, including `--json`, prints + selected fields only. It never prints `appKey`. Only `create` prints the + secret, once. +- **D4 — `update` merges with the current grant.** The CLI reads the + current grant, applies only the dimensions the user changed (spaces or + permission), and sends the whole result. It never sends an empty grant + (D1a). + +--- + +## Phase 1: dependency, toolchain and transport upgrade + +**Files:** `go.mod`, `go.sum`, `.github/workflows/{ci,release,codeql}.yml`, +`Makefile` / Dockerfile if they pin Go, `core/grpcserver/server.go`, `README.md`, +`SELF-HOSTED.md`, `CLAUDE.md` (Go version) + +- [ ] `go get github.com/anyproto/anytype-heart@v0.51.3 && go mod tidy` + (this raises the `go` line to `1.26.5`). +- [ ] Move CI, release and CodeQL to Go 1.26.5+. Check that the + `golangci-lint` version supports it. Update build requirements in + `CLAUDE.md` / `README.md` and the Dockerfile base image. +- [ ] Copy heart's `replace` directives (Go ignores them in dependencies): + - [ ] `github.com/libp2p/zeroconf/v2 => github.com/anyproto/zeroconf/v2 v2.2.1-0.20260709212715-528971bb5854` (changed in v0.51.x) + - [ ] Add the ones the CLI was already missing, and check each one: + `gogo/protobuf`, `dgraph-io/badger/v4`, `dgraph-io/ristretto`, + `multiformats/go-multiaddr`, `genproto/googleapis/rpc`, + `araddon/dateparse`, `dsoprea/go-jpeg-image-structure/v2` + (exact versions: heart `v0.51.3:go.mod`). +- [ ] **gRPC-Web transport:** bring over heart's `proxy.go` policy. Build + the `localorigin` policy from `ANYTYPE_GRPCWEB_ALLOWED_{ORIGINS,HOSTS}` + (allowing file:// and the webclipper extension, as heart does). Reject + untrusted requests before they reach any handler. Turn WebSockets off + unless `ANYTYPE_GRPCWEB_ENABLE_WEBSOCKETS=1`. Also add heart's + interceptor that copies the Origin header from gRPC metadata into the + request context. +- [ ] **Remote/proxy setups:** document `ANYTYPE_API_ALLOWED_HOSTS` / + `ANYTYPE_API_ALLOWED_ORIGINS` (and the gRPC-Web equivalents) in + `README.md` and `SELF-HOSTED.md`, including how to set them for + `anytype service install`. +- [ ] Verify with `make build`, `make test`, `make lint`, and cross-compile + for every release platform. +- [ ] Smoke tests: + - `/v1` and `/v2` respond on `localhost:31012`. + - Through a custom hostname or reverse proxy: rejected by default, + accepted once the allow-list is set. + - gRPC-Web: a disallowed Origin gets 403; a WebSocket upgrade gets 403 by + default. + +**Estimate:** 1–1.5 days (toolchain + transport + deployment docs; the +dependency bump itself is the small part). + +--- + +## Phase 2: switch keys to `JsonAPI`, safely + +**Files:** `core/apikey.go`, `cmd/auth/apikey/create/create.go` + +- [ ] Create keys with `Scope: model.AccountAuth_JsonAPI` and a grant. +- [ ] **Space and permission choices are required (D1):** + - Add `--space ` (repeatable) / `--all-spaces`, and + `--read-only` / `--read-write`. Exactly one from each pair must be + given. + - If either is missing, fail with usage (listing the user's spaces when + the space choice is missing). Nothing is sent to the server. + - Resolve spaces as described in Phase 3 → Rules. + - `Perm` is always set explicitly from the flag. + - These flags come in this PR, not Phase 3, because switching to `JsonAPI` + needs a grant and the CLI won't pick one for the user. +- [ ] **Server capability check before creating a key:** call + `AppGetVersion`. If the running server is older than v0.51.3, or + reports no version, stop with *"the running Anytype service is older + than this CLI; run `anytype service restart`"*. `anytype update` swaps + the binary without restarting the service, so this situation is + common. v0.50.4 would silently drop the grant and the expiry. + *Check first:* does the embedded heart report its real version through + `AppGetVersion` (ldflags)? If not, compare the CLI's own version, + exposed by the server, instead. +- [ ] **Read back after creating (D2):** `ListApps` → find the new key by + name + creation time (or by `appHash` if CreateApp returns it) → + compare scope, grant and expiry. On any difference: `RevokeApp` and + return an error. This protects against any server that ignores fields. +- [ ] Manual check: the new key gets 200 from `GET /v2/auth/whoami` and from + `GET /v1/spaces`. + +**Migration guidance (ships in this PR, README "Upgrading to API v2"):** + +1. `JsonAPI` keys **cannot call gRPC methods directly**. Integrations that + use a `Limited` key over gRPC (ObjectSearch, ListenSessionEvents, …) + should keep that key. +2. Moving a REST integration to `/v2`: create the new key → check the client + works → switch its credentials → `apikey revoke` the old one. +3. Keep **the same key name** when replacing a key. On `/v2`, deleting an + object requires the key's name to match the name recorded when the + object was created, so renaming loses the ability to delete what the old + key created. +4. Downgrading: keys created or updated by v0.51.3+ **don't work** on + v0.50.x. Keep the old keys until the upgrade is confirmed. + +**Estimate:** 1 day (includes resolving spaces). + +--- + +## Phase 3: expiry and output flags on `apikey create` + +**Files:** `cmd/auth/apikey/create/create.go`, `core/apikey.go`, new +`core/apikeygrant.go` (parsing flags, resolving spaces, describing +grants and compatibility) + +### Command + +``` +anytype auth apikey create + (--space ... | --all-spaces) # required, exactly one (Phase 2) + (--read-only | --read-write) # required, exactly one (Phase 2) + [--expires ] # e.g. 30d, 12h, 2026-12-31 + [--json] # {"id","name","key","scope","grant","expiresAt","compat":{"v1","v2"}} +``` + +### Rules + +- Exactly one of `--space` / `--all-spaces` and exactly one of + `--read-only` / `--read-write` are required; the flags in each pair can't + be combined (D1). A space picker in the terminal could come later; a missing + choice is never filled in automatically. +- There's no way to create a key without a grant (D1a). +- **Resolving spaces:** + - Resolve every argument once, before anything is sent, to a **full + space ID** using `core.ListSpaces`. + - An exact full ID takes priority over a name. Names must match exactly + one space; if a name matches several or none, fail and list the + candidates (name + ID). + - v2 short references are rejected. + - Duplicates are removed. The tech space is only reachable by its explicit + ID, with a warning. + - The resolved list (name + full ID) is what's displayed and what's sent. +- **Expiry:** Go durations + a `d` suffix, or `YYYY-MM-DD` (end of day, + local time), converted to unix **seconds**. Must be in the future. Heart + treats a key as expired once `now > expireAt`. +- **Name:** not empty, ≤ 128 bytes, never truncated. Warn if another key + already has the same name: on `/v2` both keys would be able to delete each + other's objects within their grants. + +### Output + +- The secret is printed **once**, on its own line. With `--json`, it goes + in the `key` field. +- Print the scope, a readable grant (spaces by name + short ID, read or + read-write), the expiry, and **which APIs the key works with** (`v1 ✅/❌`, + `v2 ✅`) using the table in Background. For example, + `--all-spaces --read-only` → `v1 ❌`. +- The docs show how to pipe or capture the key. Examples never put a real + key on the command line. + +**Estimate:** ~1 day including tests. + +--- + +## Phase 4: `apikey list` and `apikey update` + +### 4a. `apikey list` + +**Files:** `cmd/auth/apikey/list/list.go` + +- [ ] Columns: `NAME ID SCOPE ACCESS EXPIRES APIS SESSION CREATED` + - `ACCESS`: `all:rw`, `all:ro`, ` spaces:rw|ro`, `—` for `Limited`, + or `unrestricted` for existing no-grant `JsonAPI` keys created by other + clients (never shown as "full", which could be confused with `Full` + scope). + - `EXPIRES`: `never`, a date, or `expired`. + - `APIS`: which APIs the key works with, e.g. `v1,v2`, `v2`, + `v1,grpc` (`Limited`). + - `SESSION`: `active` if heart has a live session for the key + (`isActive`). This doesn't mean the key is valid. +- [ ] Drop the key-prefix column. +- [ ] `--json`: selected fields only (D3), and **no `appKey`**. Emit `[]` + when there are no keys, RFC 3339 timestamps, and the full grant with + space IDs. Diagnostics go to stderr. +- [ ] A footer hint for `Limited` keys: they don't work on `/v2`; link to the + migration section. +- [ ] A footer hint for no-grant `JsonAPI` keys: suggest narrowing them with + `apikey update --all-spaces` (or `--space …`). + +### 4b. `apikey update ` (new) + +**Files:** new `cmd/auth/apikey/update/update.go`, registered in +`cmd/auth/apikey/apikey.go`; `core/apikey.go` gets `UpdateAPIKeyGrant` + +``` +anytype auth apikey update + [--space ]... | [--all-spaces] # replace the spaces + [--add-space ]... [--remove-space ]... # edit the list + [--read-only | --read-write] + [--yes] +``` + +This is a separate command, not a flag on `create`: `create` makes a new +secret and requires every choice, while `update` keeps the same key and +changes only what you pass. + +**Finding the key:** the argument is matched first as an exact key ID +(`appHash`), then as an exact key name. If several keys have that name, +fail and list them (name, ID, created) so the user can pass the ID. + +**Space flags:** + +- `--space` / `--all-spaces` replace the whole set and can't be combined + with `--add-space` / `--remove-space`. +- `--add-space` / `--remove-space` edit the current list and can be used + together. Both resolve names like `--space` (Phase 3 → Rules). + - Adding a space the key already has, or removing one it doesn't have, + is a no-op for that space; the command says so. + - Removing the **last** space → error pointing to + `apikey revoke `, because heart rejects an empty grant. + - On an `allSpaces` key (or an existing no-grant key), there's no list to + edit. `--remove-space` fails and suggests `--space …`. `--add-space` of an ordinary space is a no-op (already + included); `--add-space ` fails and suggests `--space` with + an explicit list, since `allSpaces` never covers the tech space. + +How it works (D4): + +1. Find the key (above) via `ListApps`. If it isn't `JsonAPI`, refuse and + point to recreating it (heart rejects grants on `Limited` keys). +2. Work out the new grant from the current one: + - `--space` **replaces** the set of spaces; `--all-spaces` sets it to + all spaces; `--add-space` / `--remove-space` edit the current list. + With none of these, the current spaces are kept. + - `--read-only` / `--read-write` set the permission. With neither, the + current permission is kept. + - A key with no grant counts as "all spaces including the tech space, + read-write". So a permission flag alone → `allSpaces` + that + permission; `--space` alone → those spaces + `ReadWrite`. Every result + is a narrowing. + - The result always has a grant (D1a); no flag can remove it. + - No flags that change anything → error. If the new grant equals the + current one → "no change", exit 0. +3. Show **before → after** (spaces by name + ID, permission, which APIs the + key works with, including losing `/v1`). +4. If the change widens access (below), ask for confirmation. Without a + TTY, refuse unless `--yes` is given. +5. Just before sending, **read `ListApps` again**. If the grant changed + since step 1, stop and ask the user to retry. Heart has no conditional + update, so this only narrows the race window; it doesn't close it (see + the upstream ask below). +6. Send the whole grant, then read it back (D2). + +**When a change widens access.** It widens access if the new grant allows +any capability the old one didn't. Compare with space IDs deduplicated, and +treat `allSpaces` as "all current and future spaces except the tech space": + +- any space in the new set that isn't in the old one (`{A}→{B}` widens even + though the count is the same); +- a list of spaces → `allSpaces` (adds future spaces, even if the list had + every current space); +- `allSpaces` → a list containing the tech space; +- `Read` → `ReadWrite` on any space still granted (even if other spaces + are removed); +- anything that makes the key usable on `/v1` again. + +**Documented limitations:** + +- `update` can't change the scope, the expiry or the name; recreate the key + for those. +- A key can't be widened back to no grant. For `/v2` tech-space access, + list the tech space with `--space`. +- An open `/v2` chat stream keeps its old grant and isn't cut off at expiry + until it reconnects (heart only checks at the start of a request). + +**Upstream asks (heart), to file separately:** + +- `UpdateApp` with an "expected current grant" (compare-and-swap); +- cancel open streams when a key expires or its grant is edited; +- confirm that `AppGetVersion` reports the embedded heart version. + +**Estimate:** 1.5–2 days including tests. + +--- + +## Phase 5: shell mode (required) + +**Files:** `cmd/shell/shell.go` + +The new flags make existing shell-mode bugs matter: + +- [ ] Parse input with quotes (e.g. `github.com/google/shlex`), so + `--space "My Team"` works. +- [ ] Reset flag values between commands: create the command tree for every + line, or reset each flag and its `Changed` state. Otherwise `--yes` or + `--space` from one command carries over into the next. +- [ ] Confirmation prompts work in the shell (they read the same TTY). +- [ ] Make completion include the `apikey` subcommands and their flags + (it currently only goes two levels deep). + +**Estimate:** 0.5 day. Ships with PR 2/3, not as a follow-up. + +--- + +## Phase 6: optional follow-ups + +- [ ] **Pairing approval on a headless server.** The pairing challenge is + still how the **JSON API** authenticates, and on a headless server + nothing can approve it. Recommendation: **defer** until a concrete + integration can't accept a key created manually. If it's built, it + must: + - show the caller exactly as the event reports it (process path, origin) + and the permission it asked for; + - collect an explicit scope and grant, resolved as in Phase 3; + - require explicit approval; + - show the returned 4-digit code **only** to the person approving, + never solving it automatically or writing it to logs. + + Keys paired this way can't have an expiry. + +--- + +## Testing + +The repo's tests use the standard `testing` package, so follow that style. +Table-driven cases: + +- **Parsing `--expires`:** durations, `d` suffix, dates, past/now/negative + values, garbage. +- **Building a grant from flags:** every valid and invalid combination, + that `Perm` is always set explicitly, and that a missing space choice or + a missing permission choice is an error that sends nothing to the + server. +- **Never sending an empty grant (D1a):** every `create` and `update` path + sends a grant that isn't nil, including `update` on an existing no-grant + key. +- **Resolving spaces:** exact ID, name, ambiguous, unknown, duplicates, the + tech space, short references. +- **Merging for `update`:** keeping the current value when a flag is + omitted, replacing the set, permission only on a key with no grant, no + change, conflicting flags. +- **`--add-space` / `--remove-space`:** adding to and removing from a list, + both in one command, a no-op add or remove, removing the last space + (error), `--remove-space` on an `allSpaces` or no-grant key (error), adding + the tech space to an `allSpaces` key (error), combining with `--space` or + `--all-spaces` (error). +- **Finding the key for `update`:** exact ID, exact name, a name shared by + several keys (error listing them), unknown. +- **Detecting widened access** — every case in Phase 4b, plus the matching + narrowing cases, including `all:ro → [tech]:ro` and `[A,B]:ro → A:rw`. +- **Which APIs a key works with:** no grant, all/rw, all/ro, a list of + spaces, `Limited`, expired. +- **Output:** neither the table nor `--json` from `list` contains any key. + `create --json` contains it exactly once. +- **Read-back (D2):** a fake server that drops the grant or expiry → the key + is revoked and the command fails. +- **Shell:** consecutive commands don't carry flag values over; quoted + arguments work. + +Manual end-to-end against `anytype serve`: + +1. `--space A --read-only`: `/v2` read in A → 200; write in A → 403; space + B → 403; `/v1` → refused. +2. `--all-spaces --read-only`: `/v2` read → 200; `/v1` → refused. +3. `--all-spaces --read-write`: `/v1` + `/v2` → 200; `/v2` tech space → + 403. No space flag, or no permission flag → error, no key created (with + and without a TTY). +4. After a first request (so heart caches the session), `update` narrows + RW→RO and A+B→A → the next request is denied, including losing `/v1`. +5. `update` that widens access → asks for confirmation; cancelling changes + nothing; without a TTY and without `--yes` → refused, nothing changes. +6. `--expires 1m` → 401 "expired" after a minute, including for a key + whose session is already cached. The key and its grant survive a service + restart. +7. An old `Limited` key: `/v1` 200, `/v2` 403, gRPC methods on the allowlist + still work; `update` on it → refused, pointing to recreating it. +8. A new CLI talking to a **v0.50.4** service that is still running → + `create --space X` fails and **no key is created**. +9. Downgrade: an existing old key still works on v0.50.4; the release notes + say keys created by the new CLI don't. + +## Rollout + +1. **PR 1:** Phases 1 + 2. The toolchain, dependency bump, transport + protection, `JsonAPI` scope with required space and permission choices, + the version check, + read-back after create, and the migration and deployment docs. +2. **PR 2:** Phase 3 + 4a + the shell parsing/flag-reset parts of Phase 5. +3. **PR 3:** Phase 4b + the rest of Phase 5. +4. The upstream heart asks and Phase 6 as separate issues. + +Release notes for PR 1: + +- the Go toolchain requirement; +- Host/Origin allow-lists for remote setups; +- gRPC-Web WebSockets are now off by default; +- new keys are `JsonAPI` (no gRPC access); +- **breaking:** `apikey create` needs `--space …` or `--all-spaces`, and + `--read-only` or `--read-write`; +- the downgrade caveat; +- `anytype service restart` after updating. + +**Total estimate:** ~6–7 days. + +## Open questions + +1. Should `apikey create` also offer `--scope limited`, for integrations that + still need gRPC access? The recommendation is no unless someone asks for + it; point them to existing keys. +2. Does the embedded server report heart's version through + `AppGetVersion`? This determines how the Phase 2 version check is built. +3. Headless pairing approval (Phase 6): is it needed by Raycast, MCP or + web-clipper users on servers? diff --git a/go.mod b/go.mod index c18b69d..b08dbc6 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/anyproto/anytype-cli go 1.26.5 require ( - github.com/anyproto/any-sync v0.12.16 - github.com/anyproto/anytype-heart v0.50.20 + github.com/anyproto/any-sync v0.13.5 + github.com/anyproto/anytype-heart v0.51.4 github.com/cheggaaa/mb/v3 v3.0.3 github.com/chzyer/readline v1.5.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 @@ -12,23 +12,23 @@ require ( github.com/improbable-eng/grpc-web v0.15.0 github.com/kardianos/service v1.2.4 github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 github.com/zalando/go-keyring v0.2.8 - golang.org/x/mod v0.38.0 - google.golang.org/grpc v1.81.1 + golang.org/x/mod v0.41.0 + google.golang.org/grpc v1.83.1 gopkg.in/yaml.v3 v3.0.1 ) require ( filippo.io/edwards25519 v1.2.0 // indirect github.com/JohannesKaufmann/html-to-markdown v1.6.0 // indirect - github.com/KyleBanks/depth v1.2.1 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4 // indirect github.com/PuerkitoBio/goquery v1.12.0 // indirect - github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect - github.com/anyproto/any-store v0.4.7 // indirect - github.com/anyproto/anytype-publish-server/publishclient v0.0.0-20260407134332-83cc769fbbb8 // indirect + github.com/anyproto/any-block v0.0.0-20260919082013-9906a7c36b68 // indirect + github.com/anyproto/any-store v1.0.2 // indirect + github.com/anyproto/anytype-publish-server/publishclient v0.0.0-20260925080338-0fb960f00458 // indirect github.com/anyproto/anytype-push-server/pushclient v0.0.0-20260407093150-0b14187dfd27 // indirect github.com/anyproto/go-bip39 v1.0.0 // indirect github.com/anyproto/go-chash v0.1.0 // indirect @@ -53,12 +53,12 @@ require ( github.com/cloudwego/base64x v0.1.6 // indirect github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf // indirect github.com/danieljoos/wincred v1.2.3 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect github.com/desertbit/timer v1.0.1 // indirect github.com/dgraph-io/badger/v4 v4.9.1 // indirect - github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 // indirect github.com/didip/tollbooth/v8 v8.0.1 // indirect github.com/disintegration/imaging v1.6.2 // indirect @@ -68,13 +68,13 @@ require ( github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd // indirect github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect - github.com/dunglas/httpsfv v1.1.0 // indirect + github.com/dunglas/httpsfv v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/ethereum/go-ethereum v1.17.2 // indirect github.com/flopp/go-findfont v0.1.0 // indirect github.com/fogleman/gg v1.3.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gabriel-vasile/mimetype v1.4.15 // indirect github.com/gammazero/chanqueue v1.1.2 // indirect github.com/gammazero/deque v1.2.1 // indirect github.com/gin-contrib/sse v1.1.1 // indirect @@ -82,19 +82,9 @@ require ( github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect github.com/go-errors/errors v1.5.1 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-openapi/jsonpointer v0.23.1 // indirect - github.com/go-openapi/jsonreference v0.21.5 // indirect - github.com/go-openapi/spec v0.22.4 // indirect - github.com/go-openapi/swag/conv v0.26.0 // indirect - github.com/go-openapi/swag/jsonname v0.26.0 // indirect - github.com/go-openapi/swag/jsonutils v0.26.0 // indirect - github.com/go-openapi/swag/loading v0.26.0 // indirect - github.com/go-openapi/swag/stringutils v0.26.0 // indirect - github.com/go-openapi/swag/typeutils v0.26.0 // indirect - github.com/go-openapi/swag/yamlutils v0.26.0 // indirect github.com/go-pkgz/expirable-cache/v3 v3.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -115,6 +105,8 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/geo v0.0.0-20260415063119-550b242b3150 // indirect + github.com/golang/glog v1.2.5 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/flatbuffers v25.12.19+incompatible // indirect @@ -135,12 +127,12 @@ require ( github.com/iancoleman/strcase v0.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/ipfs/bbloom v0.1.0 // indirect - github.com/ipfs/boxo v0.41.0 // indirect + github.com/ipfs/boxo v0.43.0 // indirect github.com/ipfs/go-bitfield v1.1.0 // indirect github.com/ipfs/go-block-format v0.2.4 // indirect github.com/ipfs/go-cid v0.6.2 // indirect - github.com/ipfs/go-cidutil v0.1.1 // indirect - github.com/ipfs/go-datastore v0.9.1 // indirect + github.com/ipfs/go-cidutil v0.1.2 // indirect + github.com/ipfs/go-datastore v0.9.2 // indirect github.com/ipfs/go-ds-flatfs v0.6.0 // indirect github.com/ipfs/go-dsqueue v0.2.0 // indirect github.com/ipfs/go-ipld-format v0.6.4 // indirect @@ -155,12 +147,12 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect - github.com/klauspost/compress v1.18.5 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/klauspost/compress v1.19.1 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect github.com/kovidgoyal/imaging v1.6.4 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect - github.com/libp2p/go-libp2p v0.48.0 // indirect + github.com/libp2p/go-libp2p v0.49.0 // indirect github.com/libp2p/zeroconf/v2 v2.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/matishsiao/goInfo v0.0.0-20241216093258-66a9250504d6 // indirect @@ -168,7 +160,7 @@ require ( github.com/mattn/go-sqlite3 v1.14.42 // indirect github.com/mb0/diff v0.0.0-20131118162322-d8d9a906c24d // indirect github.com/microcosm-cc/bluemonday v1.0.27 // indirect - github.com/miekg/dns v1.1.72 // indirect + github.com/miekg/dns v1.1.73 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/miolini/datacounter v1.0.3 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -193,30 +185,28 @@ require ( github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20250313105119-ba97887b0a25 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/polydawn/refmt v0.90.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.20.1 // indirect + github.com/prometheus/client_golang v1.24.1 // indirect + github.com/prometheus/client_model v0.6.3 // indirect + github.com/prometheus/common v0.70.1 // indirect + github.com/prometheus/procfs v0.21.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.60.0 // indirect - github.com/quic-go/webtransport-go v0.11.1 // indirect + github.com/quic-go/quic-go v0.62.0 // indirect + github.com/quic-go/webtransport-go v0.13.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rs/cors v1.11.1 // indirect github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect github.com/samber/lo v1.53.0 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sasha-s/go-deadlock v0.3.9 // indirect + github.com/sashabaranov/go-openai v1.41.2 // indirect github.com/shirou/gopsutil/v4 v4.26.3 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/stretchr/objx v0.5.3 // indirect - github.com/stretchr/testify v1.11.1 // indirect - github.com/sv-tools/openapi v0.4.0 // indirect - github.com/swaggo/swag/v2 v2.0.0-rc5 // indirect + github.com/stretchr/testify v1.12.1 // indirect github.com/tetratelabs/wazero v1.11.0 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect @@ -233,28 +223,28 @@ require ( github.com/zeebo/errs v1.4.0 // indirect go.abhg.dev/goldmark/wikilink v0.6.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect + go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.44.0 // indirect - go.opentelemetry.io/otel/metric v1.44.0 // indirect - go.opentelemetry.io/otel/trace v1.44.0 // indirect - go.uber.org/atomic v1.11.0 // indirect + go.opentelemetry.io/otel v1.46.0 // indirect + go.opentelemetry.io/otel/metric v1.46.0 // indirect + go.opentelemetry.io/otel/trace v1.46.0 // indirect + go.uber.org/atomic v1.12.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect - go.yaml.in/yaml/v2 v2.4.4 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/arch v0.26.0 // indirect - golang.org/x/crypto v0.54.0 // indirect - golang.org/x/exp v0.0.0-20260603202125-055de637280b // indirect + golang.org/x/crypto v0.57.0 // indirect + golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect golang.org/x/image v0.38.0 // indirect - golang.org/x/net v0.57.0 // indirect - golang.org/x/oauth2 v0.36.0 // indirect - golang.org/x/sync v0.22.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.40.0 // indirect - golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.48.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect - google.golang.org/protobuf v1.36.11 // indirect + golang.org/x/net v0.59.0 // indirect + golang.org/x/oauth2 v0.37.0 // indirect + golang.org/x/sync v0.23.0 // indirect + golang.org/x/sys v0.48.0 // indirect + golang.org/x/text v0.42.0 // indirect + golang.org/x/time v0.16.0 // indirect + golang.org/x/tools v0.50.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260819154853-08b0e4226688 // indirect + google.golang.org/protobuf v1.36.12 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect lukechampine.com/blake3 v1.4.1 // indirect @@ -269,10 +259,17 @@ require ( replace ( github.com/JohannesKaufmann/html-to-markdown => github.com/anyproto/html-to-markdown v0.0.0-20231025221133-830bf0a6f139 + github.com/araddon/dateparse => github.com/mehanizm/dateparse v0.0.0-20210806203422-f82c8742c9f8 github.com/btcsuite/btcd => github.com/btcsuite/btcd v0.22.1 github.com/btcsuite/btcutil => github.com/btcsuite/btcd/btcutil v1.1.5 + github.com/dgraph-io/badger/v4 => github.com/anyproto/badger/v4 v4.2.1-0.20240110160636-80743fa3d580 + github.com/dgraph-io/ristretto => github.com/anyproto/ristretto v0.1.2-0.20240221153107-2b23839cc50c + github.com/dsoprea/go-jpeg-image-structure/v2 => github.com/dchesterton/go-jpeg-image-structure/v2 v2.0.0-20240318203529-c3eea088bd38 + github.com/gogo/protobuf => github.com/anyproto/protobuf v1.3.3-0.20240201225420-6e325cf0ac38 github.com/ipfs/go-ds-flatfs => github.com/anyproto/go-ds-flatfs v0.0.0-20250828183910-d49f5b2d567f github.com/ipfs/go-log/v2 => github.com/anyproto/go-log/v2 v2.1.2-0.20220721095711-bcf09ff293b2 - github.com/libp2p/zeroconf/v2 => github.com/anyproto/zeroconf/v2 v2.2.1-0.20240228113933-f90a5cc4439d + github.com/libp2p/zeroconf/v2 => github.com/anyproto/zeroconf/v2 v2.2.1-0.20260709212715-528971bb5854 + github.com/multiformats/go-multiaddr => github.com/anyproto/go-multiaddr v0.8.1-0.20250307125826-51ba58e2ebc7 + google.golang.org/genproto/googleapis/rpc => google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 gopkg.in/Graylog2/go-gelf.v2 => github.com/anyproto/go-gelf v0.0.0-20210418191311-774bd5b016e7 ) diff --git a/go.sum b/go.sum index 4cbeeac..4b803f9 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,6 @@ filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260416073033-7c2071eaa8d4 h1:/97whAzwYxMNHXeTfhAtCRzNCpyblmxCtSYpsfzCszM= @@ -24,23 +22,25 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= -github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5 h1:iW0a5ljuFxkLGPNem5Ui+KBjFJzKg4Fv2fnxe4dvzpM= github.com/alexbrainman/goissue34681 v0.0.0-20191006012335-3fc7a47baff5/go.mod h1:Y2QMoi1vgtOIfc+6DhrMOGkLoGzqSV2rKp4Sm+opsyA= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= -github.com/anyproto/any-store v0.4.7 h1:329NWY/xUzGdwKSqFgjAFaPAVkTJbR+WdJirPrvTm/w= -github.com/anyproto/any-store v0.4.7/go.mod h1:8cqb52gjZSaYnlybugqpSqSG1RQygY96D2vWMbSJsLo= -github.com/anyproto/any-sync v0.12.16 h1:oFnRLGSIgIoBA8PNubRmpbzFd6fLdKtBUdTFDqKqVzE= -github.com/anyproto/any-sync v0.12.16/go.mod h1:8J7WU2aJTzWeCMdoNXmZUnpepb0wm+7o+Si7Xjc4Ygs= -github.com/anyproto/anytype-heart v0.50.20 h1:PxkYcj27Hc1X5EOEc9CRo8+fvwQffEbAdLwmJrpbjh8= -github.com/anyproto/anytype-heart v0.50.20/go.mod h1:+iOwZpqwwtk34b6FotLgQKYw1HvhPjzv32c01gIu39M= -github.com/anyproto/anytype-publish-server/publishclient v0.0.0-20260407134332-83cc769fbbb8 h1:Oaz1B7AUr/4U/kVDBafZ7sP8VCWSYQ51fo6G2T8smH4= -github.com/anyproto/anytype-publish-server/publishclient v0.0.0-20260407134332-83cc769fbbb8/go.mod h1:lRppnNvn5vAw/ASBOYV6LTYPqEg+XC34AzloxK1E/Ac= +github.com/anyproto/any-block v0.0.0-20260919082013-9906a7c36b68 h1:7EAL+fDiqZs/1R52STcE1tFcnGGwDYC7KeMk+4q/VKQ= +github.com/anyproto/any-block v0.0.0-20260919082013-9906a7c36b68/go.mod h1:ctWUBba2Nm+mHwkh4bBej57avQHaW2HDzDZDSlbVchs= +github.com/anyproto/any-store v1.0.2 h1:vQvgKKDDmM0j9EheBWGq8bMVU7Oi3061DKJFG8D3sTQ= +github.com/anyproto/any-store v1.0.2/go.mod h1:8cqb52gjZSaYnlybugqpSqSG1RQygY96D2vWMbSJsLo= +github.com/anyproto/any-sync v0.13.5 h1:wWDEcdJReNdZh1yuVcUgWA/VsPom7uP59KM25SdqSCU= +github.com/anyproto/any-sync v0.13.5/go.mod h1:c6f4LXwoji9JkJYt14dXuVbPzYBRa3Z85+sBQ4HX5+4= +github.com/anyproto/anytype-heart v0.51.4 h1:pd+oIWZaRKU9e6+NUR3B+td/tSrRKJVXwSMgJlLWY9o= +github.com/anyproto/anytype-heart v0.51.4/go.mod h1:L/IXpU2eOy79+b4rrrVvvvrqJVNQCCOaNLnyb/Sljt4= +github.com/anyproto/anytype-publish-server/publishclient v0.0.0-20260925080338-0fb960f00458 h1:kg/BHaimoLI2suFe4Kq1MfNxFvWYlFfPPkgVehU9+FE= +github.com/anyproto/anytype-publish-server/publishclient v0.0.0-20260925080338-0fb960f00458/go.mod h1:lRppnNvn5vAw/ASBOYV6LTYPqEg+XC34AzloxK1E/Ac= github.com/anyproto/anytype-push-server/pushclient v0.0.0-20260407093150-0b14187dfd27 h1:O/n87PsZozTtfIbKOad8lS2safL/A1CuzFNrLbin18M= github.com/anyproto/anytype-push-server/pushclient v0.0.0-20260407093150-0b14187dfd27/go.mod h1:1m8LlK1kjKKP3qt/HfOOJLlpKndQoOP4kSErjhXljsM= +github.com/anyproto/badger/v4 v4.2.1-0.20240110160636-80743fa3d580 h1:Ba80IlCCxkZ9H1GF+7vFu/TSpPvbpDCxXJ5ogc4euYc= +github.com/anyproto/badger/v4 v4.2.1-0.20240110160636-80743fa3d580/go.mod h1:T/uWAYxrXdaXw64ihI++9RMbKTCpKd/yE9+saARew7k= github.com/anyproto/go-bip39 v1.0.0 h1:T6/7WowKYDeyuX/QyXtt98ZX0XXaoOh17M/LFF2M5yk= github.com/anyproto/go-bip39 v1.0.0/go.mod h1:l0rcxmXRyiWAYzE1noMAc4qbeNrbhUwxM3rqSO9ILwo= github.com/anyproto/go-chash v0.1.0 h1:I9meTPjXFRfXZHRJzjOHC/XF7Q5vzysKkiT/grsogXY= @@ -49,6 +49,8 @@ github.com/anyproto/go-ds-flatfs v0.0.0-20250828183910-d49f5b2d567f h1:OBqi9bWQi github.com/anyproto/go-ds-flatfs v0.0.0-20250828183910-d49f5b2d567f/go.mod h1:TtwE6xpVzRMpWDipS1FHEJYxB7YjGNEFZxUXOhTSGGk= github.com/anyproto/go-log/v2 v2.1.2-0.20220721095711-bcf09ff293b2 h1:X8xiwPlNiSQs1HKguhZyHYs4XFQLWsj566bFsRjN7hM= github.com/anyproto/go-log/v2 v2.1.2-0.20220721095711-bcf09ff293b2/go.mod h1:TMD+iYDL/QBjspKUN0Ypxpr2IMAz3uGUAsbCeClDQ+4= +github.com/anyproto/go-multiaddr v0.8.1-0.20250307125826-51ba58e2ebc7 h1:SD0mX7Ds438ZP6J1g7qpXN4naRi/Naa0umTvlTba76c= +github.com/anyproto/go-multiaddr v0.8.1-0.20250307125826-51ba58e2ebc7/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= github.com/anyproto/go-naturaldate/v2 v2.0.2-0.20230524105841-9829cfd13438 h1:flfZXdcXB2iVHrTZDwSMlJ7RCRS/ydiPw63xWr+waSU= github.com/anyproto/go-naturaldate/v2 v2.0.2-0.20230524105841-9829cfd13438/go.mod h1:cmdcU4pcVTftMlM89z+d8dLBJc02HtY5tI12yQucUxM= github.com/anyproto/go-slip10 v1.0.1 h1:Pa/OpYoOE668fip4ygAd4T07chLBx4XoBa5fwnGq0/M= @@ -63,14 +65,16 @@ github.com/anyproto/html-to-markdown v0.0.0-20231025221133-830bf0a6f139 h1:Wp9z0 github.com/anyproto/html-to-markdown v0.0.0-20231025221133-830bf0a6f139/go.mod h1:1zaDDQVWTRwNksmTUTkcVXqgNF28YHiEUIm8FL9Z+II= github.com/anyproto/lexid v0.0.6 h1:lTJd9K11vU1xoBs1dkZVv8VtYfCBo373lNd9kAgXEio= github.com/anyproto/lexid v0.0.6/go.mod h1:2RfpYiZkgoNmSDklXdwCCwGlso1FIp9Te8ZtoF3/Ehg= +github.com/anyproto/protobuf v1.3.3-0.20240201225420-6e325cf0ac38 h1:80jke82/c+bNQQpnx4VO3Mi/lAxARyyfUpZvFaPxdzE= +github.com/anyproto/protobuf v1.3.3-0.20240201225420-6e325cf0ac38/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/anyproto/ristretto v0.1.2-0.20240221153107-2b23839cc50c h1:GicoaTUyB2mtCIl3YMrO0OzysqRT5GA4vuvDsqEkhSM= +github.com/anyproto/ristretto v0.1.2-0.20240221153107-2b23839cc50c/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/anyproto/tantivy-go v1.0.6 h1:8FI8otTeq5OFXK7Fw7vwG2vrwW1R1Yvcbiy4rO4YfoQ= github.com/anyproto/tantivy-go v1.0.6/go.mod h1:LtipOpRjGtcYMGcop6gQN7rVl1Pc6BlIs9BTMqeWMsk= -github.com/anyproto/zeroconf/v2 v2.2.1-0.20240228113933-f90a5cc4439d h1:5bj7nX/AS8sxGpTIrapE7PC4oPlhkHMwMqXlJbUHBlg= -github.com/anyproto/zeroconf/v2 v2.2.1-0.20240228113933-f90a5cc4439d/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= +github.com/anyproto/zeroconf/v2 v2.2.1-0.20260709212715-528971bb5854 h1:9Hwnz/eHgLqchu42c0EjSRMgzabVJnbX4QOxGIs3frw= +github.com/anyproto/zeroconf/v2 v2.2.1-0.20260709212715-528971bb5854/go.mod h1:2KV4b7s+6Jfu0HM826dXIAh2Ln5EXcnpt/cW1TWFWBo= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= -github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -144,6 +148,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= +github.com/dchesterton/go-jpeg-image-structure/v2 v2.0.0-20240318203529-c3eea088bd38 h1:GDvo0S+xL3iMJYofhBVQVM6EuAcTCoEV1096iN1pedI= +github.com/dchesterton/go-jpeg-image-structure/v2 v2.0.0-20240318203529-c3eea088bd38/go.mod h1:WaARaUjQuSuDCDFAiU/GwzfxMTJBulfEhqEA2Tx6B4Y= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= @@ -151,11 +157,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjY github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= github.com/desertbit/timer v1.0.1 h1:yRpYNn5Vaaj6QXecdLMPMJsW81JLiI1eokUft5nBmeo= github.com/desertbit/timer v1.0.1/go.mod h1:htRrYeY5V/t4iu1xCJ5XsQvp4xve8QulXXctAzxqcwE= -github.com/dgraph-io/badger/v4 v4.9.1 h1:DocZXZkg5JJHJPtUErA0ibyHxOVUDVoXLSCV6t8NC8w= -github.com/dgraph-io/badger/v4 v4.9.1/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0= -github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU= -github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg= @@ -164,6 +167,8 @@ github.com/didip/tollbooth/v8 v8.0.1 h1:VAAapTo1t4Bn6bbpcHjuovwoa9u3JH++wgjbpWv+ github.com/didip/tollbooth/v8 v8.0.1/go.mod h1:oEd9l+ep373d7DmvKLc0a5gasPOev2mTewi6KPQBGJ4= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dsoprea/go-exif/v2 v2.0.0-20200321225314-640175a69fe4/go.mod h1:Lm2lMM2zx8p4a34ZemkaUV95AnMl4ZvLbCUbwOvLC2E= github.com/dsoprea/go-exif/v3 v3.0.0-20200717053412-08f1b6708903/go.mod h1:0nsO1ce0mh5czxGeLo4+OCZ/C6Eo6ZlMWsz7rH/Gxv8= github.com/dsoprea/go-exif/v3 v3.0.0-20210428042052-dca55bf8ca15/go.mod h1:cg5SNYKHMmzxsr9X6ZeLh/nfBRHHp5PngtEPcujONtk= @@ -175,8 +180,6 @@ github.com/dsoprea/go-exif/v3 v3.0.1/go.mod h1:10HkA1Wz3h398cDP66L+Is9kKDmlqlIJG github.com/dsoprea/go-iptc v0.0.0-20200609062250-162ae6b44feb/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413 h1:YDRiMEm32T60Kpm35YzOK9ZHgjsS1Qrid+XskNcsdp8= github.com/dsoprea/go-iptc v0.0.0-20200610044640-bc9ca208b413/go.mod h1:kYIdx9N9NaOyD7U6D+YtExN7QhRm+5kq7//yOsRXQtM= -github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20221012074422-4f3f7e934102 h1:gmTXQdSuuuORRFPTS2uaYpAXU5oUNkXdeYSlZe5NvsE= -github.com/dsoprea/go-jpeg-image-structure/v2 v2.0.0-20221012074422-4f3f7e934102/go.mod h1:WaARaUjQuSuDCDFAiU/GwzfxMTJBulfEhqEA2Tx6B4Y= github.com/dsoprea/go-logging v0.0.0-20190624164917-c4f10aab7696/go.mod h1:Nm/x2ZUNRW6Fe5C3LxdY1PyZY5wmDv/s5dkPJ/VB3iA= github.com/dsoprea/go-logging v0.0.0-20200517223158-a10564966e9d/go.mod h1:7I+3Pe2o/YSU88W0hWlm9S22W7XI1JFNJ86U0zPKMf8= github.com/dsoprea/go-logging v0.0.0-20200710184922-b02d349568dd h1:l+vLbuxptsC6VQyQsfD7NnEC8BZuFpz45PgY+pH8YTg= @@ -190,9 +193,10 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20221003142440-7a1927d49d9d/go.mod h1:LV github.com/dsoprea/go-utility/v2 v2.0.0-20221003160719-7bc88537c05e/go.mod h1:VZ7cB0pTjm1ADBWhJUOHESu4ZYy9JN+ZPqjfiW09EPU= github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 h1:DilThiXje0z+3UQ5YjYiSRRzVdtamFpvBQXKwMglWqw= github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349/go.mod h1:4GC5sXji84i/p+irqghpPFZBF8tRN/Q7+700G0/DLe8= -github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= -github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= +github.com/dunglas/httpsfv v1.1.1 h1:HoSs101zIE9I23DlqlmljJ/OIi7ILwrH347pXhRZdxI= +github.com/dunglas/httpsfv v1.1.1/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= @@ -220,8 +224,8 @@ github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= -github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI= +github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ= github.com/gammazero/chanqueue v1.1.2 h1:dZEsxlyANZMyeTRemABqZF8QM9BnE4NBI43Oh3y5fIU= github.com/gammazero/chanqueue v1.1.2/go.mod h1:XDN1X/jjAbmSceNFOQbtKToeSkxtdVdpKu90LiEdBEE= github.com/gammazero/deque v1.2.1 h1:9fnQVFCCZ9/NOc7ccTNqzoKd1tCWOqeI05/lPqFPMGQ= @@ -252,40 +256,13 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= -github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= -github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= -github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= -github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= -github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= -github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= -github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= -github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= -github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= -github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= -github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= -github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= -github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= -github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= -github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= -github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= -github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= -github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE= -github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= -github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= github.com/go-pkgz/expirable-cache/v3 v3.1.0 h1:s05P851/O6QJ6Mc+7o2bh9aGtD3romB1SxDTXifdoqc= github.com/go-pkgz/expirable-cache/v3 v3.1.0/go.mod h1:6pVgNleydKPj0J2/mzrI02/RDo4ivKx5v2XlNmIjhjo= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= @@ -328,11 +305,6 @@ github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q8 github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0= github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/status v1.1.1 h1:DuHXlSFHNKqTQ+/ACf5Vs6r4X/dH2EgIzR9Vr+H65kg= github.com/gogo/status v1.1.1/go.mod h1:jpG3dM5QPcqu19Hg8lkUhBFBa3TcLs1DG7+2Jqci7oU= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= @@ -347,8 +319,13 @@ github.com/golang/geo v0.0.0-20210211234256-740aa86cb551/go.mod h1:QZ0nwyI2jOfgR github.com/golang/geo v0.0.0-20260415063119-550b242b3150 h1:F5CQANtNulclG+xJt370ohUujAEHq2XNBBAjkoSukQI= github.com/golang/geo v0.0.0-20260415063119-550b242b3150/go.mod h1:Mymr9kRGDc64JPr03TSZmuIBODZ3KyswLzm1xL0HFA8= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -377,6 +354,7 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -385,6 +363,7 @@ github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= @@ -430,6 +409,8 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -460,18 +441,18 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/ipfs/bbloom v0.1.0 h1:nIWwfIE3AaG7RCDQIsrUonGCOTp7qSXzxH7ab/ss964= github.com/ipfs/bbloom v0.1.0/go.mod h1:lDy3A3i6ndgEW2z1CaRFvDi5/ZTzgM1IxA/pkL7Wgts= -github.com/ipfs/boxo v0.41.0 h1:diKlFosOG2e1mgSO1CXqcMSnHvtn6ubUvaCf9iF8AIY= -github.com/ipfs/boxo v0.41.0/go.mod h1:1Fo36UVVvq3XAZwMDD82Cm4JTUi5x1k3AsJlg9DttOY= +github.com/ipfs/boxo v0.43.0 h1:nhIbfBBxgLoAxUi8upOIO5RATy++2WigMVWniRJFjnw= +github.com/ipfs/boxo v0.43.0/go.mod h1:n9HNLPLfAGexg2DGoQ5B4BMVlmj9g4+5dIcuo7w95iI= github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= github.com/ipfs/go-block-format v0.2.4 h1:pgsT9i8zB4YQkBIQRrBwqbQiXPogRCiQnfxd2bC4koI= github.com/ipfs/go-block-format v0.2.4/go.mod h1:YpXrOge8ARskfuJuqjvJTYr4v6o9IZWgK2EEIMAhpcU= github.com/ipfs/go-cid v0.6.2 h1:VuGwJd+KJTaMJ4S4d5EEf9SXc17YUblS5axCbocn9YE= github.com/ipfs/go-cid v0.6.2/go.mod h1:Xhwg8NzHeK9xPCEZkCw4idzPiuNMpX3fARuI5Iwj1Lo= -github.com/ipfs/go-cidutil v0.1.1 h1:COuby6H8C2ml0alvHYX3WdbFM4F07YtbY0UlT5j+sgI= -github.com/ipfs/go-cidutil v0.1.1/go.mod h1:SCoUftGEUgoXe5Hjeyw5CiLZF8cwYn/TbtpFQXJCP6k= -github.com/ipfs/go-datastore v0.9.1 h1:67Po2epre/o0UxrmkzdS9ZTe2GFGODgTd2odx8Wh6Yo= -github.com/ipfs/go-datastore v0.9.1/go.mod h1:zi07Nvrpq1bQwSkEnx3bfjz+SQZbdbWyCNvyxMh9pN0= +github.com/ipfs/go-cidutil v0.1.2 h1:Sbktesx5tEFtyj4iqFe3asn35WR5Wokt8nHp6Eq/5HM= +github.com/ipfs/go-cidutil v0.1.2/go.mod h1:IGpYxfTJb75D1/ah7XTBEndyKw+NVMoAHgIUQrj8qbg= +github.com/ipfs/go-datastore v0.9.2 h1:HJOgAmvWPRMHiwD8JHBzGZQNTKhuFGYfp8bNPwye28g= +github.com/ipfs/go-datastore v0.9.2/go.mod h1:VIjDxnINIcCqBMaB8LGggHfYY7PalKWfPtRMFeOU4q4= github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= github.com/ipfs/go-ds-leveldb v0.5.2 h1:6nmxlQ2zbp4LCNdJVsmHfs9GP0eylfBNxpmY1csp0x0= @@ -492,10 +473,10 @@ github.com/ipfs/go-metrics-interface v0.3.0 h1:YwG7/Cy4R94mYDUuwsBfeziJCVm9pBMJ6 github.com/ipfs/go-metrics-interface v0.3.0/go.mod h1:OxxQjZDGocXVdyTPocns6cOLwHieqej/jos7H4POwoY= github.com/ipfs/go-peertaskqueue v0.8.3 h1:tBPpGJy+A92RqtRFq5amJn0Uuj8Pw8tXi0X3eHfHM8w= github.com/ipfs/go-peertaskqueue v0.8.3/go.mod h1:OqVync4kPOcXEGdj/LKvox9DCB5mkSBeXsPczCxLtYA= -github.com/ipfs/go-test v0.3.0 h1:0Y4Uve3tp9HI+2lIJjfOliOrOgv/YpXg/l1y3P4DEYE= -github.com/ipfs/go-test v0.3.0/go.mod h1:JK+U8pRpATZb7lsYNSJlCj3WYB3cFfWIbI6nWRM/GFk= -github.com/ipfs/go-unixfsnode v1.10.4 h1:cMmMyOrSjQkPVQbQvt8trErIn6jhayNf9pBA9oOwfxY= -github.com/ipfs/go-unixfsnode v1.10.4/go.mod h1:Vu1e/s7ToALBBRo38sJ8DwUVWmSeQMTdxk5/rcHl7d0= +github.com/ipfs/go-test v0.4.1 h1:n6uNSakIgpTQIRorqNg2O02aMIFDLQk2z4rBfrlD3Uw= +github.com/ipfs/go-test v0.4.1/go.mod h1:QmvVBf9kClNtRuFow4DASq03eFvjKla4Fy/UAkeeLO8= +github.com/ipfs/go-unixfsnode v1.10.6 h1:0rnKD4azJad4cT8t6wITqNl9Q0GTjB+YRBfMQ39C7Sw= +github.com/ipfs/go-unixfsnode v1.10.6/go.mod h1:u/9Ukl+XYpfKTMu+NXQqxbzAJVTwSCoyTYBGgE+JdSE= github.com/ipld/go-codec-dagpb v1.7.0 h1:hpuvQjCSVSLnTnHXn+QAMR0mLmb1gA6wl10LExo2Ts0= github.com/ipld/go-codec-dagpb v1.7.0/go.mod h1:rD3Zg+zub9ZnxcLwfol/OTQRVjaLzXypgy4UqHQvilM= github.com/ipld/go-ipld-prime v0.24.0 h1:6th8Z6Peh5bCWuRAVZcDO1sHzZdVF6F2cCCDG3681tg= @@ -530,19 +511,18 @@ github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2Og github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= -github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= +github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= -github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= +github.com/koron/go-ssdp v0.9.1 h1:zvxbAAuJftJIZ8Jh8mda+LI7V92hYZf/sKprmOxpxwA= +github.com/koron/go-ssdp v0.9.1/go.mod h1:C43c047jWkDaeg9YuZlSh/QGqOieuWV6dbhWi/jcaLk= github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk= github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -562,10 +542,12 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-flow-metrics v0.3.0 h1:q31zcHUvHnwDO0SHaukewPYgwOBSxtt830uJtUx6784= github.com/libp2p/go-flow-metrics v0.3.0/go.mod h1:nuhlreIwEguM1IvHAew3ij7A8BMlyHQJ279ao24eZZo= -github.com/libp2p/go-libp2p v0.48.0 h1:h2BrLAgrj7X8bEN05K7qmrjpNHYA+6tnsGRdprjTnvo= -github.com/libp2p/go-libp2p v0.48.0/go.mod h1:Q1fBZNdmC2Hf82husCTfkKJVfHm2we5zk+NWmOGEmWk= +github.com/libp2p/go-libp2p v0.49.0 h1:ibXuYPIHmMIPShob1BktQvSuFQkq/MemhQOLKfGujjw= +github.com/libp2p/go-libp2p v0.49.0/go.mod h1:lzjVcOBk5fCn1QD2XbSOKLZesB6gEsry8SLjCsAAGT4= github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= +github.com/libp2p/go-libp2p-kad-dht v0.42.2 h1:5RtESYeBbjaF+KIPmU47xz2DOb2iLqvoRCZcsjxCS48= +github.com/libp2p/go-libp2p-kad-dht v0.42.2/go.mod h1:oLGZ7xq317zXmoeJe0dwgUdOeWeJ85MDJ/+fbKqFoK0= github.com/libp2p/go-libp2p-record v0.3.1 h1:cly48Xi5GjNw5Wq+7gmjfBiG9HCzQVkiZOUZ8kUl+Fg= github.com/libp2p/go-libp2p-record v0.3.1/go.mod h1:T8itUkLcWQLCYMqtX7Th6r7SexyUJpIyPgks757td/E= github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= @@ -574,8 +556,8 @@ github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0 github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= github.com/libp2p/go-netroute v0.4.0 h1:sZZx9hyANYUx9PZyqcgE/E1GUG3iEtTZHUEvdtXT7/Q= github.com/libp2p/go-netroute v0.4.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= -github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= -github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= +github.com/libp2p/go-yamux/v5 v5.1.0 h1:8Qlxj4E9JGJAQVW6+uj2o7mqkqsIVlSUGmTWhlXzoHE= +github.com/libp2p/go-yamux/v5 v5.1.0/go.mod h1:tgIQ07ObtRR/I0IWsFOyQIL9/dR5UXgc2s8xKmNZv1o= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= @@ -599,12 +581,13 @@ github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kb github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mb0/diff v0.0.0-20131118162322-d8d9a906c24d h1:eAS2t2Vy+6psf9LZ4T5WXWsbkBt3Tu5PWekJy5AGyEU= github.com/mb0/diff v0.0.0-20131118162322-d8d9a906c24d/go.mod h1:3YMHqrw2Qu3Liy82v4QdAG17e9k91HZ7w3hqlpWqhDo= +github.com/mehanizm/dateparse v0.0.0-20210806203422-f82c8742c9f8 h1:SA/3Lk2gFZilCejaXeCTkA3+MHt9uI9ogzx45F/nOFE= +github.com/mehanizm/dateparse v0.0.0-20210806203422-f82c8742c9f8/go.mod h1:cBDq2yLJ1Hr7GOJxsu46m4vzUyPslmbimCrvSg6wxXU= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= -github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= -github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/miekg/dns v1.1.73 h1:uhT8nJxmTrPJYClxVxTCX+CVn6qnzSiybRk72Z6DgrE= +github.com/miekg/dns v1.1.73/go.mod h1:RW2Obtfd5NZHvOFe3zYG0W8koWOQtAzyHaLo8vASBuQ= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/miolini/datacounter v1.0.3 h1:tanOZPVblGXQl7/bSZWoEM8l4KK83q24qwQLMrO/HOA= @@ -629,10 +612,8 @@ github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aG github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= -github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= -github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= -github.com/multiformats/go-multiaddr-dns v0.5.0 h1:p/FTyHKX0nl59f+S+dEUe8HRK+i5Ow/QHMw8Nh3gPCo= -github.com/multiformats/go-multiaddr-dns v0.5.0/go.mod h1:yJ349b8TPIAANUyuOzn1oz9o22tV9f+06L+cCeMxC14= +github.com/multiformats/go-multiaddr-dns v0.6.0 h1:yKIW08WJHSPJ8bDAT2O/5fypCaUu9Bjl8r/1eJ4XAW8= +github.com/multiformats/go-multiaddr-dns v0.6.0/go.mod h1:dwIQwdORZfnNQCeS7xLXyn+7626oRmMsVP30Uronhf0= github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= github.com/multiformats/go-multibase v0.3.0 h1:8helZD2+4Db7NNWFiktk2NePbF0boolBe6bDQvM4r68= @@ -720,39 +701,39 @@ github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= +github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/client_model v0.6.3 h1:O0jaTVAYNxTHYInEPFJt5I3+sN8zqBtVMPTB1qyxiEo= +github.com/prometheus/client_model v0.6.3/go.mod h1:gpN5P9S7Rr6Yr92PiQ+Ixvhf6JZEkF1dnxsYL2aPBEM= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= -github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= -github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/common v0.70.1 h1:1HvjP4D5oL3t8RsPlwxA9onvvStjtIHYE5XuuwOi/PY= +github.com/prometheus/common v0.70.1/go.mod h1:VdFUQDMZK3VLkurFUVhia6uys/0suUp86TJz5qbJRhc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= -github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI= +github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= -github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= -github.com/quic-go/webtransport-go v0.11.1 h1:rrFQMO+7/52ZDJ04fsrjIaWqn6q1z1MYo9iVFq6JtbA= -github.com/quic-go/webtransport-go v0.11.1/go.mod h1:SHgEzUFVyj+9WUSuGB1P6Zd351Pww2leWV3SwlTovkA= +github.com/quic-go/quic-go v0.62.0 h1:ZHDjCk5OacATwGvs8PWE97CTvX7AqZiVoW7++ZOXTf8= +github.com/quic-go/quic-go v0.62.0/go.mod h1:RAro2j2yN9a9EiPACLHT9IB2NXCvGQmmo/alT0yYI0w= +github.com/quic-go/webtransport-go v0.13.0 h1:RJLrTUHlTj8jJaQlQJUy0z0Mf7u1fVM0I6L1b9pe2M0= +github.com/quic-go/webtransport-go v0.13.0/go.mod h1:K83X9YHbAqgSLO6ikS6BXCMdWOvqh9JTHALulvb2JVk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= @@ -772,8 +753,12 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w= github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y= +github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TVfFFOPiSM= +github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= @@ -827,15 +812,11 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/sv-tools/openapi v0.4.0 h1:UhD9DVnGox1hfTePNclpUzUFgos57FvzT2jmcAuTOJ4= -github.com/sv-tools/openapi v0.4.0/go.mod h1:kD/dG+KP0+Fom1r6nvcj/ORtLus8d8enXT6dyRZDirE= -github.com/swaggo/swag/v2 v2.0.0-rc5 h1:fK7d6ET9rrEsdB8IyuwXREWMcyQN3N7gawGFbbrjgHk= -github.com/swaggo/swag/v2 v2.0.0-rc5/go.mod h1:kCL8Fu4Zl8d5tB2Bgj96b8wRowwrwk175bZHXfuGVFI= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= @@ -897,25 +878,27 @@ go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzyb go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= +go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= +go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= +go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= +go.opentelemetry.io/otel/sdk v1.46.0 h1:h5CNQQjEbuQXY/JfZtgt3i7HVFV3aHPO2OAwO2eTYPI= +go.opentelemetry.io/otel/sdk v1.46.0/go.mod h1:GAERFXFt5SYCEB+YiKUbMBeza6UaDH7GmGOZEfh2gSM= +go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= +go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= +go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c= +go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE= +go.uber.org/atomic v1.12.0/go.mod h1:I6c4cg+6HCxRjfjSsYtApoFILnpc0CGUdGkXVqbYVNk= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -938,8 +921,11 @@ go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= +go.yaml.in/yaml/v4 v4.0.0-rc.6/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4= golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -956,13 +942,13 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= -golang.org/x/exp v0.0.0-20260603202125-055de637280b h1:v1uXiEBHo8QA0LiGCo7UgHMzHT4Kdfpl2zmtH5vaP1Q= -golang.org/x/exp v0.0.0-20260603202125-055de637280b/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= +golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -985,8 +971,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= +golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1012,9 +998,9 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -1027,12 +1013,12 @@ golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= +golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= -golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= +golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1047,8 +1033,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= +golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1078,11 +1064,9 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1093,6 +1077,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1101,8 +1086,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= -golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1125,13 +1110,12 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= +golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= +golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1154,8 +1138,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU= +golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1175,8 +1159,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1191,8 +1175,9 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.83.1 h1:HIO0+BEtBP6soyqvqC8sNUjZ7bTs+0hFQuFF+RAy++Y= +google.golang.org/grpc v1.83.1/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1203,8 +1188,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1212,6 +1197,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/dnaeon/go-vcr.v4 v4.0.7 h1:Mq/RF+mq3QwtEunJSsoTbYPt3elSAmdJhAxrEaqr88I= +gopkg.in/dnaeon/go-vcr.v4 v4.0.7/go.mod h1:cRwV/njsN/D8qNJu4NAXWswz6b4OUh3rMIu4SObbLBg= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=