From 45e16f6d2c4033f7ccd5af33b71d36c326151e3c Mon Sep 17 00:00:00 2001 From: Emmanuel Jacquier Date: Thu, 23 Apr 2026 11:54:37 -0400 Subject: [PATCH] sign unsigned commit --- .changeset/config.json | 15 + .github/workflows/release.yml | 23 + .gitignore | 1 + CHANGELOG.md | 13 + RELEASE_GUIDE.md | 47 +- cmd/common/compile.go | 49 +- cmd/common/compile_test.go | 29 +- cmd/creinit/creinit.go | 88 +- cmd/creinit/creinit_test.go | 183 ++++ cmd/creinit/wizard.go | 16 + .../bindings/abigen/bindv2.go | 31 +- .../bindings/sourcecre.ts.tpl | 26 +- .../generate-bindings_test.go | 58 ++ cmd/login/login.go | 204 +--- cmd/login/login_test.go | 24 +- cmd/registry/list/list.go | 47 + cmd/registry/list/list_test.go | 175 ++++ cmd/registry/registry.go | 20 + cmd/root.go | 79 +- cmd/secrets/common/browser_flow.go | 251 +++++ cmd/secrets/common/browser_flow_test.go | 135 +++ cmd/secrets/common/gateway.go | 71 ++ cmd/secrets/common/gateway_test.go | 72 ++ cmd/secrets/common/handler.go | 122 ++- cmd/secrets/common/handler_test.go | 104 ++ cmd/secrets/common/test_helpers.go | 12 +- cmd/secrets/delete/delete.go | 21 +- cmd/secrets/execute/execute.go | 4 + cmd/secrets/list/list.go | 21 +- cmd/secrets/secrets.go | 2 +- cmd/template/help_template.tpl | 5 +- cmd/templates/list/list.go | 78 +- cmd/workflow/activate/activate.go | 163 +--- cmd/workflow/activate/activate_test.go | 6 - .../activate/registry_activate_strategy.go | 18 + .../registry_activate_strategy_onchain.go | 166 ++++ .../registry_activate_strategy_private.go | 65 ++ cmd/workflow/build/build.go | 11 +- cmd/workflow/build/build_test.go | 8 + cmd/workflow/convert/convert.go | 5 +- cmd/workflow/convert/convert_test.go | 2 +- cmd/workflow/delete/delete.go | 179 +--- cmd/workflow/delete/delete_test.go | 7 - .../delete/registry_delete_strategy.go | 29 + .../registry_delete_strategy_onchain.go | 165 ++++ .../registry_delete_strategy_private.go | 71 ++ cmd/workflow/deploy/artifacts.go | 12 +- cmd/workflow/deploy/artifacts_test.go | 62 +- .../deploy/{autoLink.go => auto_link.go} | 21 +- .../{autoLink_test.go => auto_link_test.go} | 14 +- cmd/workflow/deploy/compile.go | 5 +- cmd/workflow/deploy/compile_test.go | 123 ++- cmd/workflow/deploy/deploy.go | 212 ++-- cmd/workflow/deploy/deploy_test.go | 410 +++++++- cmd/workflow/deploy/limits.go | 1 + cmd/workflow/deploy/prepare.go | 4 +- cmd/workflow/deploy/private_registry_test.go | 369 +++++++ cmd/workflow/deploy/register.go | 21 +- cmd/workflow/deploy/register_test.go | 67 +- .../deploy/registry_deploy_strategy.go | 38 + .../registry_deploy_strategy_onchain.go | 111 +++ .../registry_deploy_strategy_private.go | 117 +++ cmd/workflow/hash/hash.go | 12 +- cmd/workflow/pause/pause.go | 192 +--- cmd/workflow/pause/pause_test.go | 6 - cmd/workflow/pause/registry_pause_strategy.go | 18 + .../pause/registry_pause_strategy_onchain.go | 197 ++++ .../pause/registry_pause_strategy_private.go | 65 ++ cmd/workflow/simulate/capabilities.go | 5 +- cmd/workflow/simulate/limits.go | 68 ++ cmd/workflow/simulate/simulate.go | 152 ++- cmd/workflow/simulate/simulate_test.go | 162 +++ cmd/workflow/simulate/simulator_utils.go | 18 +- cmd/workflow/simulate/utils_test.go | 2 +- .../supported_chains/supported_chains.go | 25 + cmd/workflow/workflow.go | 2 + docs/cre.md | 1 + docs/cre_init.md | 1 + docs/cre_registry.md | 33 + docs/cre_registry_list.md | 38 + docs/cre_templates_list.md | 1 + docs/cre_workflow.md | 1 + docs/cre_workflow_build.md | 5 +- docs/cre_workflow_deploy.md | 1 + docs/cre_workflow_hash.md | 1 + docs/cre_workflow_simulate.md | 1 + docs/cre_workflow_supported-chains.md | 28 + go.mod | 194 ++-- go.sum | 920 +++++++++++------ internal/authvalidation/validator.go | 51 +- .../client/graphqlclient/graphqlclient.go | 10 +- .../graphqlclient/graphqlclient_test.go | 41 + .../privateregistryclient.go | 391 ++++++++ .../privateregistryclient_test.go | 423 ++++++++ .../client/storageclient/storageclient.go | 42 +- internal/constants/constants.go | 5 +- internal/context/project_context.go | 9 +- internal/context/project_context_test.go | 2 +- internal/credentials/credentials.go | 1 + internal/credentials/credentials_test.go | 41 +- internal/environments/environments.go | 6 + internal/environments/environments.yaml | 3 + internal/ethkeys/keys.go | 30 +- internal/ethkeys/keys_test.go | 57 +- internal/oauth/browser.go | 20 + internal/oauth/exchange.go | 69 ++ internal/oauth/exchange_test.go | 76 ++ .../oauth}/htmlPages/error.html | 0 .../oauth}/htmlPages/output.css | 0 internal/oauth/htmlPages/secrets_error.html | 63 ++ internal/oauth/htmlPages/secrets_success.html | 59 ++ .../oauth}/htmlPages/success.html | 0 .../oauth}/htmlPages/waiting.html | 0 internal/oauth/pages.go | 89 ++ internal/oauth/pkce.go | 20 + internal/oauth/pkce_test.go | 22 + internal/oauth/secrets_callback.go | 41 + internal/oauth/secrets_callback_test.go | 66 ++ internal/oauth/server.go | 24 + internal/oauth/state.go | 49 + internal/oauth/state_test.go | 42 + internal/runtime/runtime_context.go | 70 +- internal/settings/registry_resolution.go | 173 ++++ internal/settings/registry_resolution_test.go | 246 +++++ internal/settings/settings_get.go | 14 +- internal/settings/settings_load.go | 11 +- internal/settings/settings_test.go | 142 +++ .../workflow-onchain-named-registry.yaml | 9 + .../workflow-private-registry.yaml | 9 + internal/settings/workflow_settings.go | 84 +- .../hello-world-ts/workflow/package.json | 3 +- .../chainsim/simulated_environment.go | 60 +- internal/testutil/graphql_mock.go | 11 +- internal/testutil/testjwt/jwt.go | 35 + .../{ => testsettings}/test_settings.go | 2 +- .../testdata/test-project.yaml | 0 .../testdata/test-workflow.yaml | 0 internal/ui/prompts.go | 12 + package.json | 13 + pnpm-lock.yaml | 871 +++++++++++++++++ scripts/tag-and-push.sh | 33 + test/graphql_mock.go | 2 +- test/init_convert_simulate_ts_test.go | 14 +- .../multi_command_flows/account_happy_path.go | 7 +- .../multi_command_flows/secrets_happy_path.go | 14 +- .../workflow_happy_path_1.go | 7 +- .../workflow_happy_path_2.go | 14 +- .../workflow_happy_path_3.go | 21 +- .../workflow_private_registry.go | 921 ++++++++++++++++++ test/multi_command_test.go | 32 + 150 files changed, 9633 insertions(+), 1606 deletions(-) create mode 100644 .changeset/config.json create mode 100644 .github/workflows/release.yml create mode 100644 CHANGELOG.md create mode 100644 cmd/registry/list/list.go create mode 100644 cmd/registry/list/list_test.go create mode 100644 cmd/registry/registry.go create mode 100644 cmd/secrets/common/browser_flow.go create mode 100644 cmd/secrets/common/browser_flow_test.go create mode 100644 cmd/workflow/activate/registry_activate_strategy.go create mode 100644 cmd/workflow/activate/registry_activate_strategy_onchain.go create mode 100644 cmd/workflow/activate/registry_activate_strategy_private.go create mode 100644 cmd/workflow/delete/registry_delete_strategy.go create mode 100644 cmd/workflow/delete/registry_delete_strategy_onchain.go create mode 100644 cmd/workflow/delete/registry_delete_strategy_private.go rename cmd/workflow/deploy/{autoLink.go => auto_link.go} (92%) rename cmd/workflow/deploy/{autoLink_test.go => auto_link_test.go} (95%) create mode 100644 cmd/workflow/deploy/private_registry_test.go create mode 100644 cmd/workflow/deploy/registry_deploy_strategy.go create mode 100644 cmd/workflow/deploy/registry_deploy_strategy_onchain.go create mode 100644 cmd/workflow/deploy/registry_deploy_strategy_private.go create mode 100644 cmd/workflow/pause/registry_pause_strategy.go create mode 100644 cmd/workflow/pause/registry_pause_strategy_onchain.go create mode 100644 cmd/workflow/pause/registry_pause_strategy_private.go create mode 100644 cmd/workflow/supported_chains/supported_chains.go create mode 100644 docs/cre_registry.md create mode 100644 docs/cre_registry_list.md create mode 100644 docs/cre_workflow_supported-chains.md create mode 100644 internal/client/privateregistryclient/privateregistryclient.go create mode 100644 internal/client/privateregistryclient/privateregistryclient_test.go create mode 100644 internal/oauth/browser.go create mode 100644 internal/oauth/exchange.go create mode 100644 internal/oauth/exchange_test.go rename {cmd/login => internal/oauth}/htmlPages/error.html (100%) rename {cmd/login => internal/oauth}/htmlPages/output.css (100%) create mode 100644 internal/oauth/htmlPages/secrets_error.html create mode 100644 internal/oauth/htmlPages/secrets_success.html rename {cmd/login => internal/oauth}/htmlPages/success.html (100%) rename {cmd/login => internal/oauth}/htmlPages/waiting.html (100%) create mode 100644 internal/oauth/pages.go create mode 100644 internal/oauth/pkce.go create mode 100644 internal/oauth/pkce_test.go create mode 100644 internal/oauth/secrets_callback.go create mode 100644 internal/oauth/secrets_callback_test.go create mode 100644 internal/oauth/server.go create mode 100644 internal/oauth/state.go create mode 100644 internal/oauth/state_test.go create mode 100644 internal/settings/registry_resolution.go create mode 100644 internal/settings/registry_resolution_test.go create mode 100644 internal/settings/testdata/workflow_storage/workflow-onchain-named-registry.yaml create mode 100644 internal/settings/testdata/workflow_storage/workflow-private-registry.yaml create mode 100644 internal/testutil/testjwt/jwt.go rename internal/testutil/{ => testsettings}/test_settings.go (98%) rename internal/testutil/{ => testsettings}/testdata/test-project.yaml (100%) rename internal/testutil/{ => testsettings}/testdata/test-workflow.yaml (100%) create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100755 scripts/tag-and-push.sh create mode 100644 test/multi_command_flows/workflow_private_registry.go diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000..dcb0eb83 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "smartcontractkit/cre-cli" }], + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [], + "privatePackages": { + "version": true, + "tag": true + } +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ae0f9702 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +name: Release + +on: + push: + branches: + - main + +jobs: + release: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - name: release + uses: smartcontractkit/.github/actions/cicd-changesets@cicd-changesets/v1 + with: + git-user: app-token-issuer-dev-services[bot] + git-email: app-token-issuer-dev-services[bot]@users.noreply.github.com + aws-region: ${{ secrets.AWS_REGION }} + aws-role-arn: ${{ secrets.AWS_OIDC_CRE_CLI_CI_RELEASE_TOKEN_ISSUER_ROLE_ARN }} + aws-lambda-url: ${{ secrets.AWS_DEV_SERVICES_TOKEN_ISSUER_LAMBDA_URL }} + changesets-create-gh-release: "false" diff --git a/.gitignore b/.gitignore index 419f764e..521b4a59 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ # Dependency directories (remove the comment below to include it) # vendor/ +node_modules/ # Build outputs bin diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c91449a7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# cre-cli + +## 1.8.2 + +### Patch Changes + +- [#332](https://github.com/smartcontractkit/cre-cli/pull/332) [`31c1ab8`](https://github.com/smartcontractkit/cre-cli/commit/31c1ab8a500fa8ad1518ea610628302ba5ee76f2) Thanks [@timothyF95](https://github.com/timothyF95)! - Submit oauth secrets to vault DON + +## 1.8.1 + +### Patch Changes + +- [#333](https://github.com/smartcontractkit/cre-cli/pull/333) [`e6c2be1`](https://github.com/smartcontractkit/cre-cli/commit/e6c2be1c8ec1dfb635698b6731bbf11ca7c9ee67) Thanks [@timothyF95](https://github.com/timothyF95)! - Changeset test PR diff --git a/RELEASE_GUIDE.md b/RELEASE_GUIDE.md index c7835add..ee811362 100644 --- a/RELEASE_GUIDE.md +++ b/RELEASE_GUIDE.md @@ -1,20 +1,27 @@ -# How to create a new release -Let's assume we want to create a release for version `v0.1.0`. - -Steps: -1. Create a new branch from `main` with the name `releases/v0.1.x` -2. Create a tag on the release branch `git tag -a v0.1.0 -m "Release v0.1.0" -s` -3. Push the tag to the remote `git push origin v0.1.0` -4. Wait for `build-and-release` pipeline to run. - - Once pipeline is successful, the release will be created as a Draft - - Verify all is good and publish release as needed (set as latest if that is the case) - -# How to fix a bug in an existing release -Let's assume we want to fix a bug in the release `v0.1.0`. - -Steps: -1. Create a new branch from `releases/v0.1.x`. -2. Fix the bug. -3. Create a PR against the branch `releases/v0.1.x`. -4. Go through the review process and merge the PR. -5. Create a release with a tag `v0.1.1` from the branch `releases/v0.1.x`. +# Release Process + +This project uses [Changesets](https://github.com/changesets/changesets) for versioning and release management. + +## Adding a changeset + +When your PR includes changes that warrant a version bump, run: + +```bash +pnpm changeset +``` + +Select the bump type (major, minor, or patch) and provide a summary. This creates a `.changeset/*.md` file that should be committed with your PR. + +## How releases happen + +1. PRs with changeset files are merged to `main`. +2. The `release.yml` workflow detects pending changesets and opens (or updates) a **"Version Packages"** PR. This PR bumps `package.json`, updates `CHANGELOG.md`, and consumes the changeset files. +3. When the Version Packages PR is merged, the workflow creates a `v*` tag. +4. The tag triggers `build-and-release.yml`, which builds and signs binaries across all platforms and creates a draft GitHub Release. +5. Review the draft release and publish it. + +## Hotfixing an existing release + +1. Create a branch from the relevant release tag (e.g. `git checkout -b hotfix/v1.3.1 v1.3.0`). +2. Fix the bug and add a changeset (`pnpm changeset` -- typically a `patch`). +3. Follow the standard PR and merge process against `main`. diff --git a/cmd/common/compile.go b/cmd/common/compile.go index 764a9c51..2456ec77 100644 --- a/cmd/common/compile.go +++ b/cmd/common/compile.go @@ -17,18 +17,45 @@ const makefileName = "Makefile" var defaultWasmOutput = filepath.Join("wasm", "workflow.wasm") +const ( + // SkipTypeChecksFlag is passed through to cre-compile for TypeScript workflows (matches @chainlink/cre-sdk). + SkipTypeChecksFlag = "--skip-type-checks" + // SkipTypeChecksCLIFlag is the Cobra/Viper flag name (no leading dashes). + SkipTypeChecksCLIFlag = "skip-type-checks" +) + +// WorkflowCompileOptions configures workflow compilation for CompileWorkflowToWasm. +type WorkflowCompileOptions struct { + // StripSymbols, when true, strips debug symbols from Go WASM builds (smaller binary for deploy). + StripSymbols bool + // SkipTypeChecks, when true, passes SkipTypeChecksFlag to cre-compile for TypeScript workflows. + SkipTypeChecks bool +} + // getBuildCmd returns a single step that builds the workflow and returns the WASM bytes. -// If stripSymbols is true, debug symbols are stripped from the binary to reduce size. -func getBuildCmd(workflowRootFolder, mainFile, language string, stripSymbols bool) (func() ([]byte, error), error) { +func getBuildCmd(workflowRootFolder, mainFile, language string, opts WorkflowCompileOptions) (func() ([]byte, error), error) { tmpPath := filepath.Join(workflowRootFolder, ".cre_build_tmp.wasm") switch language { case constants.WorkflowLanguageTypeScript: - cmd := exec.Command("bun", "cre-compile", mainFile, tmpPath) + args := []string{"cre-compile", mainFile, tmpPath} + if opts.SkipTypeChecks { + args = append(args, SkipTypeChecksFlag) + } + cmd := exec.Command("bun", args...) cmd.Dir = workflowRootFolder return func() ([]byte, error) { out, err := cmd.CombinedOutput() if err != nil { - return nil, fmt.Errorf("%w\nbuild output:\n%s", err, strings.TrimSpace(string(out))) + outStr := strings.TrimSpace(string(out)) + if strings.Contains(outStr, "Script not found") && strings.Contains(outStr, "cre-compile") { + return nil, fmt.Errorf("TypeScript compilation failed: 'cre-compile' command not found.\n\n" + + "The 'cre-compile' tool is provided by the @chainlink/cre-sdk package.\n\n" + + "To fix:\n" + + " • Run 'bun install' in your project to install dependencies\n" + + " • Update your project dependencies with 'cre update '\n" + + " • If starting fresh, use 'cre workflow init' to scaffold a properly configured workflow") + } + return nil, fmt.Errorf("%w\nbuild output:\n%s", err, outStr) } b, err := os.ReadFile(tmpPath) _ = os.Remove(tmpPath) @@ -37,7 +64,7 @@ func getBuildCmd(workflowRootFolder, mainFile, language string, stripSymbols boo case constants.WorkflowLanguageGolang: // Build the package (.) so all .go files (main.go, workflow.go, etc.) are compiled together ldflags := "-buildid=" - if stripSymbols { + if opts.StripSymbols { ldflags = "-buildid= -w -s" } cmd := exec.Command( @@ -78,7 +105,7 @@ func getBuildCmd(workflowRootFolder, mainFile, language string, stripSymbols boo default: // Build the package (.) so all .go files are compiled together ldflags := "-buildid=" - if stripSymbols { + if opts.StripSymbols { ldflags = "-buildid= -w -s" } cmd := exec.Command( @@ -105,10 +132,10 @@ func getBuildCmd(workflowRootFolder, mainFile, language string, stripSymbols boo } // CompileWorkflowToWasm compiles the workflow at workflowPath and returns the WASM binary. -// If stripSymbols is true, debug symbols are stripped to reduce binary size (used for deploy). -// If false, debug symbols are kept for better error messages (used for simulate). -// For custom builds (WASM language with Makefile), stripSymbols has no effect. -func CompileWorkflowToWasm(workflowPath string, stripSymbols bool) ([]byte, error) { +// opts.StripSymbols: for Go builds, true strips debug symbols (deploy); false keeps them (simulate). +// opts.SkipTypeChecks: for TypeScript, passes SkipTypeChecksFlag to cre-compile. +// For custom Makefile WASM builds, StripSymbols and SkipTypeChecks have no effect. +func CompileWorkflowToWasm(workflowPath string, opts WorkflowCompileOptions) ([]byte, error) { workflowRootFolder, workflowMainFile, err := WorkflowPathRootAndMain(workflowPath) if err != nil { return nil, fmt.Errorf("workflow path: %w", err) @@ -140,7 +167,7 @@ func CompileWorkflowToWasm(workflowPath string, stripSymbols bool) ([]byte, erro return nil, fmt.Errorf("unsupported workflow language for file %s", workflowMainFile) } - buildStep, err := getBuildCmd(workflowRootFolder, workflowMainFile, language, stripSymbols) + buildStep, err := getBuildCmd(workflowRootFolder, workflowMainFile, language, opts) if err != nil { return nil, err } diff --git a/cmd/common/compile_test.go b/cmd/common/compile_test.go index 6fa560ac..b82df33d 100644 --- a/cmd/common/compile_test.go +++ b/cmd/common/compile_test.go @@ -47,21 +47,21 @@ func TestFindMakefileRoot(t *testing.T) { func TestCompileWorkflowToWasm_Go_Success(t *testing.T) { t.Run("basic_workflow", func(t *testing.T) { path := deployTestdataPath("basic_workflow", "main.go") - wasm, err := CompileWorkflowToWasm(path, true) + wasm, err := CompileWorkflowToWasm(path, WorkflowCompileOptions{StripSymbols: true}) require.NoError(t, err) assert.NotEmpty(t, wasm) }) t.Run("configless_workflow", func(t *testing.T) { path := deployTestdataPath("configless_workflow", "main.go") - wasm, err := CompileWorkflowToWasm(path, true) + wasm, err := CompileWorkflowToWasm(path, WorkflowCompileOptions{StripSymbols: true}) require.NoError(t, err) assert.NotEmpty(t, wasm) }) t.Run("missing_go_mod", func(t *testing.T) { path := deployTestdataPath("missing_go_mod", "main.go") - wasm, err := CompileWorkflowToWasm(path, true) + wasm, err := CompileWorkflowToWasm(path, WorkflowCompileOptions{StripSymbols: true}) require.NoError(t, err) assert.NotEmpty(t, wasm) }) @@ -69,7 +69,7 @@ func TestCompileWorkflowToWasm_Go_Success(t *testing.T) { func TestCompileWorkflowToWasm_Go_Malformed_Fails(t *testing.T) { path := deployTestdataPath("malformed_workflow", "main.go") - _, err := CompileWorkflowToWasm(path, true) + _, err := CompileWorkflowToWasm(path, WorkflowCompileOptions{StripSymbols: true}) require.Error(t, err) assert.Contains(t, err.Error(), "failed to compile workflow") assert.Contains(t, err.Error(), "undefined: sdk.RemovedFunctionThatFailsCompilation") @@ -80,7 +80,7 @@ func TestCompileWorkflowToWasm_Wasm_Success(t *testing.T) { _ = os.Remove(wasmPath) t.Cleanup(func() { _ = os.Remove(wasmPath) }) - wasm, err := CompileWorkflowToWasm(wasmPath, true) + wasm, err := CompileWorkflowToWasm(wasmPath, WorkflowCompileOptions{StripSymbols: true}) require.NoError(t, err) assert.NotEmpty(t, wasm) @@ -96,14 +96,14 @@ func TestCompileWorkflowToWasm_Wasm_Fails(t *testing.T) { wasmPath := filepath.Join(wasmDir, "workflow.wasm") require.NoError(t, os.WriteFile(wasmPath, []byte("not really wasm"), 0600)) - _, err := CompileWorkflowToWasm(wasmPath, true) + _, err := CompileWorkflowToWasm(wasmPath, WorkflowCompileOptions{StripSymbols: true}) require.Error(t, err) assert.Contains(t, err.Error(), "no Makefile found") }) t.Run("make_build_fails", func(t *testing.T) { path := deployTestdataPath("wasm_make_fails", "wasm", "workflow.wasm") - _, err := CompileWorkflowToWasm(path, true) + _, err := CompileWorkflowToWasm(path, WorkflowCompileOptions{StripSymbols: true}) require.Error(t, err) assert.Contains(t, err.Error(), "failed to compile workflow") assert.Contains(t, err.Error(), "build output:") @@ -118,7 +118,7 @@ func TestCompileWorkflowToWasm_TS_Success(t *testing.T) { mainPath := filepath.Join(dir, "main.ts") require.NoError(t, os.WriteFile(mainPath, []byte(`export async function main() { return "ok"; } `), 0600)) - require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"test","dependencies":{"@chainlink/cre-sdk":"latest"}} + require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"name":"test","dependencies":{"@chainlink/cre-sdk":"^1.5.0"}} `), 0600)) install := exec.Command("bun", "install") install.Dir = dir @@ -127,7 +127,18 @@ func TestCompileWorkflowToWasm_TS_Success(t *testing.T) { if err := install.Run(); err != nil { t.Skipf("bun install failed (network or cre-sdk): %v", err) } - wasm, err := CompileWorkflowToWasm(mainPath, true) + require.NoError(t, os.WriteFile(filepath.Join(dir, "tsconfig.json"), []byte(`{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "skipLibCheck": true, + "types": [] + }, + "include": ["main.ts"] +} +`), 0600)) + wasm, err := CompileWorkflowToWasm(mainPath, WorkflowCompileOptions{StripSymbols: true}) if err != nil { t.Skipf("TS compile failed (published cre-sdk may lack full layout): %v", err) } diff --git a/cmd/creinit/creinit.go b/cmd/creinit/creinit.go index 9aa6db55..35ad3502 100644 --- a/cmd/creinit/creinit.go +++ b/cmd/creinit/creinit.go @@ -11,6 +11,7 @@ import ( "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/viper" + "golang.org/x/term" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/runtime" @@ -22,10 +23,12 @@ import ( ) type Inputs struct { - ProjectName string `validate:"omitempty,project_name" cli:"project-name"` - TemplateName string `validate:"omitempty" cli:"template"` - WorkflowName string `validate:"omitempty,workflow_name" cli:"workflow-name"` - RpcURLs map[string]string // chain-name -> url, from --rpc-url flags + ProjectName string `validate:"omitempty,project_name" cli:"project-name"` + TemplateName string `validate:"omitempty" cli:"template"` + WorkflowName string `validate:"omitempty,workflow_name" cli:"workflow-name"` + RpcURLs map[string]string // chain-name -> url, from --rpc-url flags + NonInteractive bool + ProjectRoot string // from -R / --project-root flag } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -47,6 +50,11 @@ Templates are fetched dynamically from GitHub repositories.`, if err != nil { return err } + + // Only use -R if the user explicitly passed it on the command line + if cmd.Flags().Changed(settings.Flags.ProjectRoot.Name) { + inputs.ProjectRoot = runtimeContext.Viper.GetString(settings.Flags.ProjectRoot.Name) + } if err = h.ValidateInputs(inputs); err != nil { return err } @@ -67,6 +75,7 @@ Templates are fetched dynamically from GitHub repositories.`, initCmd.Flags().StringP("template", "t", "", "Name of the template to use (e.g., kv-store-go)") initCmd.Flags().Bool("refresh", false, "Bypass template cache and fetch fresh data") initCmd.Flags().StringArray("rpc-url", nil, "RPC URL for a network (format: chain-name=url, repeatable)") + initCmd.Flags().Bool("non-interactive", false, "Fail instead of prompting; requires all inputs via flags") // Deprecated: --template-id is kept for backwards compatibility, maps to hello-world-go initCmd.Flags().Uint32("template-id", 0, "") @@ -133,10 +142,11 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { } return Inputs{ - ProjectName: v.GetString("project-name"), - TemplateName: templateName, - WorkflowName: v.GetString("workflow-name"), - RpcURLs: rpcURLs, + ProjectName: v.GetString("project-name"), + TemplateName: templateName, + WorkflowName: v.GetString("workflow-name"), + RpcURLs: rpcURLs, + NonInteractive: v.GetBool("non-interactive"), }, nil } @@ -170,6 +180,21 @@ func (h *handler) Execute(inputs Inputs) error { } startDir := cwd + // Respect -R / --project-root flag if provided. + // For init, treat -R as the base directory for project creation. + // The directory does not need to exist yet — it will be created during scaffolding. + if inputs.ProjectRoot != "" { + absRoot, err := filepath.Abs(inputs.ProjectRoot) + if err != nil { + return fmt.Errorf("invalid --project-root path: %w", err) + } + // If -R points to a file, that's a user error — it must be a directory + if info, err := os.Stat(absRoot); err == nil && !info.IsDir() { + return fmt.Errorf("--project-root %q is a file, not a directory", inputs.ProjectRoot) + } + startDir = absRoot + } + // Detect if we're in an existing project existingProjectRoot, _, existingErr := h.findExistingProject(startDir) isNewProject := existingErr != nil @@ -218,9 +243,56 @@ func (h *handler) Execute(inputs Inputs) error { } } + // Non-interactive mode: validate all required inputs are present + if inputs.NonInteractive { + var missingFlags []string + if isNewProject && inputs.ProjectName == "" { + missingFlags = append(missingFlags, "--project-name") + } + if inputs.TemplateName == "" { + missingFlags = append(missingFlags, "--template") + } + if selectedTemplate != nil { + missing := MissingNetworks(selectedTemplate, inputs.RpcURLs) + for _, network := range missing { + missingFlags = append(missingFlags, fmt.Sprintf("--rpc-url=\"%s=\"", network)) + } + if inputs.WorkflowName == "" && selectedTemplate.ProjectDir == "" && len(selectedTemplate.Workflows) <= 1 { + missingFlags = append(missingFlags, "--workflow-name") + } + } + if len(missingFlags) > 0 { + ui.ErrorWithSuggestions( + "Non-interactive mode requires all inputs via flags", + missingFlags, + ) + return fmt.Errorf("missing required flags for --non-interactive mode") + } + } + // Run the interactive wizard result, err := RunWizard(inputs, isNewProject, startDir, workflowTemplates, selectedTemplate) if err != nil { + // If stdin is not a terminal, the wizard will fail trying to open a TTY. + // Detect this via term.IsTerminal rather than matching third-party error strings. + if !term.IsTerminal(int(os.Stdin.Fd())) { // #nosec G115 -- stdin fd is always 0 + var suggestions []string + if selectedTemplate != nil { + missing := MissingNetworks(selectedTemplate, inputs.RpcURLs) + for _, network := range missing { + suggestions = append(suggestions, fmt.Sprintf("--rpc-url=\"%s=\"", network)) + } + } + if len(suggestions) > 0 { + ui.ErrorWithSuggestions( + "Interactive mode requires a terminal (TTY). Provide the missing flags to run non-interactively", + suggestions, + ) + } else { + ui.Error("Interactive mode requires a terminal (TTY). Use --non-interactive with all required flags, or run in a terminal") + } + return fmt.Errorf("interactive mode requires a terminal (TTY)") + } return fmt.Errorf("wizard error: %w", err) } if result.Cancelled { diff --git a/cmd/creinit/creinit_test.go b/cmd/creinit/creinit_test.go index fffb1117..e4755cd4 100644 --- a/cmd/creinit/creinit_test.go +++ b/cmd/creinit/creinit_test.go @@ -830,3 +830,186 @@ func TestBuiltInTemplateBackwardsCompat(t *testing.T) { "built-in template should use user's workflow name") require.FileExists(t, filepath.Join(projectRoot, "hello-wf", constants.DefaultWorkflowSettingsFileName)) } + +func TestMissingNetworks(t *testing.T) { + cases := []struct { + name string + template *templaterepo.TemplateSummary + flags map[string]string + expected []string + }{ + { + name: "nil template", + template: nil, + flags: nil, + expected: nil, + }, + { + name: "no networks required", + template: &templaterepo.TemplateSummary{ + TemplateMetadata: templaterepo.TemplateMetadata{}, + }, + flags: nil, + expected: nil, + }, + { + name: "all provided", + template: &testMultiNetworkTemplate, + flags: map[string]string{ + "ethereum-testnet-sepolia": "https://rpc1.example.com", + "ethereum-mainnet": "https://rpc2.example.com", + }, + expected: nil, + }, + { + name: "some missing", + template: &testMultiNetworkTemplate, + flags: map[string]string{ + "ethereum-testnet-sepolia": "https://rpc1.example.com", + }, + expected: []string{"ethereum-mainnet"}, + }, + { + name: "all missing", + template: &testMultiNetworkTemplate, + flags: map[string]string{}, + expected: []string{"ethereum-testnet-sepolia", "ethereum-mainnet"}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := MissingNetworks(tc.template, tc.flags) + require.Equal(t, tc.expected, result) + }) + } +} + +func TestNonInteractiveMissingFlags(t *testing.T) { + sim := chainsim.NewSimulatedEnvironment(t) + defer sim.Close() + + tempDir := t.TempDir() + restoreCwd, err := testutil.ChangeWorkingDirectory(tempDir) + require.NoError(t, err) + defer restoreCwd() + + inputs := Inputs{ + ProjectName: "proj", + TemplateName: "test-multichain", + WorkflowName: "", + NonInteractive: true, + RpcURLs: map[string]string{}, + } + + h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) + require.NoError(t, h.ValidateInputs(inputs)) + err = h.Execute(inputs) + require.Error(t, err) + require.Contains(t, err.Error(), "missing required flags for --non-interactive mode") +} + +func TestNonInteractiveAllFlagsProvided(t *testing.T) { + sim := chainsim.NewSimulatedEnvironment(t) + defer sim.Close() + + tempDir := t.TempDir() + restoreCwd, err := testutil.ChangeWorkingDirectory(tempDir) + require.NoError(t, err) + defer restoreCwd() + + inputs := Inputs{ + ProjectName: "niProj", + TemplateName: "hello-world-go", + WorkflowName: "my-wf", + NonInteractive: true, + } + + h := newHandlerWithRegistry(sim.NewRuntimeContext(), newMockRegistry()) + require.NoError(t, h.ValidateInputs(inputs)) + require.NoError(t, h.Execute(inputs)) + + projectRoot := filepath.Join(tempDir, "niProj") + require.DirExists(t, filepath.Join(projectRoot, "my-wf")) +} + +func TestInitRespectsProjectRootFlag(t *testing.T) { + sim := chainsim.NewSimulatedEnvironment(t) + defer sim.Close() + + // CWD is a temp dir (simulating being "somewhere else") + cwdDir := t.TempDir() + restoreCwd, err := testutil.ChangeWorkingDirectory(cwdDir) + require.NoError(t, err) + defer restoreCwd() + + // Target directory is a separate temp dir (simulating -R flag) + targetDir := t.TempDir() + + inputs := Inputs{ + ProjectName: "myproj", + TemplateName: "test-go", + WorkflowName: "mywf", + RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, + ProjectRoot: targetDir, + } + + ctx := sim.NewRuntimeContext() + + h := newHandlerWithRegistry(ctx, newMockRegistry()) + require.NoError(t, h.ValidateInputs(inputs)) + require.NoError(t, h.Execute(inputs)) + + // Project should be created under targetDir, NOT cwdDir + projectRoot := filepath.Join(targetDir, "myproj") + validateInitProjectStructure(t, projectRoot, "mywf", GetTemplateFileListGo()) + + // Verify nothing was created in CWD + entries, err := os.ReadDir(cwdDir) + require.NoError(t, err) + require.Empty(t, entries, "CWD should be untouched when -R is provided") +} + +func TestInitProjectRootFlagFindsExistingProject(t *testing.T) { + sim := chainsim.NewSimulatedEnvironment(t) + defer sim.Close() + + // CWD is a clean temp dir with no project + cwdDir := t.TempDir() + restoreCwd, err := testutil.ChangeWorkingDirectory(cwdDir) + require.NoError(t, err) + defer restoreCwd() + + // Create an "existing project" in a separate directory + existingProject := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(existingProject, constants.DefaultProjectSettingsFileName), + []byte("name: existing"), 0600, + )) + require.NoError(t, os.WriteFile( + filepath.Join(existingProject, constants.DefaultEnvFileName), + []byte(""), 0600, + )) + + inputs := Inputs{ + ProjectName: "", + TemplateName: "test-go", + WorkflowName: "new-workflow", + RpcURLs: map[string]string{"ethereum-testnet-sepolia": "https://rpc.example.com"}, + ProjectRoot: existingProject, + } + + ctx := sim.NewRuntimeContext() + + h := newHandlerWithRegistry(ctx, newMockRegistry()) + require.NoError(t, h.ValidateInputs(inputs)) + require.NoError(t, h.Execute(inputs)) + + // Workflow should be scaffolded into the existing project + validateInitProjectStructure( + t, + existingProject, + "new-workflow", + GetTemplateFileListGo(), + ) +} diff --git a/cmd/creinit/wizard.go b/cmd/creinit/wizard.go index d8bf052d..7b42de8c 100644 --- a/cmd/creinit/wizard.go +++ b/cmd/creinit/wizard.go @@ -992,6 +992,22 @@ func RunWizard(inputs Inputs, isNewProject bool, startDir string, templates []te return result, nil } +// MissingNetworks returns the network names from the template that were not +// provided via --rpc-url flags. Returns nil if all networks are covered or +// the template has no network requirements. +func MissingNetworks(template *templaterepo.TemplateSummary, flagRpcURLs map[string]string) []string { + if template == nil || len(template.Networks) == 0 { + return nil + } + var missing []string + for _, network := range template.Networks { + if _, ok := flagRpcURLs[network]; !ok { + missing = append(missing, network) + } + } + return missing +} + // validateRpcURL validates that a URL is a valid HTTP/HTTPS URL. func validateRpcURL(rawURL string) error { u, err := url.Parse(rawURL) diff --git a/cmd/generate-bindings/bindings/abigen/bindv2.go b/cmd/generate-bindings/bindings/abigen/bindv2.go index cbf7d642..d1781fe9 100644 --- a/cmd/generate-bindings/bindings/abigen/bindv2.go +++ b/cmd/generate-bindings/bindings/abigen/bindv2.go @@ -380,7 +380,7 @@ func BindV2(types []string, abis []string, bytecodes []string, pkg string, libs return string(code), nil } -// Remove contract name prefixes from struct names +// Remove contract name prefixes from struct names and update field type references. func sanitizeStructNames(structs map[string]*tmplStruct, contracts map[string]*tmplContractV2) { contractNames := make([]string, 0, len(contracts)) for name := range contracts { @@ -388,9 +388,22 @@ func sanitizeStructNames(structs map[string]*tmplStruct, contracts map[string]*t } sort.Strings(contractNames) - for _, structName := range structs { + renames := make(map[string]string) + for _, s := range structs { + original := s.Name for _, contractName := range contractNames { - structName.Name = strings.TrimPrefix(structName.Name, contractName) + s.Name = strings.TrimPrefix(s.Name, contractName) + } + if s.Name != original { + renames[original] = s.Name + } + } + + for _, s := range structs { + for _, f := range s.Fields { + for old, renamed := range renames { + f.Type = strings.ReplaceAll(f.Type, old, renamed) + } } } } @@ -460,9 +473,17 @@ func tsBindType(kind abi.Type, structs map[string]*tmplStruct) string { } return "{ " + strings.Join(fields, "; ") + " }" case abi.ArrayTy: - return "readonly " + tsBindType(*kind.Elem, structs) + "[]" + elem := tsBindType(*kind.Elem, structs) + if kind.Elem.T == abi.ArrayTy || kind.Elem.T == abi.SliceTy { + return "readonly (" + elem + ")[]" + } + return "readonly " + elem + "[]" case abi.SliceTy: - return "readonly " + tsBindType(*kind.Elem, structs) + "[]" + elem := tsBindType(*kind.Elem, structs) + if kind.Elem.T == abi.ArrayTy || kind.Elem.T == abi.SliceTy { + return "readonly (" + elem + ")[]" + } + return "readonly " + elem + "[]" default: return tsBindBasicType(kind) } diff --git a/cmd/generate-bindings/bindings/sourcecre.ts.tpl b/cmd/generate-bindings/bindings/sourcecre.ts.tpl index 1b1c87e0..13c3f38e 100644 --- a/cmd/generate-bindings/bindings/sourcecre.ts.tpl +++ b/cmd/generate-bindings/bindings/sourcecre.ts.tpl @@ -20,6 +20,12 @@ import { export interface DecodedLog extends Omit { data: T } +const encodeTopicValue = (t: Hex | Hex[] | null): string[] => { + if (t == null) return [] + if (Array.isArray(t)) return t.map(hexToBase64) + return [hexToBase64(t)] +} + {{range $contract := .Contracts}} {{/* Event types: Topics (indexed only) and Decoded (all fields) */}} {{range $event := $contract.Events}} @@ -154,13 +160,13 @@ export class {{$contract.Type}} { abi: {{$contract.Type}}ABI, eventName: '{{.Original.Name}}' as const, }) - topics = encoded.map((t) => ({ values: [hexToBase64(t)] })) + topics = encoded.map((t) => ({ values: encodeTopicValue(t) })) } else if (filters.length === 1) { const f = filters[0] const args = { - {{- range $param := .Normalized.Inputs}} - {{- if $param.Indexed}} - {{$param.Name}}: f.{{$param.Name}}, + {{- range $i, $origParam := $event.Original.Inputs}} + {{- if $origParam.Indexed}} + {{$origParam.Name}}: f.{{(index $event.Normalized.Inputs $i).Name}}, {{- end}} {{- end}} } @@ -169,13 +175,13 @@ export class {{$contract.Type}} { eventName: '{{.Original.Name}}' as const, args, }) - topics = encoded.map((t) => ({ values: [hexToBase64(t)] })) + topics = encoded.map((t) => ({ values: encodeTopicValue(t) })) } else { const allEncoded = filters.map((f) => { const args = { - {{- range $param := .Normalized.Inputs}} - {{- if $param.Indexed}} - {{$param.Name}}: f.{{$param.Name}}, + {{- range $i, $origParam := $event.Original.Inputs}} + {{- if $origParam.Indexed}} + {{$origParam.Name}}: f.{{(index $event.Normalized.Inputs $i).Name}}, {{- end}} {{- end}} } @@ -186,7 +192,7 @@ export class {{$contract.Type}} { }) }) topics = allEncoded[0].map((_, i) => ({ - values: [...new Set(allEncoded.map((row) => hexToBase64(row[i])))], + values: [...new Set(allEncoded.flatMap((row) => encodeTopicValue(row[i])))], })) } const baseTrigger = this.client.logTrigger({ @@ -210,7 +216,7 @@ export class {{$contract.Type}} { const decoded = decodeEventLog({ abi: {{$contract.Type}}ABI, data: bytesToHex(log.data), - topics: log.topics.map((t) => bytesToHex(t)) as readonly Hex[], + topics: log.topics.map((t) => bytesToHex(t)) as [Hex, ...Hex[]], }) const { data: _, ...rest } = log return { ...rest, data: decoded.args as unknown as {{.Normalized.Name}}Decoded } diff --git a/cmd/generate-bindings/generate-bindings_test.go b/cmd/generate-bindings/generate-bindings_test.go index 6754f7c7..e7411fb4 100644 --- a/cmd/generate-bindings/generate-bindings_test.go +++ b/cmd/generate-bindings/generate-bindings_test.go @@ -1065,3 +1065,61 @@ func TestGenerateBindings_UnconventionalNaming(t *testing.T) { }) } } + +func TestGenerateBindings_StructNamePrefixStripping(t *testing.T) { + // ABI where internalType embeds the contract name as a namespace prefix + // (e.g. "struct MyContract.Config"). go-ethereum folds the dot so the Go + // name becomes "MyContractConfig". sanitizeStructNames must strip the + // prefix from both struct declarations and field type references. + contractABI := `[ + { + "type": "function", + "name": "getDON", + "inputs": [], + "outputs": [{ + "name": "", + "type": "tuple", + "internalType": "struct MyContract.DONInfo", + "components": [ + {"name": "id", "type": "uint32"}, + { + "name": "capabilityConfigurations", + "type": "tuple[]", + "internalType": "struct MyContract.CapabilityConfiguration[]", + "components": [ + {"name": "capabilityId", "type": "string"}, + {"name": "config", "type": "bytes"} + ] + } + ] + }], + "stateMutability": "view" + } + ]` + + tempDir, err := os.MkdirTemp("", "bindings-prefix-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + abiFile := filepath.Join(tempDir, "MyContract.abi") + err = os.WriteFile(abiFile, []byte(contractABI), 0600) + require.NoError(t, err) + + outFile := filepath.Join(tempDir, "bindings.go") + err = bindings.GenerateBindings("", abiFile, "mycontract", "MyContract", outFile) + require.NoError(t, err) + + content, err := os.ReadFile(outFile) + require.NoError(t, err) + src := string(content) + + // Struct declarations should have the prefix stripped. + assert.Contains(t, src, "type DONInfo struct") + assert.Contains(t, src, "type CapabilityConfiguration struct") + assert.NotContains(t, src, "type MyContractDONInfo struct") + assert.NotContains(t, src, "type MyContractCapabilityConfiguration struct") + + // Field type references inside structs should also be stripped. + assert.Contains(t, src, "[]CapabilityConfiguration") + assert.NotContains(t, src, "[]MyContractCapabilityConfiguration") +} diff --git a/cmd/login/login.go b/cmd/login/login.go index 9251c78b..01c271a3 100644 --- a/cmd/login/login.go +++ b/cmd/login/login.go @@ -2,17 +2,10 @@ package login import ( "context" - "crypto/rand" - "crypto/sha256" - "embed" - "encoding/base64" - "encoding/json" "fmt" - "io" "net" "net/http" "net/url" - "os/exec" rt "runtime" "strings" "time" @@ -24,28 +17,19 @@ import ( "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/oauth" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/ui" ) var ( - httpClient = &http.Client{Timeout: 10 * time.Second} - errorPage = "htmlPages/error.html" - successPage = "htmlPages/success.html" - waitingPage = "htmlPages/waiting.html" - stylePage = "htmlPages/output.css" - // OrgMembershipErrorSubstring is the error message substring returned by Auth0 // when a user doesn't belong to any organization during the auth flow. // This typically happens during sign-up when the organization hasn't been created yet. OrgMembershipErrorSubstring = "user does not belong to any organization" ) -//go:embed htmlPages/*.html -//go:embed htmlPages/*.css -var htmlFiles embed.FS - func New(runtimeCtx *runtime.Context) *cobra.Command { cmd := &cobra.Command{ Use: "login", @@ -102,7 +86,7 @@ func (h *handler) execute() error { // Use spinner for the token exchange h.spinner.Start("Exchanging authorization code...") - tokenSet, err := h.exchangeCodeForTokens(context.Background(), code) + tokenSet, err := oauth.ExchangeAuthorizationCode(context.Background(), nil, h.environmentSet, code, h.lastPKCEVerifier, "", "") if err != nil { h.spinner.StopAll() h.log.Error().Err(err).Msg("code exchange failed") @@ -162,13 +146,18 @@ func (h *handler) startAuthFlow() (string, error) { } }() - verifier, challenge, err := generatePKCE() + verifier, challenge, err := oauth.GeneratePKCE() if err != nil { h.spinner.Stop() return "", err } h.lastPKCEVerifier = verifier - h.lastState = randomState() + state, err := oauth.RandomState() + if err != nil { + h.spinner.Stop() + return "", err + } + h.lastState = state authURL := h.buildAuthURL(challenge, h.lastState) @@ -180,7 +169,7 @@ func (h *handler) startAuthFlow() (string, error) { ui.URL(authURL) ui.Line() - if err := openBrowser(authURL, rt.GOOS); err != nil { + if err := oauth.OpenBrowser(authURL, rt.GOOS); err != nil { ui.Warning("Could not open browser automatically") ui.Dim("Please open the URL above in your browser") ui.Line() @@ -199,19 +188,7 @@ func (h *handler) startAuthFlow() (string, error) { } func (h *handler) setupServer(codeCh chan string) (*http.Server, net.Listener, error) { - mux := http.NewServeMux() - mux.HandleFunc("/callback", h.callbackHandler(codeCh)) - - // TODO: Add a fallback port in case the default port is in use - listener, err := net.Listen("tcp", constants.AuthListenAddr) - if err != nil { - return nil, nil, fmt.Errorf("failed to listen on %s: %w", constants.AuthListenAddr, err) - } - - return &http.Server{ - Handler: mux, - ReadHeaderTimeout: 5 * time.Second, - }, listener, nil + return oauth.NewCallbackHTTPServer(constants.AuthListenAddr, h.callbackHandler(codeCh)) } func (h *handler) callbackHandler(codeCh chan string) http.HandlerFunc { @@ -225,120 +202,58 @@ func (h *handler) callbackHandler(codeCh chan string) http.HandlerFunc { if strings.Contains(errorDesc, OrgMembershipErrorSubstring) { if h.retryCount >= maxOrgNotFoundRetries { h.log.Error().Int("retries", h.retryCount).Msg("organization setup timed out after maximum retries") - h.serveEmbeddedHTML(w, errorPage, http.StatusBadRequest) + oauth.ServeEmbeddedHTML(h.log, w, oauth.PageError, http.StatusBadRequest) return } // Generate new authentication credentials for the retry - verifier, challenge, err := generatePKCE() + verifier, challenge, err := oauth.GeneratePKCE() if err != nil { h.log.Error().Err(err).Msg("failed to prepare authentication retry") - h.serveEmbeddedHTML(w, errorPage, http.StatusInternalServerError) + oauth.ServeEmbeddedHTML(h.log, w, oauth.PageError, http.StatusInternalServerError) return } h.lastPKCEVerifier = verifier - h.lastState = randomState() + st, err := oauth.RandomState() + if err != nil { + h.log.Error().Err(err).Msg("failed to generate OAuth state for retry") + oauth.ServeEmbeddedHTML(h.log, w, oauth.PageError, http.StatusInternalServerError) + return + } + h.lastState = st h.retryCount++ // Build the new auth URL for redirect authURL := h.buildAuthURL(challenge, h.lastState) h.log.Debug().Int("attempt", h.retryCount).Int("max", maxOrgNotFoundRetries).Msg("organization setup in progress, retrying") - h.serveWaitingPage(w, authURL) + oauth.ServeWaitingPage(h.log, w, authURL) return } // Generic Auth0 error h.log.Error().Str("error", errorParam).Str("description", errorDesc).Msg("auth error in callback") - h.serveEmbeddedHTML(w, errorPage, http.StatusBadRequest) + oauth.ServeEmbeddedHTML(h.log, w, oauth.PageError, http.StatusBadRequest) return } if st := r.URL.Query().Get("state"); st == "" || h.lastState == "" || st != h.lastState { h.log.Error().Msg("invalid state in response") - h.serveEmbeddedHTML(w, errorPage, http.StatusBadRequest) + oauth.ServeEmbeddedHTML(h.log, w, oauth.PageError, http.StatusBadRequest) return } code := r.URL.Query().Get("code") if code == "" { h.log.Error().Msg("no code in response") - h.serveEmbeddedHTML(w, errorPage, http.StatusBadRequest) + oauth.ServeEmbeddedHTML(h.log, w, oauth.PageError, http.StatusBadRequest) return } - h.serveEmbeddedHTML(w, successPage, http.StatusOK) + oauth.ServeEmbeddedHTML(h.log, w, oauth.PageSuccess, http.StatusOK) codeCh <- code } } -func (h *handler) serveEmbeddedHTML(w http.ResponseWriter, filePath string, status int) { - htmlContent, err := htmlFiles.ReadFile(filePath) - if err != nil { - h.log.Error().Err(err).Str("file", filePath).Msg("failed to read embedded HTML file") - h.sendHTTPError(w) - return - } - - cssContent, err := htmlFiles.ReadFile(stylePage) - if err != nil { - h.log.Error().Err(err).Str("file", stylePage).Msg("failed to read embedded CSS file") - h.sendHTTPError(w) - return - } - - modified := strings.Replace( - string(htmlContent), - ``, - fmt.Sprintf("", string(cssContent)), - 1, - ) - - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(status) - if _, err := w.Write([]byte(modified)); err != nil { - h.log.Error().Err(err).Msg("failed to write HTML response") - } -} - -// serveWaitingPage serves the waiting page with the redirect URL injected. -// This is used when handling organization membership errors during sign-up flow. -func (h *handler) serveWaitingPage(w http.ResponseWriter, redirectURL string) { - htmlContent, err := htmlFiles.ReadFile(waitingPage) - if err != nil { - h.log.Error().Err(err).Str("file", waitingPage).Msg("failed to read waiting page HTML file") - h.sendHTTPError(w) - return - } - - cssContent, err := htmlFiles.ReadFile(stylePage) - if err != nil { - h.log.Error().Err(err).Str("file", stylePage).Msg("failed to read embedded CSS file") - h.sendHTTPError(w) - return - } - - // Inject CSS inline - modified := strings.Replace( - string(htmlContent), - ``, - fmt.Sprintf("", string(cssContent)), - 1, - ) - - // Inject the redirect URL - modified = strings.Replace(modified, "{{REDIRECT_URL}}", redirectURL, 1) - - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte(modified)); err != nil { - h.log.Error().Err(err).Msg("failed to write waiting page response") - } -} - -func (h *handler) sendHTTPError(w http.ResponseWriter) { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) -} - func (h *handler) buildAuthURL(codeChallenge, state string) string { params := url.Values{} params.Set("client_id", h.environmentSet.ClientID) @@ -355,41 +270,6 @@ func (h *handler) buildAuthURL(codeChallenge, state string) string { return h.environmentSet.AuthBase + constants.AuthAuthorizePath + "?" + params.Encode() } -func (h *handler) exchangeCodeForTokens(ctx context.Context, code string) (*credentials.CreLoginTokenSet, error) { - form := url.Values{} - form.Set("grant_type", "authorization_code") - form.Set("client_id", h.environmentSet.ClientID) - form.Set("code", code) - form.Set("redirect_uri", constants.AuthRedirectURI) - form.Set("code_verifier", h.lastPKCEVerifier) - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.environmentSet.AuthBase+constants.AuthTokenPath, strings.NewReader(form.Encode())) - if err != nil { - return nil, fmt.Errorf("create request: %w", err) - } - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - resp, err := httpClient.Do(req) // #nosec G704 -- URL is from trusted environment config - if err != nil { - return nil, fmt.Errorf("perform request: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) - if err != nil { - return nil, fmt.Errorf("read response: %w", err) - } - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body) - } - - var tokenSet credentials.CreLoginTokenSet - if err := json.Unmarshal(body, &tokenSet); err != nil { - return nil, fmt.Errorf("unmarshal token set: %w", err) - } - return &tokenSet, nil -} - func (h *handler) fetchTenantConfig(tokenSet *credentials.CreLoginTokenSet) error { creds := &credentials.Credentials{ Tokens: tokenSet, @@ -404,35 +284,3 @@ func (h *handler) fetchTenantConfig(tokenSet *credentials.CreLoginTokenSet) erro return tenantctx.FetchAndWriteContext(context.Background(), gqlClient, envName, h.log) } - -func openBrowser(urlStr string, goos string) error { - switch goos { - case "darwin": - return exec.Command("open", urlStr).Start() - case "linux": - return exec.Command("xdg-open", urlStr).Start() - case "windows": - return exec.Command("rundll32", "url.dll,FileProtocolHandler", urlStr).Start() - default: - return fmt.Errorf("unsupported OS: %s", goos) - } -} - -func generatePKCE() (verifier, challenge string, err error) { - b := make([]byte, 32) - if _, err = rand.Read(b); err != nil { - return "", "", err - } - verifier = base64.RawURLEncoding.EncodeToString(b) - sum := sha256.Sum256([]byte(verifier)) - challenge = base64.RawURLEncoding.EncodeToString(sum[:]) - return verifier, challenge, nil -} - -func randomState() string { - b := make([]byte, 16) - if _, err := rand.Read(b); err != nil { - return fmt.Sprintf("%d", time.Now().UnixNano()) - } - return base64.RawURLEncoding.EncodeToString(b) -} diff --git a/cmd/login/login_test.go b/cmd/login/login_test.go index 782f2d18..34b4e6ec 100644 --- a/cmd/login/login_test.go +++ b/cmd/login/login_test.go @@ -14,6 +14,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/oauth" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -51,9 +52,9 @@ func TestSaveCredentials_WritesYAML(t *testing.T) { } func TestGeneratePKCE_ReturnsValidChallenge(t *testing.T) { - verifier, challenge, err := generatePKCE() + verifier, challenge, err := oauth.GeneratePKCE() if err != nil { - t.Fatalf("generatePKCE error: %v", err) + t.Fatalf("GeneratePKCE error: %v", err) } if verifier == "" || challenge == "" { t.Error("PKCE verifier or challenge is empty") @@ -61,8 +62,14 @@ func TestGeneratePKCE_ReturnsValidChallenge(t *testing.T) { } func TestRandomState_IsRandomAndNonEmpty(t *testing.T) { - state1 := randomState() - state2 := randomState() + state1, err := oauth.RandomState() + if err != nil { + t.Fatalf("RandomState: %v", err) + } + state2, err := oauth.RandomState() + if err != nil { + t.Fatalf("RandomState: %v", err) + } if state1 == "" || state2 == "" { t.Error("randomState returned empty string") } @@ -72,16 +79,16 @@ func TestRandomState_IsRandomAndNonEmpty(t *testing.T) { } func TestOpenBrowser_UnsupportedOS(t *testing.T) { - err := openBrowser("http://example.com", "plan9") + err := oauth.OpenBrowser("http://example.com", "plan9") if err == nil || !strings.Contains(err.Error(), "unsupported OS") { t.Errorf("expected unsupported OS error, got %v", err) } } func TestServeEmbeddedHTML_ErrorOnMissingFile(t *testing.T) { - h := &handler{log: &zerolog.Logger{}, spinner: ui.NewSpinner()} + log := zerolog.Nop() w := httptest.NewRecorder() - h.serveEmbeddedHTML(w, "htmlPages/doesnotexist.html", http.StatusOK) + oauth.ServeEmbeddedHTML(&log, w, "htmlPages/doesnotexist.html", http.StatusOK) resp := w.Result() if resp.StatusCode != http.StatusInternalServerError { t.Errorf("expected 500 error, got %d", resp.StatusCode) @@ -274,12 +281,11 @@ func TestCallbackHandler_GenericAuth0Error(t *testing.T) { func TestServeWaitingPage(t *testing.T) { logger := zerolog.Nop() - h := &handler{log: &logger, spinner: ui.NewSpinner()} w := httptest.NewRecorder() redirectURL := "https://auth.example.com/authorize?client_id=test&state=abc123" - h.serveWaitingPage(w, redirectURL) + oauth.ServeWaitingPage(&logger, w, redirectURL) resp := w.Result() body, _ := io.ReadAll(resp.Body) diff --git a/cmd/registry/list/list.go b/cmd/registry/list/list.go new file mode 100644 index 00000000..1ea5247d --- /dev/null +++ b/cmd/registry/list/list.go @@ -0,0 +1,47 @@ +package list + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +func New(runtimeContext *runtime.Context) *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "Lists available workflow registries for the current environment", + Long: `Displays the registries configured for your organization, including type and address.`, + Example: `cre registry list`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if runtimeContext.TenantContext == nil { + return fmt.Errorf("user context not available — run `cre login` and retry") + } + + registries := runtimeContext.TenantContext.Registries + if len(registries) == 0 { + ui.Warning("No registries found for this environment") + return nil + } + + ui.Line() + ui.Bold("Registries available to your organization") + ui.Line() + + for _, r := range registries { + ui.Bold(r.Label) + ui.Dim(fmt.Sprintf("ID: %s", r.ID)) + ui.Dim(fmt.Sprintf("Type: %s", r.Type)) + if r.Address != nil && *r.Address != "" { + ui.Dim(fmt.Sprintf("Addr: %s", *r.Address)) + } + ui.Line() + } + + return nil + }, + } +} diff --git a/cmd/registry/list/list_test.go b/cmd/registry/list/list_test.go new file mode 100644 index 00000000..131f3fbe --- /dev/null +++ b/cmd/registry/list/list_test.go @@ -0,0 +1,175 @@ +package list_test + +import ( + "io" + "os" + "strings" + "testing" + + "github.com/rs/zerolog" + + "github.com/smartcontractkit/cre-cli/cmd/registry/list" + "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" +) + +func strPtr(s string) *string { return &s } + +func TestList_NoTenantContext(t *testing.T) { + logger := zerolog.New(io.Discard) + rtCtx := &runtime.Context{ + Logger: &logger, + EnvironmentSet: &environments.EnvironmentSet{EnvName: "STAGING"}, + TenantContext: nil, + } + + cmd := list.New(rtCtx) + cmd.SetArgs([]string{}) + err := cmd.Execute() + if err == nil { + t.Fatal("expected error when TenantContext is nil") + } + if !strings.Contains(err.Error(), "user context not available") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestList_EmptyRegistries(t *testing.T) { + logger := zerolog.New(io.Discard) + rtCtx := &runtime.Context{ + Logger: &logger, + EnvironmentSet: &environments.EnvironmentSet{EnvName: "STAGING"}, + TenantContext: &tenantctx.EnvironmentContext{ + Registries: []*tenantctx.Registry{}, + }, + } + + cmd := list.New(rtCtx) + cmd.SetArgs([]string{}) + + // suppress stderr (ui.Warning writes there) + oldStderr := os.Stderr + os.Stderr, _ = os.Open(os.DevNull) + defer func() { os.Stderr = oldStderr }() + + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestList_OnChainAndOffChain(t *testing.T) { + logger := zerolog.New(io.Discard) + rtCtx := &runtime.Context{ + Logger: &logger, + EnvironmentSet: &environments.EnvironmentSet{EnvName: "STAGING"}, + TenantContext: &tenantctx.EnvironmentContext{ + Registries: []*tenantctx.Registry{ + { + ID: "onchain:ethereum-testnet-sepolia", + Label: "ethereum-testnet-sepolia (0xaE55...1135)", + Type: "on-chain", + Address: strPtr("0xaE55eB3EDAc48a1163EE2cbb1205bE1e90Ea1135"), + }, + { + ID: "private", + Label: "Private (Chainlink-hosted)", + Type: "off-chain", + }, + }, + }, + } + + cmd := list.New(rtCtx) + cmd.SetArgs([]string{}) + + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + if err := cmd.Execute(); err != nil { + w.Close() + os.Stdout = oldStdout + t.Fatalf("unexpected error: %v", err) + } + + w.Close() + os.Stdout = oldStdout + var buf strings.Builder + _, _ = io.Copy(&buf, r) + output := buf.String() + + for _, want := range []string{ + "Registries available to your organization", + "onchain:ethereum-testnet-sepolia", + "on-chain", + "0xaE55eB3EDAc48a1163EE2cbb1205bE1e90Ea1135", + "private", + "off-chain", + "Private (Chainlink-hosted)", + } { + if !strings.Contains(output, want) { + t.Errorf("output missing %q; full output:\n%s", want, output) + } + } +} + +func TestList_OffChainNoAddress(t *testing.T) { + logger := zerolog.New(io.Discard) + rtCtx := &runtime.Context{ + Logger: &logger, + EnvironmentSet: &environments.EnvironmentSet{EnvName: "STAGING"}, + TenantContext: &tenantctx.EnvironmentContext{ + Registries: []*tenantctx.Registry{ + { + ID: "private", + Label: "Private", + Type: "off-chain", + }, + }, + }, + } + + cmd := list.New(rtCtx) + cmd.SetArgs([]string{}) + + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + if err := cmd.Execute(); err != nil { + w.Close() + os.Stdout = oldStdout + t.Fatalf("unexpected error: %v", err) + } + + w.Close() + os.Stdout = oldStdout + var buf strings.Builder + _, _ = io.Copy(&buf, r) + output := buf.String() + + if strings.Contains(output, "Addr:") { + t.Errorf("expected no Addr line for off-chain registry; output:\n%s", output) + } +} + +func TestList_RejectsArgs(t *testing.T) { + logger := zerolog.New(io.Discard) + rtCtx := &runtime.Context{ + Logger: &logger, + EnvironmentSet: &environments.EnvironmentSet{}, + TenantContext: &tenantctx.EnvironmentContext{}, + } + + cmd := list.New(rtCtx) + cmd.SetArgs([]string{"extra"}) + + // cobra prints usage to stderr on arg errors; suppress + cmd.SilenceUsage = true + cmd.SilenceErrors = true + + if err := cmd.Execute(); err == nil { + t.Fatal("expected error when extra args provided") + } +} diff --git a/cmd/registry/registry.go b/cmd/registry/registry.go new file mode 100644 index 00000000..440df4fe --- /dev/null +++ b/cmd/registry/registry.go @@ -0,0 +1,20 @@ +package registry + +import ( + "github.com/spf13/cobra" + + "github.com/smartcontractkit/cre-cli/cmd/registry/list" + "github.com/smartcontractkit/cre-cli/internal/runtime" +) + +func New(runtimeContext *runtime.Context) *cobra.Command { + registryCmd := &cobra.Command{ + Use: "registry", + Short: "Manages workflow registries", + Long: `The registry command lets you view and inspect the workflow registries available for your organization.`, + } + + registryCmd.AddCommand(list.New(runtimeContext)) + + return registryCmd +} diff --git a/cmd/root.go b/cmd/root.go index f980bbc9..889f24ab 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,6 +19,7 @@ import ( generatebindings "github.com/smartcontractkit/cre-cli/cmd/generate-bindings" "github.com/smartcontractkit/cre-cli/cmd/login" "github.com/smartcontractkit/cre-cli/cmd/logout" + "github.com/smartcontractkit/cre-cli/cmd/registry" "github.com/smartcontractkit/cre-cli/cmd/secrets" "github.com/smartcontractkit/cre-cli/cmd/templates" "github.com/smartcontractkit/cre-cli/cmd/update" @@ -242,6 +243,11 @@ func newRootCommand() *cobra.Command { if showSpinner { spinner.Update("Loading settings...") } + // Capture the invocation directory before SetExecutionContext changes it. + if invocationDir, err := os.Getwd(); err == nil { + runtimeContext.InvocationDir = invocationDir + } + // Set execution context (project root + workflow directory if applicable) projectRootFlag := runtimeContext.Viper.GetString(settings.Flags.ProjectRoot.Name) if err := context.SetExecutionContext(cmd, args, projectRootFlag, rootLogger); err != nil { @@ -261,6 +267,14 @@ func newRootCommand() *cobra.Command { return fmt.Errorf("%w", err) } + if err := runtimeContext.AttachResolvedRegistry(); err != nil { + return err + } + + if err := runtimeContext.FinalizeDeferredWorkflowOwner(cmd); err != nil { + return err + } + // Restart spinner for remaining initialization if showSpinner { spinner = ui.NewSpinner() @@ -389,17 +403,20 @@ func newRootCommand() *cobra.Command { whoamiCmd := whoami.New(runtimeContext) updateCmd := update.New(runtimeContext) templatesCmd := templates.New(runtimeContext) + registryCmd := registry.New(runtimeContext) secretsCmd.RunE = helpRunE workflowCmd.RunE = helpRunE accountCmd.RunE = helpRunE templatesCmd.RunE = helpRunE + registryCmd.RunE = helpRunE // Define groups (order controls display order) rootCmd.AddGroup(&cobra.Group{ID: "getting-started", Title: "Getting Started"}) rootCmd.AddGroup(&cobra.Group{ID: "account", Title: "Account"}) rootCmd.AddGroup(&cobra.Group{ID: "workflow", Title: "Workflow"}) rootCmd.AddGroup(&cobra.Group{ID: "secret", Title: "Secret"}) + rootCmd.AddGroup(&cobra.Group{ID: "registry", Title: "Registry"}) initCmd.GroupID = "getting-started" templatesCmd.GroupID = "getting-started" @@ -411,6 +428,7 @@ func newRootCommand() *cobra.Command { secretsCmd.GroupID = "secret" workflowCmd.GroupID = "workflow" + registryCmd.GroupID = "registry" rootCmd.AddCommand( initCmd, @@ -421,6 +439,7 @@ func newRootCommand() *cobra.Command { whoamiCmd, secretsCmd, workflowCmd, + registryCmd, genBindingsCmd, updateCmd, templatesCmd, @@ -432,32 +451,35 @@ func newRootCommand() *cobra.Command { func isLoadSettings(cmd *cobra.Command) bool { // It is not expected to have the settings file when running the following commands var excludedCommands = map[string]struct{}{ - "cre version": {}, - "cre login": {}, - "cre logout": {}, - "cre whoami": {}, - "cre account access": {}, - "cre account list-key": {}, - "cre init": {}, - "cre generate-bindings": {}, - "cre completion bash": {}, - "cre completion fish": {}, - "cre completion powershell": {}, - "cre completion zsh": {}, - "cre help": {}, - "cre update": {}, - "cre workflow": {}, - "cre workflow custom-build": {}, - "cre workflow limits": {}, - "cre workflow limits export": {}, - "cre workflow build": {}, - "cre account": {}, - "cre secrets": {}, - "cre templates": {}, - "cre templates list": {}, - "cre templates add": {}, - "cre templates remove": {}, - "cre": {}, + "cre version": {}, + "cre login": {}, + "cre logout": {}, + "cre whoami": {}, + "cre account access": {}, + "cre account list-key": {}, + "cre init": {}, + "cre generate-bindings": {}, + "cre completion bash": {}, + "cre completion fish": {}, + "cre completion powershell": {}, + "cre completion zsh": {}, + "cre help": {}, + "cre update": {}, + "cre workflow": {}, + "cre workflow supported-chains": {}, + "cre workflow custom-build": {}, + "cre workflow limits": {}, + "cre workflow limits export": {}, + "cre workflow build": {}, + "cre account": {}, + "cre secrets": {}, + "cre templates": {}, + "cre templates list": {}, + "cre templates add": {}, + "cre templates remove": {}, + "cre registry": {}, + "cre registry list": {}, + "cre": {}, } _, exists := excludedCommands[cmd.CommandPath()] @@ -503,11 +525,6 @@ func isLoadDeploymentRPC(cmd *cobra.Command) bool { "cre workflow delete": {}, "cre account link-key": {}, "cre account unlink-key": {}, - "cre secrets create": {}, - "cre secrets delete": {}, - "cre secrets execute": {}, - "cre secrets list": {}, - "cre secrets update": {}, } _, exists := includedCommands[cmd.CommandPath()] return exists diff --git a/cmd/secrets/common/browser_flow.go b/cmd/secrets/common/browser_flow.go new file mode 100644 index 00000000..a979eeda --- /dev/null +++ b/cmd/secrets/common/browser_flow.go @@ -0,0 +1,251 @@ +package common + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + rt "runtime" + "strings" + "time" + + "github.com/google/uuid" + "github.com/machinebox/graphql" + + "github.com/smartcontractkit/chainlink-common/pkg/capabilities/actions/vault" + "github.com/smartcontractkit/chainlink-common/pkg/jsonrpc2" + "github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaulttypes" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/oauth" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +const createVaultAuthURLMutation = `mutation CreateVaultAuthorizationUrl($request: VaultAuthorizationUrlRequest!) { + createVaultAuthorizationUrl(request: $request) { + url + } +}` + +const exchangeAuthCodeToTokenMutation = `mutation ExchangeAuthCodeToToken($request: AuthCodeTokenExchangeRequest!) { + exchangeAuthCodeToToken(request: $request) { + accessToken + expiresIn + } +}` + +// vaultPermissionForMethod returns the API permission name for the given vault operation. +// Names match the VaultPermission enum in platform GraphQL (createVaultAuthorizationUrl). +func vaultPermissionForMethod(method string) (string, error) { + switch method { + case vaulttypes.MethodSecretsCreate: + return "VAULT_PERMISSION_CREATE_SECRETS", nil + case vaulttypes.MethodSecretsUpdate: + return "VAULT_PERMISSION_UPDATE_SECRETS", nil + case vaulttypes.MethodSecretsDelete: + return "VAULT_PERMISSION_DELETE_SECRETS", nil + case vaulttypes.MethodSecretsList: + return "VAULT_PERMISSION_LIST_SECRETS", nil + default: + return "", fmt.Errorf("unsupported method: %s", method) + } +} + +func digestHexString(digest [32]byte) string { + return "0x" + hex.EncodeToString(digest[:]) +} + +// executeBrowserUpsert handles secrets create/update when the user signs in with their organization account. +// It encrypts the payload, binds a digest, requests a platform authorization URL, completes OAuth in the browser, +// exchanges the code for a short-lived vault JWT, and POSTs the same JSON-RPC body to the gateway with Bearer auth. +// Login tokens in ~/.cre/cre.yaml are not modified; that session stays separate from this vault-only token. +func (h *Handler) executeBrowserUpsert(ctx context.Context, inputs UpsertSecretsInputs, method string) error { + if h.Credentials.AuthType == credentials.AuthTypeApiKey { + return fmt.Errorf("this sign-in flow requires an interactive login; API keys are not supported") + } + orgID := h.Credentials.OrgID + if orgID == "" { + return fmt.Errorf("organization information is missing from your session; sign in again or use owner-key-signing") + } + + ui.Dim("Using your account to authorize vault access for your organization...") + + encSecrets, err := h.EncryptSecretsForBrowserOrg(inputs, orgID) + if err != nil { + return fmt.Errorf("failed to encrypt secrets: %w", err) + } + requestID := uuid.New().String() + + var ( + digest [32]byte + requestBody []byte + ) + + switch method { + case vaulttypes.MethodSecretsCreate: + req := jsonrpc2.Request[vault.CreateSecretsRequest]{ + Version: jsonrpc2.JsonRpcVersion, + ID: requestID, + Method: method, + Params: &vault.CreateSecretsRequest{ + RequestId: requestID, + EncryptedSecrets: encSecrets, + }, + } + digest, err = CalculateDigest(req) + if err != nil { + return fmt.Errorf("failed to calculate create digest: %w", err) + } + requestBody, err = json.Marshal(req) + if err != nil { + return fmt.Errorf("failed to marshal JSON-RPC request: %w", err) + } + + case vaulttypes.MethodSecretsUpdate: + req := jsonrpc2.Request[vault.UpdateSecretsRequest]{ + Version: jsonrpc2.JsonRpcVersion, + ID: requestID, + Method: method, + Params: &vault.UpdateSecretsRequest{ + RequestId: requestID, + EncryptedSecrets: encSecrets, + }, + } + digest, err = CalculateDigest(req) + if err != nil { + return fmt.Errorf("failed to calculate update digest: %w", err) + } + requestBody, err = json.Marshal(req) + if err != nil { + return fmt.Errorf("failed to marshal JSON-RPC request: %w", err) + } + + default: + return fmt.Errorf("unsupported method %q (expected %q or %q)", method, vaulttypes.MethodSecretsCreate, vaulttypes.MethodSecretsUpdate) + } + + return h.ExecuteBrowserVaultAuthorization(ctx, method, digest, requestBody) +} + +// ExecuteBrowserVaultAuthorization completes platform OAuth for a vault JSON-RPC digest (create/update/delete/list), +// then POSTs the same request body to the gateway with the vault JWT in the Authorization header. +func (h *Handler) ExecuteBrowserVaultAuthorization(ctx context.Context, method string, digest [32]byte, requestBody []byte) error { + if h.Credentials.AuthType == credentials.AuthTypeApiKey { + return fmt.Errorf("this sign-in flow requires an interactive login; API keys are not supported") + } + if len(requestBody) == 0 { + return fmt.Errorf("empty vault request body") + } + + perm, err := vaultPermissionForMethod(method) + if err != nil { + return err + } + + verifier, challenge, err := oauth.GeneratePKCE() + if err != nil { + return err + } + + gqlClient := graphqlclient.New(h.Credentials, h.EnvironmentSet, h.Log) + gqlReq := graphql.NewRequest(createVaultAuthURLMutation) + reqVars := map[string]any{ + "codeChallenge": challenge, + "redirectUri": constants.AuthRedirectURI, + "requestDigest": digestHexString(digest), + "permission": perm, + } + // Optional: bind authorization to workflow owner when configured (omit if unset). + if w := strings.TrimSpace(h.OwnerAddress); w != "" { + reqVars["workflowOwnerAddress"] = w + } + gqlReq.Var("request", reqVars) + + var gqlResp struct { + CreateVaultAuthorizationURL struct { + URL string `json:"url"` + } `json:"createVaultAuthorizationUrl"` + } + if err := gqlClient.Execute(ctx, gqlReq, &gqlResp); err != nil { + return fmt.Errorf("could not complete the authorization request") + } + authURL := gqlResp.CreateVaultAuthorizationURL.URL + if authURL == "" { + return fmt.Errorf("could not complete the authorization request") + } + + platformState, _ := oauth.StateFromAuthorizeURL(authURL) + + codeCh := make(chan string, 1) + server, listener, err := oauth.NewCallbackHTTPServer(constants.AuthListenAddr, oauth.SecretsCallbackHandler(codeCh, platformState, h.Log)) + if err != nil { + return fmt.Errorf("could not start local callback server: %w", err) + } + defer func() { + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + _ = server.Shutdown(shutdownCtx) + }() + + go func() { + if err := server.Serve(listener); err != nil && err != http.ErrServerClosed { + h.Log.Error().Err(err).Msg("secrets oauth callback server error") + } + }() + + ui.Dim("Opening your browser to complete sign-in...") + if err := oauth.OpenBrowser(authURL, rt.GOOS); err != nil { + ui.Warning("Could not open browser automatically") + ui.Dim("Open this URL in your browser:") + } + ui.URL(authURL) + ui.Line() + ui.Dim("Waiting for authorization... (Press Ctrl+C to cancel)") + + var code string + select { + case code = <-codeCh: + case <-time.After(500 * time.Second): + return fmt.Errorf("timeout waiting for authorization") + case <-ctx.Done(): + return ctx.Err() + } + + ui.Dim("Completing vault authorization...") + exchangeReq := graphql.NewRequest(exchangeAuthCodeToTokenMutation) + exchangeReq.Var("request", map[string]any{ + "code": code, + "codeVerifier": verifier, + "redirectUri": constants.AuthRedirectURI, + }) + var exchangeResp struct { + ExchangeAuthCodeToToken struct { + AccessToken string `json:"accessToken"` + ExpiresIn int `json:"expiresIn"` + } `json:"exchangeAuthCodeToToken"` + } + if err := gqlClient.Execute(ctx, exchangeReq, &exchangeResp); err != nil { + return fmt.Errorf("token exchange failed: %w", err) + } + tok := exchangeResp.ExchangeAuthCodeToToken + if tok.AccessToken == "" { + return fmt.Errorf("token exchange failed: empty access token") + } + return h.postVaultGatewayWithBearer(method, requestBody, tok.AccessToken) +} + +// postVaultGatewayWithBearer POSTs the digest-bound JSON-RPC body with the vault JWT and parses the gateway response. +func (h *Handler) postVaultGatewayWithBearer(method string, requestBody []byte, accessToken string) error { + ui.Dim("Submitting request to vault gateway...") + respBody, status, err := h.Gw.PostWithBearer(requestBody, accessToken) + if err != nil { + return fmt.Errorf("gateway POST failed: %w", err) + } + if status != http.StatusOK { + return fmt.Errorf("gateway returned a non-200 status code: status_code=%d, body=%s", status, respBody) + } + return h.ParseVaultGatewayResponse(method, respBody) +} diff --git a/cmd/secrets/common/browser_flow_test.go b/cmd/secrets/common/browser_flow_test.go new file mode 100644 index 00000000..7d80447e --- /dev/null +++ b/cmd/secrets/common/browser_flow_test.go @@ -0,0 +1,135 @@ +package common + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "io" + "net/http" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink/v2/core/capabilities/vault/vaulttypes" + + "github.com/smartcontractkit/cre-cli/internal/oauth" +) + +func TestVaultPermissionForMethod(t *testing.T) { + p, err := vaultPermissionForMethod(vaulttypes.MethodSecretsCreate) + require.NoError(t, err) + assert.Equal(t, "VAULT_PERMISSION_CREATE_SECRETS", p) + + p, err = vaultPermissionForMethod(vaulttypes.MethodSecretsUpdate) + require.NoError(t, err) + assert.Equal(t, "VAULT_PERMISSION_UPDATE_SECRETS", p) + + p, err = vaultPermissionForMethod(vaulttypes.MethodSecretsDelete) + require.NoError(t, err) + assert.Equal(t, "VAULT_PERMISSION_DELETE_SECRETS", p) + + p, err = vaultPermissionForMethod(vaulttypes.MethodSecretsList) + require.NoError(t, err) + assert.Equal(t, "VAULT_PERMISSION_LIST_SECRETS", p) + + _, err = vaultPermissionForMethod("vault/secrets/unknown") + require.Error(t, err) +} + +func TestDigestHexString(t *testing.T) { + var d [32]byte + copy(d[:], []byte{1, 2, 3}) + assert.Equal(t, "0x0102030000000000000000000000000000000000000000000000000000000000", digestHexString(d)) +} + +// TestBrowserFlowPKCE checks PKCE S256 (RFC 7636) used by the browser secrets authorization step. +func TestBrowserFlowPKCE(t *testing.T) { + verifier, challenge, err := oauth.GeneratePKCE() + require.NoError(t, err) + require.NotEmpty(t, verifier) + require.NotEmpty(t, challenge) + + sum := sha256.Sum256([]byte(verifier)) + decoded, err := base64.RawURLEncoding.DecodeString(challenge) + require.NoError(t, err) + assert.Equal(t, sum[:], decoded) +} + +// postVaultGatewayWithBearer is the code path used after browser OAuth token exchange; it should stay aligned +// with owner-key gateway POST + ParseVaultGatewayResponse (minus allowlist retries). + +func TestPostVaultGatewayWithBearer_CreateParsesResponse(t *testing.T) { + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + var logBuf bytes.Buffer + h := newTestHandler(&logBuf) + h.Gw = &mockGatewayClient{ + post: func(gotBody []byte) ([]byte, int, error) { + assert.Contains(t, string(gotBody), "jsonrpc") + return encodeRPCBodyFromPayload(buildCreatePayloadProto(t)), http.StatusOK, nil + }, + } + + err := h.postVaultGatewayWithBearer(vaulttypes.MethodSecretsCreate, []byte(`{"jsonrpc":"2.0","id":"1","method":"x"}`), "vault-jwt") + w.Close() + os.Stdout = oldStdout + var out strings.Builder + _, _ = io.Copy(&out, r) + + require.NoError(t, err) + assert.Contains(t, out.String(), "Secret created") +} + +func TestPostVaultGatewayWithBearer_ListParsesResponse(t *testing.T) { + oldStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + h := newTestHandler(nil) + h.Gw = &mockGatewayClient{ + post: func([]byte) ([]byte, int, error) { + return encodeRPCBodyFromPayload(buildListPayloadProtoSuccessWithItems(t)), http.StatusOK, nil + }, + } + + err := h.postVaultGatewayWithBearer(vaulttypes.MethodSecretsList, []byte(`{}`), "t") + w.Close() + os.Stdout = oldStdout + var out strings.Builder + _, _ = io.Copy(&out, r) + + require.NoError(t, err) + assert.Contains(t, out.String(), "Secret identifier") +} + +func TestPostVaultGatewayWithBearer_GatewayNon200(t *testing.T) { + h, _, _ := newMockHandler(t) + h.Gw = &mockGatewayClient{ + post: func([]byte) ([]byte, int, error) { + return []byte(`denied`), http.StatusForbidden, nil + }, + } + + err := h.postVaultGatewayWithBearer(vaulttypes.MethodSecretsDelete, []byte(`{}`), "t") + require.Error(t, err) + assert.Contains(t, err.Error(), "non-200") + assert.Contains(t, err.Error(), "403") +} + +func TestPostVaultGatewayWithBearer_InvalidJSONRPC(t *testing.T) { + h, _, _ := newMockHandler(t) + h.Gw = &mockGatewayClient{ + post: func([]byte) ([]byte, int, error) { + return []byte(`not-json`), http.StatusOK, nil + }, + } + + err := h.postVaultGatewayWithBearer(vaulttypes.MethodSecretsUpdate, []byte(`{}`), "t") + require.Error(t, err) + assert.Contains(t, err.Error(), "unmarshal") +} diff --git a/cmd/secrets/common/gateway.go b/cmd/secrets/common/gateway.go index 22c1e434..4043d61f 100644 --- a/cmd/secrets/common/gateway.go +++ b/cmd/secrets/common/gateway.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "strings" "time" "github.com/avast/retry-go/v4" @@ -14,6 +15,8 @@ import ( type GatewayClient interface { Post(body []byte) (respBody []byte, status int, err error) + // PostWithBearer sends the JSON-RPC body with Authorization: Bearer for the browser OAuth flow (no allowlist retries). + PostWithBearer(body []byte, bearerToken string) (respBody []byte, status int, err error) } type HTTPClient struct { @@ -74,6 +77,48 @@ func (g *HTTPClient) Post(body []byte) ([]byte, int, error) { return respBody, status, nil } +func (g *HTTPClient) PostWithBearer(body []byte, bearerToken string) ([]byte, int, error) { + if strings.TrimSpace(bearerToken) == "" { + return nil, 0, fmt.Errorf("empty bearer token") + } + attempts := g.RetryAttempts + if attempts == 0 { + attempts = 3 + } + delay := g.RetryDelay + if delay == 0 { + delay = 4 * time.Second + } + + var respBody []byte + var status int + + err := retry.Do( + func() error { + b, s, e := g.postOnceWithBearer(body, bearerToken) + respBody, status = b, s + if e != nil { + return fmt.Errorf("gateway request failed: %w", e) + } + if s != http.StatusOK { + return retry.Unrecoverable(fmt.Errorf("gateway returned non-200: status_code=%d, body=%s", s, string(respBody))) + } + return nil + }, + retry.Attempts(uint(attempts)), + retry.Delay(delay), + retry.LastErrorOnly(true), + retry.OnRetry(func(n uint, err error) { + ui.Dim(fmt.Sprintf("Retrying vault gateway request... (attempt %d/%d): %v", n+1, attempts, err)) + }), + ) + + if err != nil { + return respBody, status, fmt.Errorf("gateway POST failed: %w", err) + } + return respBody, status, nil +} + func (g *HTTPClient) postOnce(body []byte) ([]byte, int, error) { req, err := http.NewRequest("POST", g.URL, bytes.NewBuffer(body)) if err != nil { @@ -98,3 +143,29 @@ func (g *HTTPClient) postOnce(body []byte) ([]byte, int, error) { } return b, resp.StatusCode, nil } + +func (g *HTTPClient) postOnceWithBearer(body []byte, bearerToken string) ([]byte, int, error) { + req, err := http.NewRequest("POST", g.URL, bytes.NewBuffer(body)) + if err != nil { + return nil, 0, fmt.Errorf("create HTTP request: %w", err) + } + req.Header.Set("Content-Type", "application/jsonrpc") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+bearerToken) + + if g.Client == nil { + return nil, 0, fmt.Errorf("HTTP client is not initialized") + } + + resp, err := g.Client.Do(req) // #nosec G704 -- URL is from trusted CLI configuration + if err != nil { + return nil, 0, fmt.Errorf("HTTP request to gateway failed: %w", err) + } + defer resp.Body.Close() + + b, rerr := io.ReadAll(resp.Body) + if rerr != nil { + return nil, resp.StatusCode, fmt.Errorf("read response body: %w", rerr) + } + return b, resp.StatusCode, nil +} diff --git a/cmd/secrets/common/gateway_test.go b/cmd/secrets/common/gateway_test.go index 8194565d..9a177676 100644 --- a/cmd/secrets/common/gateway_test.go +++ b/cmd/secrets/common/gateway_test.go @@ -5,9 +5,11 @@ import ( "errors" "io" "net/http" + "net/http/httptest" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // errReadCloser simulates a failure while reading the body. @@ -187,3 +189,73 @@ func TestPostToGateway(t *testing.T) { assert.Equal(t, 2, rt.Calls) // retried and succeeded }) } + +func TestPostWithBearer(t *testing.T) { + var sawAuth string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sawAuth = r.Header.Get("Authorization") + assert.Equal(t, "application/jsonrpc", r.Header.Get("Content-Type")) + _, _ = io.Copy(io.Discard, r.Body) + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":"x","result":{}}`)) + })) + t.Cleanup(srv.Close) + + g := &HTTPClient{ + URL: srv.URL, + Client: srv.Client(), + RetryAttempts: 1, + RetryDelay: 0, + } + body := []byte(`{"jsonrpc":"2.0","id":"1","method":"test","params":{}}`) + resp, status, err := g.PostWithBearer(body, "my-jwt") + require.NoError(t, err) + assert.Equal(t, 200, status) + assert.Contains(t, string(resp), "jsonrpc") + assert.Equal(t, "Bearer my-jwt", sawAuth) +} + +func TestPostWithBearer_EmptyToken(t *testing.T) { + g := &HTTPClient{URL: "http://example.com", Client: http.DefaultClient} + _, _, err := g.PostWithBearer([]byte(`{}`), " ") + assert.Error(t, err) + assert.Contains(t, err.Error(), "empty bearer token") +} + +func TestPostWithBearer_Non200(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "unauthorized", http.StatusUnauthorized) + })) + t.Cleanup(srv.Close) + + g := &HTTPClient{ + URL: srv.URL, + Client: srv.Client(), + RetryAttempts: 3, + RetryDelay: 0, + } + _, status, err := g.PostWithBearer([]byte(`{}`), "tok") + assert.Error(t, err) + assert.Equal(t, http.StatusUnauthorized, status) + assert.Contains(t, err.Error(), "non-200") +} + +func TestPostWithBearer_TransportErrorThenSuccess(t *testing.T) { + body := `{"ok":true}` + rt := &SeqRoundTripper{ + Seq: []RTResponse{ + {Err: errors.New("connection reset")}, + {Response: makeResp(200, body)}, + }, + } + g := &HTTPClient{ + URL: "https://unit-test.gw", + Client: &http.Client{Transport: rt}, + RetryAttempts: 3, + RetryDelay: 0, + } + respBytes, status, err := g.PostWithBearer([]byte(`{}`), "jwt") + assert.NoError(t, err) + assert.Equal(t, 200, status) + assert.Equal(t, body, string(respBytes)) + assert.Equal(t, 2, rt.Calls) +} diff --git a/cmd/secrets/common/handler.go b/cmd/secrets/common/handler.go index 48975140..1f604e3a 100644 --- a/cmd/secrets/common/handler.go +++ b/cmd/secrets/common/handler.go @@ -3,6 +3,7 @@ package common import ( "context" "crypto/ecdsa" + "crypto/sha256" "encoding/hex" "encoding/json" "fmt" @@ -103,6 +104,11 @@ func NewHandler(ctx *runtime.Context, secretsFilePath string) (*Handler, error) return h, nil } +// EnsureDeploymentRPCForOwnerKeySecrets checks project settings for an RPC URL on the workflow registry chain (owner-key / allowlist flows only). +func (h *Handler) EnsureDeploymentRPCForOwnerKeySecrets() error { + return settings.ValidateDeploymentRPC(&h.Settings.Workflow, h.EnvironmentSet.WorkflowRegistryChainName) +} + // ResolveInputs loads secrets from a YAML file. // Errors if the path is not .yaml/.yml — MSIG step 2 is handled by `cre secrets execute`. func (h *Handler) ResolveInputs() (UpsertSecretsInputs, error) { @@ -220,8 +226,8 @@ func (h *Handler) LogMSIGNextSteps(txData string, digest [32]byte, bundlePath st return nil } -// EncryptSecrets takes the raw secrets and encrypts them, returning pointers. -func (h *Handler) EncryptSecrets(rawSecrets UpsertSecretsInputs) ([]*vault.EncryptedSecret, error) { +// fetchVaultMasterPublicKeyHex loads the vault master public key from the gateway (publicKey/get). +func (h *Handler) fetchVaultMasterPublicKeyHex() (string, error) { requestID := uuid.New().String() getPublicKeyRequest := jsonrpc2.Request[vault.GetPublicKeyRequest]{ Version: jsonrpc2.JsonRpcVersion, @@ -232,38 +238,72 @@ func (h *Handler) EncryptSecrets(rawSecrets UpsertSecretsInputs) ([]*vault.Encry reqBody, err := json.Marshal(getPublicKeyRequest) if err != nil { - return nil, fmt.Errorf("failed to marshal public key request: %w", err) + return "", fmt.Errorf("failed to marshal public key request: %w", err) } respBody, status, err := h.Gw.Post(reqBody) if err != nil { - return nil, fmt.Errorf("gateway POST failed: %w", err) + return "", fmt.Errorf("gateway POST failed: %w", err) } if status != http.StatusOK { - return nil, fmt.Errorf("gateway returned non-200: %d body=%s", status, string(respBody)) + return "", fmt.Errorf("gateway returned non-200: %d body=%s", status, string(respBody)) } var rpcResp jsonrpc2.Response[vault.GetPublicKeyResponse] if err := json.Unmarshal(respBody, &rpcResp); err != nil { - return nil, fmt.Errorf("failed to unmarshal public key response: %w", err) + return "", fmt.Errorf("failed to unmarshal public key response: %w", err) } if rpcResp.Error != nil { - return nil, fmt.Errorf("vault public key fetch error: %s", rpcResp.Error.Error()) + return "", fmt.Errorf("vault public key fetch error: %s", rpcResp.Error.Error()) } if rpcResp.Version != jsonrpc2.JsonRpcVersion { - return nil, fmt.Errorf("jsonrpc version mismatch: got %q", rpcResp.Version) + return "", fmt.Errorf("jsonrpc version mismatch: got %q", rpcResp.Version) } if rpcResp.ID != requestID { - return nil, fmt.Errorf("jsonrpc id mismatch: got %q want %q", rpcResp.ID, requestID) + return "", fmt.Errorf("jsonrpc id mismatch: got %q want %q", rpcResp.ID, requestID) } if rpcResp.Method != vaulttypes.MethodPublicKeyGet { - return nil, fmt.Errorf("jsonrpc method mismatch: got %q", rpcResp.Method) + return "", fmt.Errorf("jsonrpc method mismatch: got %q", rpcResp.Method) } if rpcResp.Result == nil || rpcResp.Result.PublicKey == "" { - return nil, fmt.Errorf("empty result in public key response") + return "", fmt.Errorf("empty result in public key response") + } + + return rpcResp.Result.PublicKey, nil +} + +// ResolveEffectiveOwner returns the owner string to use for vault secret identifiers. +// When SecretsOrgOwned is enabled, the org ID (from auth validation) is used; +// otherwise, the workflow owner address is used and must be a valid hex address. +func (h *Handler) ResolveEffectiveOwner() (string, error) { + if h.EnvironmentSet != nil && h.EnvironmentSet.SecretsOrgOwned { + if h.Credentials == nil || h.Credentials.OrgID == "" { + return "", fmt.Errorf("org ID required when CRE_CLI_SECRETS_ORG_OWNED is enabled; ensure auth validation succeeds") + } + return h.Credentials.OrgID, nil + } + if !common.IsHexAddress(h.OwnerAddress) { + return "", fmt.Errorf("owner address %q is not a valid hex address", h.OwnerAddress) + } + return common.HexToAddress(h.OwnerAddress).Hex(), nil +} + +// EncryptSecrets takes the raw secrets and encrypts them, returning pointers. +// When SecretsOrgOwned is enabled, uses SHA256(orgID) as the TDH2 label and orgID as the owner. +// Otherwise, uses the workflow owner address left-padded to 32 bytes as the TDH2 label. +func (h *Handler) EncryptSecrets(rawSecrets UpsertSecretsInputs) ([]*vault.EncryptedSecret, error) { + if h.EnvironmentSet != nil && h.EnvironmentSet.SecretsOrgOwned { + owner, err := h.ResolveEffectiveOwner() + if err != nil { + return nil, err + } + return h.EncryptSecretsForBrowserOrg(rawSecrets, owner) } - pubKeyHex := rpcResp.Result.PublicKey + pubKeyHex, err := h.fetchVaultMasterPublicKeyHex() + if err != nil { + return nil, err + } encryptedSecrets := make([]*vault.EncryptedSecret, 0, len(rawSecrets)) for _, item := range rawSecrets { @@ -284,7 +324,38 @@ func (h *Handler) EncryptSecrets(rawSecrets UpsertSecretsInputs) ([]*vault.Encry return encryptedSecrets, nil } -func EncryptSecret(secret, masterPublicKeyHex string, ownerAddress string) (string, error) { +// EncryptSecretsForBrowserOrg encrypts secrets scoped to the signed-in organization (interactive sign-in flow). +// TDH2 label is SHA256(orgID); SecretIdentifier.Owner is the org id string. This is a separate binding from the +// owner-key path (EOA left-padded label + workflow owner address); both remain supported via their respective entrypoints. +func (h *Handler) EncryptSecretsForBrowserOrg(rawSecrets UpsertSecretsInputs, orgID string) ([]*vault.EncryptedSecret, error) { + pubKeyHex, err := h.fetchVaultMasterPublicKeyHex() + if err != nil { + return nil, err + } + + label := sha256.Sum256([]byte(orgID)) + + encryptedSecrets := make([]*vault.EncryptedSecret, 0, len(rawSecrets)) + for _, item := range rawSecrets { + cipherHex, err := encryptSecretWithLabel(item.Value, pubKeyHex, label) + if err != nil { + return nil, fmt.Errorf("failed to encrypt secret (key=%s ns=%s): %w", item.ID, item.Namespace, err) + } + secID := &vault.SecretIdentifier{ + Key: item.ID, + Namespace: item.Namespace, + Owner: orgID, + } + encryptedSecrets = append(encryptedSecrets, &vault.EncryptedSecret{ + Id: secID, + EncryptedValue: cipherHex, + }) + } + return encryptedSecrets, nil +} + +// encryptSecretWithLabel encrypts a secret using the vault master public key and the given label. +func encryptSecretWithLabel(secret, masterPublicKeyHex string, label [32]byte) (string, error) { masterPublicKey := tdh2easy.PublicKey{} masterPublicKeyBytes, err := hex.DecodeString(masterPublicKeyHex) if err != nil { @@ -294,9 +365,6 @@ func EncryptSecret(secret, masterPublicKeyHex string, ownerAddress string) (stri return "", fmt.Errorf("failed to unmarshal master public key: %w", err) } - addr := common.HexToAddress(ownerAddress) // canonical 20-byte address - var label [32]byte - copy(label[12:], addr.Bytes()) // left-pad with 12 zero bytes cipher, err := tdh2easy.EncryptWithLabel(&masterPublicKey, []byte(secret), label) if err != nil { return "", fmt.Errorf("failed to encrypt secret: %w", err) @@ -308,6 +376,14 @@ func EncryptSecret(secret, masterPublicKeyHex string, ownerAddress string) (stri return hex.EncodeToString(cipherBytes), nil } +// EncryptSecret encrypts for the owner-key / web3 flow using a 32-byte label derived from the EOA (12 zero bytes + 20-byte address). +func EncryptSecret(secret, masterPublicKeyHex string, ownerAddress string) (string, error) { + addr := common.HexToAddress(ownerAddress) // canonical 20-byte address + var label [32]byte + copy(label[12:], addr.Bytes()) // left-pad with 12 zero bytes + return encryptSecretWithLabel(secret, masterPublicKeyHex, label) +} + func CalculateDigest[I any](r jsonrpc2.Request[I]) ([32]byte, error) { b, err := json.Marshal(r.Params) if err != nil { @@ -344,15 +420,21 @@ func HexToBytes32(h string) ([32]byte, error) { return out, nil } -// Execute is shared for 'create' and 'update' (YAML-only). -// - MSIG => step 1: build request, save bundle, print instructions -// - EOA => build request, allowlist if needed, POST +// Execute implements secrets create and update from YAML (multisig bundle, owner-key with allowlist, or interactive org sign-in). func (h *Handler) Execute( inputs UpsertSecretsInputs, method string, duration time.Duration, secretsAuth string, ) error { + if IsBrowserFlow(secretsAuth) { + return h.executeBrowserUpsert(context.Background(), inputs, method) + } + + if err := h.EnsureDeploymentRPCForOwnerKeySecrets(); err != nil { + return err + } + ui.Dim("Verifying ownership...") if err := h.EnsureOwnerLinkedOrFail(); err != nil { return err @@ -623,7 +705,7 @@ func (h *Handler) ParseVaultGatewayResponse(method string, respBody []byte) erro return nil } -// EnsureOwnerLinkedOrFail TODO this reuses the same logic as in autoLink.go which is tied to deploy; consider refactoring to avoid duplication +// EnsureOwnerLinkedOrFail TODO this reuses the same logic as in auto_link.go which is tied to deploy; consider refactoring to avoid duplication func (h *Handler) EnsureOwnerLinkedOrFail() error { ownerAddr := common.HexToAddress(h.OwnerAddress) diff --git a/cmd/secrets/common/handler_test.go b/cmd/secrets/common/handler_test.go index 391fd5db..bc109581 100644 --- a/cmd/secrets/common/handler_test.go +++ b/cmd/secrets/common/handler_test.go @@ -28,6 +28,10 @@ func (m *mockGatewayClient) Post(b []byte) ([]byte, int, error) { return m.post(b) } +func (m *mockGatewayClient) PostWithBearer(b []byte, _ string) ([]byte, int, error) { + return m.post(b) +} + // It represents a hex-encoded tdh2easy.PublicKey blob. const vaultPublicKeyHex = "7b2247726f7570223a2250323536222c22475f626172223a22424d704759487a2b33333432596436582f2b6d4971396d5468556c6d2f317355716b51783333343564303373472b2f2f307257494d39795a70454b44566c6c2b616f36586c513743366546452b665472356568785a4f343d222c2248223a22424257546f7638394b546b41505a7566474454504e35626f456d6453305368697975696e3847336e58517774454931536333394453314b41306a595a6576546155476775444d694431746e6e4d686575373177574b57593d222c22484172726179223a5b22424937726649364c646f7654413948676a684b5955516a4744456a5a66374f30774378466c432f2f384e394733464c796247436d6e54734236632b50324c34596a39477548555a4936386d54342b4e77786f794b6261513d222c22424736634369395574317a65433753786b4c442b6247354751505473717463324a7a544b4c726b784d496e4c36484e7658376541324b6167423243447a4b6a6f76783570414c6a74523734537a6c7146543366746662513d222c224245576f7147546d6b47314c31565a53655874345147446a684d4d2b656e7a6b426b7842782b484f72386e39336b51543963594938486f513630356a65504a732f53575866355a714534564e676b4f672f643530395a6b3d222c22424a31552b6e5344783269567a654177475948624e715242564869626b74466b624f4762376158562f3946744c6876314b4250416c3272696e73714171754459504e2f54667870725a6e655259594a2b2f453162536a673d222c224243675a623770424d777732337138577767736e322b6c4d665259343561347576445345715a7559614e2f356e64744970355a492f4a6f454d372b36304a6338735978682b535365364645683052364f57666855706d453d222c2242465a5942524a336d6647695644312b4f4b4e4f374c54355a6f6574515442624a6b464152757143743268492f52757832756b7166794c6c364d71566e55613557336e49726e71506132566d5345755758546d39456f733d222c22424f716b662f356232636c4d314a78615831446d6a76494c4437334f6734566b42732f4b686b6e4d6867435772552f30574a36734e514a6b425462686b4a5535576b48506342626d45786c6362706a49743349494632303d225d7d" @@ -122,6 +126,106 @@ func TestEncryptSecrets(t *testing.T) { }) } +func TestResolveEffectiveOwner(t *testing.T) { + t.Run("returns canonicalized address when SecretsOrgOwned is false", func(t *testing.T) { + h, _, _ := newMockHandler(t) + h.OwnerAddress = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + h.EnvironmentSet.SecretsOrgOwned = false + + owner, err := h.ResolveEffectiveOwner() + require.NoError(t, err) + require.Equal(t, "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", owner) + }) + + t.Run("errors when SecretsOrgOwned is false and owner address is empty", func(t *testing.T) { + h, _, _ := newMockHandler(t) + h.OwnerAddress = "" + h.EnvironmentSet.SecretsOrgOwned = false + + _, err := h.ResolveEffectiveOwner() + require.Error(t, err) + require.Contains(t, err.Error(), "not a valid hex address") + }) + + t.Run("errors when SecretsOrgOwned is false and owner address is malformed", func(t *testing.T) { + h, _, _ := newMockHandler(t) + h.OwnerAddress = "not-an-address" + h.EnvironmentSet.SecretsOrgOwned = false + + _, err := h.ResolveEffectiveOwner() + require.Error(t, err) + require.Contains(t, err.Error(), "not a valid hex address") + }) + + t.Run("returns org ID when SecretsOrgOwned is true and org ID is set", func(t *testing.T) { + h, _, _ := newMockHandler(t) + h.OwnerAddress = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + h.EnvironmentSet.SecretsOrgOwned = true + h.Credentials.OrgID = "org-123" + + owner, err := h.ResolveEffectiveOwner() + require.NoError(t, err) + require.Equal(t, "org-123", owner) + }) + + t.Run("errors when SecretsOrgOwned is true but org ID is empty", func(t *testing.T) { + h, _, _ := newMockHandler(t) + h.OwnerAddress = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" + h.EnvironmentSet.SecretsOrgOwned = true + h.Credentials.OrgID = "" + + _, err := h.ResolveEffectiveOwner() + require.Error(t, err) + require.Contains(t, err.Error(), "org ID required") + }) +} + +func TestEncryptSecrets_OrgOwned(t *testing.T) { + mockGw := &mockGatewayClient{ + post: func(body []byte) ([]byte, int, error) { + var req jsonrpc2.Request[vaultcommon.GetPublicKeyRequest] + _ = json.Unmarshal(body, &req) + resp := jsonrpc2.Response[vaultcommon.GetPublicKeyResponse]{ + Version: jsonrpc2.JsonRpcVersion, + ID: req.ID, + Method: vaulttypes.MethodPublicKeyGet, + Result: &vaultcommon.GetPublicKeyResponse{PublicKey: vaultPublicKeyHex}, + } + b, _ := json.Marshal(resp) + return b, http.StatusOK, nil + }, + } + + raw := UpsertSecretsInputs{ + {ID: "secret-1", Value: "val1", Namespace: "main"}, + } + + t.Run("uses orgID as owner when SecretsOrgOwned is true", func(t *testing.T) { + h, _, _ := newMockHandler(t) + h.Gw = mockGw + h.EnvironmentSet.SecretsOrgOwned = true + h.Credentials.OrgID = "org-456" + + enc, err := h.EncryptSecrets(raw) + require.NoError(t, err) + require.Len(t, enc, 1) + require.Equal(t, "org-456", enc[0].Id.Owner) + require.Equal(t, "secret-1", enc[0].Id.Key) + }) + + t.Run("uses address as owner when SecretsOrgOwned is false", func(t *testing.T) { + h, _, _ := newMockHandler(t) + h.Gw = mockGw + h.OwnerAddress = "0xabc" + h.EnvironmentSet.SecretsOrgOwned = false + + enc, err := h.EncryptSecrets(raw) + require.NoError(t, err) + require.Len(t, enc, 1) + require.Equal(t, "0xabc", enc[0].Id.Owner) + }) +} + func TestPackAllowlistRequestTxData_Success_With0x(t *testing.T) { h, _, _ := newMockHandler(t) diff --git a/cmd/secrets/common/test_helpers.go b/cmd/secrets/common/test_helpers.go index cf585631..648f6df4 100644 --- a/cmd/secrets/common/test_helpers.go +++ b/cmd/secrets/common/test_helpers.go @@ -11,6 +11,8 @@ import ( "github.com/test-go/testify/mock" "github.com/smartcontractkit/cre-cli/cmd/client" + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/environments" ) func newMockHandler(t *testing.T) (*Handler, *MockClientFactory, *ecdsa.PrivateKey) { @@ -21,10 +23,12 @@ func newMockHandler(t *testing.T) (*Handler, *MockClientFactory, *ecdsa.PrivateK t.Fatalf("failed to generate private key: %v", err) } h := &Handler{ - Log: &logger, - ClientFactory: mockClientFactory, - PrivateKey: privateKey, - OwnerAddress: "0xabc", + Log: &logger, + ClientFactory: mockClientFactory, + PrivateKey: privateKey, + OwnerAddress: "0xabc", + EnvironmentSet: &environments.EnvironmentSet{}, + Credentials: &credentials.Credentials{}, } return h, mockClientFactory, privateKey } diff --git a/cmd/secrets/delete/delete.go b/cmd/secrets/delete/delete.go index 573c64c2..d7a630d9 100644 --- a/cmd/secrets/delete/delete.go +++ b/cmd/secrets/delete/delete.go @@ -1,6 +1,7 @@ package delete import ( + "context" "encoding/hex" "encoding/json" "fmt" @@ -108,6 +109,12 @@ func New(ctx *runtime.Context) *cobra.Command { // - MSIG step 1: build request, compute digest, write bundle, print steps // - EOA: allowlist if needed, then POST to gateway func Execute(h *common.Handler, inputs DeleteSecretsInputs, duration time.Duration, secretsAuth string) error { + if !common.IsBrowserFlow(secretsAuth) { + if err := h.EnsureDeploymentRPCForOwnerKeySecrets(); err != nil { + return err + } + } + spinner := ui.NewSpinner() spinner.Start("Verifying ownership...") if err := h.EnsureOwnerLinkedOrFail(); err != nil { @@ -116,14 +123,11 @@ func Execute(h *common.Handler, inputs DeleteSecretsInputs, duration time.Durati } spinner.Stop() - // Validate and canonicalize owner address - owner := strings.TrimSpace(h.OwnerAddress) - if !ethcommon.IsHexAddress(owner) { - return fmt.Errorf("invalid owner address: %q", h.OwnerAddress) + owner, err := h.ResolveEffectiveOwner() + if err != nil { + return err } - owner = ethcommon.HexToAddress(owner).Hex() // checksummed string - // Prepare the list of SecretIdentifiers to be deleted. ptrIDs := make([]*vault.SecretIdentifier, len(inputs)) for i, item := range inputs { ptrIDs[i] = &vault.SecretIdentifier{ @@ -158,6 +162,11 @@ func Execute(h *common.Handler, inputs DeleteSecretsInputs, duration time.Durati return fmt.Errorf("failed to calculate request digest: %w", err) } + if common.IsBrowserFlow(secretsAuth) { + ui.Dim("Using your account to authorize vault access for this delete request...") + return h.ExecuteBrowserVaultAuthorization(context.Background(), vaulttypes.MethodSecretsDelete, digest, requestBody) + } + gatewayPost := func() error { respBody, status, err := h.Gw.Post(requestBody) if err != nil { diff --git a/cmd/secrets/execute/execute.go b/cmd/secrets/execute/execute.go index 9ef16fa0..4525efb0 100644 --- a/cmd/secrets/execute/execute.go +++ b/cmd/secrets/execute/execute.go @@ -65,6 +65,10 @@ func New(ctx *runtime.Context) *cobra.Command { return fmt.Errorf("invalid bundle digest: %w", err) } + if err := h.EnsureDeploymentRPCForOwnerKeySecrets(); err != nil { + return err + } + ownerAddr := ethcommon.HexToAddress(h.OwnerAddress) allowlisted, err := h.Wrc.IsRequestAllowlisted(ownerAddr, digest) diff --git a/cmd/secrets/list/list.go b/cmd/secrets/list/list.go index ba15e056..48596c3e 100644 --- a/cmd/secrets/list/list.go +++ b/cmd/secrets/list/list.go @@ -1,13 +1,13 @@ package list import ( + "context" "encoding/hex" "encoding/json" "fmt" "net/http" "os" "path/filepath" - "strings" "time" ethcommon "github.com/ethereum/go-ethereum/common" @@ -79,6 +79,12 @@ func New(ctx *runtime.Context) *cobra.Command { // Execute performs: build request → (MSIG step 1 bundle OR EOA allowlist+post) → parse. func Execute(h *common.Handler, namespace string, duration time.Duration, secretsAuth string) error { + if !common.IsBrowserFlow(secretsAuth) { + if err := h.EnsureDeploymentRPCForOwnerKeySecrets(); err != nil { + return err + } + } + spinner := ui.NewSpinner() spinner.Start("Verifying ownership...") if err := h.EnsureOwnerLinkedOrFail(); err != nil { @@ -91,12 +97,10 @@ func Execute(h *common.Handler, namespace string, duration time.Duration, secret namespace = "main" } - // Validate and canonicalize owner address (checksummed) - owner := strings.TrimSpace(h.OwnerAddress) - if !ethcommon.IsHexAddress(owner) { - return fmt.Errorf("invalid owner address: %q", h.OwnerAddress) + owner, err := h.ResolveEffectiveOwner() + if err != nil { + return err } - owner = ethcommon.HexToAddress(owner).Hex() // Fresh request ID requestID := uuid.New().String() @@ -122,6 +126,11 @@ func Execute(h *common.Handler, namespace string, duration time.Duration, secret return fmt.Errorf("failed to marshal JSON-RPC request: %w", err) } + if common.IsBrowserFlow(secretsAuth) { + ui.Dim("Using your account to authorize vault access for this list request...") + return h.ExecuteBrowserVaultAuthorization(context.Background(), vaulttypes.MethodSecretsList, digest, body) + } + ownerAddr := ethcommon.HexToAddress(owner) allowlisted, err := h.Wrc.IsRequestAllowlisted(ownerAddr, digest) diff --git a/cmd/secrets/secrets.go b/cmd/secrets/secrets.go index 8ad618c9..836e5f84 100644 --- a/cmd/secrets/secrets.go +++ b/cmd/secrets/secrets.go @@ -32,7 +32,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { "Timeout for secrets operations (e.g. 30m, 2h, 48h).", ) - secretsCmd.PersistentFlags().String("secrets-auth", "owner-key-signing", "Auth flow for secrets operations (owner-key-signing, browser).") + secretsCmd.PersistentFlags().String("secrets-auth", "owner-key-signing", "Authentication mode: owner-key-signing (workflow owner) or browser (organization sign-in).") _ = secretsCmd.PersistentFlags().MarkHidden("secrets-auth") secretsCmd.AddCommand(create.New(runtimeContext)) diff --git a/cmd/template/help_template.tpl b/cmd/template/help_template.tpl index f2d04869..c8dfab41 100644 --- a/cmd/template/help_template.tpl +++ b/cmd/template/help_template.tpl @@ -86,6 +86,8 @@ {{styleDim (printf "Use \"%s [command] --help\" for more information about a command." .CommandPath)}} {{- end }} +{{- if not .HasParent}} + {{styleSuccess "Tip:"}} New here? Run: {{styleCode "$ cre login"}} to login into your cre account, then: @@ -94,9 +96,10 @@ {{- if needsDeployAccess}} 🔑 Ready to deploy? Run: - $ cre account access + {{styleCode "$ cre account access"}} to request deployment access. {{- end}} +{{- end}} {{styleSection "Need more help?"}} Visit {{styleURL "https://docs.chain.link/cre"}} diff --git a/cmd/templates/list/list.go b/cmd/templates/list/list.go index 45934209..8ddb6d7e 100644 --- a/cmd/templates/list/list.go +++ b/cmd/templates/list/list.go @@ -1,25 +1,32 @@ package list import ( + "encoding/json" "fmt" "strings" "github.com/rs/zerolog" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/templateconfig" "github.com/smartcontractkit/cre-cli/internal/templaterepo" "github.com/smartcontractkit/cre-cli/internal/ui" + "github.com/smartcontractkit/cre-cli/internal/validation" ) +type Inputs struct { + Refresh bool + JSONOutput bool +} + type handler struct { - log *zerolog.Logger + log *zerolog.Logger + validated bool } func New(runtimeContext *runtime.Context) *cobra.Command { - var refresh bool - cmd := &cobra.Command{ Use: "list", Short: "Lists available templates", @@ -27,16 +34,52 @@ func New(runtimeContext *runtime.Context) *cobra.Command { Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { h := &handler{log: runtimeContext.Logger} - return h.Execute(refresh) + + inputs, err := h.ResolveInputs(runtimeContext.Viper) + if err != nil { + return err + } + + if err := h.ValidateInputs(inputs); err != nil { + return err + } + + return h.Execute(inputs) }, } - cmd.Flags().BoolVar(&refresh, "refresh", false, "Bypass cache and fetch fresh data") + cmd.Flags().Bool("refresh", false, "Bypass cache and fetch fresh data") + cmd.Flags().Bool("json", false, "Output template list as JSON") return cmd } -func (h *handler) Execute(refresh bool) error { +func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { + return Inputs{ + Refresh: v.GetBool("refresh"), + JSONOutput: v.GetBool("json"), + }, nil +} + +func (h *handler) ValidateInputs(inputs Inputs) error { + validator, err := validation.NewValidator() + if err != nil { + return fmt.Errorf("failed to create validator: %w", err) + } + + if err := validator.Struct(inputs); err != nil { + return fmt.Errorf("validation failed: %w", err) + } + + h.validated = true + return nil +} + +func (h *handler) Execute(inputs Inputs) error { + if !h.validated { + return fmt.Errorf("handler inputs not validated") + } + if err := templateconfig.EnsureDefaultConfig(h.log); err != nil { return fmt.Errorf("failed to initialize template config: %w", err) } @@ -58,7 +101,7 @@ func (h *handler) Execute(refresh bool) error { spinner := ui.NewSpinner() spinner.Start("Fetching templates...") - templates, err := registry.ListTemplates(refresh) + templates, err := registry.ListTemplates(inputs.Refresh) spinner.Stop() if err != nil { return fmt.Errorf("failed to list templates: %w", err) @@ -71,6 +114,21 @@ func (h *handler) Execute(refresh bool) error { return nil } + if inputs.JSONOutput { + var filtered []templaterepo.TemplateSummary + for _, t := range templates { + if t.Category == templaterepo.CategoryWorkflow { + filtered = append(filtered, t) + } + } + data, err := json.MarshalIndent(filtered, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal templates: %w", err) + } + fmt.Println(string(data)) + return nil + } + ui.Line() ui.Title("Available Templates") ui.Line() @@ -107,6 +165,9 @@ func (h *handler) Execute(refresh bool) error { if len(t.Tags) > 0 { ui.Dim(fmt.Sprintf(" Tags: %s", strings.Join(t.Tags, ", "))) } + if len(t.Networks) > 0 { + ui.Dim(fmt.Sprintf(" Networks: %s", strings.Join(t.Networks, ", "))) + } ui.Line() } @@ -114,6 +175,9 @@ func (h *handler) Execute(refresh bool) error { ui.Dim("Install a template with:") ui.Command(" cre init --template=") ui.Line() + ui.Dim("If a template requires Networks, provide them with:") + ui.Command(" cre init --template= --rpc-url=\"=\"") + ui.Line() return nil } diff --git a/cmd/workflow/activate/activate.go b/cmd/workflow/activate/activate.go index 0eb759da..039b3f67 100644 --- a/cmd/workflow/activate/activate.go +++ b/cmd/workflow/activate/activate.go @@ -1,24 +1,16 @@ package activate import ( - "encoding/hex" "fmt" - "math/big" - "sort" - "sync" - "time" - "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/smartcontractkit/cre-cli/cmd/client" - cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" - "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" ) @@ -28,11 +20,9 @@ const ( ) type Inputs struct { - WorkflowName string `validate:"workflow_name"` - WorkflowOwner string `validate:"workflow_owner"` - DonFamily string `validate:"required"` - WorkflowRegistryContractAddress string `validate:"required"` - WorkflowRegistryContractChainName string `validate:"required"` + WorkflowName string `validate:"workflow_name"` + WorkflowOwner string `validate:"workflow_owner"` + DonFamily string `validate:"required"` } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -70,13 +60,9 @@ type handler struct { settings *settings.Settings environmentSet *environments.EnvironmentSet inputs Inputs - wrc *client.WorkflowRegistryV2Client runtimeContext *runtime.Context validated bool - - wg sync.WaitGroup - wrcErr error } func newHandler(ctx *runtime.Context) *handler { @@ -87,30 +73,21 @@ func newHandler(ctx *runtime.Context) *handler { environmentSet: ctx.EnvironmentSet, runtimeContext: ctx, validated: false, - wg: sync.WaitGroup{}, - wrcErr: nil, } - h.wg.Add(1) - go func() { - defer h.wg.Done() - wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() - if err != nil { - h.wrcErr = fmt.Errorf("failed to create workflow registry client: %w", err) - return - } - h.wrc = wrc - }() return &h } func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { + resolvedWorkflowOwner, err := h.resolveWorkflowOwner(h.runtimeContext.ResolvedRegistry.Type()) + if err != nil { + return Inputs{}, fmt.Errorf("failed to resolve workflow owner: %w", err) + } + return Inputs{ - WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, - WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, - DonFamily: h.environmentSet.DonFamily, - WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, - WorkflowRegistryContractChainName: h.environmentSet.WorkflowRegistryChainName, + WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, + WorkflowOwner: resolvedWorkflowOwner, + DonFamily: h.runtimeContext.ResolvedRegistry.DonFamily(), }, nil } @@ -133,124 +110,36 @@ func (h *handler) Execute() error { return fmt.Errorf("handler inputs not validated") } - workflowName := h.inputs.WorkflowName - workflowOwner := h.inputs.WorkflowOwner - h.displayWorkflowDetails() - h.wg.Wait() - if h.wrcErr != nil { - return h.wrcErr - } - - ownerAddr := common.HexToAddress(workflowOwner) - - const pageLimit = 200 - workflows, err := h.wrc.GetWorkflowListByOwnerAndName(ownerAddr, workflowName, big.NewInt(0), big.NewInt(pageLimit)) + strategy, err := newRegistryActivateStrategy(h.runtimeContext.ResolvedRegistry, h) if err != nil { - return fmt.Errorf("failed to get workflow list: %w", err) - } - if len(workflows) == 0 { - return fmt.Errorf("no workflows found for owner=%s name=%s", workflowOwner, workflowName) - } - - // Sort by CreatedAt descending - sort.Slice(workflows, func(i, j int) bool { - return workflows[i].CreatedAt > workflows[j].CreatedAt - }) - - latest := workflows[0] - - h.runtimeContext.Workflow.ID = hex.EncodeToString(latest.WorkflowId[:]) - - // Validate precondition: workflow must be in paused state - if latest.Status != WorkflowStatusPaused { - return fmt.Errorf("workflow is already active, cancelling transaction") - } - - if err := h.wrc.CheckUserDonLimit(ownerAddr, h.inputs.DonFamily, 1); err != nil { return err } - ui.Dim(fmt.Sprintf("Activating workflow: Name=%s, Owner=%s, WorkflowID=%s", workflowName, workflowOwner, hex.EncodeToString(latest.WorkflowId[:]))) + return strategy.Activate() +} - txOut, err := h.wrc.ActivateWorkflow(latest.WorkflowId, h.inputs.DonFamily) - if err != nil { - return fmt.Errorf("failed to activate workflow: %w", err) +// resolveWorkflowOwner returns the effective owner address for workflow ID computation. +// For private registry deploys, the derived workflow owner from the runtime context is used. +// For onchain deploys, the configured WorkflowOwner address is used directly. +func (h *handler) resolveWorkflowOwner(registryType settings.RegistryType) (string, error) { + if registryType != settings.RegistryTypeOffChain { + return h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, nil } - switch txOut.Type { - case client.Regular: - ui.Success(fmt.Sprintf("Transaction confirmed: %s", txOut.Hash)) - ui.URL(fmt.Sprintf("%s/tx/%s", h.environmentSet.WorkflowRegistryChainExplorerURL, txOut.Hash)) - ui.Line() - ui.Success("Workflow activated successfully") - ui.Dim(fmt.Sprintf(" Contract address: %s", h.environmentSet.WorkflowRegistryAddress)) - ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) - ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) - ui.Dim(fmt.Sprintf(" Workflow ID: %s", hex.EncodeToString(latest.WorkflowId[:]))) - - case client.Raw: - ui.Line() - ui.Success("MSIG workflow activation transaction prepared!") - ui.Dim(fmt.Sprintf("To Activate %s with workflowID: %s", workflowName, hex.EncodeToString(latest.WorkflowId[:]))) - ui.Line() - ui.Bold("Next steps:") - ui.Line() - ui.Print(" 1. Submit the following transaction on the target chain:") - ui.Dim(fmt.Sprintf(" Chain: %s", h.inputs.WorkflowRegistryContractChainName)) - ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) - ui.Line() - ui.Print(" 2. Use the following transaction data:") - ui.Line() - ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) - ui.Line() - - case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(h.environmentSet.WorkflowRegistryChainName) - if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", h.environmentSet.WorkflowRegistryChainName, err) - } - mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) - if err != nil { - ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") - } - cldSettings := h.settings.CLDSettings - changesets := []types.Changeset{ - { - ActivateWorkflow: &types.ActivateWorkflow{ - Payload: types.UserWorkflowActivateInput{ - WorkflowID: h.runtimeContext.Workflow.ID, - DonFamily: h.inputs.DonFamily, - - ChainSelector: chainSelector, - MCMSConfig: mcmsConfig, - WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, - }, - }, - }, - } - csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) - - var fileName string - if cldSettings.ChangesetFile != "" { - fileName = cldSettings.ChangesetFile - } else { - fileName = fmt.Sprintf("ActivateWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) - } - - return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) - - default: - h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) + owner := h.runtimeContext.DerivedWorkflowOwner + if owner == "" { + return "", fmt.Errorf("derived workflow owner is not available; ensure authentication succeeded") } - return nil + + return owner, nil } func (h *handler) displayWorkflowDetails() { ui.Line() ui.Title(fmt.Sprintf("Activating Workflow: %s", h.inputs.WorkflowName)) ui.Dim(fmt.Sprintf("Target: %s", h.settings.User.TargetName)) - ui.Dim(fmt.Sprintf("Owner Address: %s", h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress)) + ui.Dim(fmt.Sprintf("Owner Address: %s", h.inputs.WorkflowOwner)) ui.Line() } diff --git a/cmd/workflow/activate/activate_test.go b/cmd/workflow/activate/activate_test.go index 07f32e7d..2b06bf00 100644 --- a/cmd/workflow/activate/activate_test.go +++ b/cmd/workflow/activate/activate_test.go @@ -18,12 +18,6 @@ func TestWorkflowActivateCommand(t *testing.T) { t.Parallel() fillRequired := func(in Inputs) Inputs { - if in.WorkflowRegistryContractAddress == "" { - in.WorkflowRegistryContractAddress = "0x0000000000000000000000000000000000000000" - } - if in.WorkflowRegistryContractChainName == "" { - in.WorkflowRegistryContractChainName = "ethereum-testnet-sepolia" - } return in } diff --git a/cmd/workflow/activate/registry_activate_strategy.go b/cmd/workflow/activate/registry_activate_strategy.go new file mode 100644 index 00000000..038241f6 --- /dev/null +++ b/cmd/workflow/activate/registry_activate_strategy.go @@ -0,0 +1,18 @@ +package activate + +import ( + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +// registryActivateStrategy encapsulates target-specific activate logic. +type registryActivateStrategy interface { + Activate() error +} + +// newRegistryActivateStrategy returns the appropriate strategy for the given target. +func newRegistryActivateStrategy(resolvedRegistry settings.ResolvedRegistry, h *handler) (registryActivateStrategy, error) { + if resolvedRegistry.Type() == settings.RegistryTypeOffChain { + return newPrivateRegistryActivateStrategy(h), nil + } + return newOnchainRegistryActivateStrategy(h) +} diff --git a/cmd/workflow/activate/registry_activate_strategy_onchain.go b/cmd/workflow/activate/registry_activate_strategy_onchain.go new file mode 100644 index 00000000..ea9df4dd --- /dev/null +++ b/cmd/workflow/activate/registry_activate_strategy_onchain.go @@ -0,0 +1,166 @@ +package activate + +import ( + "encoding/hex" + "fmt" + "math/big" + "sort" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/cre-cli/cmd/client" + cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/types" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type onchainRegistryActivateStrategy struct { + h *handler + wrc *client.WorkflowRegistryV2Client + onChain *settings.OnChainRegistry + wg sync.WaitGroup + initErr error +} + +func newOnchainRegistryActivateStrategy(h *handler) (*onchainRegistryActivateStrategy, error) { + onChain, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "activate") + if err != nil { + return nil, err + } + + a := &onchainRegistryActivateStrategy{h: h, onChain: onChain} + a.wg.Add(1) + go func() { + defer a.wg.Done() + wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() + if err != nil { + a.initErr = fmt.Errorf("failed to create workflow registry client: %w", err) + return + } + a.wrc = wrc + }() + return a, nil +} + +func (a *onchainRegistryActivateStrategy) Activate() error { + h := a.h + + a.wg.Wait() + if a.initErr != nil { + return a.initErr + } + + workflowName := h.inputs.WorkflowName + workflowOwner := h.inputs.WorkflowOwner + + ownerAddr := common.HexToAddress(workflowOwner) + + const pageLimit = 200 + workflows, err := a.wrc.GetWorkflowListByOwnerAndName(ownerAddr, workflowName, big.NewInt(0), big.NewInt(pageLimit)) + if err != nil { + return fmt.Errorf("failed to get workflow list: %w", err) + } + if len(workflows) == 0 { + return fmt.Errorf("no workflows found for owner=%s name=%s", workflowOwner, workflowName) + } + + // Sort by CreatedAt descending + sort.Slice(workflows, func(i, j int) bool { + return workflows[i].CreatedAt > workflows[j].CreatedAt + }) + + latest := workflows[0] + + h.runtimeContext.Workflow.ID = hex.EncodeToString(latest.WorkflowId[:]) + + // Validate precondition: workflow must be in paused state + if latest.Status != WorkflowStatusPaused { + return fmt.Errorf("workflow is already active, cancelling transaction") + } + + if err := a.wrc.CheckUserDonLimit(ownerAddr, h.inputs.DonFamily, 1); err != nil { + return err + } + + ui.Dim(fmt.Sprintf("Activating workflow: Name=%s, Owner=%s, WorkflowID=%s", workflowName, workflowOwner, hex.EncodeToString(latest.WorkflowId[:]))) + + txOut, err := a.wrc.ActivateWorkflow(latest.WorkflowId, h.inputs.DonFamily) + if err != nil { + return fmt.Errorf("failed to activate workflow: %w", err) + } + + oc := a.onChain + + switch txOut.Type { + case client.Regular: + ui.Success(fmt.Sprintf("Transaction confirmed: %s", txOut.Hash)) + ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) + ui.Line() + ui.Success("Workflow activated successfully") + ui.Line() + ui.Bold("Details:") + ui.Dim(fmt.Sprintf(" Registry: %s", h.runtimeContext.ResolvedRegistry.ID())) + ui.Dim(fmt.Sprintf(" Contract address: %s", oc.Address())) + ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) + ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) + ui.Dim(fmt.Sprintf(" Workflow ID: %s", hex.EncodeToString(latest.WorkflowId[:]))) + + case client.Raw: + ui.Line() + ui.Success("MSIG workflow activation transaction prepared!") + ui.Dim(fmt.Sprintf("To Activate %s with workflowID: %s", workflowName, hex.EncodeToString(latest.WorkflowId[:]))) + ui.Line() + ui.Bold("Next steps:") + ui.Line() + ui.Print(" 1. Submit the following transaction on the target chain:") + ui.Dim(fmt.Sprintf(" Chain: %s", oc.ChainName())) + ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) + ui.Line() + ui.Print(" 2. Use the following transaction data:") + ui.Line() + ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) + ui.Line() + + case client.Changeset: + chainSelector, err := settings.GetChainSelectorByChainName(oc.ChainName()) + if err != nil { + return fmt.Errorf("failed to get chain selector for chain %q: %w", oc.ChainName(), err) + } + mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) + if err != nil { + ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") + } + cldSettings := h.settings.CLDSettings + changesets := []types.Changeset{ + { + ActivateWorkflow: &types.ActivateWorkflow{ + Payload: types.UserWorkflowActivateInput{ + WorkflowID: h.runtimeContext.Workflow.ID, + DonFamily: h.inputs.DonFamily, + + ChainSelector: chainSelector, + MCMSConfig: mcmsConfig, + WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, + }, + }, + }, + } + csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) + + var fileName string + if cldSettings.ChangesetFile != "" { + fileName = cldSettings.ChangesetFile + } else { + fileName = fmt.Sprintf("ActivateWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) + } + + return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) + + default: + h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) + } + return nil +} diff --git a/cmd/workflow/activate/registry_activate_strategy_private.go b/cmd/workflow/activate/registry_activate_strategy_private.go new file mode 100644 index 00000000..1b1e1df9 --- /dev/null +++ b/cmd/workflow/activate/registry_activate_strategy_private.go @@ -0,0 +1,65 @@ +package activate + +import ( + "fmt" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/client/privateregistryclient" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type privateRegistryActivateStrategy struct { + h *handler + prc *privateregistryclient.Client +} + +func newPrivateRegistryActivateStrategy(h *handler) *privateRegistryActivateStrategy { + return &privateRegistryActivateStrategy{h: h} +} + +func (a *privateRegistryActivateStrategy) ensureClient() { + if a.prc == nil { + gql := graphqlclient.New(a.h.runtimeContext.Credentials, a.h.environmentSet, a.h.log) + a.prc = privateregistryclient.New(gql, a.h.log) + } +} + +func (a *privateRegistryActivateStrategy) Activate() error { + a.ensureClient() + + h := a.h + workflowName := h.inputs.WorkflowName + + ui.Dim(fmt.Sprintf("Fetching workflow to activate... Name=%s", workflowName)) + + workflow, err := a.prc.GetWorkflowByName(workflowName) + if err != nil { + return fmt.Errorf("failed to get workflow: %w", err) + } + + if workflow.Status == privateregistryclient.WorkflowStatusActive { + return fmt.Errorf("workflow is already active, cancelling transaction") + } + + h.runtimeContext.Workflow.ID = workflow.WorkflowID + + ui.Dim(fmt.Sprintf("Processing activation for workflow ID %s...", workflow.WorkflowID)) + + result, err := a.prc.ActivateWorkflowInRegistry(workflow.WorkflowID) + if err != nil { + return fmt.Errorf("failed to activate workflow in private registry: %w", err) + } + + ui.Success("Workflow activated successfully") + ui.Line() + ui.Bold("Details:") + ui.Dim(fmt.Sprintf(" Registry: %s", h.runtimeContext.ResolvedRegistry.ID())) + ui.Dim(fmt.Sprintf(" Workflow Name: %s", result.WorkflowName)) + ui.Dim(fmt.Sprintf(" Workflow ID: %s", result.WorkflowID)) + ui.Dim(fmt.Sprintf(" Status: %s", privateregistryclient.FormatStatus(result.Status))) + if result.Owner != "" { + ui.Dim(fmt.Sprintf(" Owner: %s", result.Owner)) + } + + return nil +} diff --git a/cmd/workflow/build/build.go b/cmd/workflow/build/build.go index 9ff70621..f92f6973 100644 --- a/cmd/workflow/build/build.go +++ b/cmd/workflow/build/build.go @@ -25,14 +25,16 @@ func New(runtimeContext *runtime.Context) *cobra.Command { Example: `cre workflow build ./my-workflow`, RunE: func(cmd *cobra.Command, args []string) error { outputPath, _ := cmd.Flags().GetString("output") - return execute(args[0], outputPath) + skipTypeChecks, _ := cmd.Flags().GetBool(cmdcommon.SkipTypeChecksCLIFlag) + return execute(args[0], outputPath, skipTypeChecks) }, } buildCmd.Flags().StringP("output", "o", "", "Output file path for the compiled WASM binary (default: /binary.wasm)") + buildCmd.Flags().Bool(cmdcommon.SkipTypeChecksCLIFlag, false, "Skip TypeScript project typecheck during compilation (passes "+cmdcommon.SkipTypeChecksFlag+" to cre-compile)") return buildCmd } -func execute(workflowFolder, outputPath string) error { +func execute(workflowFolder, outputPath string, skipTypeChecks bool) error { workflowDir, err := filepath.Abs(workflowFolder) if err != nil { return fmt.Errorf("resolve workflow folder: %w", err) @@ -58,7 +60,10 @@ func execute(workflowFolder, outputPath string) error { outputPath = cmdcommon.EnsureWasmExtension(outputPath) ui.Dim("Compiling workflow...") - wasmBytes, err := cmdcommon.CompileWorkflowToWasm(resolvedPath, true) + wasmBytes, err := cmdcommon.CompileWorkflowToWasm(resolvedPath, cmdcommon.WorkflowCompileOptions{ + StripSymbols: true, + SkipTypeChecks: skipTypeChecks, + }) if err != nil { ui.Error("Build failed:") return fmt.Errorf("failed to compile workflow: %w", err) diff --git a/cmd/workflow/build/build_test.go b/cmd/workflow/build/build_test.go index 9955160c..e18e0431 100644 --- a/cmd/workflow/build/build_test.go +++ b/cmd/workflow/build/build_test.go @@ -73,6 +73,14 @@ func TestBuildCommandDefaultFlag(t *testing.T) { assert.Equal(t, "o", f.Shorthand) } +func TestBuildCommandSkipTypeChecksFlag(t *testing.T) { + t.Parallel() + cmd := New(nil) + f := cmd.Flags().Lookup(cmdcommon.SkipTypeChecksCLIFlag) + require.NotNil(t, f) + assert.Equal(t, "false", f.DefValue) +} + func TestBuildMissingWorkflowYAML(t *testing.T) { t.Parallel() tmpDir := t.TempDir() diff --git a/cmd/workflow/convert/convert.go b/cmd/workflow/convert/convert.go index 078ba0e6..e1dba583 100644 --- a/cmd/workflow/convert/convert.go +++ b/cmd/workflow/convert/convert.go @@ -168,9 +168,10 @@ func makefileContent(workflowDir, lang string, mainFile string) (string, error) } func makefileContentTS(_, mainFile string) (string, error) { - return fmt.Sprintf(`.PHONY: build + return fmt.Sprintf(`# Append %s after wasm/workflow.wasm to skip TypeScript typecheck (not recommended for production). +.PHONY: build build: bun cre-compile %s wasm/workflow.wasm -`, mainFile), nil +`, cmdcommon.SkipTypeChecksFlag, mainFile), nil } diff --git a/cmd/workflow/convert/convert_test.go b/cmd/workflow/convert/convert_test.go index a400749b..3b7275f3 100644 --- a/cmd/workflow/convert/convert_test.go +++ b/cmd/workflow/convert/convert_test.go @@ -239,7 +239,7 @@ production-settings: ` require.NoError(t, os.WriteFile(workflowYAML, []byte(yamlContent), 0600)) require.NoError(t, os.WriteFile(mainTS, []byte("export default function run() { return Promise.resolve({ result: \"ok\" }); }\n"), 0600)) - require.NoError(t, os.WriteFile(packageJSON, []byte(`{"name":"test","private":true,"dependencies":{"@chainlink/cre-sdk":"^1.0.3"}}`), 0600)) + require.NoError(t, os.WriteFile(packageJSON, []byte(`{"name":"test","private":true,"dependencies":{"@chainlink/cre-sdk":"^1.5.0"}}`), 0600)) h := newHandler(nil) err := h.Execute(Inputs{WorkflowFolder: dir, Force: true}) diff --git a/cmd/workflow/delete/delete.go b/cmd/workflow/delete/delete.go index 78ee36e8..50ab39cc 100644 --- a/cmd/workflow/delete/delete.go +++ b/cmd/workflow/delete/delete.go @@ -1,26 +1,18 @@ package delete import ( - "encoding/hex" - "errors" "fmt" "io" - "math/big" - "sync" - "time" - "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/viper" "github.com/smartcontractkit/cre-cli/cmd/client" - cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" - "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" ) @@ -29,9 +21,6 @@ type Inputs struct { WorkflowName string `validate:"workflow_name"` WorkflowOwner string `validate:"workflow_owner"` SkipConfirmation bool - - WorkflowRegistryContractAddress string `validate:"required"` - WorkflowRegistryContractChainName string `validate:"required"` } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -72,13 +61,9 @@ type handler struct { credentials *credentials.Credentials environmentSet *environments.EnvironmentSet inputs Inputs - wrc *client.WorkflowRegistryV2Client runtimeContext *runtime.Context validated bool - - wg sync.WaitGroup - wrcErr error } func newHandler(ctx *runtime.Context, stdin io.Reader) *handler { @@ -92,33 +77,40 @@ func newHandler(ctx *runtime.Context, stdin io.Reader) *handler { environmentSet: ctx.EnvironmentSet, runtimeContext: ctx, validated: false, - wg: sync.WaitGroup{}, - wrcErr: nil, } - h.wg.Add(1) - go func() { - defer h.wg.Done() - wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() - if err != nil { - h.wrcErr = fmt.Errorf("failed to create workflow registry client: %w", err) - return - } - h.wrc = wrc - }() return &h } func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { + resolvedWorkflowOwner, err := h.resolveWorkflowOwner() + if err != nil { + return Inputs{}, fmt.Errorf("failed to resolve workflow owner: %w", err) + } + return Inputs{ - WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, - WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, - SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), - WorkflowRegistryContractChainName: h.environmentSet.WorkflowRegistryChainName, - WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, + WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, + WorkflowOwner: resolvedWorkflowOwner, + SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), }, nil } +// resolveWorkflowOwner returns the effective owner address for workflow ID computation. +// For private registry deploys, the derived workflow owner from the runtime context is used. +// For onchain deploys, the configured WorkflowOwner address is used directly. +func (h *handler) resolveWorkflowOwner() (string, error) { + if h.runtimeContext.ResolvedRegistry.Type() != settings.RegistryTypeOffChain { + return h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, nil + } + + owner := h.runtimeContext.DerivedWorkflowOwner + if owner == "" { + return "", fmt.Errorf("derived workflow owner is not available; ensure authentication succeeded") + } + + return owner, nil +} + func (h *handler) ValidateInputs() error { validate, err := validation.NewValidator() if err != nil { @@ -134,42 +126,40 @@ func (h *handler) ValidateInputs() error { } func (h *handler) Execute() error { - workflowName := h.inputs.WorkflowName - workflowOwner := common.HexToAddress(h.inputs.WorkflowOwner) + adapter, err := newRegistryDeleteStrategy(h.runtimeContext.ResolvedRegistry, h) + if err != nil { + return err + } h.displayWorkflowDetails() - h.wg.Wait() - if h.wrcErr != nil { - return h.wrcErr - } - - allWorkflows, err := h.wrc.GetWorkflowListByOwnerAndName(workflowOwner, workflowName, big.NewInt(0), big.NewInt(100)) + workflows, err := adapter.FetchWorkflows() if err != nil { - return fmt.Errorf("failed to get workflow list: %w", err) + return err } - if len(allWorkflows) == 0 { - ui.Warning(fmt.Sprintf("No workflows found for name: %s", workflowName)) + + if len(workflows) == 0 { + ui.Warning(fmt.Sprintf("No workflows found for name: %s", h.inputs.WorkflowName)) return nil } // Note: The way deploy is set up, there will only ever be one workflow in the command for now - h.runtimeContext.Workflow.ID = hex.EncodeToString(allWorkflows[0].WorkflowId[:]) + h.runtimeContext.Workflow.ID = workflows[0].ID - ui.Bold(fmt.Sprintf("Found %d workflow(s) to delete for name: %s", len(allWorkflows), workflowName)) - for i, wf := range allWorkflows { - status := map[uint8]string{0: "ACTIVE", 1: "PAUSED"}[wf.Status] + ui.Bold(fmt.Sprintf("Found %d workflow(s) to delete for name: %s", len(workflows), h.inputs.WorkflowName)) + for i, wf := range workflows { ui.Print(fmt.Sprintf(" %d. Workflow", i+1)) - ui.Dim(fmt.Sprintf(" ID: %s", hex.EncodeToString(wf.WorkflowId[:]))) - ui.Dim(fmt.Sprintf(" Owner: %s", wf.Owner.Hex())) - ui.Dim(fmt.Sprintf(" DON Family: %s", wf.DonFamily)) - ui.Dim(fmt.Sprintf(" Tag: %s", wf.Tag)) - ui.Dim(fmt.Sprintf(" Binary URL: %s", wf.BinaryUrl)) - ui.Dim(fmt.Sprintf(" Workflow Status: %s", status)) + ui.Dim(fmt.Sprintf(" Registry: %s", h.runtimeContext.ResolvedRegistry.ID())) + ui.Dim(fmt.Sprintf(" ID: %s", wf.ID)) + ui.Dim(fmt.Sprintf(" Owner: %s", wf.Owner)) + ui.Dim(fmt.Sprintf(" DON Family: %s", wf.DonFamily)) + ui.Dim(fmt.Sprintf(" Tag: %s", wf.Tag)) + ui.Dim(fmt.Sprintf(" Binary URL: %s", wf.BinaryURL)) + ui.Dim(fmt.Sprintf(" Workflow Status: %s", wf.Status)) ui.Line() } - shouldDeleteWorkflow, err := h.shouldDeleteWorkflow(h.inputs.SkipConfirmation, workflowName) + shouldDeleteWorkflow, err := h.shouldDeleteWorkflow(h.inputs.SkipConfirmation, h.inputs.WorkflowName) if err != nil { return err } @@ -178,82 +168,13 @@ func (h *handler) Execute() error { return nil } - ui.Dim(fmt.Sprintf("Deleting %d workflow(s)...", len(allWorkflows))) - var errs []error - for _, wf := range allWorkflows { - txOut, err := h.wrc.DeleteWorkflow(wf.WorkflowId) - if err != nil { - h.log.Error(). - Err(err). - Str("workflowId", hex.EncodeToString(wf.WorkflowId[:])). - Msg("Failed to delete workflow") - errs = append(errs, err) - continue - } - switch txOut.Type { - case client.Regular: - ui.Success("Transaction confirmed") - ui.URL(fmt.Sprintf("%s/tx/%s", h.environmentSet.WorkflowRegistryChainExplorerURL, txOut.Hash)) - ui.Success(fmt.Sprintf("Deleted workflow ID: %s", hex.EncodeToString(wf.WorkflowId[:]))) - - case client.Raw: - ui.Line() - ui.Success("MSIG workflow deletion transaction prepared!") - ui.Line() - ui.Bold("Next steps:") - ui.Line() - ui.Print(" 1. Submit the following transaction on the target chain:") - ui.Dim(fmt.Sprintf(" Chain: %s", h.inputs.WorkflowRegistryContractChainName)) - ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) - ui.Line() - ui.Print(" 2. Use the following transaction data:") - ui.Line() - ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) - ui.Line() - - case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(h.environmentSet.WorkflowRegistryChainName) - if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", h.environmentSet.WorkflowRegistryChainName, err) - } - mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) - if err != nil { - ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") - } - cldSettings := h.settings.CLDSettings - changesets := []types.Changeset{ - { - DeleteWorkflow: &types.DeleteWorkflow{ - Payload: types.UserWorkflowDeleteInput{ - WorkflowID: h.runtimeContext.Workflow.ID, - - ChainSelector: chainSelector, - MCMSConfig: mcmsConfig, - WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, - }, - }, - }, - } - csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) + ui.Dim(fmt.Sprintf("Deleting %d workflow(s)...", len(workflows))) - var fileName string - if cldSettings.ChangesetFile != "" { - fileName = cldSettings.ChangesetFile - } else { - fileName = fmt.Sprintf("DeleteWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) - } - - return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) - - default: - h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) - } - - // Workflow artifacts deletion will be handled by a background cleanup process. - } - if len(errs) > 0 { - return fmt.Errorf("failed to delete some workflows: %w", errors.Join(errs...)) + err = adapter.DeleteWorkflows(workflows) + if err != nil { + return err } + ui.Success("Workflows deleted successfully") return nil } @@ -287,6 +208,6 @@ func (h *handler) displayWorkflowDetails() { ui.Line() ui.Title(fmt.Sprintf("Deleting Workflow: %s", h.inputs.WorkflowName)) ui.Dim(fmt.Sprintf("Target: %s", h.settings.User.TargetName)) - ui.Dim(fmt.Sprintf("Owner Address: %s", h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress)) + ui.Dim(fmt.Sprintf("Owner Address: %s", h.inputs.WorkflowOwner)) ui.Line() } diff --git a/cmd/workflow/delete/delete_test.go b/cmd/workflow/delete/delete_test.go index 55ea63f7..dbdc07ad 100644 --- a/cmd/workflow/delete/delete_test.go +++ b/cmd/workflow/delete/delete_test.go @@ -88,13 +88,6 @@ func TestWorkflowDeleteCommand(t *testing.T) { } ctx.Settings.Workflow.UserWorkflowSettings.WorkflowOwnerType = constants.WorkflowOwnerTypeEOA - if tt.inputs.WorkflowRegistryContractAddress == "" { - tt.inputs.WorkflowRegistryContractAddress = "0x0000000000000000000000000000000000000000" - } - if tt.inputs.WorkflowRegistryContractChainName == "" { - tt.inputs.WorkflowRegistryContractChainName = "ethereum-testnet-sepolia" - } - handler := newHandler(ctx, testutil.EmptyMockStdinReader()) handler.inputs = tt.inputs diff --git a/cmd/workflow/delete/registry_delete_strategy.go b/cmd/workflow/delete/registry_delete_strategy.go new file mode 100644 index 00000000..3e347565 --- /dev/null +++ b/cmd/workflow/delete/registry_delete_strategy.go @@ -0,0 +1,29 @@ +package delete + +import ( + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +type WorkflowToDelete struct { + ID string + Owner string + DonFamily string + Tag string + BinaryURL string + Status string + RawID any // Holds the registry-specific ID type ([32]byte for on-chain, string for private) +} + +// registryDeleteStrategy encapsulates target-specific delete logic. +type registryDeleteStrategy interface { + FetchWorkflows() ([]WorkflowToDelete, error) + DeleteWorkflows(workflows []WorkflowToDelete) error +} + +// newRegistryDeleteStrategy returns the appropriate strategy for the given target. +func newRegistryDeleteStrategy(resolvedRegistry settings.ResolvedRegistry, h *handler) (registryDeleteStrategy, error) { + if resolvedRegistry.Type() == settings.RegistryTypeOffChain { + return newPrivateRegistryDeleteStrategy(h), nil + } + return newOnchainRegistryDeleteStrategy(h) +} diff --git a/cmd/workflow/delete/registry_delete_strategy_onchain.go b/cmd/workflow/delete/registry_delete_strategy_onchain.go new file mode 100644 index 00000000..1dd7335b --- /dev/null +++ b/cmd/workflow/delete/registry_delete_strategy_onchain.go @@ -0,0 +1,165 @@ +package delete + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/cre-cli/cmd/client" + cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/types" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type onchainRegistryDeleteStrategy struct { + h *handler + wrc *client.WorkflowRegistryV2Client + onChain *settings.OnChainRegistry + wg sync.WaitGroup + initErr error +} + +func newOnchainRegistryDeleteStrategy(h *handler) (*onchainRegistryDeleteStrategy, error) { + onChain, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "delete") + if err != nil { + return nil, err + } + + a := &onchainRegistryDeleteStrategy{h: h, onChain: onChain} + a.wg.Add(1) + go func() { + defer a.wg.Done() + wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() + if err != nil { + a.initErr = fmt.Errorf("failed to create workflow registry client: %w", err) + return + } + a.wrc = wrc + }() + return a, nil +} + +func (a *onchainRegistryDeleteStrategy) FetchWorkflows() ([]WorkflowToDelete, error) { + h := a.h + + a.wg.Wait() + if a.initErr != nil { + return nil, a.initErr + } + + workflowName := h.inputs.WorkflowName + workflowOwner := common.HexToAddress(h.inputs.WorkflowOwner) + + allWorkflows, err := a.wrc.GetWorkflowListByOwnerAndName(workflowOwner, workflowName, big.NewInt(0), big.NewInt(100)) + if err != nil { + return nil, fmt.Errorf("failed to get workflow list: %w", err) + } + + var workflows []WorkflowToDelete + for _, wf := range allWorkflows { + status := map[uint8]string{0: "ACTIVE", 1: "PAUSED"}[wf.Status] + workflows = append(workflows, WorkflowToDelete{ + ID: hex.EncodeToString(wf.WorkflowId[:]), + Owner: wf.Owner.Hex(), + DonFamily: wf.DonFamily, + Tag: wf.Tag, + BinaryURL: wf.BinaryUrl, + Status: status, + RawID: wf.WorkflowId, + }) + } + + return workflows, nil +} + +func (a *onchainRegistryDeleteStrategy) DeleteWorkflows(workflows []WorkflowToDelete) error { + h := a.h + var errs []error + for _, wf := range workflows { + workflowID := wf.RawID.([32]byte) + txOut, err := a.wrc.DeleteWorkflow(workflowID) + if err != nil { + h.log.Error(). + Err(err). + Str("workflowId", wf.ID). + Msg("Failed to delete workflow") + errs = append(errs, err) + continue + } + oc := a.onChain + + switch txOut.Type { + case client.Regular: + ui.Success("Transaction confirmed") + ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) + ui.Success(fmt.Sprintf("Deleted workflow ID: %s", wf.ID)) + + case client.Raw: + ui.Line() + ui.Success("MSIG workflow deletion transaction prepared!") + ui.Line() + ui.Bold("Next steps:") + ui.Line() + ui.Print(" 1. Submit the following transaction on the target chain:") + ui.Dim(fmt.Sprintf(" Chain: %s", oc.ChainName())) + ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) + ui.Line() + ui.Print(" 2. Use the following transaction data:") + ui.Line() + ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) + ui.Line() + + case client.Changeset: + chainSelector, err := settings.GetChainSelectorByChainName(oc.ChainName()) + if err != nil { + return fmt.Errorf("failed to get chain selector for chain %q: %w", oc.ChainName(), err) + } + mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) + if err != nil { + ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") + } + cldSettings := h.settings.CLDSettings + changesets := []types.Changeset{ + { + DeleteWorkflow: &types.DeleteWorkflow{ + Payload: types.UserWorkflowDeleteInput{ + WorkflowID: wf.ID, + + ChainSelector: chainSelector, + MCMSConfig: mcmsConfig, + WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, + }, + }, + }, + } + csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) + + var fileName string + if cldSettings.ChangesetFile != "" { + fileName = cldSettings.ChangesetFile + } else { + fileName = fmt.Sprintf("DeleteWorkflow_%s_%s.yaml", h.inputs.WorkflowName, time.Now().Format("20060102_150405")) + } + + err = cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) + if err != nil { + return err + } + + default: + h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) + } + + // Workflow artifacts deletion will be handled by a background cleanup process. + } + if len(errs) > 0 { + return fmt.Errorf("failed to delete some workflows: %w", errors.Join(errs...)) + } + return nil +} diff --git a/cmd/workflow/delete/registry_delete_strategy_private.go b/cmd/workflow/delete/registry_delete_strategy_private.go new file mode 100644 index 00000000..22f832bf --- /dev/null +++ b/cmd/workflow/delete/registry_delete_strategy_private.go @@ -0,0 +1,71 @@ +package delete + +import ( + "fmt" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/client/privateregistryclient" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type privateRegistryDeleteStrategy struct { + h *handler + prc *privateregistryclient.Client +} + +func newPrivateRegistryDeleteStrategy(h *handler) *privateRegistryDeleteStrategy { + return &privateRegistryDeleteStrategy{h: h} +} + +func (a *privateRegistryDeleteStrategy) ensureClient() { + if a.prc == nil { + gql := graphqlclient.New(a.h.runtimeContext.Credentials, a.h.environmentSet, a.h.log) + a.prc = privateregistryclient.New(gql, a.h.log) + } +} + +func (a *privateRegistryDeleteStrategy) FetchWorkflows() ([]WorkflowToDelete, error) { + a.ensureClient() + + h := a.h + workflowName := h.inputs.WorkflowName + + ui.Dim(fmt.Sprintf("Fetching workflow to delete... Name=%s", workflowName)) + + workflow, err := a.prc.GetWorkflowByName(workflowName) + if err != nil { + return nil, fmt.Errorf("failed to get workflow: %w", err) + } + + return []WorkflowToDelete{ + { + ID: workflow.WorkflowID, + Owner: workflow.Owner, + DonFamily: workflow.DonFamily, + Tag: workflow.Tag, + BinaryURL: workflow.BinaryURL, + Status: privateregistryclient.FormatStatus(workflow.Status), + RawID: workflow.WorkflowID, + }, + }, nil +} + +func (a *privateRegistryDeleteStrategy) DeleteWorkflows(workflows []WorkflowToDelete) error { + h := a.h + + for _, wf := range workflows { + workflowID := wf.RawID.(string) + deletedID, err := a.prc.DeleteWorkflowInRegistry(workflowID) + if err != nil { + h.log.Error(). + Err(err). + Str("workflowId", workflowID). + Msg("Failed to delete workflow") + return fmt.Errorf("failed to delete workflow in private registry: %w", err) + } + + ui.Success(fmt.Sprintf("Deleted workflow ID: %s", deletedID)) + } + + return nil +} diff --git a/cmd/workflow/deploy/artifacts.go b/cmd/workflow/deploy/artifacts.go index 7e85e185..070b1421 100644 --- a/cmd/workflow/deploy/artifacts.go +++ b/cmd/workflow/deploy/artifacts.go @@ -5,7 +5,6 @@ import ( "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" "github.com/smartcontractkit/cre-cli/internal/client/storageclient" - "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -13,6 +12,9 @@ func (h *handler) uploadArtifacts() error { if h.workflowArtifact == nil { return fmt.Errorf("workflowArtifact is nil") } + if h.inputs.WorkflowOwner == "" { + return fmt.Errorf("workflow owner is empty") + } // User-provided URLs (via --wasm URL / --config URL) skip the corresponding uploads. binaryFromURL := h.urlBinaryData != nil && h.inputs.BinaryURL != "" @@ -33,12 +35,7 @@ func (h *handler) uploadArtifacts() error { gql := graphqlclient.New(h.credentials, h.environmentSet, h.log) - chainSelector, err := settings.GetChainSelectorByChainName(h.environmentSet.WorkflowRegistryChainName) - if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", h.environmentSet.WorkflowRegistryChainName, err) - } - - storageClient := storageclient.New(gql, h.environmentSet.WorkflowRegistryAddress, h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, chainSelector, h.log) + storageClient := storageclient.New(gql, h.inputs.WorkflowOwner, h.log) if h.settings.StorageSettings.CREStorage.ServiceTimeout != 0 { storageClient.SetServiceTimeout(h.settings.StorageSettings.CREStorage.ServiceTimeout) } @@ -60,6 +57,7 @@ func (h *handler) uploadArtifacts() error { if !configFromURL && len(configData) > 0 { ui.Success(fmt.Sprintf("Loaded config from: %s", h.inputs.ConfigPath)) + var err error configURL, err = storageClient.UploadArtifactWithRetriesAndGetURL( workflowID, storageclient.ArtifactTypeConfig, configData, "text/plain") if err != nil { diff --git a/cmd/workflow/deploy/artifacts_test.go b/cmd/workflow/deploy/artifacts_test.go index d9591b04..24833d9c 100644 --- a/cmd/workflow/deploy/artifacts_test.go +++ b/cmd/workflow/deploy/artifacts_test.go @@ -16,12 +16,15 @@ import ( "github.com/smartcontractkit/cre-cli/internal/testutil/chainsim" ) -func mockGraphQL() { +func mockGraphQL(onRequest func(GraphQLRequest)) { httpmock.RegisterResponder("POST", "http://graphql.endpoint", func(req *http.Request) (*http.Response, error) { bodyBytes, _ := io.ReadAll(req.Body) var gqlReq GraphQLRequest _ = json.Unmarshal(bodyBytes, &gqlReq) + if onRequest != nil { + onRequest(gqlReq) + } if strings.Contains(gqlReq.Query, "mutation GeneratePresignedPostUrlForArtifact") { resp, _ := httpmock.NewJsonResponse(200, map[string]interface{}{ @@ -81,7 +84,7 @@ func TestUpload_SuccessAndErrorCases(t *testing.T) { "", ) - mockGraphQL() + mockGraphQL(nil) // Mock origin upload response httpmock.RegisterResponder("POST", "http://origin/upload", @@ -157,7 +160,7 @@ func TestUploadArtifactToStorageService_OriginError(t *testing.T) { "", ) - mockGraphQL() + mockGraphQL(nil) // Mock origin upload response httpmock.RegisterResponder("POST", "http://origin/upload", @@ -242,3 +245,56 @@ func TestUploadArtifactToStorageService_AlreadyExistsError(t *testing.T) { require.Equal(t, "http://origin/get", h.inputs.BinaryURL) require.Equal(t, "http://origin/get", *h.inputs.ConfigURL) } + +func TestUpload_UsesResolvedWorkflowOwnerForPresignedUrls(t *testing.T) { + httpmock.Activate() + t.Cleanup(httpmock.DeactivateAndReset) + + t.Setenv(credentials.CreApiKeyVar, "test-api") + + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + t.Cleanup(simulatedEnvironment.Close) + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + h.inputs.WorkflowOwner = "0x2222222222222222222222222222222222222222" + h.inputs.WorkflowName = "test_workflow" + h.inputs.DonFamily = "test_label" + + // Intentionally set a different configured owner to ensure uploads use the resolved owner. + h.settings = createTestSettings( + "0x1111111111111111111111111111111111111111", + "eoa", + "test_workflow", + "", + "", + ) + + var ownersUsed []string + mockGraphQL(func(gqlReq GraphQLRequest) { + if !strings.Contains(gqlReq.Query, "mutation GeneratePresignedPostUrlForArtifact") { + return + } + artifact, ok := gqlReq.Variables["artifact"].(map[string]interface{}) + require.True(t, ok, "expected artifact input in GraphQL variables") + owner, ok := artifact["workflowOwnerAddress"].(string) + require.True(t, ok, "expected workflowOwnerAddress in artifact input") + ownersUsed = append(ownersUsed, owner) + }) + + httpmock.RegisterResponder("POST", "http://origin/upload", + httpmock.NewStringResponder(201, "")) + + h.environmentSet.GraphQLURL = "http://graphql.endpoint" + h.workflowArtifact = &workflowArtifact{ + BinaryData: []byte("binarydata"), + ConfigData: []byte("configdata"), + WorkflowID: "workflow-id", + } + + err := h.uploadArtifacts() + require.NoError(t, err) + require.NotEmpty(t, ownersUsed) + for _, owner := range ownersUsed { + require.Equal(t, h.inputs.WorkflowOwner, owner) + } +} diff --git a/cmd/workflow/deploy/autoLink.go b/cmd/workflow/deploy/auto_link.go similarity index 92% rename from cmd/workflow/deploy/autoLink.go rename to cmd/workflow/deploy/auto_link.go index 48eae2fa..2ee140bf 100644 --- a/cmd/workflow/deploy/autoLink.go +++ b/cmd/workflow/deploy/auto_link.go @@ -13,6 +13,7 @@ import ( linkkey "github.com/smartcontractkit/cre-cli/cmd/account/link_key" "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" "github.com/smartcontractkit/cre-cli/internal/runtime" + "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/ui" ) @@ -21,7 +22,7 @@ const ( ) // ensureOwnerLinkedOrFail checks if the owner is linked and attempts auto-link if needed -func (h *handler) ensureOwnerLinkedOrFail() error { +func (h *handler) ensureOwnerLinkedOrFail(onChain *settings.OnChainRegistry) error { ownerAddr := common.HexToAddress(h.inputs.WorkflowOwner) linked, err := h.wrc.IsOwnerLinked(ownerAddr) @@ -47,7 +48,7 @@ func (h *handler) ensureOwnerLinkedOrFail() error { } ui.Dim(fmt.Sprintf("Owner not linked. Attempting auto-link: owner=%s", ownerAddr.Hex())) - if err := h.tryAutoLink(); err != nil { + if err := h.tryAutoLink(onChain); err != nil { return fmt.Errorf("auto-link attempt failed: %w", err) } @@ -62,7 +63,7 @@ func (h *handler) ensureOwnerLinkedOrFail() error { } // autoLinkMSIGAndExit handles MSIG auto-link and exits if manual intervention is needed -func (h *handler) autoLinkMSIGAndExit() (halt bool, err error) { +func (h *handler) autoLinkMSIGAndExit(onChain *settings.OnChainRegistry) (halt bool, err error) { ownerAddr := common.HexToAddress(h.inputs.WorkflowOwner) linked, err := h.wrc.IsOwnerLinked(ownerAddr) @@ -88,7 +89,7 @@ func (h *handler) autoLinkMSIGAndExit() (halt bool, err error) { ui.Dim(fmt.Sprintf("MSIG workflow owner link status: owner=%s, linked=%v", ownerAddr.Hex(), linked)) ui.Dim(fmt.Sprintf("MSIG owner: attempting auto-link... owner=%s", ownerAddr.Hex())) - if err := h.tryAutoLink(); err != nil { + if err := h.tryAutoLink(onChain); err != nil { return false, fmt.Errorf("MSIG auto-link attempt failed: %w", err) } @@ -97,7 +98,7 @@ func (h *handler) autoLinkMSIGAndExit() (halt bool, err error) { } // tryAutoLink executes the auto-link process using the link-key command -func (h *handler) tryAutoLink() error { +func (h *handler) tryAutoLink(onChain *settings.OnChainRegistry) error { rtx := &runtime.Context{ Settings: h.settings, Credentials: h.credentials, @@ -106,13 +107,11 @@ func (h *handler) tryAutoLink() error { EnvironmentSet: h.environmentSet, } - lkInputs := linkkey.Inputs{ - WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, - WorkflowRegistryContractAddress: h.inputs.WorkflowRegistryContractAddress, + return linkkey.Exec(rtx, linkkey.Inputs{ + WorkflowOwner: h.inputs.WorkflowOwner, + WorkflowRegistryContractAddress: onChain.Address(), WorkflowOwnerLabel: h.inputs.OwnerLabel, - } - - return linkkey.Exec(rtx, lkInputs) + }) } // checkLinkStatusViaGraphQL checks if the owner is linked and verified by querying the service diff --git a/cmd/workflow/deploy/autoLink_test.go b/cmd/workflow/deploy/auto_link_test.go similarity index 95% rename from cmd/workflow/deploy/autoLink_test.go rename to cmd/workflow/deploy/auto_link_test.go index fc1d8981..e192ccfa 100644 --- a/cmd/workflow/deploy/autoLink_test.go +++ b/cmd/workflow/deploy/auto_link_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/testutil/chainsim" ) @@ -348,21 +349,22 @@ func TestWaitForBackendLinkProcessing(t *testing.T) { } } -// TestTryAutoLink tests the auto-link execution setup -func TestTryAutoLink(t *testing.T) { +func TestTryAutoLinkUsesOnChainRegistry(t *testing.T) { t.Parallel() - t.Run("sets up linkkey inputs correctly", func(t *testing.T) { + t.Run("contract address comes from resolved on-chain registry", func(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) defer simulatedEnvironment.Close() ctx, _ := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() h := newHandler(ctx, nil) h.inputs.WorkflowOwner = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" - h.inputs.WorkflowRegistryContractAddress = "0x1234567890123456789012345678901234567890" + h.inputs.OwnerLabel = "my-label" - // Verify the handler is set up correctly + onChain, err := settings.AsOnChain(ctx.ResolvedRegistry, "test") + assert.NoError(t, err) + assert.Equal(t, simulatedEnvironment.Contracts.WorkflowRegistry.Contract.Hex(), onChain.Address()) assert.Equal(t, "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", h.inputs.WorkflowOwner) - assert.Equal(t, "0x1234567890123456789012345678901234567890", h.inputs.WorkflowRegistryContractAddress) + assert.Equal(t, "my-label", h.inputs.OwnerLabel) }) } diff --git a/cmd/workflow/deploy/compile.go b/cmd/workflow/deploy/compile.go index 4fa27b49..ecb3c064 100644 --- a/cmd/workflow/deploy/compile.go +++ b/cmd/workflow/deploy/compile.go @@ -67,7 +67,10 @@ func (h *handler) Compile() error { h.runtimeContext.Workflow.Language = cmdcommon.GetWorkflowLanguage(workflowMainFile) } - wasmFile, err = cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath, true) + wasmFile, err = cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ + StripSymbols: true, + SkipTypeChecks: h.inputs.SkipTypeChecks, + }) if err != nil { ui.Error("Build failed:") return fmt.Errorf("failed to compile workflow: %w", err) diff --git a/cmd/workflow/deploy/compile_test.go b/cmd/workflow/deploy/compile_test.go index acc2a30d..149ac19a 100644 --- a/cmd/workflow/deploy/compile_test.go +++ b/cmd/workflow/deploy/compile_test.go @@ -51,10 +51,8 @@ func TestCompileCmd(t *testing.T) { { name: "Invalid ConfigPath", cmd: Inputs{ - WorkflowPath: "testdata/test_workflow.yaml", - ConfigPath: "nonexistent.yaml", - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowPath: "testdata/test_workflow.yaml", + ConfigPath: "nonexistent.yaml", }, WorkflowOwnerType: constants.WorkflowOwnerTypeEOA, wantError: true, @@ -64,10 +62,8 @@ func TestCompileCmd(t *testing.T) { { name: "Non-ASCII ConfigPath", cmd: Inputs{ - WorkflowPath: "testdata/test_workflow.yaml", - ConfigPath: "./testdata/đuveč.yaml", - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowPath: "testdata/test_workflow.yaml", + ConfigPath: "./testdata/đuveč.yaml", }, WorkflowOwnerType: constants.WorkflowOwnerTypeEOA, wantError: true, @@ -77,10 +73,8 @@ func TestCompileCmd(t *testing.T) { { name: "Non-ASCII OutputPath", cmd: Inputs{ - WorkflowPath: "testdata/test_workflow.yaml", - OutputPath: "outputŠČ.yaml", - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowPath: "testdata/test_workflow.yaml", + OutputPath: "outputŠČ.yaml", }, WorkflowOwnerType: constants.WorkflowOwnerTypeEOA, wantError: true, @@ -177,13 +171,11 @@ func TestCompileCmd(t *testing.T) { defer simulatedEnvironment.Close() err := runCompile(simulatedEnvironment, Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "malformed_workflow", "main.go"), - OutputPath: outputPath, - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + DonFamily: "test_label", + WorkflowPath: filepath.Join("testdata", "malformed_workflow", "main.go"), + OutputPath: outputPath, }, constants.WorkflowOwnerTypeEOA) require.Error(t, err) assert.ErrorContains(t, err, "failed to compile workflow") @@ -195,13 +187,11 @@ func TestCompileCmd(t *testing.T) { func TestCompileOutputMatchesUnderlying(t *testing.T) { simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) baseInputs := Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - ConfigPath: filepath.Join("testdata", "basic_workflow", "config.yml"), - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + DonFamily: "test_label", + WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), + ConfigPath: filepath.Join("testdata", "basic_workflow", "config.yml"), } t.Run("default output path", func(t *testing.T) { @@ -238,6 +228,7 @@ func createTestSettings(workflowOwnerAddress, workflowOwnerType, workflowName, w WorkflowOwnerAddress string `mapstructure:"workflow-owner-address" yaml:"workflow-owner-address"` WorkflowOwnerType string `mapstructure:"workflow-owner-type" yaml:"workflow-owner-type"` WorkflowName string `mapstructure:"workflow-name" yaml:"workflow-name"` + DeploymentRegistry string `mapstructure:"deployment-registry" yaml:"deployment-registry"` }{ WorkflowOwnerAddress: workflowOwnerAddress, WorkflowOwnerType: workflowOwnerType, @@ -295,7 +286,10 @@ func outputPathWithExtensions(path string) string { // file content equals CompileWorkflowToWasm(workflowPath) + brotli + base64. func assertCompileOutputMatchesUnderlying(t *testing.T, simulatedEnvironment *chainsim.SimulatedEnvironment, inputs Inputs, ownerType string) { t.Helper() - wasm, err := cmdcommon.CompileWorkflowToWasm(inputs.WorkflowPath, true) + wasm, err := cmdcommon.CompileWorkflowToWasm(inputs.WorkflowPath, cmdcommon.WorkflowCompileOptions{ + StripSymbols: true, + SkipTypeChecks: inputs.SkipTypeChecks, + }) require.NoError(t, err) compressed, err := cmdcommon.CompressBrotli(wasm) require.NoError(t, err) @@ -326,14 +320,12 @@ func TestCompileWithWasmPath(t *testing.T) { t.Cleanup(func() { _ = os.Remove(outputPath) }) inputs := Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - WasmPath: wasmFile, - OutputPath: outputPath, - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + DonFamily: "test_label", + WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), + WasmPath: wasmFile, + OutputPath: outputPath, } err := runCompile(simulatedEnvironment, inputs, constants.WorkflowOwnerTypeEOA) @@ -370,14 +362,12 @@ func TestCompileWithWasmPath(t *testing.T) { t.Cleanup(func() { _ = os.Remove(outputPath) }) inputs := Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - WasmPath: wasmFile, - OutputPath: outputPath, - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + DonFamily: "test_label", + WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), + WasmPath: wasmFile, + OutputPath: outputPath, } err = runCompile(simulatedEnvironment, inputs, constants.WorkflowOwnerTypeEOA) @@ -409,13 +399,11 @@ func TestCompileWithWasmPath(t *testing.T) { ) handler.settings = ctx.Settings handler.inputs = Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - WasmPath: "/nonexistent/path/to/file.wasm", - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + DonFamily: "test_label", + WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), + WasmPath: "/nonexistent/path/to/file.wasm", } err := handler.ValidateInputs() @@ -438,13 +426,11 @@ func TestCompileWithWasmPath(t *testing.T) { ) handler.settings = ctx.Settings handler.inputs = Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - WasmPath: "https://example.com/binary.wasm", - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + DonFamily: "test_label", + WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), + WasmPath: "https://example.com/binary.wasm", } handler.validated = true @@ -474,7 +460,7 @@ func TestCompileWithWasmPath(t *testing.T) { handler.urlBinaryData = wasmContent handler.workflowArtifact = &workflowArtifact{} - err := handler.PrepareWorkflowArtifact() + err := handler.PrepareWorkflowArtifact(chainsim.TestAddress) require.NoError(t, err) assert.NotEmpty(t, handler.workflowArtifact.WorkflowID) assert.Nil(t, handler.workflowArtifact.BinaryData, "BinaryData should be nil for URL case") @@ -505,7 +491,7 @@ func TestCompileWithWasmPath(t *testing.T) { handler.urlConfigData = configContent handler.workflowArtifact = &workflowArtifact{} - err = handler.PrepareWorkflowArtifact() + err = handler.PrepareWorkflowArtifact(chainsim.TestAddress) require.NoError(t, err) assert.NotEmpty(t, handler.workflowArtifact.WorkflowID) assert.Nil(t, handler.workflowArtifact.ConfigData, "ConfigData should be nil for URL case") @@ -522,6 +508,11 @@ func TestCustomWasmWorkflowRunsMakeBuild(t *testing.T) { _ = os.Remove(wasmPath) t.Cleanup(func() { _ = os.Remove(wasmPath) }) + require.NoError(t, os.MkdirAll(filepath.Dir(wasmPath), 0o755)) + // ValidateInputs requires a readable workflow path; seed a minimal wasm header so make can replace the binary. + minimalWasm := append([]byte{0x00, 0x61, 0x73, 0x6d}, make([]byte, 8)...) + require.NoError(t, os.WriteFile(wasmPath, minimalWasm, 0600)) + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) defer simulatedEnvironment.Close() @@ -529,14 +520,12 @@ func TestCustomWasmWorkflowRunsMakeBuild(t *testing.T) { t.Cleanup(func() { _ = os.Remove(outputPath) }) inputs := Inputs{ - WorkflowName: "custom_wasm_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: wasmPath, - ConfigPath: filepath.Join(customWasmDir, "config.yml"), - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", - OutputPath: outputPath, + WorkflowName: "custom_wasm_workflow", + WorkflowOwner: chainsim.TestAddress, + DonFamily: "test_label", + WorkflowPath: wasmPath, + ConfigPath: filepath.Join(customWasmDir, "config.yml"), + OutputPath: outputPath, } // runCompile calls ValidateInputs then Compile; CompileWorkflowToWasm runs make build internally. No manual make build. diff --git a/cmd/workflow/deploy/deploy.go b/cmd/workflow/deploy/deploy.go index 2fa9e312..c243faae 100644 --- a/cmd/workflow/deploy/deploy.go +++ b/cmd/workflow/deploy/deploy.go @@ -5,9 +5,7 @@ import ( "errors" "fmt" "io" - "sync" - "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -15,7 +13,6 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/client" cmdcommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/accessrequest" - "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" @@ -39,11 +36,10 @@ type Inputs struct { OutputPath string `validate:"omitempty,filepath,ascii,max=97" cli:"--output"` WasmPath string `validate:"omitempty,file,ascii,max=2048" cli:"--wasm"` - WorkflowRegistryContractAddress string `validate:"required"` - WorkflowRegistryContractChainName string `validate:"required"` - OwnerLabel string `validate:"omitempty"` SkipConfirmation bool + // SkipTypeChecks passes --skip-type-checks to cre-compile for TypeScript workflows. + SkipTypeChecks bool } func (i *Inputs) ResolveConfigURL(fallbackURL string) string { @@ -66,8 +62,7 @@ type handler struct { wrc *client.WorkflowRegistryV2Client runtimeContext *runtime.Context accessRequester *accessrequest.Requester - - validated bool + validated bool // URL-fetched data for WorkflowID computation when --wasm or --config are URLs. urlBinaryData []byte @@ -76,9 +71,6 @@ type handler struct { // existingWorkflowStatus stores the status of an existing workflow when updating. // nil means this is a new workflow, otherwise it contains the current status (0=active, 1=paused). existingWorkflowStatus *uint8 - - wg sync.WaitGroup - wrcErr error } var defaultOutputPath = "./binary.wasm.br.b64" @@ -114,6 +106,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { deployCmd.Flags().String("config", "", "Override the config file path from workflow.yaml") deployCmd.Flags().Bool("no-config", false, "Deploy without a config file") deployCmd.Flags().Bool("default-config", false, "Use the config path from workflow.yaml settings (default behavior)") + deployCmd.Flags().Bool(cmdcommon.SkipTypeChecksCLIFlag, false, "Skip TypeScript project typecheck during compilation (passes "+cmdcommon.SkipTypeChecksFlag+" to cre-compile)") deployCmd.MarkFlagsMutuallyExclusive("config", "no-config", "default-config") return deployCmd @@ -129,30 +122,13 @@ func newHandler(ctx *runtime.Context, stdin io.Reader) *handler { credentials: ctx.Credentials, environmentSet: ctx.EnvironmentSet, workflowArtifact: &workflowArtifact{}, - wrc: nil, runtimeContext: ctx, accessRequester: accessrequest.NewRequester(ctx.Credentials, ctx.EnvironmentSet, ctx.Logger), - validated: false, - wg: sync.WaitGroup{}, - wrcErr: nil, } return &h } -func (h *handler) initWorkflowRegistryClient() { - h.wg.Add(1) - go func() { - defer h.wg.Done() - wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() - if err != nil { - h.wrcErr = fmt.Errorf("failed to create workflow registry client: %w", err) - return - } - h.wrc = wrc - }() -} - func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { var configURL *string if v.IsSet("config-url") { @@ -160,17 +136,22 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { configURL = &url } + resolvedWorkflowOwner, err := h.resolveWorkflowOwner(h.runtimeContext.ResolvedRegistry.Type()) + if err != nil { + return Inputs{}, fmt.Errorf("failed to resolve workflow owner: %w", err) + } + workflowTag := h.settings.Workflow.UserWorkflowSettings.WorkflowName if len(workflowTag) > 32 { workflowTag = workflowTag[:32] } - return Inputs{ + inputs := Inputs{ WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, - WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, + WorkflowOwner: resolvedWorkflowOwner, WorkflowTag: workflowTag, ConfigURL: configURL, - DonFamily: h.environmentSet.DonFamily, + DonFamily: h.runtimeContext.ResolvedRegistry.DonFamily(), WorkflowPath: h.settings.Workflow.WorkflowArtifactSettings.WorkflowPath, KeepAlive: false, @@ -179,11 +160,12 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { OutputPath: v.GetString("output"), WasmPath: v.GetString("wasm"), - WorkflowRegistryContractChainName: h.environmentSet.WorkflowRegistryChainName, - WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, - OwnerLabel: v.GetString("owner-label"), - SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), - }, nil + OwnerLabel: v.GetString("owner-label"), + SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), + SkipTypeChecks: v.GetBool(cmdcommon.SkipTypeChecksCLIFlag), + } + + return inputs, nil } func (h *handler) ValidateInputs() error { @@ -229,8 +211,55 @@ func (h *handler) Execute(ctx context.Context) error { return h.accessRequester.PromptAndSubmitRequest(ctx) } - h.initWorkflowRegistryClient() + adapter, err := newRegistryDeployStrategy(h.runtimeContext.ResolvedRegistry, h) + if err != nil { + return err + } + + if err := h.prepareArtifacts(); err != nil { + return err + } + + if err := adapter.RunPreDeployChecks(); err != nil { + if errors.Is(err, errDeployHalted) { + return nil + } + return err + } + + exists, existingStatus, err := adapter.CheckWorkflowExists( + h.inputs.WorkflowOwner, + h.inputs.WorkflowName, + h.inputs.WorkflowTag, + h.workflowArtifact.WorkflowID, + ) + if err != nil { + return fmt.Errorf("failed to check if workflow exists: %w", err) + } + h.existingWorkflowStatus = existingStatus + if exists { + if err := confirmWorkflowOverwrite(h.inputs.WorkflowName, h.inputs.SkipConfirmation); err != nil { + return err + } + } + + ui.Line() + ui.Dim("Uploading files...") + if err := h.uploadArtifacts(); err != nil { + return fmt.Errorf("failed to upload workflow: %w", err) + } + + err = adapter.Upsert() + if err == nil { + warnIfPausedWorkflowUpdate(h.existingWorkflowStatus) + } + return err +} +// prepareArtifacts handles compile/fetch, artifact preparation, and hashing. +// Artifact upload is deferred to the deploy service so it runs after any +// existing-workflow update confirmation. +func (h *handler) prepareArtifacts() error { h.displayWorkflowDetails() if cmdcommon.IsURL(h.inputs.WasmPath) { @@ -261,7 +290,7 @@ func (h *handler) Execute(ctx context.Context) error { ui.Success(fmt.Sprintf("Using config URL: %s", url)) } - if err := h.PrepareWorkflowArtifact(); err != nil { + if err := h.PrepareWorkflowArtifact(h.inputs.WorkflowOwner); err != nil { return fmt.Errorf("failed to prepare workflow artifact: %w", err) } @@ -271,93 +300,52 @@ func (h *handler) Execute(ctx context.Context) error { h.runtimeContext.Workflow.ID = h.workflowArtifact.WorkflowID - h.wg.Wait() - if h.wrcErr != nil { - return h.wrcErr - } + return nil +} - ui.Line() - ui.Dim("Verifying ownership...") - if h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerType == constants.WorkflowOwnerTypeMSIG { - halt, err := h.autoLinkMSIGAndExit() - if err != nil { - return fmt.Errorf("failed to check/handle MSIG owner link status: %w", err) - } - if halt { - return nil - } - } else { - if err := h.ensureOwnerLinkedOrFail(); err != nil { - return err - } +// resolveWorkflowOwner returns the effective owner address for workflow ID computation. +// For private registry deploys, the derived workflow owner from the runtime context is used. +// For onchain deploys, the configured WorkflowOwner address is used directly. +func (h *handler) resolveWorkflowOwner(registryType settings.RegistryType) (string, error) { + if registryType != settings.RegistryTypeOffChain { + return h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, nil } - existsErr := h.workflowExists() - if existsErr != nil { - if existsErr.Error() == "workflow with name "+h.inputs.WorkflowName+" already exists" { - ui.Warning(fmt.Sprintf("Workflow %s already exists", h.inputs.WorkflowName)) - ui.Dim("This will update the existing workflow.") - // Ask for user confirmation before updating existing workflow - if !h.inputs.SkipConfirmation { - confirm, err := ui.Confirm("Are you sure you want to overwrite the workflow?") - if err != nil { - return err - } - if !confirm { - return errors.New("deployment cancelled by user") - } - } - } else { - return existsErr - } + owner := h.runtimeContext.DerivedWorkflowOwner + if owner == "" { + return "", fmt.Errorf("derived workflow owner is not available; ensure authentication succeeded") } - if err := checkUserDonLimitBeforeDeploy( - h.wrc, - h.wrc, - common.HexToAddress(h.inputs.WorkflowOwner), - h.inputs.DonFamily, - h.inputs.WorkflowName, - h.inputs.KeepAlive, - h.existingWorkflowStatus, - ); err != nil { - return err - } + return owner, nil +} +func (h *handler) displayWorkflowDetails() { ui.Line() - ui.Dim("Uploading files...") - if err := h.uploadArtifacts(); err != nil { - return fmt.Errorf("failed to upload workflow: %w", err) - } + ui.Title(fmt.Sprintf("Deploying Workflow: %s", h.inputs.WorkflowName)) + ui.Dim(fmt.Sprintf("Target: %s", h.settings.User.TargetName)) + ui.Dim(fmt.Sprintf("Owner Address: %s", h.inputs.WorkflowOwner)) ui.Line() - ui.Dim("Preparing deployment transaction...") - if err := h.upsert(); err != nil { - return fmt.Errorf("failed to register workflow: %w", err) - } - return nil } -func (h *handler) workflowExists() error { - workflow, err := h.wrc.GetWorkflow(common.HexToAddress(h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress), h.inputs.WorkflowName, h.inputs.WorkflowTag) - if err != nil { - return err - } - if workflow.WorkflowId == [32]byte(common.Hex2Bytes(h.workflowArtifact.WorkflowID)) { - return fmt.Errorf("workflow with id %s already exists", h.workflowArtifact.WorkflowID) +func confirmWorkflowOverwrite(workflowName string, skipConfirmation bool) error { + ui.Warning(fmt.Sprintf("Workflow %s already exists", workflowName)) + ui.Dim("This will update the existing workflow.") + if !skipConfirmation { + confirm, err := ui.Confirm("Are you sure you want to overwrite the workflow?") + if err != nil { + return err + } + if !confirm { + return errors.New("deployment cancelled by user") + } } - if workflow.WorkflowName == h.inputs.WorkflowName { - status := workflow.Status - h.existingWorkflowStatus = &status - return fmt.Errorf("workflow with name %s already exists", h.inputs.WorkflowName) - } + return nil } -func (h *handler) displayWorkflowDetails() { - ui.Line() - ui.Title(fmt.Sprintf("Deploying Workflow: %s", h.inputs.WorkflowName)) - ui.Dim(fmt.Sprintf("Target: %s", h.settings.User.TargetName)) - ui.Dim(fmt.Sprintf("Owner Address: %s", h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress)) - ui.Line() +func warnIfPausedWorkflowUpdate(status *uint8) { + if status != nil && *status == workflowStatusPaused { + ui.Warning("Your workflow is paused and has been updated") + } } diff --git a/cmd/workflow/deploy/deploy_test.go b/cmd/workflow/deploy/deploy_test.go index 087a204d..e462a52b 100644 --- a/cmd/workflow/deploy/deploy_test.go +++ b/cmd/workflow/deploy/deploy_test.go @@ -1,9 +1,16 @@ package deploy import ( + "bytes" + "context" + "encoding/json" "errors" "io" "math/big" + "net/http" + "net/http/httptest" + "os" + "strings" "testing" "github.com/ethereum/go-ethereum/common" @@ -367,13 +374,11 @@ func TestValidateInputs_URLBypass(t *testing.T) { ) handler.settings = ctx.Settings handler.inputs = Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: "testdata/basic_workflow/main.go", - WasmPath: "https://example.com/binary.wasm", - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + DonFamily: "test_label", + WorkflowPath: "testdata/basic_workflow/main.go", + WasmPath: "https://example.com/binary.wasm", } err := handler.ValidateInputs() @@ -396,13 +401,11 @@ func TestValidateInputs_URLBypass(t *testing.T) { ) handler.settings = ctx.Settings handler.inputs = Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - DonFamily: "test_label", - WorkflowPath: "testdata/basic_workflow/main.go", - ConfigPath: "https://example.com/config.yaml", - WorkflowRegistryContractAddress: "0x1234567890123456789012345678901234567890", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + DonFamily: "test_label", + WorkflowPath: "testdata/basic_workflow/main.go", + ConfigPath: "https://example.com/config.yaml", } err := handler.ValidateInputs() @@ -426,6 +429,344 @@ func TestConfigFlagsMutuallyExclusive(t *testing.T) { assert.Contains(t, err.Error(), "if any flags in the group [config no-config default-config] are set none of the others can be") } +func TestValidateInputs_PrivateRegistry(t *testing.T) { + t.Run("accepts URL wasm and config with off-chain resolved registry and no on-chain contract inputs", func(t *testing.T) { + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t).WithPrivateRegistry("42", "test_label") + defer simulatedEnvironment.Close() + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + ctx.Settings = createTestSettings( + chainsim.TestAddress, + "eoa", + "test_workflow", + "testdata/basic_workflow/main.go", + "", + ) + h.settings = ctx.Settings + h.environmentSet.EnvName = "STAGING" + token := makeTestJWT(t, map[string]interface{}{ + "sub": "user1", + "org_id": "org-test-123", + }) + h.credentials = makeBearerCredentials(t, token) + h.runtimeContext.DerivedWorkflowOwner = "0xabcdef1234567890abcdef1234567890abcdef12" + ctx.Viper.Set("wasm", "https://example.com/workflow.wasm") + ctx.Viper.Set("config", "https://example.com/workflow-config.json") + + inputs, err := h.ResolveInputs(ctx.Viper) + require.NoError(t, err) + h.inputs = inputs + + err = h.ValidateInputs() + require.NoError(t, err) + assert.True(t, h.validated) + }) + + t.Run("fails when required don family is missing for private target", func(t *testing.T) { + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t).WithPrivateRegistry("42", "") + defer simulatedEnvironment.Close() + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + ctx.Settings = createTestSettings( + chainsim.TestAddress, + "eoa", + "test_workflow", + "testdata/basic_workflow/main.go", + "", + ) + h.settings = ctx.Settings + h.environmentSet.EnvName = "STAGING" + token := makeTestJWT(t, map[string]interface{}{ + "sub": "user1", + "org_id": "org-test-123", + }) + h.credentials = makeBearerCredentials(t, token) + h.runtimeContext.DerivedWorkflowOwner = "0xabcdef1234567890abcdef1234567890abcdef12" + ctx.Viper.Set("wasm", "https://example.com/workflow.wasm") + + inputs, err := h.ResolveInputs(ctx.Viper) + require.NoError(t, err) + h.inputs = inputs + + err = h.ValidateInputs() + require.Error(t, err) + var verrs validation.ValidationErrors + assert.True(t, errors.As(err, &verrs)) + validation.AssertValidationErrs(t, verrs, "Inputs.DonFamily", "DonFamily is a required field") + }) +} + +func TestExecute_PrivateRegistry(t *testing.T) { + t.Run("executes private deploy path with GraphQL success", func(t *testing.T) { + wasmContent := []byte("workflow wasm payload") + wasmServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write(wasmContent) + })) + defer wasmServer.Close() + + gqlServer := newAssertGQLServer(t, func(t *testing.T, req deployMockGraphQLRequest) (int, map[string]any) { + switch { + case req.Query != "" && containsQuery(req.Query, "query GetOffchainWorkflowByName"): + rawRequest, ok := req.Variables["request"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "test_workflow", rawRequest["workflowName"]) + return http.StatusOK, map[string]any{ + "errors": []map[string]string{{"message": "workflow not found"}}, + } + case req.Query != "" && containsQuery(req.Query, "mutation UpsertOffchainWorkflow"): + rawRequest, ok := req.Variables["request"].(map[string]any) + require.True(t, ok) + rawWorkflow, ok := rawRequest["workflow"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "test_workflow", rawWorkflow["workflowName"]) + assert.Equal(t, "test-don", rawWorkflow["donFamily"]) + assert.Equal(t, wasmServer.URL+"/binary.wasm", rawWorkflow["binaryUrl"]) + assert.Equal(t, "WORKFLOW_STATUS_ACTIVE", rawWorkflow["status"]) + + return http.StatusOK, map[string]any{ + "data": map[string]any{ + "upsertOffchainWorkflow": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": chainsim.TestAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "test_workflow", + "binaryUrl": wasmServer.URL + "/binary.wasm", + "configUrl": "", + "tag": "test_workflow", + "attributes": "", + "donFamily": "test-don", + }, + }, + }, + } + default: + t.Fatalf("unexpected GraphQL operation: %s", req.Query) + return 0, nil + } + }) + defer gqlServer.Close() + + h := newPrivateRegistryExecuteHandler(t, wasmServer.URL+"/binary.wasm", gqlServer.URL) + require.NoError(t, h.ValidateInputs()) + require.NoError(t, h.Execute(context.Background())) + assert.NotEmpty(t, h.workflowArtifact.WorkflowID) + }) + + t.Run("continues when private workflow lookup returns not found", func(t *testing.T) { + wasmServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("workflow wasm payload")) + })) + defer wasmServer.Close() + + gqlServer := newAssertGQLServer(t, func(t *testing.T, req deployMockGraphQLRequest) (int, map[string]any) { + if containsQuery(req.Query, "query GetOffchainWorkflowByName") { + return http.StatusOK, map[string]any{ + "errors": []map[string]string{{"message": "workflow not found"}}, + } + } + if containsQuery(req.Query, "mutation UpsertOffchainWorkflow") { + return http.StatusOK, map[string]any{ + "data": map[string]any{ + "upsertOffchainWorkflow": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": chainsim.TestAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "test_workflow", + "binaryUrl": wasmServer.URL + "/binary.wasm", + "configUrl": "", + "tag": "test_workflow", + "attributes": "", + "donFamily": "test-don", + }, + }, + }, + } + } + t.Fatalf("unexpected GraphQL operation: %s", req.Query) + return 0, nil + }) + defer gqlServer.Close() + + h := newPrivateRegistryExecuteHandler(t, wasmServer.URL+"/binary.wasm", gqlServer.URL) + require.NoError(t, h.ValidateInputs()) + require.NoError(t, h.Execute(context.Background())) + }) + + t.Run("prompts overwrite path can proceed with skip confirmation", func(t *testing.T) { + wasmServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("workflow wasm payload")) + })) + defer wasmServer.Close() + + gqlServer := newAssertGQLServer(t, func(t *testing.T, req deployMockGraphQLRequest) (int, map[string]any) { + if containsQuery(req.Query, "query GetOffchainWorkflowByName") { + return http.StatusOK, map[string]any{ + "data": map[string]any{ + "getOffchainWorkflowByName": map[string]any{ + "workflow": map[string]any{ + "workflowId": "existing-wf-id", + "owner": chainsim.TestAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "test_workflow", + "binaryUrl": "https://example.com/old.wasm", + "configUrl": "", + "tag": "test_workflow", + "attributes": "", + "donFamily": "test-don", + }, + }, + }, + } + } + if containsQuery(req.Query, "mutation UpsertOffchainWorkflow") { + return http.StatusOK, map[string]any{ + "data": map[string]any{ + "upsertOffchainWorkflow": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": chainsim.TestAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "test_workflow", + "binaryUrl": wasmServer.URL + "/binary.wasm", + "configUrl": "", + "tag": "test_workflow", + "attributes": "", + "donFamily": "test-don", + }, + }, + }, + } + } + t.Fatalf("unexpected GraphQL operation: %s", req.Query) + return 0, nil + }) + defer gqlServer.Close() + + h := newPrivateRegistryExecuteHandler(t, wasmServer.URL+"/binary.wasm", gqlServer.URL) + h.inputs.SkipConfirmation = true + require.NoError(t, h.ValidateInputs()) + require.NoError(t, h.Execute(context.Background())) + }) + + t.Run("surfaces GraphQL errors from private execute upsert path", func(t *testing.T) { + wasmServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("workflow wasm payload")) + })) + defer wasmServer.Close() + + gqlServer := newAssertGQLServer(t, func(t *testing.T, req deployMockGraphQLRequest) (int, map[string]any) { + if containsQuery(req.Query, "query GetOffchainWorkflowByName") { + return http.StatusOK, map[string]any{ + "errors": []map[string]string{{"message": "workflow not found"}}, + } + } + if containsQuery(req.Query, "mutation UpsertOffchainWorkflow") { + return http.StatusOK, map[string]any{ + "errors": []map[string]string{{"message": "unauthorized"}}, + } + } + t.Fatalf("unexpected GraphQL operation: %s", req.Query) + return 0, nil + }) + defer gqlServer.Close() + + h := newPrivateRegistryExecuteHandler(t, wasmServer.URL+"/binary.wasm", gqlServer.URL) + require.NoError(t, h.ValidateInputs()) + err := h.Execute(context.Background()) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to register workflow in private registry") + assert.Contains(t, err.Error(), "unauthorized") + }) + + t.Run("surfaces transport errors from private existence check", func(t *testing.T) { + wasmServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte("workflow wasm payload")) + })) + defer wasmServer.Close() + + gqlServer := newAssertGQLServer(t, func(t *testing.T, req deployMockGraphQLRequest) (int, map[string]any) { + if containsQuery(req.Query, "query GetOffchainWorkflowByName") { + return http.StatusInternalServerError, map[string]any{ + "errors": []map[string]string{{"message": "server exploded"}}, + } + } + t.Fatalf("unexpected GraphQL operation: %s", req.Query) + return 0, nil + }) + defer gqlServer.Close() + + h := newPrivateRegistryExecuteHandler(t, wasmServer.URL+"/binary.wasm", gqlServer.URL) + require.NoError(t, h.ValidateInputs()) + err := h.Execute(context.Background()) + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to check if workflow exists") + }) +} + +type deployMockGraphQLRequest struct { + Query string `json:"query"` + Variables map[string]interface{} `json:"variables"` +} + +func newAssertGQLServer( + t *testing.T, + handler func(t *testing.T, req deployMockGraphQLRequest) (status int, response map[string]any), +) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req deployMockGraphQLRequest + err := json.NewDecoder(r.Body).Decode(&req) + require.NoError(t, err) + status, response := handler(t, req) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if response != nil { + _ = json.NewEncoder(w).Encode(response) + } + })) +} + +func containsQuery(query, operation string) bool { + return query != "" && strings.Contains(query, operation) +} + +func newPrivateRegistryExecuteHandler(t *testing.T, wasmURL, gqlURL string) *handler { + t.Helper() + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t).WithPrivateRegistry("42", "test-don") + t.Cleanup(simulatedEnvironment.Close) + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + ctx.Settings = createTestSettings( + chainsim.TestAddress, + "eoa", + "test_workflow", + "testdata/basic_workflow/main.go", + "", + ) + h.settings = ctx.Settings + h.credentials = makeAPIKeyCredentials(t) + h.environmentSet.GraphQLURL = gqlURL + h.inputs = Inputs{ + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + WorkflowTag: "test_workflow", + DonFamily: "test-don", + WorkflowPath: "testdata/basic_workflow/main.go", + WasmPath: wasmURL, + } + + return h +} + func stringPtr(s string) *string { return &s } @@ -454,6 +795,47 @@ func (f fakeUserDonLimitClient) GetWorkflowListByOwnerAndName(common.Address, st return f.workflowsByOwnerName, nil } +func TestWarnExistingPausedWorkflowUpdate(t *testing.T) { + // Do not use t.Parallel: stderr redirection uses package-global os.Stderr. + + captureStderr := func(f func()) string { + t.Helper() + old := os.Stderr + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stderr = w + + f() + + require.NoError(t, w.Close()) + os.Stderr = old + + var buf bytes.Buffer + _, copyErr := io.Copy(&buf, r) + require.NoError(t, copyErr) + require.NoError(t, r.Close()) + return buf.String() + } + + t.Run("no output when status is nil", func(t *testing.T) { + out := captureStderr(func() { warnIfPausedWorkflowUpdate(nil) }) + assert.Empty(t, strings.TrimSpace(out)) + }) + + t.Run("no output when workflow is active", func(t *testing.T) { + active := workflowStatusActive + out := captureStderr(func() { warnIfPausedWorkflowUpdate(&active) }) + assert.Empty(t, strings.TrimSpace(out)) + }) + + t.Run("prints warning when workflow is paused", func(t *testing.T) { + paused := workflowStatusPaused + out := captureStderr(func() { warnIfPausedWorkflowUpdate(&paused) }) + assert.Contains(t, out, "Your workflow is paused") + assert.Contains(t, out, "and has been updated") + }) +} + func TestCheckUserDonLimitBeforeDeploy(t *testing.T) { owner := common.HexToAddress(chainsim.TestAddress) donFamily := "test-don" diff --git a/cmd/workflow/deploy/limits.go b/cmd/workflow/deploy/limits.go index 402f91fe..4a93f003 100644 --- a/cmd/workflow/deploy/limits.go +++ b/cmd/workflow/deploy/limits.go @@ -11,6 +11,7 @@ import ( const ( workflowStatusActive = uint8(0) + workflowStatusPaused = uint8(1) workflowListPageSize = int64(200) ) diff --git a/cmd/workflow/deploy/prepare.go b/cmd/workflow/deploy/prepare.go index 7ff5bdf8..41ee5073 100644 --- a/cmd/workflow/deploy/prepare.go +++ b/cmd/workflow/deploy/prepare.go @@ -42,7 +42,7 @@ func (h *handler) prepareWorkflowConfig() ([]byte, error) { return configData, nil } -func (h *handler) PrepareWorkflowArtifact() error { +func (h *handler) PrepareWorkflowArtifact(workflowOwner string) error { var binaryForID []byte if h.urlBinaryData != nil { @@ -76,7 +76,7 @@ func (h *handler) PrepareWorkflowArtifact() error { h.workflowArtifact.ConfigData = configData } - workflowID, err := workflowUtils.GenerateWorkflowIDFromStrings(h.inputs.WorkflowOwner, h.inputs.WorkflowName, binaryForID, configData, "") + workflowID, err := workflowUtils.GenerateWorkflowIDFromStrings(workflowOwner, h.inputs.WorkflowName, binaryForID, configData, "") if err != nil { return fmt.Errorf("failed to generate workflow ID: %w", err) } diff --git a/cmd/workflow/deploy/private_registry_test.go b/cmd/workflow/deploy/private_registry_test.go new file mode 100644 index 00000000..d3deb9a4 --- /dev/null +++ b/cmd/workflow/deploy/private_registry_test.go @@ -0,0 +1,369 @@ +package deploy + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + workflowUtils "github.com/smartcontractkit/chainlink-common/pkg/workflows" + + "github.com/smartcontractkit/cre-cli/internal/client/privateregistryclient" + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/ethkeys" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/testutil" + "github.com/smartcontractkit/cre-cli/internal/testutil/chainsim" +) + +func TestBuildPrivateRegistryInput(t *testing.T) { + t.Parallel() + + t.Run("maps required fields and sets active status", func(t *testing.T) { + t.Parallel() + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + defer simulatedEnvironment.Close() + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + h.inputs = Inputs{ + WorkflowName: "my-workflow", + BinaryURL: "https://storage.example.com/binary.wasm", + DonFamily: "zone-a", + WorkflowTag: "v1-tag", + } + h.workflowArtifact = &workflowArtifact{ + WorkflowID: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + } + + input := h.buildPrivateRegistryInput() + + assert.Equal(t, "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", input.WorkflowID) + assert.Equal(t, privateregistryclient.WorkflowStatusActive, input.Status) + assert.Equal(t, "my-workflow", input.WorkflowName) + assert.Equal(t, "https://storage.example.com/binary.wasm", input.BinaryURL) + assert.Equal(t, "zone-a", input.DonFamily) + require.NotNil(t, input.Tag) + assert.Equal(t, "v1-tag", *input.Tag) + }) + + t.Run("includes config URL when present", func(t *testing.T) { + t.Parallel() + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + defer simulatedEnvironment.Close() + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + configURL := "https://storage.example.com/config.yaml" + h.inputs = Inputs{ + WorkflowName: "my-workflow", + BinaryURL: "https://storage.example.com/binary.wasm", + DonFamily: "zone-a", + ConfigURL: &configURL, + } + h.workflowArtifact = &workflowArtifact{ + WorkflowID: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + } + + input := h.buildPrivateRegistryInput() + + require.NotNil(t, input.ConfigURL) + assert.Equal(t, "https://storage.example.com/config.yaml", *input.ConfigURL) + }) + + t.Run("omits config URL when absent", func(t *testing.T) { + t.Parallel() + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + defer simulatedEnvironment.Close() + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + h.inputs = Inputs{ + WorkflowName: "my-workflow", + BinaryURL: "https://storage.example.com/binary.wasm", + DonFamily: "zone-a", + } + h.workflowArtifact = &workflowArtifact{ + WorkflowID: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + } + + input := h.buildPrivateRegistryInput() + + assert.Nil(t, input.ConfigURL) + }) + + t.Run("omits tag when empty", func(t *testing.T) { + t.Parallel() + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + defer simulatedEnvironment.Close() + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + h.inputs = Inputs{ + WorkflowName: "my-workflow", + BinaryURL: "https://storage.example.com/binary.wasm", + DonFamily: "zone-a", + WorkflowTag: "", + } + h.workflowArtifact = &workflowArtifact{ + WorkflowID: "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890", + } + + input := h.buildPrivateRegistryInput() + + assert.Nil(t, input.Tag) + }) +} + +func TestCheckWorkflowExists_PrivateRegistry(t *testing.T) { + tests := []struct { + name string + serverStatus int + response map[string]any + wantExists bool + wantStatus *uint8 + wantErr bool + }{ + { + name: "found active workflow returns active status", + serverStatus: http.StatusOK, + response: map[string]any{ + "data": map[string]any{ + "getOffchainWorkflowByName": map[string]any{ + "workflow": map[string]any{ + "workflowId": "00a2b96d2f06961c3e0cf6fbba5cfa30d3b577026de094e5202d5fc3e3aabb87", + "owner": "6028e8bd8759240ffe7bd80bdd5c99ca662f3363", + "createdAt": "2026-04-10T14:07:25Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "jnowak-workflow-test-v5", + "binaryUrl": "https://storage.cre.stage.external.griddle.sh/artifacts/00a2b96d2f06961c3e0cf6fbba5cfa30d3b577026de094e5202d5fc3e3aabb87/binary.wasm", + "configUrl": "https://storage.cre.stage.external.griddle.sh/artifacts/00a2b96d2f06961c3e0cf6fbba5cfa30d3b577026de094e5202d5fc3e3aabb87/config", + "tag": "", + "attributes": "{\"app\": \"test\"}", + "donFamily": "zone-a", + "organizationId": "org_meoybOR7KEkNhEFf", + }, + }, + }, + }, + wantExists: true, + wantStatus: uint8Ptr(0), + wantErr: false, + }, + { + name: "found paused workflow returns paused status", + serverStatus: http.StatusOK, + response: map[string]any{ + "data": map[string]any{ + "getOffchainWorkflowByName": map[string]any{ + "workflow": map[string]any{ + "workflowId": "00a2b96d2f06961c3e0cf6fbba5cfa30d3b577026de094e5202d5fc3e3aabb87", + "owner": "6028e8bd8759240ffe7bd80bdd5c99ca662f3363", + "createdAt": "2026-04-10T14:07:25Z", + "status": "WORKFLOW_STATUS_PAUSED", + "workflowName": "jnowak-workflow-test-v5", + "binaryUrl": "https://storage.cre.stage.external.griddle.sh/artifacts/00a2b96d2f06961c3e0cf6fbba5cfa30d3b577026de094e5202d5fc3e3aabb87/binary.wasm", + "configUrl": "https://storage.cre.stage.external.griddle.sh/artifacts/00a2b96d2f06961c3e0cf6fbba5cfa30d3b577026de094e5202d5fc3e3aabb87/config", + "tag": "", + "attributes": "{\"app\": \"test\"}", + "donFamily": "zone-a", + "organizationId": "org_meoybOR7KEkNhEFf", + }, + }, + }, + }, + wantExists: true, + wantStatus: uint8Ptr(1), + wantErr: false, + }, + { + name: "not found returns no error and no status", + serverStatus: http.StatusOK, + response: map[string]any{ + "errors": []map[string]any{ + { + "message": "workflow not found", + "path": []string{"getOffchainWorkflowByName"}, + "extensions": map[string]any{ + "code": "NOT_FOUND", + }, + }, + }, + "data": nil, + }, + wantExists: false, + wantStatus: nil, + wantErr: false, + }, + { + name: "transport failure returns error", + serverStatus: http.StatusInternalServerError, + response: map[string]any{ + "errors": []map[string]any{ + { + "message": "server exploded", + }, + }, + }, + wantExists: false, + wantStatus: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + defer simulatedEnvironment.Close() + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + h.credentials = makeAPIKeyCredentials(t) + + gqlServer := newAssertGQLServer(t, func(t *testing.T, req deployMockGraphQLRequest) (int, map[string]any) { + require.True(t, containsQuery(req.Query, "query GetOffchainWorkflowByName")) + return tt.serverStatus, tt.response + }) + defer gqlServer.Close() + + h.environmentSet.GraphQLURL = gqlServer.URL + strategy := newPrivateRegistryDeployStrategy(h) + + exists, status, err := strategy.CheckWorkflowExists("", "jnowak-workflow-test-v5", "", "") + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.wantExists, exists) + assert.Equal(t, tt.wantStatus, status) + }) + } +} + +func makeTestJWT(t *testing.T, claims map[string]interface{}) string { + t.Helper() + header, _ := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"}) + payload, err := json.Marshal(claims) + require.NoError(t, err) + sig := base64.RawURLEncoding.EncodeToString([]byte("sig")) + return base64.RawURLEncoding.EncodeToString(header) + "." + + base64.RawURLEncoding.EncodeToString(payload) + "." + sig +} + +func makeTestCredentials(t *testing.T) *credentials.Credentials { + t.Helper() + logger := testutil.NewTestLogger() + prev, hadPrev := os.LookupEnv(credentials.CreApiKeyVar) + os.Setenv(credentials.CreApiKeyVar, "test-key-for-init") + creds, err := credentials.New(logger) + if hadPrev { + os.Setenv(credentials.CreApiKeyVar, prev) + } else { + os.Unsetenv(credentials.CreApiKeyVar) + } + require.NoError(t, err) + return creds +} + +func makeBearerCredentials(t *testing.T, token string) *credentials.Credentials { + t.Helper() + creds := makeTestCredentials(t) + creds.AuthType = credentials.AuthTypeBearer + creds.Tokens = &credentials.CreLoginTokenSet{AccessToken: token} + return creds +} + +func makeAPIKeyCredentials(t *testing.T) *credentials.Credentials { + t.Helper() + creds := makeTestCredentials(t) + creds.AuthType = credentials.AuthTypeApiKey + creds.APIKey = "test-key" + return creds +} + +func uint8Ptr(v uint8) *uint8 { + return &v +} + +func TestResolveWorkflowOwner(t *testing.T) { + t.Parallel() + + t.Run("onchain target returns configured WorkflowOwner", func(t *testing.T) { + t.Parallel() + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t) + defer simulatedEnvironment.Close() + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + ctx.Settings = createTestSettings( + chainsim.TestAddress, + "eoa", + "test_workflow", + "testdata/basic_workflow/main.go", + "", + ) + h.settings = ctx.Settings + + owner, err := h.resolveWorkflowOwner(settings.RegistryTypeOnChain) + require.NoError(t, err) + assert.Equal(t, chainsim.TestAddress, owner) + }) + + t.Run("private target uses derived workflow owner from runtime context", func(t *testing.T) { + t.Parallel() + + expectedBytes, err := workflowUtils.GenerateWorkflowOwnerAddress("42", "org-test-123") + require.NoError(t, err) + rawOwner := "0x" + hex.EncodeToString(expectedBytes) + expectedOwner, err := ethkeys.FormatWorkflowOwnerAddress(rawOwner) + require.NoError(t, err) + require.NotEmpty(t, expectedOwner) + + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t).WithPrivateRegistry("42", "test-don") + defer simulatedEnvironment.Close() + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + ctx.Settings = createTestSettings( + chainsim.TestAddress, + "eoa", + "test_workflow", + "testdata/basic_workflow/main.go", + "", + ) + h.settings = ctx.Settings + h.runtimeContext.DerivedWorkflowOwner = expectedOwner + + owner, err := h.resolveWorkflowOwner(settings.RegistryTypeOffChain) + require.NoError(t, err) + assert.Equal(t, expectedOwner, owner) + }) + + t.Run("private target errors when derived workflow owner is empty", func(t *testing.T) { + t.Parallel() + simulatedEnvironment := chainsim.NewSimulatedEnvironment(t).WithPrivateRegistry("42", "test-don") + defer simulatedEnvironment.Close() + + ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() + h := newHandler(ctx, buf) + ctx.Settings = createTestSettings( + chainsim.TestAddress, + "eoa", + "test_workflow", + "testdata/basic_workflow/main.go", + "", + ) + h.settings = ctx.Settings + h.runtimeContext.DerivedWorkflowOwner = "" + + _, err := h.resolveWorkflowOwner(settings.RegistryTypeOffChain) + require.Error(t, err) + assert.Contains(t, err.Error(), "derived workflow owner is not available") + }) +} diff --git a/cmd/workflow/deploy/register.go b/cmd/workflow/deploy/register.go index 4042c9db..6c47e237 100644 --- a/cmd/workflow/deploy/register.go +++ b/cmd/workflow/deploy/register.go @@ -14,7 +14,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/ui" ) -func (h *handler) upsert() error { +func (h *handler) upsert(onChain *settings.OnChainRegistry) error { if !h.validated { return fmt.Errorf("handler inputs not validated") } @@ -23,11 +23,7 @@ func (h *handler) upsert() error { if err != nil { return err } - return h.submitWorkflow(params) -} - -func (h *handler) submitWorkflow(params client.RegisterWorkflowV2Parameters) error { - return h.handleUpsert(params) + return h.handleUpsert(params, onChain) } func (h *handler) prepareUpsertParams() (client.RegisterWorkflowV2Parameters, error) { @@ -57,7 +53,7 @@ func (h *handler) prepareUpsertParams() (client.RegisterWorkflowV2Parameters, er }, nil } -func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters) error { +func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters, onChain *settings.OnChainRegistry) error { workflowName := h.inputs.WorkflowName workflowTag := h.inputs.WorkflowTag h.log.Debug().Interface("Workflow parameters", params).Msg("Registering workflow...") @@ -68,12 +64,13 @@ func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters) error switch txOut.Type { case client.Regular: ui.Success("Transaction confirmed") - ui.URL(fmt.Sprintf("%s/tx/%s", h.environmentSet.WorkflowRegistryChainExplorerURL, txOut.Hash)) + ui.URL(fmt.Sprintf("%s/tx/%s", onChain.ExplorerURL(), txOut.Hash)) ui.Line() ui.Success("Workflow deployed successfully") ui.Line() ui.Bold("Details:") - ui.Dim(fmt.Sprintf(" Contract address: %s", h.environmentSet.WorkflowRegistryAddress)) + ui.Dim(fmt.Sprintf(" Registry: %s", h.runtimeContext.ResolvedRegistry.ID())) + ui.Dim(fmt.Sprintf(" Contract address: %s", onChain.Address())) ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) ui.Dim(fmt.Sprintf(" Workflow ID: %s", h.workflowArtifact.WorkflowID)) @@ -90,7 +87,7 @@ func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters) error ui.Bold("Next steps:") ui.Line() ui.Print(" 1. Submit the following transaction on the target chain:") - ui.Dim(fmt.Sprintf(" Chain: %s", h.inputs.WorkflowRegistryContractChainName)) + ui.Dim(fmt.Sprintf(" Chain: %s", onChain.ChainName())) ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) ui.Line() ui.Print(" 2. Use the following transaction data:") @@ -99,9 +96,9 @@ func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters) error ui.Line() case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(h.environmentSet.WorkflowRegistryChainName) + chainSelector, err := settings.GetChainSelectorByChainName(onChain.ChainName()) if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", h.environmentSet.WorkflowRegistryChainName, err) + return fmt.Errorf("failed to get chain selector for chain %q: %w", onChain.ChainName(), err) } mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) if err != nil { diff --git a/cmd/workflow/deploy/register_test.go b/cmd/workflow/deploy/register_test.go index b0b8a6cb..da3b0241 100644 --- a/cmd/workflow/deploy/register_test.go +++ b/cmd/workflow/deploy/register_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/testutil/chainsim" ) @@ -23,16 +24,15 @@ func TestWorkflowUpsert(t *testing.T) { { name: "Valid Inputs", inputs: Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - ConfigPath: filepath.Join("testdata", "basic_workflow", "config.yml"), - DonFamily: "zone-a", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", - BinaryURL: "https://example.com/binary", - KeepAlive: true, - ConfigURL: nil, - WorkflowTag: "test_tag", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), + ConfigPath: filepath.Join("testdata", "basic_workflow", "config.yml"), + DonFamily: "zone-a", + BinaryURL: "https://example.com/binary", + KeepAlive: true, + ConfigURL: nil, + WorkflowTag: "test_tag", }, wantErr: false, wantKey: "", @@ -46,7 +46,6 @@ func TestWorkflowUpsert(t *testing.T) { ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput() handler := newHandler(ctx, buf) - tt.inputs.WorkflowRegistryContractAddress = simulatedEnvironment.Contracts.WorkflowRegistry.Contract.Hex() wrc, err := handler.clientFactory.NewWorkflowRegistryV2Client() require.NoError(t, err) @@ -64,7 +63,9 @@ func TestWorkflowUpsert(t *testing.T) { handler.workflowArtifact = &wfArt - err = handler.upsert() + onChain, err := settings.AsOnChain(ctx.ResolvedRegistry, "test") + require.NoError(t, err) + err = handler.upsert(onChain) require.NoError(t, err) }) } @@ -81,14 +82,12 @@ func TestPrepareUpsertParams_StatusPreservation(t *testing.T) { handler := newHandler(ctx, buf) handler.inputs = Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - DonFamily: "zone-a", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", - WorkflowRegistryContractAddress: simulatedEnvironment.Contracts.WorkflowRegistry.Contract.Hex(), - BinaryURL: "https://example.com/binary", - WorkflowTag: "test_tag", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), + DonFamily: "zone-a", + BinaryURL: "https://example.com/binary", + WorkflowTag: "test_tag", } handler.workflowArtifact = &workflowArtifact{ BinaryData: []byte("0x1234"), @@ -112,14 +111,12 @@ func TestPrepareUpsertParams_StatusPreservation(t *testing.T) { handler := newHandler(ctx, buf) handler.inputs = Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - DonFamily: "zone-a", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", - WorkflowRegistryContractAddress: simulatedEnvironment.Contracts.WorkflowRegistry.Contract.Hex(), - BinaryURL: "https://example.com/binary", - WorkflowTag: "test_tag", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), + DonFamily: "zone-a", + BinaryURL: "https://example.com/binary", + WorkflowTag: "test_tag", } handler.workflowArtifact = &workflowArtifact{ BinaryData: []byte("0x1234"), @@ -146,14 +143,12 @@ func TestPrepareUpsertParams_StatusPreservation(t *testing.T) { handler := newHandler(ctx, buf) handler.inputs = Inputs{ - WorkflowName: "test_workflow", - WorkflowOwner: chainsim.TestAddress, - WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), - DonFamily: "zone-a", - WorkflowRegistryContractChainName: "ethereum-testnet-sepolia", - WorkflowRegistryContractAddress: simulatedEnvironment.Contracts.WorkflowRegistry.Contract.Hex(), - BinaryURL: "https://example.com/binary", - WorkflowTag: "test_tag", + WorkflowName: "test_workflow", + WorkflowOwner: chainsim.TestAddress, + WorkflowPath: filepath.Join("testdata", "basic_workflow", "main.go"), + DonFamily: "zone-a", + BinaryURL: "https://example.com/binary", + WorkflowTag: "test_tag", } handler.workflowArtifact = &workflowArtifact{ BinaryData: []byte("0x1234"), diff --git a/cmd/workflow/deploy/registry_deploy_strategy.go b/cmd/workflow/deploy/registry_deploy_strategy.go new file mode 100644 index 00000000..434b62de --- /dev/null +++ b/cmd/workflow/deploy/registry_deploy_strategy.go @@ -0,0 +1,38 @@ +package deploy + +import ( + "errors" + + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +// errDeployHalted is a sentinel returned by RunPreDeployChecks when the deploy +// must stop without error (e.g. MSIG auto-link requires a manual step before +// re-running the command). +var errDeployHalted = errors.New("deploy halted") + +// registryDeployStrategy encapsulates target-specific deployment logic. +// The orchestrator calls these methods in a fixed sequence with common steps +// (artifact upload) between RunPreDeployChecks and Upsert. +type registryDeployStrategy interface { + // RunPreDeployChecks validates readiness and runs registry-specific + // prechecks (ownership linking, duplicate detection, etc.). + // Return errDeployHalted to stop the deploy without returning an error. + RunPreDeployChecks() error + + // CheckWorkflowExists returns whether a same-name workflow exists for this + // registry target and includes the existing workflow status for updates. + CheckWorkflowExists(workflowOwner, workflowName, workflowTag, workflowID string) (bool, *uint8, error) + + // Upsert registers or updates the workflow in the target registry + // and displays the result. + Upsert() error +} + +// newRegistryDeployStrategy returns the appropriate strategy for the given target. +func newRegistryDeployStrategy(resolvedRegistry settings.ResolvedRegistry, h *handler) (registryDeployStrategy, error) { + if resolvedRegistry.Type() == settings.RegistryTypeOffChain { + return newPrivateRegistryDeployStrategy(h), nil + } + return newOnchainRegistryDeployStrategy(h) +} diff --git a/cmd/workflow/deploy/registry_deploy_strategy_onchain.go b/cmd/workflow/deploy/registry_deploy_strategy_onchain.go new file mode 100644 index 00000000..ea6f1583 --- /dev/null +++ b/cmd/workflow/deploy/registry_deploy_strategy_onchain.go @@ -0,0 +1,111 @@ +package deploy + +import ( + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/common" + + "github.com/smartcontractkit/cre-cli/cmd/client" + "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +// onchainRegistryDeployStrategy deploys workflows to the onchain workflow registry. +// It wraps async WRC client initialization and handles ownership linking, +// duplicate detection, and DON limit checks. +type onchainRegistryDeployStrategy struct { + h *handler + wrc *client.WorkflowRegistryV2Client + onChain *settings.OnChainRegistry + wg sync.WaitGroup + initErr error +} + +func newOnchainRegistryDeployStrategy(h *handler) (*onchainRegistryDeployStrategy, error) { + onChain, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "deploy") + if err != nil { + return nil, err + } + + a := &onchainRegistryDeployStrategy{h: h, onChain: onChain} + a.wg.Add(1) + go func() { + defer a.wg.Done() + wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() + if err != nil { + a.initErr = fmt.Errorf("failed to create workflow registry client: %w", err) + return + } + a.wrc = wrc + h.wrc = wrc + }() + return a, nil +} + +func (a *onchainRegistryDeployStrategy) RunPreDeployChecks() error { + h := a.h + + a.wg.Wait() + if a.initErr != nil { + return a.initErr + } + + ui.Line() + ui.Dim("Verifying ownership...") + if h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerType == constants.WorkflowOwnerTypeMSIG { + halt, err := h.autoLinkMSIGAndExit(a.onChain) + if err != nil { + return fmt.Errorf("failed to check/handle MSIG owner link status: %w", err) + } + if halt { + return errDeployHalted + } + } else { + if err := h.ensureOwnerLinkedOrFail(a.onChain); err != nil { + return err + } + } + + return nil +} + +func (a *onchainRegistryDeployStrategy) CheckWorkflowExists(workflowOwner, workflowName, workflowTag, workflowID string) (bool, *uint8, error) { + workflow, err := a.wrc.GetWorkflow(common.HexToAddress(workflowOwner), workflowName, workflowTag) + if err != nil { + return false, nil, err + } + if workflow.WorkflowId == [32]byte(common.Hex2Bytes(workflowID)) { + return false, nil, fmt.Errorf("workflow with id %s already exists", workflowID) + } + if workflow.WorkflowName == workflowName { + status := workflow.Status + return true, &status, nil + } + + return false, nil, nil +} + +func (a *onchainRegistryDeployStrategy) Upsert() error { + h := a.h + + if err := checkUserDonLimitBeforeDeploy( + a.wrc, + a.wrc, + common.HexToAddress(h.inputs.WorkflowOwner), + h.inputs.DonFamily, + h.inputs.WorkflowName, + h.inputs.KeepAlive, + h.existingWorkflowStatus, + ); err != nil { + return err + } + + ui.Line() + ui.Dim("Preparing deployment transaction...") + if err := h.upsert(a.onChain); err != nil { + return fmt.Errorf("failed to register workflow: %w", err) + } + return nil +} diff --git a/cmd/workflow/deploy/registry_deploy_strategy_private.go b/cmd/workflow/deploy/registry_deploy_strategy_private.go new file mode 100644 index 00000000..883f54ea --- /dev/null +++ b/cmd/workflow/deploy/registry_deploy_strategy_private.go @@ -0,0 +1,117 @@ +package deploy + +import ( + "fmt" + "strings" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/client/privateregistryclient" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +// privateRegistryDeployStrategy deploys workflows to the private workflow registry +// via GraphQL. Ownership linking and onchain prechecks are not applicable. +type privateRegistryDeployStrategy struct { + h *handler + prc *privateregistryclient.Client +} + +func newPrivateRegistryDeployStrategy(h *handler) *privateRegistryDeployStrategy { + return &privateRegistryDeployStrategy{h: h} +} + +func (a *privateRegistryDeployStrategy) ensureClient() { + if a.prc == nil { + gql := graphqlclient.New(a.h.credentials, a.h.environmentSet, a.h.log) + a.prc = privateregistryclient.New(gql, a.h.log) + } +} + +func (a *privateRegistryDeployStrategy) RunPreDeployChecks() error { + return nil +} + +func (a *privateRegistryDeployStrategy) CheckWorkflowExists(_, workflowName, _, _ string) (bool, *uint8, error) { + a.ensureClient() + + workflow, err := a.prc.GetWorkflowByName(workflowName) + if err == nil { + return true, offchainStatusToUint8(workflow.Status), nil + } + if isWorkflowNotFoundError(err) { + return false, nil, nil + } + + return false, nil, err +} + +func (a *privateRegistryDeployStrategy) Upsert() error { + a.ensureClient() + + h := a.h + input := h.buildPrivateRegistryInput() + + ui.Line() + ui.Dim(fmt.Sprintf("Registering workflow in private registry (workflowID: %s)...", input.WorkflowID)) + + result, err := a.prc.UpsertWorkflowInRegistry(input) + if err != nil { + return fmt.Errorf("failed to register workflow in private registry: %w", err) + } + + ui.Success("Workflow registered in private registry") + ui.Line() + ui.Bold("Details:") + ui.Dim(fmt.Sprintf(" Registry: %s", h.runtimeContext.ResolvedRegistry.ID())) + ui.Dim(fmt.Sprintf(" Workflow Name: %s", result.WorkflowName)) + ui.Dim(fmt.Sprintf(" Workflow ID: %s", result.WorkflowID)) + ui.Dim(fmt.Sprintf(" Status: %s", privateregistryclient.FormatStatus(result.Status))) + ui.Dim(fmt.Sprintf(" Binary URL: %s", result.BinaryURL)) + if result.ConfigURL != "" { + ui.Dim(fmt.Sprintf(" Config URL: %s", result.ConfigURL)) + } + if result.Owner != "" { + ui.Dim(fmt.Sprintf(" Owner: %s", result.Owner)) + } + + return nil +} + +func (h *handler) buildPrivateRegistryInput() privateregistryclient.OffchainWorkflowInput { + input := privateregistryclient.OffchainWorkflowInput{ + WorkflowID: h.workflowArtifact.WorkflowID, + Status: privateregistryclient.WorkflowStatusActive, + WorkflowName: h.inputs.WorkflowName, + BinaryURL: h.inputs.BinaryURL, + DonFamily: h.inputs.DonFamily, + } + + if configURL := h.inputs.ResolveConfigURL(""); configURL != "" { + input.ConfigURL = &configURL + } + + if h.inputs.WorkflowTag != "" { + tag := h.inputs.WorkflowTag + input.Tag = &tag + } + + return input +} + +func isWorkflowNotFoundError(err error) bool { + msg := strings.ToLower(err.Error()) + return strings.Contains(msg, "not found") +} + +func offchainStatusToUint8(status privateregistryclient.OffchainWorkflowStatus) *uint8 { + switch status { + case privateregistryclient.WorkflowStatusActive: + v := uint8(0) + return &v + case privateregistryclient.WorkflowStatusPaused: + v := uint8(1) + return &v + default: + return nil + } +} diff --git a/cmd/workflow/hash/hash.go b/cmd/workflow/hash/hash.go index b4ac33bd..99cc311c 100644 --- a/cmd/workflow/hash/hash.go +++ b/cmd/workflow/hash/hash.go @@ -23,6 +23,7 @@ type Inputs struct { WorkflowPath string OwnerFromSettings string PrivateKey string + SkipTypeChecks bool } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -49,6 +50,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { WorkflowPath: s.Workflow.WorkflowArtifactSettings.WorkflowPath, OwnerFromSettings: s.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, PrivateKey: settings.NormalizeHexKey(rawPrivKey), + SkipTypeChecks: v.GetBool(cmdcommon.SkipTypeChecksCLIFlag), } return Execute(inputs) @@ -64,12 +66,13 @@ func New(runtimeContext *runtime.Context) *cobra.Command { hashCmd.Flags().Bool("no-config", false, "Hash without a config file") hashCmd.Flags().Bool("default-config", false, "Use the config path from workflow.yaml settings (default behavior)") hashCmd.MarkFlagsMutuallyExclusive("config", "no-config", "default-config") + hashCmd.Flags().Bool(cmdcommon.SkipTypeChecksCLIFlag, false, "Skip TypeScript project typecheck during compilation (passes "+cmdcommon.SkipTypeChecksFlag+" to cre-compile)") return hashCmd } func Execute(inputs Inputs) error { - rawBinary, err := loadBinary(inputs.WasmPath, inputs.WorkflowPath) + rawBinary, err := loadBinary(inputs.WasmPath, inputs.WorkflowPath, inputs.SkipTypeChecks) if err != nil { return err } @@ -124,7 +127,7 @@ func ResolveOwner(forUser, ownerFromSettings, privateKey string) (string, error) return "", fmt.Errorf("cannot determine workflow owner: provide --public_key or ensure CRE_ETH_PRIVATE_KEY is set") } -func loadBinary(wasmFlag, workflowPathFromSettings string) ([]byte, error) { +func loadBinary(wasmFlag, workflowPathFromSettings string, skipTypeChecks bool) ([]byte, error) { if wasmFlag != "" { if cmdcommon.IsURL(wasmFlag) { ui.Dim("Fetching WASM binary from URL...") @@ -155,7 +158,10 @@ func loadBinary(wasmFlag, workflowPathFromSettings string) ([]byte, error) { spinner := ui.NewSpinner() spinner.Start("Compiling workflow...") - wasmBytes, err := cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath, true) + wasmBytes, err := cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ + StripSymbols: true, + SkipTypeChecks: skipTypeChecks, + }) spinner.Stop() if err != nil { ui.Error("Build failed:") diff --git a/cmd/workflow/pause/pause.go b/cmd/workflow/pause/pause.go index a1564764..4507b351 100644 --- a/cmd/workflow/pause/pause.go +++ b/cmd/workflow/pause/pause.go @@ -1,25 +1,16 @@ package pause import ( - "encoding/hex" "fmt" - "math/big" - "sync" - "time" - "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/viper" - workflow_registry_v2_wrapper "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/workflow_registry_wrapper_v2" - "github.com/smartcontractkit/cre-cli/cmd/client" - cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" "github.com/smartcontractkit/cre-cli/internal/settings" - "github.com/smartcontractkit/cre-cli/internal/types" "github.com/smartcontractkit/cre-cli/internal/ui" "github.com/smartcontractkit/cre-cli/internal/validation" ) @@ -29,10 +20,8 @@ const ( ) type Inputs struct { - WorkflowName string `validate:"workflow_name"` - WorkflowOwner string `validate:"workflow_owner"` - WorkflowRegistryContractAddress string `validate:"required"` - WorkflowRegistryContractChainName string `validate:"required"` + WorkflowName string `validate:"workflow_name"` + WorkflowOwner string `validate:"workflow_owner"` } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -69,13 +58,9 @@ type handler struct { settings *settings.Settings environmentSet *environments.EnvironmentSet inputs Inputs - wrc *client.WorkflowRegistryV2Client runtimeContext *runtime.Context validated bool - - wg sync.WaitGroup - wrcErr error } func newHandler(ctx *runtime.Context) *handler { @@ -86,29 +71,20 @@ func newHandler(ctx *runtime.Context) *handler { environmentSet: ctx.EnvironmentSet, runtimeContext: ctx, validated: false, - wg: sync.WaitGroup{}, - wrcErr: nil, } - h.wg.Add(1) - go func() { - defer h.wg.Done() - wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() - if err != nil { - h.wrcErr = fmt.Errorf("failed to create workflow registry client: %w", err) - return - } - h.wrc = wrc - }() return &h } func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { + resolvedWorkflowOwner, err := h.resolveWorkflowOwner(h.runtimeContext.ResolvedRegistry.Type()) + if err != nil { + return Inputs{}, fmt.Errorf("failed to resolve workflow owner: %w", err) + } + return Inputs{ - WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, - WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, - WorkflowRegistryContractChainName: h.environmentSet.WorkflowRegistryChainName, - WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, + WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, + WorkflowOwner: resolvedWorkflowOwner, }, nil } @@ -131,156 +107,36 @@ func (h *handler) Execute() error { return fmt.Errorf("handler inputs not validated") } - workflowName := h.inputs.WorkflowName - workflowOwner := common.HexToAddress(h.inputs.WorkflowOwner) - h.displayWorkflowDetails() - h.wg.Wait() - if h.wrcErr != nil { - return h.wrcErr - } - - ui.Dim(fmt.Sprintf("Fetching workflows to pause... Name=%s, Owner=%s", workflowName, workflowOwner.Hex())) - - workflows, err := fetchAllWorkflows(h.wrc, workflowOwner, workflowName) - if err != nil { - return fmt.Errorf("failed to list workflows: %w", err) - } - if len(workflows) == 0 { - return fmt.Errorf("no workflows found for name %q and owner %q", workflowName, workflowOwner.Hex()) - } - - // Validate precondition: only pause workflows that are currently active - var activeWorkflowIDs [][32]byte - for _, workflow := range workflows { - if workflow.Status == WorkflowStatusActive { - activeWorkflowIDs = append(activeWorkflowIDs, workflow.WorkflowId) - } - } - - if len(activeWorkflowIDs) == 0 { - return fmt.Errorf("workflow is already paused, cancelling transaction") - } - - // Note: The way deploy is set up, there will only ever be one workflow in the command for now - h.runtimeContext.Workflow.ID = hex.EncodeToString(activeWorkflowIDs[0][:]) - - ui.Dim(fmt.Sprintf("Processing batch pause... count=%d", len(activeWorkflowIDs))) - - txOut, err := h.wrc.BatchPauseWorkflows(activeWorkflowIDs) + strategy, err := newRegistryPauseStrategy(h.runtimeContext.ResolvedRegistry, h) if err != nil { - return fmt.Errorf("failed to batch pause workflows: %w", err) + return err } - switch txOut.Type { - case client.Regular: - ui.Success("Transaction confirmed") - ui.URL(fmt.Sprintf("%s/tx/%s", h.environmentSet.WorkflowRegistryChainExplorerURL, txOut.Hash)) - ui.Success("Workflows paused successfully") - ui.Line() - ui.Bold("Details:") - ui.Dim(fmt.Sprintf(" Contract address: %s", h.environmentSet.WorkflowRegistryAddress)) - ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) - ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) - for _, w := range activeWorkflowIDs { - ui.Dim(fmt.Sprintf(" Workflow ID: %s", hex.EncodeToString(w[:]))) - } - - case client.Raw: - ui.Line() - ui.Success("MSIG workflow pause transaction prepared!") - ui.Dim(fmt.Sprintf("To Pause %s", workflowName)) - ui.Line() - ui.Bold("Next steps:") - ui.Line() - ui.Print(" 1. Submit the following transaction on the target chain:") - ui.Dim(fmt.Sprintf(" Chain: %s", h.inputs.WorkflowRegistryContractChainName)) - ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) - ui.Line() - ui.Print(" 2. Use the following transaction data:") - ui.Line() - ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) - ui.Line() - - case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(h.environmentSet.WorkflowRegistryChainName) - if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", h.environmentSet.WorkflowRegistryChainName, err) - } - mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) - if err != nil { - ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") - } - cldSettings := h.settings.CLDSettings - changesets := []types.Changeset{ - { - BatchPauseWorkflow: &types.BatchPauseWorkflow{ - Payload: types.UserWorkflowBatchPauseInput{ - WorkflowIDs: h.runtimeContext.Workflow.ID, // Note: The way deploy is set up, there will only ever be one workflow in the command for now - - ChainSelector: chainSelector, - MCMSConfig: mcmsConfig, - WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, - }, - }, - }, - } - csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) - - var fileName string - if cldSettings.ChangesetFile != "" { - fileName = cldSettings.ChangesetFile - } else { - fileName = fmt.Sprintf("BatchPauseWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) - } - - return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) - - default: - h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) - } - return nil + return strategy.Pause() } -func fetchAllWorkflows( - wrc interface { - GetWorkflowListByOwnerAndName(owner common.Address, workflowName string, start, limit *big.Int) ([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, error) - }, - owner common.Address, - name string, -) ([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, error) { - const pageSize = int64(200) - var ( - start = big.NewInt(0) - limit = big.NewInt(pageSize) - workflows = make([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, 0, pageSize) - ) - - for { - list, err := wrc.GetWorkflowListByOwnerAndName(owner, name, start, limit) - if err != nil { - return nil, err - } - if len(list) == 0 { - break - } - - workflows = append(workflows, list...) +// resolveWorkflowOwner returns the effective owner address for workflow ID computation. +// For private registry deploys, the derived workflow owner from the runtime context is used. +// For onchain deploys, the configured WorkflowOwner address is used directly. +func (h *handler) resolveWorkflowOwner(registryType settings.RegistryType) (string, error) { + if registryType != settings.RegistryTypeOffChain { + return h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, nil + } - start = big.NewInt(start.Int64() + int64(len(list))) - if int64(len(list)) < pageSize { - break - } + owner := h.runtimeContext.DerivedWorkflowOwner + if owner == "" { + return "", fmt.Errorf("derived workflow owner is not available; ensure authentication succeeded") } - return workflows, nil + return owner, nil } func (h *handler) displayWorkflowDetails() { ui.Line() ui.Title(fmt.Sprintf("Pausing Workflow: %s", h.inputs.WorkflowName)) ui.Dim(fmt.Sprintf("Target: %s", h.settings.User.TargetName)) - ui.Dim(fmt.Sprintf("Owner Address: %s", h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress)) + ui.Dim(fmt.Sprintf("Owner Address: %s", h.inputs.WorkflowOwner)) ui.Line() } diff --git a/cmd/workflow/pause/pause_test.go b/cmd/workflow/pause/pause_test.go index e2022216..3cca5e27 100644 --- a/cmd/workflow/pause/pause_test.go +++ b/cmd/workflow/pause/pause_test.go @@ -18,12 +18,6 @@ func TestWorkflowPauseCommand(t *testing.T) { t.Parallel() validRequired := func(in Inputs) Inputs { - if in.WorkflowRegistryContractAddress == "" { - in.WorkflowRegistryContractAddress = "0x0000000000000000000000000000000000000000" - } - if in.WorkflowRegistryContractChainName == "" { - in.WorkflowRegistryContractChainName = "ethereum-testnet-sepolia" - } return in } diff --git a/cmd/workflow/pause/registry_pause_strategy.go b/cmd/workflow/pause/registry_pause_strategy.go new file mode 100644 index 00000000..cc80c842 --- /dev/null +++ b/cmd/workflow/pause/registry_pause_strategy.go @@ -0,0 +1,18 @@ +package pause + +import ( + "github.com/smartcontractkit/cre-cli/internal/settings" +) + +// registryPauseStrategy encapsulates target-specific pause logic. +type registryPauseStrategy interface { + Pause() error +} + +// newRegistryPauseStrategy returns the appropriate strategy for the given target. +func newRegistryPauseStrategy(resolvedRegistry settings.ResolvedRegistry, h *handler) (registryPauseStrategy, error) { + if resolvedRegistry.Type() == settings.RegistryTypeOffChain { + return newPrivateRegistryPauseStrategy(h), nil + } + return newOnchainRegistryPauseStrategy(h) +} diff --git a/cmd/workflow/pause/registry_pause_strategy_onchain.go b/cmd/workflow/pause/registry_pause_strategy_onchain.go new file mode 100644 index 00000000..f038d6aa --- /dev/null +++ b/cmd/workflow/pause/registry_pause_strategy_onchain.go @@ -0,0 +1,197 @@ +package pause + +import ( + "encoding/hex" + "fmt" + "math/big" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + + workflow_registry_v2_wrapper "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/workflow_registry_wrapper_v2" + + "github.com/smartcontractkit/cre-cli/cmd/client" + cmdCommon "github.com/smartcontractkit/cre-cli/cmd/common" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/types" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type onchainRegistryPauseStrategy struct { + h *handler + wrc *client.WorkflowRegistryV2Client + onChain *settings.OnChainRegistry + wg sync.WaitGroup + initErr error +} + +func newOnchainRegistryPauseStrategy(h *handler) (*onchainRegistryPauseStrategy, error) { + onChain, err := settings.AsOnChain(h.runtimeContext.ResolvedRegistry, "pause") + if err != nil { + return nil, err + } + + a := &onchainRegistryPauseStrategy{h: h, onChain: onChain} + a.wg.Add(1) + go func() { + defer a.wg.Done() + wrc, err := h.clientFactory.NewWorkflowRegistryV2Client() + if err != nil { + a.initErr = fmt.Errorf("failed to create workflow registry client: %w", err) + return + } + a.wrc = wrc + }() + return a, nil +} + +func (a *onchainRegistryPauseStrategy) Pause() error { + h := a.h + + a.wg.Wait() + if a.initErr != nil { + return a.initErr + } + + workflowName := h.inputs.WorkflowName + workflowOwner := common.HexToAddress(h.inputs.WorkflowOwner) + + ui.Dim(fmt.Sprintf("Fetching workflows to pause... Name=%s, Owner=%s", workflowName, workflowOwner.Hex())) + + workflows, err := fetchAllWorkflows(a.wrc, workflowOwner, workflowName) + if err != nil { + return fmt.Errorf("failed to list workflows: %w", err) + } + if len(workflows) == 0 { + return fmt.Errorf("no workflows found for name %q and owner %q", workflowName, workflowOwner.Hex()) + } + + // Validate precondition: only pause workflows that are currently active + var activeWorkflowIDs [][32]byte + for _, workflow := range workflows { + if workflow.Status == WorkflowStatusActive { + activeWorkflowIDs = append(activeWorkflowIDs, workflow.WorkflowId) + } + } + + if len(activeWorkflowIDs) == 0 { + return fmt.Errorf("workflow is already paused, cancelling transaction") + } + + // Note: The way deploy is set up, there will only ever be one workflow in the command for now + h.runtimeContext.Workflow.ID = hex.EncodeToString(activeWorkflowIDs[0][:]) + + ui.Dim(fmt.Sprintf("Processing batch pause... count=%d", len(activeWorkflowIDs))) + + txOut, err := a.wrc.BatchPauseWorkflows(activeWorkflowIDs) + if err != nil { + return fmt.Errorf("failed to batch pause workflows: %w", err) + } + + oc := a.onChain + + switch txOut.Type { + case client.Regular: + ui.Success("Transaction confirmed") + ui.URL(fmt.Sprintf("%s/tx/%s", oc.ExplorerURL(), txOut.Hash)) + ui.Success("Workflows paused successfully") + ui.Line() + ui.Bold("Details:") + ui.Dim(fmt.Sprintf(" Registry: %s", h.runtimeContext.ResolvedRegistry.ID())) + ui.Dim(fmt.Sprintf(" Contract address: %s", oc.Address())) + ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) + ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) + for _, w := range activeWorkflowIDs { + ui.Dim(fmt.Sprintf(" Workflow ID: %s", hex.EncodeToString(w[:]))) + } + + case client.Raw: + ui.Line() + ui.Success("MSIG workflow pause transaction prepared!") + ui.Dim(fmt.Sprintf("To Pause %s", workflowName)) + ui.Line() + ui.Bold("Next steps:") + ui.Line() + ui.Print(" 1. Submit the following transaction on the target chain:") + ui.Dim(fmt.Sprintf(" Chain: %s", oc.ChainName())) + ui.Dim(fmt.Sprintf(" Contract Address: %s", txOut.RawTx.To)) + ui.Line() + ui.Print(" 2. Use the following transaction data:") + ui.Line() + ui.Code(fmt.Sprintf(" %x", txOut.RawTx.Data)) + ui.Line() + + case client.Changeset: + chainSelector, err := settings.GetChainSelectorByChainName(oc.ChainName()) + if err != nil { + return fmt.Errorf("failed to get chain selector for chain %q: %w", oc.ChainName(), err) + } + mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) + if err != nil { + ui.Warning("MCMS config not found or is incorrect, skipping MCMS config in changeset") + } + cldSettings := h.settings.CLDSettings + changesets := []types.Changeset{ + { + BatchPauseWorkflow: &types.BatchPauseWorkflow{ + Payload: types.UserWorkflowBatchPauseInput{ + WorkflowIDs: h.runtimeContext.Workflow.ID, // Note: The way deploy is set up, there will only ever be one workflow in the command for now + + ChainSelector: chainSelector, + MCMSConfig: mcmsConfig, + WorkflowRegistryQualifier: cldSettings.WorkflowRegistryQualifier, + }, + }, + }, + } + csFile := types.NewChangesetFile(cldSettings.Environment, cldSettings.Domain, cldSettings.MergeProposals, changesets) + + var fileName string + if cldSettings.ChangesetFile != "" { + fileName = cldSettings.ChangesetFile + } else { + fileName = fmt.Sprintf("BatchPauseWorkflow_%s_%s.yaml", workflowName, time.Now().Format("20060102_150405")) + } + + return cmdCommon.WriteChangesetFile(fileName, csFile, h.settings) + + default: + h.log.Warn().Msgf("Unsupported transaction type: %s", txOut.Type) + } + return nil +} + +func fetchAllWorkflows( + wrc interface { + GetWorkflowListByOwnerAndName(owner common.Address, workflowName string, start, limit *big.Int) ([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, error) + }, + owner common.Address, + name string, +) ([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, error) { + const pageSize = int64(200) + var ( + start = big.NewInt(0) + limit = big.NewInt(pageSize) + workflows = make([]workflow_registry_v2_wrapper.WorkflowRegistryWorkflowMetadataView, 0, pageSize) + ) + + for { + list, err := wrc.GetWorkflowListByOwnerAndName(owner, name, start, limit) + if err != nil { + return nil, err + } + if len(list) == 0 { + break + } + + workflows = append(workflows, list...) + + start = big.NewInt(start.Int64() + int64(len(list))) + if int64(len(list)) < pageSize { + break + } + } + + return workflows, nil +} diff --git a/cmd/workflow/pause/registry_pause_strategy_private.go b/cmd/workflow/pause/registry_pause_strategy_private.go new file mode 100644 index 00000000..f0f05534 --- /dev/null +++ b/cmd/workflow/pause/registry_pause_strategy_private.go @@ -0,0 +1,65 @@ +package pause + +import ( + "fmt" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/client/privateregistryclient" + "github.com/smartcontractkit/cre-cli/internal/ui" +) + +type privateRegistryPauseStrategy struct { + h *handler + prc *privateregistryclient.Client +} + +func newPrivateRegistryPauseStrategy(h *handler) *privateRegistryPauseStrategy { + return &privateRegistryPauseStrategy{h: h} +} + +func (a *privateRegistryPauseStrategy) ensureClient() { + if a.prc == nil { + gql := graphqlclient.New(a.h.runtimeContext.Credentials, a.h.environmentSet, a.h.log) + a.prc = privateregistryclient.New(gql, a.h.log) + } +} + +func (a *privateRegistryPauseStrategy) Pause() error { + a.ensureClient() + + h := a.h + workflowName := h.inputs.WorkflowName + + ui.Dim(fmt.Sprintf("Fetching workflow to pause... Name=%s", workflowName)) + + workflow, err := a.prc.GetWorkflowByName(workflowName) + if err != nil { + return fmt.Errorf("failed to get workflow: %w", err) + } + + if workflow.Status == privateregistryclient.WorkflowStatusPaused { + return fmt.Errorf("workflow is already paused, cancelling transaction") + } + + h.runtimeContext.Workflow.ID = workflow.WorkflowID + + ui.Dim(fmt.Sprintf("Processing pause for workflow ID %s...", workflow.WorkflowID)) + + result, err := a.prc.PauseWorkflowInRegistry(workflow.WorkflowID) + if err != nil { + return fmt.Errorf("failed to pause workflow in private registry: %w", err) + } + + ui.Success("Workflow paused successfully") + ui.Line() + ui.Bold("Details:") + ui.Dim(fmt.Sprintf(" Registry: %s", h.runtimeContext.ResolvedRegistry.ID())) + ui.Dim(fmt.Sprintf(" Workflow Name: %s", result.WorkflowName)) + ui.Dim(fmt.Sprintf(" Workflow ID: %s", result.WorkflowID)) + ui.Dim(fmt.Sprintf(" Status: %s", privateregistryclient.FormatStatus(result.Status))) + if result.Owner != "" { + ui.Dim(fmt.Sprintf(" Owner: %s", result.Owner)) + } + + return nil +} diff --git a/cmd/workflow/simulate/capabilities.go b/cmd/workflow/simulate/capabilities.go index 57cc2b5b..4133af2a 100644 --- a/cmd/workflow/simulate/capabilities.go +++ b/cmd/workflow/simulate/capabilities.go @@ -42,7 +42,10 @@ func NewManualTriggerCapabilities( limits *SimulationLimits, ) (*ManualTriggers, error) { // Cron - manualCronTrigger := fakes.NewManualCronTriggerService(lggr) + manualCronTrigger, err := fakes.NewManualCronTriggerService(lggr) + if err != nil { + return nil, err + } manualCronTriggerServer := crontrigger.NewCronServer(manualCronTrigger) if err := registry.Add(ctx, manualCronTriggerServer); err != nil { return nil, err diff --git a/cmd/workflow/simulate/limits.go b/cmd/workflow/simulate/limits.go index 4feffa84..d1dbdb71 100644 --- a/cmd/workflow/simulate/limits.go +++ b/cmd/workflow/simulate/limits.go @@ -4,9 +4,13 @@ import ( _ "embed" "encoding/json" "fmt" + "math" "os" "strings" + "time" + "github.com/smartcontractkit/chainlink-common/pkg/config" + "github.com/smartcontractkit/chainlink-common/pkg/settings" "github.com/smartcontractkit/chainlink-common/pkg/settings/cresettings" ) @@ -93,6 +97,70 @@ func applyEngineLimits(cfg *cresettings.Workflows, limits *SimulationLimits) { // NOTE: ChainAllowed is NOT overridden — simulation keeps allow-all } +// disableEngineLimits sets all engine limit fields to very high values, +// effectively disabling limit enforcement when --limits none is used. +func disableEngineLimits(cfg *cresettings.Workflows) { + maxInt := settings.Setting[int]{DefaultValue: math.MaxInt32} + maxSize := settings.Setting[config.Size]{DefaultValue: math.MaxInt32} + maxDuration := settings.Setting[time.Duration]{DefaultValue: 24 * time.Hour} + + // Execution limits + cfg.ExecutionTimeout = maxDuration + cfg.ExecutionResponseLimit = maxSize + cfg.ExecutionConcurrencyLimit = maxInt + + // Capability limits + cfg.CapabilityConcurrencyLimit = maxInt + cfg.CapabilityCallTimeout = maxDuration + cfg.SecretsConcurrencyLimit = maxInt + + // Trigger limits + cfg.TriggerRegistrationsTimeout = maxDuration + cfg.TriggerEventQueueLimit = maxInt + cfg.TriggerEventQueueTimeout = maxDuration + cfg.TriggerSubscriptionTimeout = maxDuration + cfg.TriggerSubscriptionLimit = maxInt + + // WASM limits + cfg.WASMMemoryLimit = maxSize + cfg.WASMBinarySizeLimit = maxSize + cfg.WASMCompressedBinarySizeLimit = maxSize + cfg.WASMConfigSizeLimit = maxSize + cfg.WASMSecretsSizeLimit = maxSize + + // Log limits + cfg.LogLineLimit = maxSize + cfg.LogEventLimit = maxInt + + // HTTPAction limits + cfg.HTTPAction.CallLimit = maxInt + cfg.HTTPAction.CacheAgeLimit = maxDuration + cfg.HTTPAction.ConnectionTimeout = maxDuration + cfg.HTTPAction.RequestSizeLimit = maxSize + cfg.HTTPAction.ResponseSizeLimit = maxSize + + // ConfidentialHTTP limits + cfg.ConfidentialHTTP.CallLimit = maxInt + cfg.ConfidentialHTTP.ConnectionTimeout = maxDuration + cfg.ConfidentialHTTP.RequestSizeLimit = maxSize + cfg.ConfidentialHTTP.ResponseSizeLimit = maxSize + + // Consensus limits + cfg.Consensus.CallLimit = maxInt + cfg.Consensus.ObservationSizeLimit = maxSize + + // ChainWrite limits + cfg.ChainWrite.TargetsLimit = maxInt + cfg.ChainWrite.ReportSizeLimit = maxSize + + // ChainRead limits + cfg.ChainRead.CallLimit = maxInt + cfg.ChainRead.PayloadSizeLimit = maxSize + + // Secrets limits + cfg.Secrets.CallLimit = maxInt +} + // HTTPRequestSizeLimit returns the HTTP action request size limit in bytes. func (l *SimulationLimits) HTTPRequestSizeLimit() int { return int(l.Workflows.HTTPAction.RequestSizeLimit.DefaultValue) diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 771eef49..babc3322 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -9,6 +9,7 @@ import ( "math/big" "os" "os/signal" + "path/filepath" "strconv" "strings" "syscall" @@ -65,6 +66,12 @@ type Inputs struct { ExperimentalForwarders map[uint64]common.Address `validate:"-"` // forwarders keyed by chain ID // Limits enforcement LimitsPath string `validate:"-"` // "default" or path to custom limits JSON + // SkipTypeChecks passes --skip-type-checks to cre-compile for TypeScript workflows. + SkipTypeChecks bool `validate:"-"` + // InvocationDir is the working directory at the time the CLI was invoked, before + // SetExecutionContext changes it to the workflow directory. Used to resolve file + // paths entered interactively or via flags relative to where the user ran the command. + InvocationDir string `validate:"-"` } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -103,6 +110,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { simulateCmd.Flags().String("evm-tx-hash", "", "EVM trigger transaction hash (0x...)") simulateCmd.Flags().Int("evm-event-index", -1, "EVM trigger log index (0-based)") simulateCmd.Flags().String("limits", "default", "Production limits to enforce during simulation: 'default' for prod defaults, path to a limits JSON file (e.g. from 'cre workflow limits export'), or 'none' to disable") + simulateCmd.Flags().Bool(cmdcommon.SkipTypeChecksCLIFlag, false, "Skip TypeScript project typecheck during compilation (passes "+cmdcommon.SkipTypeChecksFlag+" to cre-compile)") return simulateCmd } @@ -207,14 +215,41 @@ func (h *handler) ResolveInputs(v *viper.Viper, creSettings *settings.Settings) } if len(clients) == 0 { - return Inputs{}, fmt.Errorf("no RPC URLs found for supported or experimental chains") + target, _ := settings.GetTarget(v) + if target == "" { + target = "(none)" + } + return Inputs{}, fmt.Errorf( + "no RPC URLs found for target %q\n\n"+ + "To fix:\n"+ + " • Check that your project.yaml has an 'rpcs' section under the target %q\n"+ + " • Ensure chain names are valid (run 'cre workflow supported-chains' to see all supported names)\n"+ + " • Verify the correct target is selected via --target or CRE_TARGET", + target, target, + ) } pk, err := crypto.HexToECDSA(creSettings.User.EthPrivateKey) if err != nil { + // If the user explicitly set a key that looks like a hex string but is + // malformed (wrong length, invalid chars), always error with guidance. + // Skip placeholder values like "your-eth-private-key" from the default .env template. + if creSettings.User.EthPrivateKey != "" && isHexString(creSettings.User.EthPrivateKey) { + return Inputs{}, fmt.Errorf( + "invalid private key: expected 64 hex characters (256 bits), got %d characters.\n\n"+ + "The CLI reads CRE_ETH_PRIVATE_KEY from your .env file or system environment.\n"+ + "The 0x prefix is supported and stripped automatically.\n\n"+ + "Common issues:\n"+ + " • Pasted an Ethereum address (40 chars) instead of a private key (64 chars)\n"+ + " • Value has extra quotes — use CRE_ETH_PRIVATE_KEY=abc123... without wrapping quotes\n"+ + " • Key was truncated during copy-paste", + len(creSettings.User.EthPrivateKey)) + } + // Key not set or placeholder — require it for broadcast, otherwise use default for simulation if v.GetBool("broadcast") { return Inputs{}, fmt.Errorf( - "failed to parse private key, required to broadcast. Please check CRE_ETH_PRIVATE_KEY in your .env file or system environment: %w", err) + "a private key is required for --broadcast mode.\n" + + "Set CRE_ETH_PRIVATE_KEY in your .env file or system environment") } pk, err = crypto.HexToECDSA("0000000000000000000000000000000000000000000000000000000000000001") if err != nil { @@ -240,6 +275,8 @@ func (h *handler) ResolveInputs(v *viper.Viper, creSettings *settings.Settings) EVMEventIndex: v.GetInt("evm-event-index"), ExperimentalForwarders: experimentalForwarders, LimitsPath: v.GetString("limits"), + SkipTypeChecks: v.GetBool(cmdcommon.SkipTypeChecksCLIFlag), + InvocationDir: h.runtimeContext.InvocationDir, }, nil } @@ -267,8 +304,11 @@ func (h *handler) ValidateInputs(inputs Inputs) error { inputs.ConfigPath = savedConfig // forbid the default 0x...01 key when broadcasting - if inputs.Broadcast && inputs.EthPrivateKey != nil && inputs.EthPrivateKey.D.Cmp(big.NewInt(1)) == 0 { - return fmt.Errorf("you must configure a valid private key to perform on-chain writes. Please set your private key in the .env file before using the -–broadcast flag") + if inputs.Broadcast && inputs.EthPrivateKey != nil { + keyBytes, keyBytesErr := inputs.EthPrivateKey.Bytes() + if keyBytesErr == nil && new(big.Int).SetBytes(keyBytes).Cmp(big.NewInt(1)) == 0 { + return fmt.Errorf("you must configure a valid private key to perform on-chain writes. Please set your private key in the .env file before using the -–broadcast flag") + } } rpcErr := ui.WithSpinner("Checking RPC connectivity...", func() error { @@ -330,7 +370,10 @@ func (h *handler) Execute(inputs Inputs) error { spinner := ui.NewSpinner() spinner.Start("Compiling workflow...") - wasmFileBinary, err = cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath, false) + wasmFileBinary, err = cmdcommon.CompileWorkflowToWasm(resolvedWorkflowPath, cmdcommon.WorkflowCompileOptions{ + StripSymbols: false, + SkipTypeChecks: inputs.SkipTypeChecks, + }) spinner.Stop() if err != nil { ui.Error("Build failed:") @@ -671,6 +714,8 @@ func run( // Apply simulation limits to engine-level settings when --limits is set if simLimits != nil { applyEngineLimits(cfg, simLimits) + } else if inputs.LimitsPath == "none" { + disableEngineLimits(cfg) } // Always allow all chains in simulation, overriding any chain restrictions from limits cfg.ChainAllowed = commonsettings.PerChainSelector( @@ -734,10 +779,28 @@ func makeBeforeStartInteractive(holder *TriggerInfoAndBeforeStart, inputs Inputs switch { case trigger == "cron-trigger@1.0.0": holder.TriggerFunc = func() error { - return triggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, time.Now()) + skipWaitSignal := make(chan struct{}, 1) + + userPromptCtx, cancel := context.WithCancel(ctx) + defer cancel() + + go func() { + ui.Line() + pressed := ui.WaitForEnter(userPromptCtx, "Cron scheduler started. Press Enter to skip waiting...") + if pressed { + skipWaitSignal <- struct{}{} + } + }() + + err := triggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, skipWaitSignal) + if err != nil { + return err + } + + return nil } case trigger == "http-trigger@1.0.0-alpha": - payload, err := getHTTPTriggerPayload() + payload, err := getHTTPTriggerPayload(inputs.InvocationDir) if err != nil { ui.Error(fmt.Sprintf("Failed to get HTTP trigger payload: %v", err)) os.Exit(1) @@ -809,14 +872,23 @@ func makeBeforeStartNonInteractive(holder *TriggerInfoAndBeforeStart, inputs Inp switch { case trigger == "cron-trigger@1.0.0": holder.TriggerFunc = func() error { - return triggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, time.Now()) + skipWaitSignal := make(chan struct{}, 1) + err := triggerCaps.ManualCronTrigger.ManualTrigger(ctx, triggerRegistrationID, skipWaitSignal) + if err != nil { + return err + } + + // With cron schedule on non-interactive mode + skipWaitSignal <- struct{}{} + + return nil } case trigger == "http-trigger@1.0.0-alpha": if strings.TrimSpace(inputs.HTTPPayload) == "" { ui.Error("--http-payload is required for http-trigger@1.0.0-alpha in non-interactive mode") os.Exit(1) } - payload, err := getHTTPTriggerPayloadFromInput(inputs.HTTPPayload) + payload, err := getHTTPTriggerPayloadFromInput(inputs.HTTPPayload, inputs.InvocationDir) if err != nil { ui.Error(fmt.Sprintf("Failed to parse HTTP trigger payload: %v", err)) os.Exit(1) @@ -893,8 +965,11 @@ func cleanupBeholder() error { return nil } -// getHTTPTriggerPayload prompts user for HTTP trigger data -func getHTTPTriggerPayload() (*httptypedapi.Payload, error) { +// getHTTPTriggerPayload prompts user for HTTP trigger data. +// invocationDir is the working directory at the time the CLI was invoked; relative +// paths entered by the user are resolved against it rather than the current working +// directory (which may have been changed to the workflow folder by SetExecutionContext). +func getHTTPTriggerPayload(invocationDir string) (*httptypedapi.Payload, error) { ui.Line() input, err := ui.Input("HTTP Trigger Configuration", ui.WithInputDescription("Enter a file path or JSON directly for the HTTP trigger"), @@ -911,19 +986,22 @@ func getHTTPTriggerPayload() (*httptypedapi.Payload, error) { var jsonData map[string]interface{} - // Check if input is a file path - if _, err := os.Stat(input); err == nil { - // It's a file path - data, err := os.ReadFile(input) + // Resolve the path against the invocation directory so that relative paths + // like ./production.json work from where the user ran the command, even though + // the process cwd has been changed to the workflow subdirectory. + resolvedPath := resolvePathFromInvocation(input, invocationDir) + + if _, err := os.Stat(resolvedPath); err == nil { + data, err := os.ReadFile(resolvedPath) if err != nil { - return nil, fmt.Errorf("failed to read file %s: %w", input, err) + return nil, fmt.Errorf("failed to read file %s: %w", resolvedPath, err) } if err := json.Unmarshal(data, &jsonData); err != nil { - return nil, fmt.Errorf("failed to parse JSON from file %s: %w", input, err) + return nil, fmt.Errorf("failed to parse JSON from file %s: %w", resolvedPath, err) } - ui.Success(fmt.Sprintf("Loaded JSON from file: %s", input)) + ui.Success(fmt.Sprintf("Loaded JSON from file: %s", resolvedPath)) } else { - // It's direct JSON input + // Treat as direct JSON input if err := json.Unmarshal([]byte(input), &jsonData); err != nil { return nil, fmt.Errorf("failed to parse JSON: %w", err) } @@ -934,7 +1012,6 @@ func getHTTPTriggerPayload() (*httptypedapi.Payload, error) { if err != nil { return nil, fmt.Errorf("failed to marshal JSON: %w", err) } - // Create the payload payload := &httptypedapi.Payload{ Input: jsonDataBytes, // Key is optional for simulation @@ -944,6 +1021,16 @@ func getHTTPTriggerPayload() (*httptypedapi.Payload, error) { return payload, nil } +// resolvePathFromInvocation converts a (potentially relative) path to an absolute +// path anchored at invocationDir. Absolute paths and paths that are already +// reachable from the current working directory are returned unchanged. +func resolvePathFromInvocation(path, invocationDir string) string { + if filepath.IsAbs(path) || invocationDir == "" { + return path + } + return filepath.Join(invocationDir, path) +} + // getEVMTriggerLog prompts user for EVM trigger data and fetches the log func getEVMTriggerLog(ctx context.Context, ethClient *ethclient.Client) (*evm.Log, error) { var txHashInput string @@ -1054,8 +1141,10 @@ func getEVMTriggerLog(ctx context.Context, ethClient *ethclient.Client) (*evm.Lo return pbLog, nil } -// getHTTPTriggerPayloadFromInput builds an HTTP trigger payload from a JSON string or a file path (optionally prefixed with '@') -func getHTTPTriggerPayloadFromInput(input string) (*httptypedapi.Payload, error) { +// getHTTPTriggerPayloadFromInput builds an HTTP trigger payload from a JSON string or a file path +// (optionally prefixed with '@'). invocationDir is used to resolve relative paths against the +// directory where the user invoked the CLI rather than the current working directory. +func getHTTPTriggerPayloadFromInput(input, invocationDir string) (*httptypedapi.Payload, error) { trimmed := strings.TrimSpace(input) if trimmed == "" { return nil, fmt.Errorf("empty http payload input") @@ -1063,17 +1152,18 @@ func getHTTPTriggerPayloadFromInput(input string) (*httptypedapi.Payload, error) var raw []byte if strings.HasPrefix(trimmed, "@") { - path := strings.TrimPrefix(trimmed, "@") + path := resolvePathFromInvocation(strings.TrimPrefix(trimmed, "@"), invocationDir) data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("failed to read file %s: %w", path, err) } raw = data } else { - if _, err := os.Stat(trimmed); err == nil { - data, err := os.ReadFile(trimmed) + resolvedPath := resolvePathFromInvocation(trimmed, invocationDir) + if _, err := os.Stat(resolvedPath); err == nil { + data, err := os.ReadFile(resolvedPath) if err != nil { - return nil, fmt.Errorf("failed to read file %s: %w", trimmed, err) + return nil, fmt.Errorf("failed to read file %s: %w", resolvedPath, err) } raw = data } else { @@ -1140,3 +1230,13 @@ func getEVMTriggerLogFromValues(ctx context.Context, ethClient *ethclient.Client } return pbLog, nil } + +// isHexString returns true if s contains only hexadecimal characters (0-9, a-f, A-F). +func isHexString(s string) bool { + for _, c := range s { + if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') { + return false + } + } + return len(s) > 0 +} diff --git a/cmd/workflow/simulate/simulate_test.go b/cmd/workflow/simulate/simulate_test.go index b29d3f52..1d24423a 100644 --- a/cmd/workflow/simulate/simulate_test.go +++ b/cmd/workflow/simulate/simulate_test.go @@ -111,6 +111,7 @@ func createSimulateTestSettings(workflowName, workflowPath, configPath string) * WorkflowOwnerAddress string `mapstructure:"workflow-owner-address" yaml:"workflow-owner-address"` WorkflowOwnerType string `mapstructure:"workflow-owner-type" yaml:"workflow-owner-type"` WorkflowName string `mapstructure:"workflow-name" yaml:"workflow-name"` + DeploymentRegistry string `mapstructure:"deployment-registry" yaml:"deployment-registry"` }{ WorkflowName: workflowName, }, @@ -276,6 +277,167 @@ func TestSimulateWasmFormatHandling(t *testing.T) { }) } +func TestResolvePathFromInvocation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + path string + invocationDir string + want string + }{ + { + name: "absolute path returned unchanged regardless of invocationDir", + path: "/absolute/path/file.json", + invocationDir: "/some/other/dir", + want: "/absolute/path/file.json", + }, + { + name: "relative path with empty invocationDir returned unchanged", + path: "relative/file.json", + invocationDir: "", + want: "relative/file.json", + }, + { + name: "relative path joined with invocationDir", + path: "file.json", + invocationDir: "/invocation/dir", + want: "/invocation/dir/file.json", + }, + { + name: "relative path with subdirs joined with invocationDir", + path: "sub/dir/file.json", + invocationDir: "/invocation/dir", + want: "/invocation/dir/sub/dir/file.json", + }, + { + name: "dot-slash relative path joined with invocationDir", + path: "./file.json", + invocationDir: "/invocation/dir", + want: "/invocation/dir/file.json", + }, + { + name: "absolute path with empty invocationDir returned unchanged", + path: "/abs/path.json", + invocationDir: "", + want: "/abs/path.json", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := resolvePathFromInvocation(tt.path, tt.invocationDir) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestGetHTTPTriggerPayloadFromInput(t *testing.T) { + t.Parallel() + + // Create a temp dir with a payload file for file-based tests. + tmpDir := t.TempDir() + payloadJSON := `{"method":"GET","path":"/hello"}` + payloadFile := filepath.Join(tmpDir, "payload.json") + require.NoError(t, os.WriteFile(payloadFile, []byte(payloadJSON), 0600)) + + t.Run("empty input returns error", func(t *testing.T) { + t.Parallel() + _, err := getHTTPTriggerPayloadFromInput("", "") + require.Error(t, err) + assert.Contains(t, err.Error(), "empty http payload input") + }) + + t.Run("whitespace-only input returns error", func(t *testing.T) { + t.Parallel() + _, err := getHTTPTriggerPayloadFromInput(" ", "") + require.Error(t, err) + assert.Contains(t, err.Error(), "empty http payload input") + }) + + t.Run("at-prefix with absolute file path reads file", func(t *testing.T) { + t.Parallel() + payload, err := getHTTPTriggerPayloadFromInput("@"+payloadFile, "") + require.NoError(t, err) + assert.Equal(t, []byte(payloadJSON), payload.Input) + }) + + t.Run("at-prefix with relative path resolved against invocationDir", func(t *testing.T) { + t.Parallel() + payload, err := getHTTPTriggerPayloadFromInput("@payload.json", tmpDir) + require.NoError(t, err) + assert.Equal(t, []byte(payloadJSON), payload.Input) + }) + + t.Run("at-prefix with nonexistent file returns error", func(t *testing.T) { + t.Parallel() + _, err := getHTTPTriggerPayloadFromInput("@/nonexistent/no-such-file.json", "") + require.Error(t, err) + assert.Contains(t, err.Error(), "failed to read file") + }) + + t.Run("absolute file path without at-prefix reads file", func(t *testing.T) { + t.Parallel() + payload, err := getHTTPTriggerPayloadFromInput(payloadFile, "") + require.NoError(t, err) + assert.Equal(t, []byte(payloadJSON), payload.Input) + }) + + t.Run("relative file path resolved against invocationDir reads file", func(t *testing.T) { + t.Parallel() + payload, err := getHTTPTriggerPayloadFromInput("payload.json", tmpDir) + require.NoError(t, err) + assert.Equal(t, []byte(payloadJSON), payload.Input) + }) + + t.Run("inline JSON string used as raw bytes", func(t *testing.T) { + t.Parallel() + inlineJSON := `{"method":"POST","path":"/api"}` + payload, err := getHTTPTriggerPayloadFromInput(inlineJSON, "") + require.NoError(t, err) + assert.Equal(t, []byte(inlineJSON), payload.Input) + }) + + t.Run("nonexistent relative path with empty invocationDir treated as raw bytes", func(t *testing.T) { + t.Parallel() + // A path that doesn't exist is treated as raw bytes (no error). + input := "no-such-file-or-json" + payload, err := getHTTPTriggerPayloadFromInput(input, "") + require.NoError(t, err) + assert.Equal(t, []byte(input), payload.Input) + }) + + t.Run("relative path not found in invocationDir treated as raw bytes", func(t *testing.T) { + t.Parallel() + // A relative path that resolves to a nonexistent file is used as raw bytes. + input := "does-not-exist.json" + payload, err := getHTTPTriggerPayloadFromInput(input, tmpDir) + require.NoError(t, err) + assert.Equal(t, []byte(input), payload.Input) + }) +} + +func TestSimulateResolveInputs_InvocationDir(t *testing.T) { + t.Parallel() + + invocationDir := "/some/invocation/dir" + v := createSimulateTestViper(t) + creSettings := createSimulateTestSettings("test-workflow", "main.go", "config.json") + + runtimeCtx := &runtime.Context{ + Logger: testutil.NewTestLogger(), + Viper: v, + Settings: creSettings, + InvocationDir: invocationDir, + } + h := newHandler(runtimeCtx) + + inputs, err := h.ResolveInputs(v, creSettings) + require.NoError(t, err) + assert.Equal(t, invocationDir, inputs.InvocationDir) +} + func TestSimulateConfigFlagsMutuallyExclusive(t *testing.T) { t.Parallel() diff --git a/cmd/workflow/simulate/simulator_utils.go b/cmd/workflow/simulate/simulator_utils.go index 6334a2c5..2f62e95e 100644 --- a/cmd/workflow/simulate/simulator_utils.go +++ b/cmd/workflow/simulate/simulator_utils.go @@ -6,6 +6,7 @@ import ( "fmt" "net/url" "regexp" + "sort" "strconv" "strings" "time" @@ -27,6 +28,21 @@ type ChainConfig struct { Forwarder string } +// SupportedChainNames returns the human-readable names of all supported EVM chains, +// sorted alphabetically. +func SupportedChainNames() []string { + var names []string + for _, chain := range SupportedEVM { + name, err := settings.GetChainNameByChainSelector(chain.Selector) + if err != nil { + continue + } + names = append(names, name) + } + sort.Strings(names) + return names +} + // SupportedEVM is the canonical list you can range over. var SupportedEVM = []ChainConfig{ // Ethereum @@ -185,7 +201,7 @@ func redactURL(rawURL string) string { // experimentalForwarders keys identify experimental chains (not in chain-selectors). func runRPCHealthCheck(clients map[uint64]*ethclient.Client, experimentalForwarders map[uint64]common.Address) error { if len(clients) == 0 { - return fmt.Errorf("check your settings: no RPC URLs found for supported or experimental chains") + return fmt.Errorf("no RPC URLs found for supported or experimental chains. Run 'cre workflow supported-chains' to see all supported chain names") } var errs []error diff --git a/cmd/workflow/simulate/utils_test.go b/cmd/workflow/simulate/utils_test.go index 14c5fd26..881d794c 100644 --- a/cmd/workflow/simulate/utils_test.go +++ b/cmd/workflow/simulate/utils_test.go @@ -151,7 +151,7 @@ func TestHealthCheck_NoClientsConfigured(t *testing.T) { if err == nil { t.Fatalf("expected error for no clients configured") } - mustContain(t, err.Error(), "check your settings: no RPC URLs found for supported or experimental chains") + mustContain(t, err.Error(), "no RPC URLs found for supported or experimental chains") } func TestHealthCheck_NilClient(t *testing.T) { diff --git a/cmd/workflow/supported_chains/supported_chains.go b/cmd/workflow/supported_chains/supported_chains.go new file mode 100644 index 00000000..79db5966 --- /dev/null +++ b/cmd/workflow/supported_chains/supported_chains.go @@ -0,0 +1,25 @@ +package supported_chains + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate" +) + +func New() *cobra.Command { + return &cobra.Command{ + Use: "supported-chains", + Short: "List all supported chain names", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + names := simulate.SupportedChainNames() + fmt.Println("Supported chain names:") + for _, name := range names { + fmt.Printf(" %s\n", name) + } + return nil + }, + } +} diff --git a/cmd/workflow/workflow.go b/cmd/workflow/workflow.go index f03a7bdf..b7dd24e7 100644 --- a/cmd/workflow/workflow.go +++ b/cmd/workflow/workflow.go @@ -12,6 +12,7 @@ import ( "github.com/smartcontractkit/cre-cli/cmd/workflow/limits" "github.com/smartcontractkit/cre-cli/cmd/workflow/pause" "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate" + supported_chains "github.com/smartcontractkit/cre-cli/cmd/workflow/supported_chains" "github.com/smartcontractkit/cre-cli/cmd/workflow/test" "github.com/smartcontractkit/cre-cli/internal/runtime" ) @@ -23,6 +24,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { Long: `The workflow command allows you to register and manage existing workflows.`, } + workflowCmd.AddCommand(supported_chains.New()) workflowCmd.AddCommand(activate.New(runtimeContext)) workflowCmd.AddCommand(build.New(runtimeContext)) workflowCmd.AddCommand(convert.New(runtimeContext)) diff --git a/docs/cre.md b/docs/cre.md index 2ff0c534..51c21b28 100644 --- a/docs/cre.md +++ b/docs/cre.md @@ -28,6 +28,7 @@ cre [optional flags] * [cre init](cre_init.md) - Initialize a new cre project (recommended starting point) * [cre login](cre_login.md) - Start authentication flow * [cre logout](cre_logout.md) - Revoke authentication tokens and remove local credentials +* [cre registry](cre_registry.md) - Manages workflow registries * [cre secrets](cre_secrets.md) - Handles secrets management * [cre templates](cre_templates.md) - Manages template repository sources * [cre update](cre_update.md) - Update the cre CLI to the latest version diff --git a/docs/cre_init.md b/docs/cre_init.md index 7ae34dcf..35a4ad31 100644 --- a/docs/cre_init.md +++ b/docs/cre_init.md @@ -19,6 +19,7 @@ cre init [optional flags] ``` -h, --help help for init + --non-interactive Fail instead of prompting; requires all inputs via flags -p, --project-name string Name for the new project --refresh Bypass template cache and fetch fresh data --rpc-url stringArray RPC URL for a network (format: chain-name=url, repeatable) diff --git a/docs/cre_registry.md b/docs/cre_registry.md new file mode 100644 index 00000000..7f586631 --- /dev/null +++ b/docs/cre_registry.md @@ -0,0 +1,33 @@ +## cre registry + +Manages workflow registries + +### Synopsis + +The registry command lets you view and inspect the workflow registries available for your organization. + +``` +cre registry [optional flags] +``` + +### Options + +``` + -h, --help help for registry +``` + +### Options inherited from parent commands + +``` + -e, --env string Path to .env file which contains sensitive info + -R, --project-root string Path to the project root + -E, --public-env string Path to .env.public file which contains shared, non-sensitive build config + -T, --target string Use target settings from YAML config + -v, --verbose Run command in VERBOSE mode +``` + +### SEE ALSO + +* [cre](cre.md) - CRE CLI tool +* [cre registry list](cre_registry_list.md) - Lists available workflow registries for the current environment + diff --git a/docs/cre_registry_list.md b/docs/cre_registry_list.md new file mode 100644 index 00000000..b27e7e21 --- /dev/null +++ b/docs/cre_registry_list.md @@ -0,0 +1,38 @@ +## cre registry list + +Lists available workflow registries for the current environment + +### Synopsis + +Displays the registries configured for your organization, including type and address. + +``` +cre registry list [optional flags] +``` + +### Examples + +``` +cre registry list +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + -e, --env string Path to .env file which contains sensitive info + -R, --project-root string Path to the project root + -E, --public-env string Path to .env.public file which contains shared, non-sensitive build config + -T, --target string Use target settings from YAML config + -v, --verbose Run command in VERBOSE mode +``` + +### SEE ALSO + +* [cre registry](cre_registry.md) - Manages workflow registries + diff --git a/docs/cre_templates_list.md b/docs/cre_templates_list.md index 2b34c319..f873c4c9 100644 --- a/docs/cre_templates_list.md +++ b/docs/cre_templates_list.md @@ -14,6 +14,7 @@ cre templates list [optional flags] ``` -h, --help help for list + --json Output template list as JSON --refresh Bypass cache and fetch fresh data ``` diff --git a/docs/cre_workflow.md b/docs/cre_workflow.md index 0bdfb9a6..e151addd 100644 --- a/docs/cre_workflow.md +++ b/docs/cre_workflow.md @@ -38,4 +38,5 @@ cre workflow [optional flags] * [cre workflow limits](cre_workflow_limits.md) - Manage simulation limits * [cre workflow pause](cre_workflow_pause.md) - Pauses workflow on the Workflow Registry contract * [cre workflow simulate](cre_workflow_simulate.md) - Simulates a workflow +* [cre workflow supported-chains](cre_workflow_supported-chains.md) - List all supported chain names diff --git a/docs/cre_workflow_build.md b/docs/cre_workflow_build.md index c8d55c42..dcd0f2ab 100644 --- a/docs/cre_workflow_build.md +++ b/docs/cre_workflow_build.md @@ -19,8 +19,9 @@ cre workflow build ./my-workflow ### Options ``` - -h, --help help for build - -o, --output string Output file path for the compiled WASM binary (default: /binary.wasm) + -h, --help help for build + -o, --output string Output file path for the compiled WASM binary (default: /binary.wasm) + --skip-type-checks Skip TypeScript project typecheck during compilation (passes --skip-type-checks to cre-compile) ``` ### Options inherited from parent commands diff --git a/docs/cre_workflow_deploy.md b/docs/cre_workflow_deploy.md index 239e70b1..a938b2bb 100644 --- a/docs/cre_workflow_deploy.md +++ b/docs/cre_workflow_deploy.md @@ -25,6 +25,7 @@ cre workflow deploy ./my-workflow --no-config Deploy without a config file -o, --output string The output file for the compiled WASM binary encoded in base64 (default "./binary.wasm.br.b64") -l, --owner-label string Label for the workflow owner (used during auto-link if owner is not already linked) + --skip-type-checks Skip TypeScript project typecheck during compilation (passes --skip-type-checks to cre-compile) --unsigned If set, the command will either return the raw transaction instead of sending it to the network or execute the second step of secrets operations using a previously generated raw transaction --wasm string Path to a pre-built WASM binary (skips compilation) --yes If set, the command will skip the confirmation prompt and proceed with the operation even if it is potentially destructive diff --git a/docs/cre_workflow_hash.md b/docs/cre_workflow_hash.md index b1abe6bb..80311ca2 100644 --- a/docs/cre_workflow_hash.md +++ b/docs/cre_workflow_hash.md @@ -25,6 +25,7 @@ cre workflow hash [optional flags] -h, --help help for hash --no-config Hash without a config file --public_key string Owner address to use for computing the workflow hash. Required when CRE_ETH_PRIVATE_KEY is not set and no workflow-owner-address is configured. Defaults to the address derived from CRE_ETH_PRIVATE_KEY or the workflow-owner-address in project settings. + --skip-type-checks Skip TypeScript project typecheck during compilation (passes --skip-type-checks to cre-compile) --wasm string Path or URL to a pre-built WASM binary (skips compilation) ``` diff --git a/docs/cre_workflow_simulate.md b/docs/cre_workflow_simulate.md index ce844fd2..ad62f15c 100644 --- a/docs/cre_workflow_simulate.md +++ b/docs/cre_workflow_simulate.md @@ -30,6 +30,7 @@ cre workflow simulate ./my-workflow --limits string Production limits to enforce during simulation: 'default' for prod defaults, path to a limits JSON file (e.g. from 'cre workflow limits export'), or 'none' to disable (default "default") --no-config Simulate without a config file --non-interactive Run without prompts; requires --trigger-index and inputs for the selected trigger type + --skip-type-checks Skip TypeScript project typecheck during compilation (passes --skip-type-checks to cre-compile) --trigger-index int Index of the trigger to run (0-based) (default -1) --wasm string Path or URL to a pre-built WASM binary (skips compilation) ``` diff --git a/docs/cre_workflow_supported-chains.md b/docs/cre_workflow_supported-chains.md new file mode 100644 index 00000000..7f8406ae --- /dev/null +++ b/docs/cre_workflow_supported-chains.md @@ -0,0 +1,28 @@ +## cre workflow supported-chains + +List all supported chain names + +``` +cre workflow supported-chains [optional flags] +``` + +### Options + +``` + -h, --help help for supported-chains +``` + +### Options inherited from parent commands + +``` + -e, --env string Path to .env file which contains sensitive info + -R, --project-root string Path to the project root + -E, --public-env string Path to .env.public file which contains shared, non-sensitive build config + -T, --target string Use target settings from YAML config + -v, --verbose Run command in VERBOSE mode +``` + +### SEE ALSO + +* [cre workflow](cre_workflow.md) - Manages workflows + diff --git a/go.mod b/go.mod index 31923346..ffb6c346 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,21 @@ module github.com/smartcontractkit/cre-cli -go 1.25.7 +go 1.26.2 require ( github.com/BurntSushi/toml v1.5.0 github.com/Masterminds/semver/v3 v3.4.0 github.com/andybalholm/brotli v1.2.0 - github.com/avast/retry-go/v4 v4.6.1 + github.com/avast/retry-go/v4 v4.7.0 github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 github.com/charmbracelet/bubbletea v1.3.6 github.com/charmbracelet/huh v0.8.0 github.com/charmbracelet/lipgloss v1.1.0 github.com/denisbrodbeck/machineid v1.0.1 - github.com/ethereum/go-ethereum v1.17.0 + github.com/ethereum/go-ethereum v1.17.2 github.com/go-playground/locales v0.14.1 github.com/go-playground/universal-translator v0.18.1 - github.com/go-playground/validator/v10 v10.28.0 + github.com/go-playground/validator/v10 v10.30.1 github.com/google/uuid v1.6.0 github.com/jarcoal/httpmock v1.4.1 github.com/jedib0t/go-pretty/v6 v6.6.5 @@ -23,30 +23,30 @@ require ( github.com/machinebox/graphql v0.2.2 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.34.0 - github.com/smartcontractkit/chain-selectors v1.0.97 - github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 + github.com/smartcontractkit/chain-selectors v1.0.98 + github.com/smartcontractkit/chainlink-common v0.11.2-0.20260421191147-d10b9943ac71 github.com/smartcontractkit/chainlink-common/keystore v1.0.2 - github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251222115927-36a18321243c - github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 - github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f - github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.3 - github.com/smartcontractkit/chainlink/deployment v0.0.0-20260224120304-949cf5d66bc6 - github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260305114259-bea2267bbe93 + github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd + github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 + github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997 + github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 + github.com/smartcontractkit/chainlink/deployment v0.0.0-20260422181348-efa818697ce5 + github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260422181348-efa818697ce5 github.com/smartcontractkit/cre-sdk-go v1.7.0 github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.9 - github.com/smartcontractkit/mcms v0.35.1-0.20260209175626-b68b54b6e8d0 + github.com/smartcontractkit/mcms v0.41.1 github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20251120172354-e8ec0386b06c - github.com/spf13/cobra v1.10.1 + github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 github.com/test-go/testify v1.1.4 go.uber.org/zap v1.27.1 - golang.org/x/term v0.40.0 + golang.org/x/term v0.41.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 - sigs.k8s.io/yaml v1.4.0 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -72,12 +72,12 @@ require ( github.com/VictoriaMetrics/fastcache v1.13.0 // indirect github.com/XSAM/otelsql v0.37.0 // indirect github.com/apache/arrow-go/v18 v18.3.1 // indirect - github.com/aptos-labs/aptos-go-sdk v1.12.0 // indirect + github.com/aptos-labs/aptos-go-sdk v1.12.1 // indirect github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/avast/retry-go v3.0.0+incompatible // indirect github.com/awalterschulze/gographviz v2.0.3+incompatible // indirect - github.com/aws/aws-sdk-go v1.55.7 // indirect + github.com/aws/aws-sdk-go v1.55.8 // indirect github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -93,7 +93,8 @@ require ( github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce // indirect github.com/buger/goterm v1.0.4 // indirect - github.com/buger/jsonparser v1.1.1 // indirect + github.com/buger/jsonparser v1.1.2 // indirect + github.com/buraksezer/consistent v0.10.0 // indirect github.com/bytecodealliance/wasmtime-go/v28 v28.0.0 // indirect github.com/bytedance/sonic v1.12.3 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect @@ -130,14 +131,17 @@ require ( github.com/cosmos/ics23/go v0.11.0 // indirect github.com/cosmos/ledger-cosmos-go v0.14.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect - github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.5.0 // indirect + github.com/creachadair/jrpc2 v1.2.0 // indirect + github.com/creachadair/mds v0.13.4 // indirect github.com/danieljoos/wincred v1.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dchest/siphash v1.2.3 // indirect - github.com/deckarep/golang-set/v2 v2.7.0 // indirect + github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/dgraph-io/badger/v4 v4.7.0 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect + github.com/digital-asset/dazl-client/v8 v8.9.0 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/dominikbraun/graph v0.23.0 // indirect github.com/doyensec/safeurl v0.2.1 // indirect @@ -146,17 +150,18 @@ require ( github.com/emicklei/dot v1.6.2 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/esote/minmaxheap v1.0.0 // indirect - github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab // indirect github.com/expr-lang/expr v1.17.7 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fbsobreira/gotron-sdk v0.0.0-20250403083053-2943ce8c759b // indirect github.com/ferranbt/fastssz v0.1.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gagliardetto/anchor-go v1.0.0 // indirect github.com/gagliardetto/binary v0.8.0 // indirect + github.com/gagliardetto/metaplex-go v0.2.1 // indirect github.com/gagliardetto/solana-go v1.13.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/gagliardetto/utilz v0.1.3 // indirect @@ -164,6 +169,7 @@ require ( github.com/gin-contrib/sessions v0.0.5 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-gonic/gin v1.10.1 // indirect + github.com/go-co-op/gocron/v2 v2.18.0 // indirect github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 // indirect github.com/go-kit/kit v0.13.0 // indirect github.com/go-kit/log v0.2.1 // indirect @@ -171,30 +177,33 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/swag v0.25.5 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/goccy/go-yaml v1.18.0 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.3 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e // indirect + github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.2.2 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - github.com/grafana/pyroscope-go v1.2.7 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grafana/otel-profiling-go v0.5.1 // indirect + github.com/grafana/pyroscope-go v1.2.8 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 // indirect github.com/hashicorp/go-bexpr v0.1.10 // indirect @@ -221,7 +230,8 @@ require ( github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgtype v1.14.4 // indirect - github.com/jackc/pgx/v4 v4.18.3 // indirect + github.com/jackc/pgx/v5 v5.9.2 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jinzhu/copier v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -230,8 +240,8 @@ require ( github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52 // indirect - github.com/klauspost/compress v1.18.2 // indirect + github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect @@ -247,7 +257,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/miekg/dns v1.1.65 // indirect + github.com/miekg/dns v1.1.72 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -255,7 +265,7 @@ require ( github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect @@ -268,7 +278,7 @@ require ( github.com/oklog/run v1.2.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pelletier/go-toml/v2 v2.3.0 // indirect github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pion/dtls/v2 v2.2.12 // indirect @@ -280,7 +290,7 @@ require ( 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.66.1 // indirect + github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect @@ -300,53 +310,56 @@ require ( github.com/shopspring/decimal v1.4.0 // indirect github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 // indirect github.com/smartcontractkit/ccip-owner-contracts v0.1.0 // indirect - github.com/smartcontractkit/chainlink-aptos v0.0.0-20260304104421-dd6ab4ea9452 // indirect + github.com/smartcontractkit/chainlink-aptos v0.0.0-20260407161350-a86b1969da65 // indirect github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect - github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260303102708-6caf8c4ea3b4 // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d // indirect - github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 // indirect + github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260417153334-3b564ef614de // indirect + github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260415165642-49f23e4d76cc // indirect + github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc // indirect + github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260415165642-49f23e4d76cc // indirect github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 // indirect - github.com/smartcontractkit/chainlink-data-streams v0.1.12-0.20260227110503-42b236799872 // indirect - github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 // indirect - github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260303141232-9cc3feb83863 // indirect - github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260107191744-4b93f62cffe3 // indirect + github.com/smartcontractkit/chainlink-data-streams v0.1.13 // indirect + github.com/smartcontractkit/chainlink-deployments-framework v0.95.0 // indirect + github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260416173445-80f6efde0a03 // indirect + github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501 // indirect github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563 // indirect - github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20251210101658-1c5c8e4c4f15 // indirect - github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251210101658-1c5c8e4c4f15 // indirect - github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942 // indirect + github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57 // indirect + github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260410144512-ca02ad6ed16a // indirect + github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a // indirect github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 // indirect - github.com/smartcontractkit/chainlink-protos/job-distributor v0.17.0 // indirect + github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 // indirect github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b // indirect - github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 // indirect + github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 // indirect + github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260331131315-f08a616d8dcd // indirect github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 // indirect github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3 // indirect - github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260223222711-2fa6b0e07db0 // indirect - github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 // indirect - github.com/smartcontractkit/chainlink-ton v0.0.0-20260223231247-735246035dab // indirect - github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260218133534-cbd44da2856b // indirect - github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e // indirect + github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260421131224-c46cbfe7bc6c // indirect + github.com/smartcontractkit/chainlink-sui v0.0.0-20260409184948-5b16fae57fe0 // indirect + github.com/smartcontractkit/chainlink-ton v0.0.0-20260415120434-cecc380f8d87 // indirect + github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260408092456-3c6369888d4a // indirect + github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad // indirect github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 // indirect - github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e // indirect + github.com/smartcontractkit/libocr v0.0.0-20260403184524-b6409238958d // indirect github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb // indirect github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect + github.com/stellar/go-stellar-sdk v0.1.0 // indirect + github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 // indirect github.com/stephenlacy/go-ethereum-hdwallet v0.0.0-20230913225845-a4fa94429863 // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/supranational/blst v0.3.16 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect - github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0 // indirect github.com/theodesp/go-heaps v0.0.0-20190520121037-88e35354fe0a // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect - github.com/tklauser/go-sysconf v0.3.15 // indirect - github.com/tklauser/numcpus v0.10.0 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/ugorji/go/codec v1.2.12 // indirect @@ -368,48 +381,53 @@ require ( go.mongodb.org/mongo-driver v1.17.2 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 // indirect - go.opentelemetry.io/otel/log v0.15.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/sdk v1.39.0 // indirect - go.opentelemetry.io/otel/sdk/log v0.15.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect - go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 // indirect + go.opentelemetry.io/otel/log v0.19.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.19.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.11.0 // indirect - golang.org/x/crypto v0.48.0 // indirect + golang.org/x/crypto v0.49.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.50.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 // indirect - golang.org/x/text v0.34.0 // indirect - golang.org/x/time v0.14.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.42.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gonum.org/v1/gonum v0.17.0 // indirect google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect - google.golang.org/grpc v1.78.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect + google.golang.org/grpc v1.80.0 // indirect gopkg.in/guregu/null.v4 v4.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gotest.tools/v3 v3.5.2 // indirect + k8s.io/api v0.35.3 // indirect + k8s.io/client-go v0.35.3 // indirect + k8s.io/klog/v2 v2.140.0 // indirect nhooyr.io/websocket v1.8.14 // indirect pgregory.net/rapid v1.1.0 // indirect ) diff --git a/go.sum b/go.sum index 452c5100..6fa0e20d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,29 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.43.0/go.mod h1:BOSR3VbTLkk6FDC/TcffxP4NF/FFBGA5ku+jvKOP7pg= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +contrib.go.opencensus.io/exporter/stackdriver v0.12.6/go.mod h1:8x999/OcIPy5ivx/wDiV7Gx4D+VUPODf0mWRGRc5kSk= +contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= cosmossdk.io/api v0.7.6 h1:PC20PcXy1xYKH2KU4RMurVoFjjKkCgYRbVAD4PdqUuY= cosmossdk.io/api v0.7.6/go.mod h1:IcxpYS5fMemZGqyYtErK7OqvdM0C8kdW3dq8Q/XIG38= cosmossdk.io/collections v0.4.0 h1:PFmwj2W8szgpD5nOd8GWH6AbYNi1f2J6akWXJ7P5t9s= @@ -20,8 +44,10 @@ cosmossdk.io/x/tx v0.13.7 h1:8WSk6B/OHJLYjiZeMKhq7DK7lHDMyK0UfDbBMxVmeOI= cosmossdk.io/x/tx v0.13.7/go.mod h1:V6DImnwJMTq5qFjeGWpXNiT/fjgE4HtmclRmTqRVM3w= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8= filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= @@ -40,12 +66,15 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzS github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible h1:qSG2N4FghB1He/r2mFrWKCaL7dXCilEuNEeAn20fdD4= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.5.6 h1:LbEglqepa/ipmmQJUDnSsfvA8e8IStVcGaFWDuxvGOY= github.com/DataDog/zstd v1.5.6/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/Depado/ginprom v1.8.0 h1:zaaibRLNI1dMiiuj1MKzatm8qrcHzikMlCc1anqOdyo= github.com/Depado/ginprom v1.8.0/go.mod h1:XBaKzeNBqPF4vxJpNLincSQZeMDnZp1tIbU0FU0UKgg= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= @@ -57,6 +86,7 @@ github.com/NethermindEth/juno v0.12.5 h1:a+KYQg8MxzNJIbbqGHq+vU9nTyuWu3acbyXxcUP github.com/NethermindEth/juno v0.12.5/go.mod h1:XonWmZVRwCVHv1gjoVCoTFiZnYObwdukpd3NCsl04bA= github.com/NethermindEth/starknet.go v0.8.0 h1:mGh7qDWrvuXJPcgGJP31DpifzP6+Ef2gt/BQhaqsV40= github.com/NethermindEth/starknet.go v0.8.0/go.mod h1:slNA8PxtxA/0LQv0FwHnL3lHFDNhVZfTK6U2gjVb7l8= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= @@ -66,6 +96,7 @@ github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/ github.com/XSAM/otelsql v0.37.0 h1:ya5RNw028JW0eJW8Ma4AmoKxAYsJSGuNVbC7F1J457A= github.com/XSAM/otelsql v0.37.0/go.mod h1:LHbCu49iU8p255nCn1oi04oX2UjSoRcUMiKEHo2a5qM= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -73,6 +104,7 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -82,53 +114,58 @@ github.com/apache/thrift v0.21.0 h1:tdPmh/ptjE1IJnhbhrcl2++TauVjy242rkV/UzJChnE= github.com/apache/thrift v0.21.0/go.mod h1:W1H8aR/QRtYNvrPeFXBtobyRkd0/YVhTc6i07XIAgDw= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/aptos-labs/aptos-go-sdk v1.12.0 h1:deHZ7NJlFhHm2i+eaPHt6EPa3BuXXnIYx2X5J3/U0Es= -github.com/aptos-labs/aptos-go-sdk v1.12.0/go.mod h1:FTgKp0RLfEefllCdkCj0jPU14xWk11yA7SFVfCDLUj8= +github.com/aptos-labs/aptos-go-sdk v1.12.1 h1:EXtA9GF9fJndRcjWVZZ3Hf5hXxvGWNPu+1k3A6eGOfM= +github.com/aptos-labs/aptos-go-sdk v1.12.1/go.mod h1:FTgKp0RLfEefllCdkCj0jPU14xWk11yA7SFVfCDLUj8= +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-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c h1:cxQVoh6kY+c4b0HUchHjGWBI8288VhH50qxKG3hdEg0= github.com/atombender/go-jsonschema v0.16.1-0.20240916205339-a74cd4e2851c/go.mod h1:3XzxudkrYVUvbduN/uI2fl4lSrMSzU0+3RCu2mpnfx8= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= -github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= -github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= +github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= +github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E= github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= -github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= -github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= -github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= -github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= -github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8= -github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI= -github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE= -github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.57.17 h1:kYAxFlyBhmhdjel6MNFf5lYQlTcMUOXPC33mor8rFz0= -github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.57.17/go.mod h1:NSRHRisUPKx5y8RD+HpeCjIn8SYz5m6HhNGkd0GLB1o= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= -github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= -github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= -github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= -github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/aws/aws-sdk-go v1.22.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.59.2 h1:I1oExVl2b6nJGv//TcU78k9Covm/htQ5gwPIcDlM2PI= +github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.59.2/go.mod h1:sxvHFUS0fM9Y3BpmDvwrO9fnQC0CrFSG8KD9THjv6k4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.1 h1:wb/PYYm3wlcqGzw7Ls4GD3X5+seDDoNdVYIB6I/V87E= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.1/go.mod h1:xvHowJ6J9CuaFE04S8fitWQXytf4sHz3DTPGhw9FtmU= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -148,10 +185,12 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM= github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= github.com/block-vision/sui-go-sdk v1.1.4 h1:1PPgYxQjo1P9UCgFOPTvDCuGEglRL32NwjKPulR4FQk= @@ -190,8 +229,9 @@ github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/buger/jsonparser v1.1.2 h1:frqHqw7otoVbk5M8LlE/L7HTnIq2v9RX6EJ48i9AxJk= +github.com/buger/jsonparser v1.1.2/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/buraksezer/consistent v0.10.0 h1:hqBgz1PvNLC5rkWcEBVAL9dFMBWz6I0VgUCW25rrZlU= github.com/buraksezer/consistent v0.10.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0maujuPowduSpZqmw= github.com/bytecodealliance/wasmtime-go/v28 v28.0.0 h1:aBU8cexP2rPZ0Qz488kvn2NXvWZHL2aG1/+n7Iv+xGc= @@ -212,6 +252,7 @@ github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F9 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU= github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -261,7 +302,6 @@ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= @@ -293,13 +333,17 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= -github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= +github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cosmos/btcutil v1.0.5 h1:t+ZFcX77LpKtDBhjucvnOH8C2l2ioGsBNEQ3jef8xFk= github.com/cosmos/btcutil v1.0.5/go.mod h1:IyB7iuqZMJlthe2tkIFL33xPyzbFYP0XVdS8P5lUPis= github.com/cosmos/cosmos-db v1.1.1 h1:FezFSU37AlBC8S98NlSagL76oqBRWq/prTPvFcEJNCM= @@ -322,11 +366,16 @@ github.com/cosmos/ledger-cosmos-go v0.14.0 h1:WfCHricT3rPbkPSVKRH+L4fQGKYHuGOK9E github.com/cosmos/ledger-cosmos-go v0.14.0/go.mod h1:E07xCWSBl3mTGofZ2QnL4cIUzMbbGVyik84QYKbX3RA= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= -github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-eth-kzg v1.5.0 h1:FYRiJMJG2iv+2Dy3fi14SVGjcPteZ5HAAUe4YWlJygc= +github.com/crate-crypto/go-eth-kzg v1.5.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/creachadair/jrpc2 v1.2.0 h1:SXr0OgnwM0X18P+HccJP0uT3KGSDk/BCSRlJBvE2bMY= +github.com/creachadair/jrpc2 v1.2.0/go.mod h1:66uKSdr6tR5ZeNvkIjDSbbVUtOv0UhjS/vcd8ECP7Iw= +github.com/creachadair/mds v0.13.4 h1:RgU0MhiVqkzp6/xtNWhK6Pw7tDeaVuGFtA0UA2RBYvY= +github.com/creachadair/mds v0.13.4/go.mod h1:4vrFYUzTXMJpMBU+OA292I6IUxKWCCfZkgXg+/kBZMo= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= @@ -337,6 +386,7 @@ github.com/cucumber/godog v0.15.1 h1:rb/6oHDdvVZKS66hrhpjFQFHjthFSrQBCOI1LwshNTI github.com/cucumber/godog v0.15.1/go.mod h1:qju+SQDewOljHuq9NSM66s0xEhogx0q30flfxL4WUk8= github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI= github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs= github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps= github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e h1:5jVSh2l/ho6ajWhSPNN84eHEdq3dp0T7+f6r3Tc6hsk= @@ -349,8 +399,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/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= -github.com/deckarep/golang-set/v2 v2.7.0 h1:gIloKvD7yH2oip4VLhsv3JyLLFnC0Y2mlusgcvJYW5k= -github.com/deckarep/golang-set/v2 v2.7.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= +github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 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= @@ -364,16 +414,22 @@ github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMS github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= +github.com/dfuse-io/logging v0.0.0-20201110202154-26697de88c79/go.mod h1:V+ED4kT/t/lKtH99JQmKIb0v9WL3VaYkJ36CfHlVECI= +github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70/go.mod h1:EoK/8RFbMEteaCaz89uessDTnCWjbbcr+DXcBh4el5o= github.com/dgraph-io/badger/v4 v4.7.0 h1:Q+J8HApYAY7UMpL8d9owqiB+odzEc0zn/aqOD9jhc6Y= github.com/dgraph-io/badger/v4 v4.7.0/go.mod h1:He7TzG3YBy3j4f5baj5B7Zl2XyfNe5bl4Udl0aPemVA= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 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/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/digital-asset/dazl-client/v8 v8.9.0 h1:F2qTUWtHAjhGyRGV+xTim+VAFwM99FpcOx4+wowvPnY= +github.com/digital-asset/dazl-client/v8 v8.9.0/go.mod h1:q1KevCJ8FpH8je2MnnjN8/QUfhstB4fKpyKyqDtqFh0= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= -github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v28.5.3-0.20260325154711-31a1689cb0a1+incompatible h1:f51eIlZsZqGKXyNeCHs5oVo/xQiR9zh+pDYMfnu3VPQ= +github.com/docker/docker v28.5.3-0.20260325154711-31a1689cb0a1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -386,12 +442,12 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvsekhvalnov/jose2go v1.7.0 h1:bnQc8+GMnidJZA8zc6lLEAb4xNrIqHwO+9TzqvtQZPo= github.com/dvsekhvalnov/jose2go v1.7.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= -github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= -github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= -github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= -github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -401,16 +457,18 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/esote/minmaxheap v1.0.0 h1:rgA7StnXXpZG6qlM0S7pUmEv1KpWe32rYT4x8J8ntaA= github.com/esote/minmaxheap v1.0.0/go.mod h1:Ln8+i7fS1k3PLgZI2JAo0iA1as95QnIYiGCrqSJ5FZk= -github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= -github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= -github.com/ethereum/go-ethereum v1.17.0 h1:2D+1Fe23CwZ5tQoAS5DfwKFNI1HGcTwi65/kRlAVxes= -github.com/ethereum/go-ethereum v1.17.0/go.mod h1:2W3msvdosS/MCWytpqTcqgFiRYbTH59FxDJzqah120o= +github.com/ethereum/go-ethereum v1.17.2 h1:ag6geu0kn8Hv5FLKTpH+Hm2DHD+iuFtuqKxEuwUsDOI= +github.com/ethereum/go-ethereum v1.17.2/go.mod h1:KHcRXfGOUfUmKg51IhQ0IowiqZ6PqZf08CMtk0g5K1o= github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8= github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/failsafe-go/failsafe-go v0.9.0 h1:w0g7iv48RpQvV3UH1VlgUnLx9frQfCwI7ljnJzqEhYg= github.com/failsafe-go/failsafe-go v0.9.0/go.mod h1:sX5TZ4HrMLYSzErWeckIHRZWgZj9PbKMAEKOVLFWtfM= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= @@ -427,21 +485,26 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gagliardetto/anchor-go v1.0.0 h1:YNt9I/9NOrNzz5uuzfzByAcbp39Ft07w63iPqC/wi34= github.com/gagliardetto/anchor-go v1.0.0/go.mod h1:X6c9bx9JnmwNiyy8hmV5pAsq1c/zzPvkdzeq9/qmlCg= +github.com/gagliardetto/binary v0.6.1/go.mod h1:aOfYkc20U0deHaHn/LVZXiqlkDbFAX0FpTlDhsXa0S0= github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw= github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= github.com/gagliardetto/hashsearch v0.0.0-20191005111333-09dd671e19f9/go.mod h1:513DXpQPzeRo7d4dsCP3xO3XI8hgvruMl9njxyQeraQ= +github.com/gagliardetto/metaplex-go v0.2.1 h1:NMBsgJe3I2avKZ39dfYQvXsGsr2BxUgARkA9LZ6szBg= +github.com/gagliardetto/metaplex-go v0.2.1/go.mod h1:6ZLYBvlWcXktXQ/QcBJYRzKgK7Q3WgiGD7BjE7Zxpw4= +github.com/gagliardetto/solana-go v1.4.0/go.mod h1:NFuoDwHPvw858ZMHUJr6bkhN8qHt4x6e+U3EYHxAwNY= github.com/gagliardetto/solana-go v1.13.0 h1:uNzhjwdAdbq9xMaX2DF0MwXNMw6f8zdZ7JPBtkJG7Ig= github.com/gagliardetto/solana-go v1.13.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/gagliardetto/utilz v0.1.1/go.mod h1:b+rGFkRHz3HWJD0RYMzat47JyvbTtpE0iEcYTRJTLLA= github.com/gagliardetto/utilz v0.1.3 h1:A+asc+6/3a9qNBrgticApj3yW5F7y4TaJd8Ijg+o0zM= github.com/gagliardetto/utilz v0.1.3/go.mod h1:b+rGFkRHz3HWJD0RYMzat47JyvbTtpE0iEcYTRJTLLA= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= @@ -465,10 +528,17 @@ github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-co-op/gocron/v2 v2.18.0 h1:DS3Uhru66q1jy/5f9V0itmi3cLXcn2b7N+duGfgT7gU= +github.com/go-co-op/gocron/v2 v2.18.0/go.mod h1:Zii6he+Zfgy5W9B+JKk/KwejFOW0kZTFvHtwIpR4aBI= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= +github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874 h1:F8d1AJ6M9UQCavhwmO6ZsrYLfG8zVFWfEfMS2MXPkSY= github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -486,6 +556,7 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -493,22 +564,44 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre 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.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +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/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= +github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= +github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= -github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= -github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= +github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= @@ -522,8 +615,8 @@ github.com/go-webauthn/x v0.1.5 h1:V2TCzDU2TGLd0kSZOXdrqDVV5JB9ILnKxA9S53CSBw0= github.com/go-webauthn/x v0.1.5/go.mod h1:qbzWwcFcv4rTwtCLOZd+icnr6B7oSsAGZJqlt8cukqY= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -536,10 +629,18 @@ 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/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/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-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -564,17 +665,20 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6 github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 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.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -588,53 +692,75 @@ github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= -github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gorilla/rpc v1.2.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ= +github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= +github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= -github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= +github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= +github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M= +github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8= github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= github.com/graph-gophers/dataloader v5.0.0+incompatible h1:R+yjsbrNq1Mo3aPG+Z/EKYrXrXXUNJHOgbRt+U6jOug= github.com/graph-gophers/dataloader v5.0.0+incompatible/go.mod h1:jk4jk0c5ZISbKaMe8WsVopGB5/15GvGHMdMdPtwlRp4= github.com/graph-gophers/graphql-go v1.5.0 h1:fDqblo50TEpD0LY7RXk/LFVYEVqo3+tXMNMPSVXA1yc= github.com/graph-gophers/graphql-go v1.5.0/go.mod h1:YtmJZDLbF1YYNrlNAuiO5zAStUWc3XZT07iGsVqe1Os= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 h1:BpJ2o0OR5FV7vrkDYfXYVJQeMNWa8RhklZOpW2ITAIQ= github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-envparse v0.1.0 h1:bE++6bhIsNCPLvgDZkYqo3nA+/PFI51pkrHdmPSDFPY= @@ -648,19 +774,33 @@ github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+ github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y= github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= -github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/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 v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 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/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hasura/go-graphql-client v0.15.1 h1:mCb5I+8Bk3FU3GKWvf/zDXkTh7FbGlqJmP3oisBdnN8= @@ -680,11 +820,13 @@ github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= @@ -744,10 +886,14 @@ github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgS github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= +github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jarcoal/httpmock v1.4.1 h1:0Ju+VCFuARfFlhVXFc2HxlcQkfB+Xq12/EotHko+x2A= @@ -760,6 +906,7 @@ github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5Xum github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -770,10 +917,9 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= @@ -783,18 +929,22 @@ github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52 h1:msKODTL1m0wigztaqILOtla9HeW1ciscYG4xjLtvk5I= -github.com/karalabe/hid v1.0.1-0.20240306101548-573246063e52/go.mod h1:qk1sX/IBgppQNcGCRoj90u6EGC056EBoIc1oEjCWla8= +github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb h1:Ag83At00qa4FLkcdMgrwHVSakqky/eZczOlxd4q336E= +github.com/karalabe/hid v1.0.1-0.20260315100226-f5d04adeffeb/go.mod h1:qk1sX/IBgppQNcGCRoj90u6EGC056EBoIc1oEjCWla8= 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/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/klauspost/asmfmt v1.3.2 h1:4Ri7ox3EwapiOjCki+hw14RyKk201CN4rzyCJRFLpK4= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= -github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +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.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= @@ -834,29 +984,37 @@ github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/z github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= -github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k= -github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM= +github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo= github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE= +github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM= github.com/manyminds/api2go v0.0.0-20171030193247-e7b693844a6f h1:tVvGiZQFjOXP+9YyGqSA6jE55x1XVxmoPYudncxrZ8U= github.com/manyminds/api2go v0.0.0-20171030193247-e7b693844a6f/go.mod h1:Z60vy0EZVSu0bOugCHdcN5ZxFMKSpjRgsnh0XKPFqqk= github.com/marcboeker/go-duckdb v1.8.5 h1:tkYp+TANippy0DaIOP5OEfBEwbUINqiFqgwMQ44jME0= github.com/marcboeker/go-duckdb v1.8.5/go.mod h1:6mK7+WQE4P4u5AFLvVBmhFxY5fvhymFptghgJX6B+/8= github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ= github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -875,9 +1033,10 @@ github.com/maxatome/go-testdeep v1.14.0 h1:rRlLv1+kI8eOI3OaBXZwb3O7xY3exRzdW5QyX github.com/maxatome/go-testdeep v1.14.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= -github.com/miekg/dns v1.1.65 h1:0+tIPHzUW0GCge7IiK3guGP57VAw7hoPDfApjkMD1Fc= -github.com/miekg/dns v1.1.65/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck= +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/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI= @@ -886,14 +1045,21 @@ github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= 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/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -901,12 +1067,12 @@ github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjU github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= -github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= +github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= @@ -920,12 +1086,13 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= @@ -945,6 +1112,7 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= @@ -954,6 +1122,7 @@ github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dl github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -978,14 +1147,16 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= @@ -1016,12 +1187,14 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/pressly/goose/v3 v3.26.0 h1:KJakav68jdH0WDvoAcj8+n61WqOIaPGgH0bJWS6jpmM= github.com/pressly/goose/v3 v3.26.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= @@ -1034,21 +1207,25 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: 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/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +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/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 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.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/prometheus/prometheus v0.304.2 h1:HhjbaAwet87x8Be19PFI/5W96UMubGy3zt24kayEuh4= -github.com/prometheus/prometheus v0.304.2/go.mod h1:ioGx2SGKTY+fLnJSQCdTHqARVldGNS8OlIe3kvp98so= +github.com/prometheus/prometheus v0.311.2-0.20260410083055-07c6232d159b h1:tjxqNQlYTJzrQrY7HM2SbnxqzuE64vnvlSmSbAvBBDE= +github.com/prometheus/prometheus v0.311.2-0.20260410083055-07c6232d159b/go.mod h1:h4Ogksuo6VUZmnm6q/ruKTUzrg9Vvu6u/6O/rQ5xPMg= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4= github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= @@ -1062,6 +1239,7 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= @@ -1075,8 +1253,10 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= @@ -1094,6 +1274,9 @@ github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6v github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/scylladb/go-reflectx v1.0.1 h1:b917wZM7189pZdlND9PbIJ6NQxfDPfBvUaQ7cjj1iZQ= github.com/scylladb/go-reflectx v1.0.1/go.mod h1:rWnOfDIRWBGN0miMLIcoPt/Dhi2doCMZqwMCJ3KupFc= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBKz6mJnzuHioeEat74PuQ4Sgvbf8eus695sc= +github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y= github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= @@ -1102,8 +1285,8 @@ github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKl github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil/v3 v3.24.3 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE= github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg= -github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU= -github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8= +github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= +github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= @@ -1111,60 +1294,63 @@ github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFR github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3 h1:aQKxg3+2p+IFXXg97McgDGT5zcMrQoi0EICZs8Pgchs= github.com/sigurn/crc16 v0.0.0-20211026045750-20ab5afb07e3/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/smartcontractkit/ccip-owner-contracts v0.1.0 h1:GiBDtlx7539o7AKlDV+9LsA7vTMPv+0n7ClhSFnZFAk= github.com/smartcontractkit/ccip-owner-contracts v0.1.0/go.mod h1:NnT6w4Kj42OFFXhSx99LvJZWPpMjmo4+CpDEWfw61xY= -github.com/smartcontractkit/chain-selectors v1.0.97 h1:ECOin+SkJv2MUrfqTUu28J0kub04Epds5NPMHERfGjo= -github.com/smartcontractkit/chain-selectors v1.0.97/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260304104421-dd6ab4ea9452 h1:8Txp5kpnQPYpeXioezwrJpsYxfnBK1ya0ND22RCTuaw= -github.com/smartcontractkit/chainlink-aptos v0.0.0-20260304104421-dd6ab4ea9452/go.mod h1:CQGkKp3YDsUuxixxmmngmRKfh6yIcftGEZsQrsSIIM8= +github.com/smartcontractkit/chain-selectors v1.0.98 h1:fuI7CQ1o5cX64eO4/LvwtfhdpGFH5vnsM/bFHRwEiww= +github.com/smartcontractkit/chain-selectors v1.0.98/go.mod h1:qy7whtgG5g+7z0jt0nRyii9bLND9m15NZTzuQPkMZ5w= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260407161350-a86b1969da65 h1:b6+ZvoZxXSj7HywoZ0CfWtC6k47eBSaxNzc2LqtiXBA= +github.com/smartcontractkit/chainlink-aptos v0.0.0-20260407161350-a86b1969da65/go.mod h1:BbVsx2VcwSVWkd0C5TcAkQBnFaeYFnogJgUa9BUla18= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= -github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260303102708-6caf8c4ea3b4 h1:z/44R1o+Kpc5ZDJxKV3oJYBxE7Ots73vZrrffAcf2UU= -github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260303102708-6caf8c4ea3b4/go.mod h1:KsZAvGHRP0+mHRwrMQY5nNJYaIXu222n0FQoaMeSkeo= -github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260216170932-c8081efc1ae5 h1:yZnCjPNKnH66Mm4uYUvXShBDriM7afd7LiSYMyk1qBo= -github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260216170932-c8081efc1ae5/go.mod h1:Gl35ExaFLinqVhp50+Yq1GnMuHb3fnDtZUFPCtcfV3M= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d h1:xdFpzbApEMz4Rojg2Y2OjFlrh0wu7eB10V2tSZGW5y8= -github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260121163256-85accaf3d28d/go.mod h1:bgmqE7x9xwmIVr8PqLbC0M5iPm4AV2DBl596lO6S5Sw= -github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5 h1:Z4t2ZY+ZyGWxtcXvPr11y4o3CGqhg3frJB5jXkCSvWA= -github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20250912190424-fd2e35d7deb5/go.mod h1:xtZNi6pOKdC3sLvokDvXOhgHzT+cyBqH/gWwvxTxqrg= -github.com/smartcontractkit/chainlink-ccv v0.0.0-20260225114453-965dabf4bcb0 h1:kaIN9AjmCEZAEmIMhIqmKddKFqGBVsKToNABk+TWsRY= -github.com/smartcontractkit/chainlink-ccv v0.0.0-20260225114453-965dabf4bcb0/go.mod h1:RnuNcn7DZmjmzEkeEWX0uL5y1oslB3c9URPLOjFU+jE= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144 h1:XKvx3xnke2K7/5z6rM/r5k8kE1hWriDm8V/f2TKC/b4= -github.com/smartcontractkit/chainlink-common v0.10.1-0.20260302172713-40eba758f144/go.mod h1:0ghbAr7tRO0tT5ZqBXhOyzgUO37tNNe33Yn0hskauVM= +github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260417153334-3b564ef614de h1:coysmw4zHm6TLOZawoe2h0hHh/25ft+hq9+9mRNkqTs= +github.com/smartcontractkit/chainlink-ccip v0.1.1-solana.0.20260417153334-3b564ef614de/go.mod h1:1XxxpkgCmG/z6y30yRuVrcxre6zixIVX3xzi706Db/8= +github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260406180811-0ec22f0243a4 h1:b6IxxglkWivZ5nfYdYkHF4w0l2BJyEmSdyMYRm47aB4= +github.com/smartcontractkit/chainlink-ccip/ccv/chains/evm v0.0.0-20260406180811-0ec22f0243a4/go.mod h1:zLqdD2kBX7NsntBneclb2yrHhjFaJdoyA8dK5eimlrE= +github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260415165642-49f23e4d76cc h1:dP1ERzdTbiJbHVXfHYdBAi1+8NjgkyQuY2oFNWWWDsQ= +github.com/smartcontractkit/chainlink-ccip/chains/evm v0.0.0-20260415165642-49f23e4d76cc/go.mod h1:7XR5wfgT8hjSsiV+t0EAWvna+rYQeMPaoZf/0g+dios= +github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc h1:mvobZx5JV5PhG/9IXPReV+8mAGnupl0HIWQZ43zxzd4= +github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20260415165642-49f23e4d76cc/go.mod h1:gzCVLUlNov/zFXSC7G6zcGkZU1IfNOHaakbAPDe5Woc= +github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260415165642-49f23e4d76cc h1:War93neyFmv7pzuElZeZC3qc/OfGtLvEXvqL3qeBfM0= +github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings v0.0.0-20260415165642-49f23e4d76cc/go.mod h1:67YbnoglYD61Pz/jTVCgav9wFq7S35OU8UyQSvPllRw= +github.com/smartcontractkit/chainlink-ccv v0.0.0-20260408181529-b5080e662563 h1:1sYQ2lG3zbAG2vASNF5kLke8DhGk5lNaJirwPDx3Vi4= +github.com/smartcontractkit/chainlink-ccv v0.0.0-20260408181529-b5080e662563/go.mod h1:nEuyjUh4wrK6mNXEAaOncl/AhCl31oaxOS160gNW0vc= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260421191147-d10b9943ac71 h1:WSNUds78NMlwDttROK/hJZ6ZOremyrR5JXJmPlT8hO8= +github.com/smartcontractkit/chainlink-common v0.11.2-0.20260421191147-d10b9943ac71/go.mod h1:kOIIjzxuRXK31j1JdZgUAGjqbGwmJ5gU5qI+FMkP6/I= github.com/smartcontractkit/chainlink-common/keystore v1.0.2 h1:AWisx4JT3QV8tcgh6J5NCrex+wAgTYpWyHsyNPSXzsQ= github.com/smartcontractkit/chainlink-common/keystore v1.0.2/go.mod h1:rSkIHdomyak3YnUtXLenl6poIq8q0V3UZPiiyYqPdGA= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10 h1:FJAFgXS9oqASnkS03RE1HQwYQQxrO4l46O5JSzxqLgg= github.com/smartcontractkit/chainlink-common/pkg/chipingress v0.0.10/go.mod h1:oiDa54M0FwxevWwyAX773lwdWvFYYlYHHQV1LQ5HpWY= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20251215152504-b1e41f508340 h1:PsjEI+5jZIz9AS4eOsLS5VpSWJINf38clXV3wryPyMk= github.com/smartcontractkit/chainlink-common/pkg/monitoring v0.0.0-20251215152504-b1e41f508340/go.mod h1:P/0OSXUlFaxxD4B/P6HWbxYtIRmmWGDJAvanq19879c= -github.com/smartcontractkit/chainlink-data-streams v0.1.12-0.20260227110503-42b236799872 h1:/nhvP6cBqGLrf4JwA/1FHLxnJjFhKRP6xtXAPcpE8g0= -github.com/smartcontractkit/chainlink-data-streams v0.1.12-0.20260227110503-42b236799872/go.mod h1:5jROIH/4cgHBQn875A+E2DCqvkBtrkHs+ciedcGTjNI= -github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6 h1:wVGho+uL3UEqhzMtAXmtZDUQ14J1Fmm7PkqJDuJWd1c= -github.com/smartcontractkit/chainlink-deployments-framework v0.80.1-0.20260209182815-b296b7df28a6/go.mod h1:0EzSyjHDLYSNqo3Bp9lSQs53CTaGbXHB5ovCa6BoOOM= -github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260303141232-9cc3feb83863 h1:RdeCztSxb4F6GYtN8PbkwuS10OeZL4AHCO+VjUaDfKA= -github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260303141232-9cc3feb83863/go.mod h1:RbSY8We8s4ac7uO7Q3cJ7f1IqnCzTD/TErVoLmXH8N8= -github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260107191744-4b93f62cffe3 h1:V22ITnWmgBAyxH+VVVo1jxm/LeJ3jcVMCVYB+zLN5mU= -github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260107191744-4b93f62cffe3/go.mod h1:u5vhpPHVUdGUni9o00MBu2aKPE0Q2DRoipAGPYD01e0= -github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251222115927-36a18321243c h1:eX7SCn5AGUGduv5OrjbVJkUSOnyeal0BtVem6zBSB2Y= -github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20251222115927-36a18321243c/go.mod h1:oyfOm4k0uqmgZIfxk1elI/59B02shbbJQiiUdPdbMgI= +github.com/smartcontractkit/chainlink-data-streams v0.1.13 h1:YOmt545DW6U0SyaqBf+NTGDLm1yMurVI7yOvxP5hlJk= +github.com/smartcontractkit/chainlink-data-streams v0.1.13/go.mod h1:00aL7OK0BJdF9gn/4t4f/pctUu2VLwwfA8G/tl9rCrM= +github.com/smartcontractkit/chainlink-deployments-framework v0.95.0 h1:PHncc++Xk9OIP7JDiCvpqdMdP85YZWKQCpnRR7l477g= +github.com/smartcontractkit/chainlink-deployments-framework v0.95.0/go.mod h1:pTA1JrdlMSfb9WkrIfphq2KV/+paW7GHf15Oc/uJBxs= +github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260416173445-80f6efde0a03 h1:z+Au1CpZhVYpn7mkmG/mYFBFkdZoqibQ3LngEHm8Fqs= +github.com/smartcontractkit/chainlink-evm v0.3.4-0.20260416173445-80f6efde0a03/go.mod h1:6vCMfxz7cMW0wWseNKtct+b1JJbbRVJJhh/t6pQWN3M= +github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501 h1:QJiXTG9CmaQAuMRn5JGi+Jhji7fSkehVnKpjc8oNJJY= +github.com/smartcontractkit/chainlink-evm/contracts/cre/gobindings v0.0.0-20260403151002-2c91155b5501/go.mod h1:4cT1BeNF8DAn6In9zr3LayVCv1KzFeuxT7zcuNkfIb0= +github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd h1:sK+pK4epQp20yQ7XztwrVgkTkRAr4FY+TvEegW8RuQk= +github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20260119171452-39c98c3b33cd/go.mod h1:7Jlt72+V9891y3LnGwHzmQwt9tfEGYryRKiGlQHo/o8= github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135 h1:8u9xUrC+yHrTDexOKDd+jrA6LCzFFHeX1G82oj2fsSI= github.com/smartcontractkit/chainlink-feeds v0.1.2-0.20250227211209-7cd000095135/go.mod h1:NkvE4iQgiT7dMCP6U3xPELHhWhN5Xr6rHC0axRebyMU= github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563 h1:ACpDbAxG4fa4sA83dbtYcrnlpE/y7thNIZfHxTv2ZLs= github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563/go.mod h1:jP5mrOLFEYZZkl7EiCHRRIMSSHCQsYypm1OZSus//iI= -github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20251210101658-1c5c8e4c4f15 h1:Mf+IRvrXutcKAKpuOxq5Ae+AAw4Z5vc66q1xI7qimZQ= -github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20251210101658-1c5c8e4c4f15/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251210101658-1c5c8e4c4f15 h1:IXF7+k8I1YY/yvXC1wnS3FAAggtCy6ByEQ9hv/F2FvQ= -github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20251210101658-1c5c8e4c4f15/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942 h1:T/eCDsUI8EJT4n5zSP4w1mz4RHH+ap8qieA17QYfBhk= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20251021173435-e86785845942/go.mod h1:2JTBNp3FlRdO/nHc4dsc9bfxxMClMO1Qt8sLJgtreBY= +github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57 h1:sCrr1Oy/JZstf/Oi2cRuU4mDN1BRUKfXP2CKByCMADg= +github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20260326122810-b657beadfb57/go.mod h1:kGprqyjsz6qFNVszOQoHc24wfvCjyipNZFste/3zcbs= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260410144512-ca02ad6ed16a h1:QbP7JIzDNvgmGL9TLM5VdzvCA90Ncg7E0ommuodKzEc= +github.com/smartcontractkit/chainlink-framework/metrics v0.0.0-20260410144512-ca02ad6ed16a/go.mod h1:HG/aei0MgBOpsyRLexdKGtOUO8yjSJO3iUu0Uu8KBm4= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a h1:PsFckZp3Dhb5pVc0Xccj1lvnOEg0H3eQdjtZgnCKd+4= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20260410144512-ca02ad6ed16a/go.mod h1:7ketk4ischPQW/JQgmyHz6zdzLUJv1VC29SiSgosydQ= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4 h1:GCzrxDWn3b7jFfEA+WiYRi8CKoegsayiDoJBCjYkneE= github.com/smartcontractkit/chainlink-protos/billing/go v0.0.0-20251024234028-0988426d98f4/go.mod h1:HHGeDUpAsPa0pmOx7wrByCitjQ0mbUxf0R9v+g67uCA= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/committee-verifier v0.0.0-20251211142334-5c3421fe2c8d h1:VYoBBNnQpZ5p+enPTl8SkKBRaubqyGpO0ul3B1np++I= @@ -1175,58 +1361,58 @@ github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0. github.com/smartcontractkit/chainlink-protos/chainlink-ccv/message-discovery v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:ATjAPIVJibHRcIfiG47rEQkUIOoYa6KDvWj3zwCAw6g= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d h1:AJy55QJ/pBhXkZjc7N+ATnWfxrcjq9BI9DmdtdjwDUQ= github.com/smartcontractkit/chainlink-protos/chainlink-ccv/verifier v0.0.0-20251211142334-5c3421fe2c8d/go.mod h1:5JdppgngCOUS76p61zCinSCgOhPeYQ+OcDUuome5THQ= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4 h1:fkS5FJpSozwxL2FA6OJDi7az2DrtMNiK1X5DWuHDyfA= -github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260320153346-314ec8dbe5a4/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= -github.com/smartcontractkit/chainlink-protos/job-distributor v0.17.0 h1:xHPmFDhff7QpeFxKsZfk+24j4AlnQiFjjRh5O87Peu4= -github.com/smartcontractkit/chainlink-protos/job-distributor v0.17.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877 h1:6UueUIbck1Ogarm9rm/9TS6b09mKgMmx+YE8XFg63AQ= +github.com/smartcontractkit/chainlink-protos/cre/go v0.0.0-20260420204255-a3f3bdd56877/go.mod h1:Jqt53s27Tr0jDl8mdBXg1xhu6F8Fci8JOuq43tgHOM8= +github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0 h1:q+VDPcxWrj5k9QizSYfUOSMnDH3Sd5HvbPguZOgfXTY= +github.com/smartcontractkit/chainlink-protos/job-distributor v0.18.0/go.mod h1:/dVVLXrsp+V0AbcYGJo3XMzKg3CkELsweA/TTopCsKE= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b h1:QuI6SmQFK/zyUlVWEf0GMkiUYBPY4lssn26nKSd/bOM= github.com/smartcontractkit/chainlink-protos/linking-service/go v0.0.0-20251002192024-d2ad9222409b/go.mod h1:qSTSwX3cBP3FKQwQacdjArqv0g6QnukjV4XuzO6UyoY= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9 h1:hhevsu8k7tlDRrYZmgAh7V4avGQDMvus1bwIlial3Ps= -github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260211172625-dff40e83b3c9/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785 h1:oli+2uLU6jcrJGCuYFqk3475hiwL17SWlITWLv+tx/w= +github.com/smartcontractkit/chainlink-protos/node-platform v0.0.0-20260319180422-b5808c964785/go.mod h1:dkR2uYg9XYJuT1JASkPzWE51jjFkVb86P7a/yXe5/GM= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 h1:AEnxv4HM3WD1RbQkRiFyb9cJ6YKAcqBp1CpIcFdZfuo= github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4/go.mod h1:PjZD54vr6rIKEKQj6HNA4hllvYI/QpT+Zefj3tqkFAs= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0 h1:0eroOyBwmdoGUwUdvMI0/J7m5wuzNnJDMglSOK1sfNY= github.com/smartcontractkit/chainlink-protos/orchestrator v0.10.0/go.mod h1:m/A3lqD7ms/RsQ9BT5P2uceYY0QX5mIt4KQxT2G6qEo= -github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260128151123-605e9540b706 h1:z3sQK3dyfl9Rbm8Inj8irwvX6yQihASp1UvMjrfz6/w= -github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260128151123-605e9540b706/go.mod h1:aifeP3SnsVrO1eSN5Smur3iHjAmi3poaLt6TAbgK0Hw= +github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260331131315-f08a616d8dcd h1:7DURXB3+Qf9REr3XA+q0FNyZO3CSAeSgJvNaek/GiZI= +github.com/smartcontractkit/chainlink-protos/ring/go v0.0.0-20260331131315-f08a616d8dcd/go.mod h1:aifeP3SnsVrO1eSN5Smur3iHjAmi3poaLt6TAbgK0Hw= github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6 h1:L6KJ4kGv/yNNoCk8affk7Y1vAY0qglPMXC/hevV/IsA= github.com/smartcontractkit/chainlink-protos/rmn/v1.6/go v0.0.0-20250131130834-15e0d4cde2a6/go.mod h1:FRwzI3hGj4CJclNS733gfcffmqQ62ONCkbGi49s658w= github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0 h1:B7itmjy+CMJ26elVw/cAJqqhBQ3Xa/mBYWK0/rQ5MuI= github.com/smartcontractkit/chainlink-protos/storage-service v0.3.0/go.mod h1:h6kqaGajbNRrezm56zhx03p0mVmmA2xxj7E/M4ytLUA= github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3 h1:X8Pekpv+cy0eW1laZTwATuYLTLZ6gRTxz1ZWOMtU74o= github.com/smartcontractkit/chainlink-protos/svr v1.1.1-0.20260203131522-bb8bc5c423b3/go.mod h1:TcOliTQU6r59DwG4lo3U+mFM9WWyBHGuFkkxQpvSujo= -github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f h1:3+vQMwuWL6+OqNutFqo/+gkczJwcr+MBPqeSxcjfI1Y= -github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260217043601-5cc966896c4f/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260223222711-2fa6b0e07db0 h1:9ltUDPuyvM1o/PW8P3U/jIUAHIMDUpktn+SKLmaeFJk= -github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260223222711-2fa6b0e07db0/go.mod h1:UsRdX5DVRd2HTkx6amXG1RYJSsL+1/SDB/iPRQjfD+Q= -github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0 h1:4mGJySR1GAJAAFRwEo6YiSKM2zSHzYT5b/FSmrpNUGI= -github.com/smartcontractkit/chainlink-sui v0.0.0-20260304150206-c64e48eb0cb0/go.mod h1:U3XStbEnbx/+L22n1/8aOIdgcGVxtsZB7p59xJGngAs= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.14.1-0.20260212100725-fbd6b3bca4d1 h1:w1KRBigXgoBYQBi4IU0gKbA2mBF6vq5vW/zbtan+mPo= -github.com/smartcontractkit/chainlink-testing-framework/framework v0.14.1-0.20260212100725-fbd6b3bca4d1/go.mod h1:43xdIQuqw/gzfazsqJkBrGdF25TIJDiY/Ak/YrWFTmU= -github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.3 h1:TZ0Yk+vjAJpoWnfsPdftWkq/NwZTrk734a/H4RHKnY8= -github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.3/go.mod h1:kHYJnZUqiPF7/xN5273prV+srrLJkS77GbBXHLKQpx0= -github.com/smartcontractkit/chainlink-ton v0.0.0-20260223231247-735246035dab h1:9CPYGRg8aAt8DTNpMALGRySJl5i0yk6wCeV3wqLdUYE= -github.com/smartcontractkit/chainlink-ton v0.0.0-20260223231247-735246035dab/go.mod h1:FDDjLuc4vrfclu3JHkMaREg0XZz7Lw1MK47Z4jJ4U5Q= -github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260218133534-cbd44da2856b h1:0XLtETkgkzwnEgUIIgyO/oydkUpzDVVuuFLf6aBeNPg= -github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260218133534-cbd44da2856b/go.mod h1:XMp5GoxJzF/L5xoA2Og5uAMIUK0WDnZIHzhIilCV8zM= +github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997 h1:W0HKHO8eE8BckTRnhSdqjHKbJcnk068nEWYnWRu6tJY= +github.com/smartcontractkit/chainlink-protos/workflows/go v0.0.0-20260323124644-faea187e6997/go.mod h1:GTpDgyK0OObf7jpch6p8N281KxN92wbB8serZhU9yRc= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260421131224-c46cbfe7bc6c h1:2ZdBZCZWKUMOWLtReaBBHkmDtXc0WtwcqIROHAcm3j4= +github.com/smartcontractkit/chainlink-solana v1.1.2-0.20260421131224-c46cbfe7bc6c/go.mod h1:sUsEwLtVPBlz0wPcysaolS+HVj9cOAt4jYhwE6J8dXg= +github.com/smartcontractkit/chainlink-sui v0.0.0-20260409184948-5b16fae57fe0 h1:nmuT5gKyTHpsHBEJMDM1C+v1d8jR/N8Xfg3KvqJUm8U= +github.com/smartcontractkit/chainlink-sui v0.0.0-20260409184948-5b16fae57fe0/go.mod h1:YQDu2RcdoAzI5xlhtpbjvaQQZwkUt/Q+IhLbP25M614= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.16 h1:pzrAgF6QFMQLS/kukXenLN87PCa48SEMlE7QvJxTOHs= +github.com/smartcontractkit/chainlink-testing-framework/framework v0.15.16/go.mod h1:BALK9cj8sk12e15UF6uDhifHgIApa+6N11TcQfInEro= +github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5 h1:RwZXxdIAOyjp6cwc9Quxgr38k8r7ACz+Lxh9o/A6oH0= +github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.5/go.mod h1:kHYJnZUqiPF7/xN5273prV+srrLJkS77GbBXHLKQpx0= +github.com/smartcontractkit/chainlink-ton v0.0.0-20260415120434-cecc380f8d87 h1:NgA2+Q0wfHicP/QeY1hgULQ1ZBk1sgBpOJi3GpxfjE8= +github.com/smartcontractkit/chainlink-ton v0.0.0-20260415120434-cecc380f8d87/go.mod h1:UmQdvE8BtbLdoOFY0+Adqoc7HT1Hd1bbFY/yymuM0NU= +github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260408092456-3c6369888d4a h1:Xu8iBnBQEibWIXTCwKYf8okXjFtzJ0KochjL03h+T40= +github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20260408092456-3c6369888d4a/go.mod h1:1eaXR+Fe6TlpP+CKXozfYlFM8QgN/N5C7OMvTRWNT8I= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014143056-a0c6328c91e9 h1:/Q1gD5gI0glBMztVH9XUVci3aOy8h+qTDV6o42MsqMM= github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20251014143056-a0c6328c91e9/go.mod h1:ea1LESxlSSOgc2zZBqf1RTkXTMthHaspdqUHd7W4lF0= -github.com/smartcontractkit/chainlink/deployment v0.0.0-20260224120304-949cf5d66bc6 h1:UFcQ1buOa3nR2mNOG4s69Ai65mFeoKxGj+nd/3e02F4= -github.com/smartcontractkit/chainlink/deployment v0.0.0-20260224120304-949cf5d66bc6/go.mod h1:uVwlLk9yxBBAoqL3FVL6iC9p2qtKRZKCLoluWEbilTo= -github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260305114259-bea2267bbe93 h1:0p14wY1PWCdR7YjDAoPbsPYNMn56GLkLx2HHDgD58Fs= -github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260305114259-bea2267bbe93/go.mod h1:Wk4FBVAHobT16aX0yLPnEDzN4JYgTUb/G6ympqML66g= +github.com/smartcontractkit/chainlink/deployment v0.0.0-20260422181348-efa818697ce5 h1:5Vh1ulQMReXwg8qgT5by12MqJt+Dc9y3Y/df02QeLuk= +github.com/smartcontractkit/chainlink/deployment v0.0.0-20260422181348-efa818697ce5/go.mod h1:pMBypeRoT8Nnb5gbkW6kIs+fBVsc/OzUx2eR9MtyFaw= +github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260422181348-efa818697ce5 h1:G2TvBxwvzLMIU5hMPsQlVAY027mT1P/VTiWA6yegmpM= +github.com/smartcontractkit/chainlink/v2 v2.29.1-cre-beta.0.0.20260422181348-efa818697ce5/go.mod h1:8/a/Nlo4ZndgBEumXoE+gvBA5LH40BfDM+6SZa4Q89M= github.com/smartcontractkit/cre-sdk-go v1.7.0 h1:MtaJ4jXS/5RcRCrjoza52/g3c0qrGXGB3V5yO9l6tUA= github.com/smartcontractkit/cre-sdk-go v1.7.0/go.mod h1:yYrQFz1UH7hhRbPO0q4fgo1tfsJNd4yXnI3oCZE0RzM= github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.9 h1:UORlnFd/BNjSX9MjUDjSg7/awWwgXqS+BdWOnyEIqWk= github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm v1.0.0-beta.9/go.mod h1:M4EQIX5V66V7wKyDBa/8L3JLFf/m0FNmGDvjIqKPqSw= -github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e h1:Hv9Mww35LrufCdM9wtS9yVi/rEWGI1UnjHbcKKU0nVY= -github.com/smartcontractkit/freeport v0.1.3-0.20250716200817-cb5dfd0e369e/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= +github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad h1:lgHxTHuzJIF3Vj6LSMOnjhqKgRqYW+0MV2SExtCYL1Q= +github.com/smartcontractkit/freeport v0.1.3-0.20250828155247-add56fa28aad/go.mod h1:T4zH9R8R8lVWKfU7tUvYz2o2jMv1OpGCdpY2j2QZXzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= -github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e h1:poXTj5cFVM6XfC4HICIDYkDVc/A6OYB0eeID0wU2JQE= -github.com/smartcontractkit/libocr v0.0.0-20260304194147-a03701e2c02e/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= -github.com/smartcontractkit/mcms v0.35.1-0.20260209175626-b68b54b6e8d0 h1:H6GXXs71EGZBk3Vr6Ren3PzkY5l3F6RUFaAJ5Gn7Gj8= -github.com/smartcontractkit/mcms v0.35.1-0.20260209175626-b68b54b6e8d0/go.mod h1:GVgE0Friw/XKcgua03ZRPeo23lxaMCkwuIIjk6qbKW0= +github.com/smartcontractkit/libocr v0.0.0-20260403184524-b6409238958d h1:PvXor5Fjer7FIONSqYXbpd1LkA14hWrlAyxXzOrC9t8= +github.com/smartcontractkit/libocr v0.0.0-20260403184524-b6409238958d/go.mod h1:PLdNK6GlqfxIWXzziPkU7dCAVlVFeYkyyW7AQY0R+4Q= +github.com/smartcontractkit/mcms v0.41.1 h1:rK5X7if29gRhL6yqpUwxwaLYV0CqgwSJivdDqEJGFv4= +github.com/smartcontractkit/mcms v0.41.1/go.mod h1:9AJhwHSVwV2mETizHBNfEF9CemL/Fmf0yPxNGdTtL/0= github.com/smartcontractkit/quarantine v0.0.0-20250909213106-ece491bef618 h1:rN8PnOZj53L70zlm1aYz1k14lXNCt7NoV666TDfcTJA= github.com/smartcontractkit/quarantine v0.0.0-20250909213106-ece491bef618/go.mod h1:iwy4yWFuK+1JeoIRTaSOA9pl+8Kf//26zezxEXrAQEQ= github.com/smartcontractkit/smdkg v0.0.0-20251029093710-c38905e58aeb h1:kLHdQQkijaPGsBbtV2rJgpzVpQ96e7T10pzjNlWfK8U= @@ -1237,19 +1423,35 @@ github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20251120172354-e8ec0386b06c h1:S github.com/smartcontractkit/tdh2/go/tdh2 v0.0.0-20251120172354-e8ec0386b06c/go.mod h1:NSc7hgOQbXG3DAwkOdWnZzLTZENXSwDJ7Va1nBp0YU0= github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945 h1:zxcODLrFytOKmAd8ty8S/XK6WcIEJEgRBaL7sY/7l4Y= github.com/smartcontractkit/wsrpc v0.8.5-0.20250502134807-c57d3d995945/go.mod h1:m3pdp17i4bD50XgktkzWetcV5yaLsi7Gunbv4ZgN6qg= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= -github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/stellar/go-stellar-sdk v0.1.0 h1:MfV7dv4k6xQQrWeKT7npWyKhjoayphLVGwXKtTLNeH8= +github.com/stellar/go-stellar-sdk v0.1.0/go.mod h1:fZPcxQZw1I0zZ+X76uFcVPqmQCaYbWc87lDFW/kQJaY= +github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 h1:OzCVd0SV5qE3ZcDeSFCmOWLZfEWZ3Oe8KtmSOYKEVWE= +github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps= github.com/stephenlacy/go-ethereum-hdwallet v0.0.0-20230913225845-a4fa94429863 h1:ba4VRWSkRzgdP5hB5OxexIzBXZbSwgcw8bEu06ivGQI= github.com/stephenlacy/go-ethereum-hdwallet v0.0.0-20230913225845-a4fa94429863/go.mod h1:oPTjPNrRucLv9mU27iNPj6n0CWWcNFhoXFOLVGJwHCA= github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= @@ -1277,25 +1479,31 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.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/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= -github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/suzuki-shunsuke/go-convmap v0.2.1 h1:g94CxI6ENYluXZhdEH+1WVGhMAE8nLvAmWLUCwBw6W0= +github.com/suzuki-shunsuke/go-convmap v0.2.1/go.mod h1:3XfGRbtyNBMGfXAxhROSRki6/UIlUX31Qt6DvdI6lUs= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= +github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= +github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= -github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts= -github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8= -github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0 h1:REJz+XwNpGC/dCgTfYvM4SKqobNqDBfvhq74s2oHTUM= -github.com/testcontainers/testcontainers-go/modules/postgres v0.39.0/go.mod h1:4K2OhtHEeT+JSIFX4V8DkGKsyLa96Y2vLdd3xsxD5HE= +github.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais= +github.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI= +github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0 h1:AOtFXssrDlLm84A2sTTR/AhvJiYbrIuCO59d+Ro9Tb0= +github.com/testcontainers/testcontainers-go/modules/postgres v0.41.0/go.mod h1:k2a09UKhgSp6vNpliIY0QSgm4Hi7GXVTzWvWgUemu/8= github.com/theodesp/go-heaps v0.0.0-20190520121037-88e35354fe0a h1:YuO+afVc3eqrjiCUizNCxI53bl/BnPiVwXqLzqYTqgU= github.com/theodesp/go-heaps v0.0.0-20190520121037-88e35354fe0a/go.mod h1:/sfW47zCZp9FrtGcWyo1VjbgDaodxX9ovZvgLb/MxaA= github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI= github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY= +github.com/tidwall/gjson v1.9.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -1307,11 +1515,12 @@ github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhso github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= -github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= -github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= -github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= -github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -1335,11 +1544,15 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xdrpp/goxdr v0.1.1 h1:E1B2c6E8eYhOVyd7yEpOyopzTPirUeF6mVOfXfGyJyc= +github.com/xdrpp/goxdr v0.1.1/go.mod h1:dXo1scL/l6s7iME1gxHWo2XCppbHEKZS7m/KyYWkNzA= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= @@ -1348,6 +1561,7 @@ github.com/xssnick/tonutils-go v1.14.1 h1:zV/iVYl/h3hArS+tPsd9XrSFfGert3r21caMlt github.com/xssnick/tonutils-go v1.14.1/go.mod h1:68xwWjpoGGqiTbLJ0gT63sKu1Z1moCnDLLzA+DKanIg= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= @@ -1374,56 +1588,68 @@ go.dedis.ch/kyber/v3 v3.1.0/go.mod h1:kXy7p3STAurkADD+/aZcsznZGKVHEqbtmdIzvPfrs1 go.dedis.ch/protobuf v1.0.5/go.mod h1:eIV4wicvi6JK0q/QnfIEGeSFNG0ZeB24kzut5+HaRLo= go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI4= go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I= go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM= go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.1/go.mod h1:Ap50jQcDJrx6rB6VgeeFPtuPIf3wMRvRfrfYDO6+BmA= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 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/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.49.0 h1:1f31+6grJmV3X4lxcEvUy13i5/kfDw1nJZwhd8mA4tg= go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.49.0/go.mod h1:1P/02zM3OwkX9uki+Wmxw3a5GVb6KUXRsa7m7bOC9Fg= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2 h1:06ZeJRe5BnYXceSM9Vya83XXVaNGe3H1QqsvqRANQq8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.12.2/go.mod h1:DvPtKE63knkDVP88qpatBj81JxN+w1bqfVbsbCbj1WY= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2 h1:tPLwQlXbJ8NSOfZc4OkgU5h2A38M4c9kfHSVc4PFQGs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.12.2/go.mod h1:QTnxBwT/1rBIgAG1goq6xMydfYOBKU6KTiYF4fp5zL8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0 h1:gAU726w9J8fwr4qRDqu1GYMNNs4gXrU+Pv20/N1UpB4= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.36.0/go.mod h1:RboSDkp7N292rgu+T0MgVt2qgFGu6qa1RpZDOtpL76w= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0 h1:yEX3aC9KDgvYPhuKECHbOlr5GLwH6KTjLJ1sBSkkxkc= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.13.0/go.mod h1:/GXR0tBmmkxDaCUGahvksvp66mx4yh5+cFXgSlhg0vQ= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0 h1:G8Xec/SgZQricwWBJF/mHZc7A02YHedfFDENwJEdRA0= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.36.0/go.mod h1:PD57idA/AiFD5aqoxGxCvT/ILJPeHy3MjqU/NS7KogY= -go.opentelemetry.io/otel/log v0.15.0 h1:0VqVnc3MgyYd7QqNVIldC3dsLFKgazR6P3P3+ypkyDY= -go.opentelemetry.io/otel/log v0.15.0/go.mod h1:9c/G1zbyZfgu1HmQD7Qj84QMmwTp2QCQsZH1aeoWDE4= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/log v0.15.0 h1:WgMEHOUt5gjJE93yqfqJOkRflApNif84kxoHWS9VVHE= -go.opentelemetry.io/otel/sdk/log v0.15.0/go.mod h1:qDC/FlKQCXfH5hokGsNg9aUBGMJQsrUyeOiW5u+dKBQ= -go.opentelemetry.io/otel/sdk/log/logtest v0.13.0 h1:9yio6AFZ3QD9j9oqshV1Ibm9gPLlHNxurno5BreMtIA= -go.opentelemetry.io/otel/sdk/log/logtest v0.13.0/go.mod h1:QOGiAJHl+fob8Nu85ifXfuQYmJTFAvcrxL6w5/tu168= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= -go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 h1:Dn8rkudDzY6KV9dr/D/bTUuWgqDf9xe0rr4G2elrn0Y= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0/go.mod h1:gMk9F0xDgyN9M/3Ed5Y1wKcx/9mlU91NXY2SNq7RQuU= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0 h1:HIBTQ3VO5aupLKjC90JgMqpezVXwFuq6Ryjn0/izoag= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0/go.mod h1:ji9vId85hMxqfvICA0Jt8JqEdrXaAkcpkI9HPXya0ro= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 h1:8UQVDcZxOJLtX6gxtDt3vY2WTgvZqMQRzjsqiIHQdkc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0/go.mod h1:2lmweYCiHYpEjQ/lSJBYhj9jP1zvCvQW4BqL9dnT7FQ= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0 h1:w1K+pCJoPpQifuVpsKamUdn9U0zM3xUziVOqsGksUrY= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0/go.mod h1:HBy4BjzgVE8139ieRI75oXm3EcDN+6GhD88JT1Kjvxg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0 h1:GJkybS+crDMdExT/BUNCEgfrmfboztcS6PhvSo88HKM= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.19.0/go.mod h1:NuAyxRYIG2lKX3YQkB+83StTxM7s52PUUkRRiC0wnYI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0 h1:mS47AX77OtFfKG4vtp+84kuGSFZHTyxtXIN269vChY0= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.43.0/go.mod h1:PJnsC41lAGncJlPUniSwM81gc80GkgWJWr3cu2nKEtU= +go.opentelemetry.io/otel/log v0.19.0 h1:KUZs/GOsw79TBBMfDWsXS+KZ4g2Ckzksd1ymzsIEbo4= +go.opentelemetry.io/otel/log v0.19.0/go.mod h1:5DQYeGmxVIr4n0/BcJvF4upsraHjg6vudJJpnkL6Ipk= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/log v0.19.0 h1:scYVLqT22D2gqXItnWiocLUKGH9yvkkeql5dBDiXyko= +go.opentelemetry.io/otel/sdk/log v0.19.0/go.mod h1:vFBowwXGLlW9AvpuF7bMgnNI95LiW10szrOdvzBHlAg= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0 h1:BEbF7ZBB6qQloV/Ub1+3NQoOUnVtcGkU3XX4Ws3GQfk= +go.opentelemetry.io/otel/sdk/log/logtest v0.19.0/go.mod h1:Lua81/3yM0wOmoHTokLj9y9ADeA02v1naRrVrkAZuKk= +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.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= 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= @@ -1440,27 +1666,32 @@ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKY go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.14.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +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= golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1468,6 +1699,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -1477,18 +1709,39 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= 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-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -1500,15 +1753,26 @@ golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73r 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= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -1521,6 +1785,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -1531,49 +1796,68 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= 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.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= -golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= 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= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +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= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1590,6 +1874,7 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210331175145-43e1dd70ce54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/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= @@ -1608,15 +1893,17 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= @@ -1624,9 +1911,11 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -1637,24 +1926,53 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200601175630-2caf76543d99/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -1675,30 +1993,69 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.10.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190716160619-c506a9f90610/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200324203455-a04cca1dde73/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20210401141331-865547bb08e2/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78= google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk= -google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 h1:X9z6obt+cWRX8XjDVOn+SZWhWe5kZHm46TThU9j+jss= -google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= +google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= 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= @@ -1720,18 +2077,21 @@ gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= -gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -1748,31 +2108,37 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= -k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= -k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= -k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= -k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= -k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= +k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= +k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= nhooyr.io/websocket v1.8.14 h1:3gKlV2P9bMu1U85zh1T2yLOmseFbRTbnYVOprNSEYKQ= nhooyr.io/websocket v1.8.14/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw= pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/authvalidation/validator.go b/internal/authvalidation/validator.go index 991734f1..6448ca51 100644 --- a/internal/authvalidation/validator.go +++ b/internal/authvalidation/validator.go @@ -12,13 +12,20 @@ import ( "github.com/smartcontractkit/cre-cli/internal/environments" ) -const queryOrganization = ` -query GetOrganizationDetails { - getOrganization { - organizationId +const queryCreOrganizationInfo = ` +query GetCreOrganizationInfo { + getCreOrganizationInfo { + orgId + derivedWorkflowOwners } }` +// ValidationResult holds the data returned by credential validation. +type ValidationResult struct { + OrgID string + DerivedWorkflowOwner string +} + // Validator validates authentication credentials type Validator struct { gqlClient *graphqlclient.Client @@ -35,28 +42,42 @@ func NewValidator(creds *credentials.Credentials, environmentSet *environments.E } // ValidateCredentials validates the provided credentials by making a lightweight GraphQL query -// The GraphQL client automatically handles token refresh if needed -func (v *Validator) ValidateCredentials(validationCtx context.Context, creds *credentials.Credentials) error { +// and returns organization info including the derived workflow owner. +// The GraphQL client automatically handles token refresh if needed. +func (v *Validator) ValidateCredentials(validationCtx context.Context, creds *credentials.Credentials) (*ValidationResult, error) { if creds == nil { - return fmt.Errorf("credentials not provided") + return nil, fmt.Errorf("credentials not provided") } - // Skip validation if already validated if creds.IsValidated { - return nil + return nil, nil } - req := graphql.NewRequest(queryOrganization) + req := graphql.NewRequest(queryCreOrganizationInfo) var respEnvelope struct { - GetOrganization struct { - OrganizationID string `json:"organizationId"` - } `json:"getOrganization"` + GetCreOrganizationInfo struct { + OrgID string `json:"orgId"` + DerivedWorkflowOwners []string `json:"derivedWorkflowOwners"` + } `json:"getCreOrganizationInfo"` } if err := v.gqlClient.Execute(validationCtx, req, &respEnvelope); err != nil { - return fmt.Errorf("authentication validation failed: %w", err) + return nil, fmt.Errorf("authentication failed: unable to retrieve organization info. Your account may not be fully set up yet — please try again in a few minutes: %w", err) + } + + info := respEnvelope.GetCreOrganizationInfo + + if info.OrgID == "" || len(info.DerivedWorkflowOwners) == 0 { + return nil, fmt.Errorf("authentication failed: unable to retrieve organization info. Your account may not be fully set up yet — please try again in a few minutes") + } + + result := &ValidationResult{ + OrgID: info.OrgID, + DerivedWorkflowOwner: info.DerivedWorkflowOwners[0], } - return nil + creds.OrgID = result.OrgID + + return result, nil } diff --git a/internal/client/graphqlclient/graphqlclient.go b/internal/client/graphqlclient/graphqlclient.go index d1e21bc0..8d1bb5ee 100644 --- a/internal/client/graphqlclient/graphqlclient.go +++ b/internal/client/graphqlclient/graphqlclient.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "regexp" "strings" @@ -62,7 +63,14 @@ func (c *Client) Execute(ctx context.Context, req *graphql.Request, resp any) er req.Header.Set("Authorization", "Bearer "+c.creds.Tokens.AccessToken) } } - return c.client.Run(ctx, req, resp) + err = c.client.Run(ctx, req, resp) + if err != nil { + if strings.HasPrefix(err.Error(), "graphql: ") { + return errors.New(strings.Replace(err.Error(), "graphql: ", "cre api error: ", 1)) + } + return err + } + return nil } func (c *Client) refreshTokens(ctx context.Context) error { diff --git a/internal/client/graphqlclient/graphqlclient_test.go b/internal/client/graphqlclient/graphqlclient_test.go index 878fbf37..301de821 100644 --- a/internal/client/graphqlclient/graphqlclient_test.go +++ b/internal/client/graphqlclient/graphqlclient_test.go @@ -1,7 +1,16 @@ package graphqlclient import ( + "context" + "net/http" + "net/http/httptest" "testing" + + "github.com/machinebox/graphql" + "github.com/rs/zerolog" + + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/environments" ) func TestRedactSensitiveHeaders(t *testing.T) { @@ -51,3 +60,35 @@ func TestRedactSensitiveHeaders(t *testing.T) { }) } } + +func TestExecute_ErrorPrefixReplacement(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + // This will cause the machinebox/graphql client to return an error starting with "graphql: " + _, _ = w.Write([]byte(`{"errors": [{"message": "DON family \"zone-a\" is not supported"}]}`)) + })) + defer srv.Close() + + creds := &credentials.Credentials{ + AuthType: credentials.AuthTypeApiKey, + APIKey: "test-api-key", + } + envSet := &environments.EnvironmentSet{GraphQLURL: srv.URL} + logger := zerolog.Nop() + + client := New(creds, envSet, &logger) + + req := graphql.NewRequest(`query { test }`) + var resp interface{} + + err := client.Execute(context.Background(), req, &resp) + + if err == nil { + t.Fatal("expected error, got nil") + } + + expectedErr := "cre api error: DON family \"zone-a\" is not supported" + if err.Error() != expectedErr { + t.Errorf("expected error %q, got %q", expectedErr, err.Error()) + } +} diff --git a/internal/client/privateregistryclient/privateregistryclient.go b/internal/client/privateregistryclient/privateregistryclient.go new file mode 100644 index 00000000..5e8f084b --- /dev/null +++ b/internal/client/privateregistryclient/privateregistryclient.go @@ -0,0 +1,391 @@ +package privateregistryclient + +import ( + "context" + "fmt" + "time" + + "github.com/machinebox/graphql" + "github.com/rs/zerolog" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" +) + +type Client struct { + graphql *graphqlclient.Client + log *zerolog.Logger + serviceTimeout time.Duration +} + +func New(gql *graphqlclient.Client, log *zerolog.Logger) *Client { + return &Client{ + graphql: gql, + log: log, + serviceTimeout: 2 * time.Minute, + } +} + +func (c *Client) SetServiceTimeout(timeout time.Duration) { + c.serviceTimeout = timeout +} + +func (c *Client) CreateServiceContextWithTimeout() (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), c.serviceTimeout) //nolint:gosec // G118 -- cancel is deferred by callers +} + +type OffchainWorkflow struct { + WorkflowID string `json:"workflowId"` + Owner string `json:"owner"` + CreatedAt string `json:"createdAt"` + Status OffchainWorkflowStatus `json:"status"` + WorkflowName string `json:"workflowName"` + BinaryURL string `json:"binaryUrl"` + ConfigURL string `json:"configUrl"` + Tag string `json:"tag"` + Attributes string `json:"attributes"` + DonFamily string `json:"donFamily"` + OrganizationID string `json:"organizationId"` +} + +type OffchainWorkflowStatus string + +const ( + WorkflowStatusUnspecified OffchainWorkflowStatus = "WORKFLOW_STATUS_UNSPECIFIED" + WorkflowStatusActive OffchainWorkflowStatus = "WORKFLOW_STATUS_ACTIVE" + WorkflowStatusPaused OffchainWorkflowStatus = "WORKFLOW_STATUS_PAUSED" +) + +// FormatStatus returns a short human-readable label for CLI output. +func FormatStatus(status OffchainWorkflowStatus) string { + switch status { + case WorkflowStatusActive: + return "Active" + case WorkflowStatusPaused: + return "Paused" + case WorkflowStatusUnspecified: + return "Unspecified" + default: + if status == "" { + return "" + } + return string(status) + } +} + +const ( + maxWorkflowNameLength = 64 + maxBinaryURLLength = 200 + maxConfigURLLength = 200 + maxTagLength = 32 + maxAttributesLength = 1024 +) + +type OffchainWorkflowInput struct { + WorkflowID string `json:"workflowId"` + Status OffchainWorkflowStatus `json:"status"` + WorkflowName string `json:"workflowName"` + BinaryURL string `json:"binaryUrl"` + ConfigURL *string `json:"configUrl,omitempty"` + Tag *string `json:"tag,omitempty"` + Attributes *string `json:"attributes,omitempty"` + DonFamily string `json:"donFamily"` +} + +type UpsertOffchainWorkflowRequest struct { + Workflow OffchainWorkflowInput `json:"workflow"` +} + +type UpsertOffchainWorkflowResponse struct { + Workflow OffchainWorkflow `json:"workflow"` +} + +type PauseOffchainWorkflowRequest struct { + WorkflowID string `json:"workflowId"` +} + +type PauseOffchainWorkflowResponse struct { + Workflow OffchainWorkflow `json:"workflow"` +} + +type ActivateOffchainWorkflowRequest struct { + WorkflowID string `json:"workflowId"` +} + +type ActivateOffchainWorkflowResponse struct { + Workflow OffchainWorkflow `json:"workflow"` +} + +type DeleteOffchainWorkflowRequest struct { + WorkflowID string `json:"workflowId"` +} + +type DeleteOffchainWorkflowResponse struct { + WorkflowID string `json:"workflowId"` +} + +type GetOffchainWorkflowByNameRequest struct { + WorkflowName string `json:"workflowName"` +} + +type GetOffchainWorkflowByNameResponse struct { + Workflow OffchainWorkflow `json:"workflow"` +} + +func (c *Client) GetWorkflowByName(workflowName string) (OffchainWorkflow, error) { + if workflowName == "" { + return OffchainWorkflow{}, fmt.Errorf("workflowName is required") + } + if len(workflowName) > maxWorkflowNameLength { + return OffchainWorkflow{}, fmt.Errorf("workflowName exceeds max length %d", maxWorkflowNameLength) + } + + const query = ` +query GetOffchainWorkflowByName($request: GetOffchainWorkflowByNameRequest!) { + getOffchainWorkflowByName(request: $request) { + workflow { + workflowId + owner + createdAt + status + workflowName + binaryUrl + configUrl + tag + attributes + donFamily + organizationId + } + } +}` + + req := graphql.NewRequest(query) + req.Var("request", GetOffchainWorkflowByNameRequest{WorkflowName: workflowName}) + + var container struct { + GetOffchainWorkflowByName GetOffchainWorkflowByNameResponse `json:"getOffchainWorkflowByName"` + } + + ctx, cancel := c.CreateServiceContextWithTimeout() + defer cancel() + + if err := c.graphql.Execute(ctx, req, &container); err != nil { + return OffchainWorkflow{}, fmt.Errorf("get workflow by name in registry: %w", err) + } + + c.log.Debug().Str("workflowName", workflowName). + Msg("Fetched workflow by name from private registry") + + return container.GetOffchainWorkflowByName.Workflow, nil +} + +func (c *Client) UpsertWorkflowInRegistry(workflow OffchainWorkflowInput) (OffchainWorkflow, error) { + if err := validateUpsertWorkflowInput(workflow); err != nil { + return OffchainWorkflow{}, err + } + + const mutation = ` +mutation UpsertOffchainWorkflow($request: UpsertOffchainWorkflowRequest!) { + upsertOffchainWorkflow(request: $request) { + workflow { + workflowId + owner + createdAt + status + workflowName + binaryUrl + configUrl + tag + attributes + donFamily + organizationId + } + } +}` + + req := graphql.NewRequest(mutation) + req.Var("request", UpsertOffchainWorkflowRequest{Workflow: workflow}) + + var container struct { + UpsertOffchainWorkflow UpsertOffchainWorkflowResponse `json:"upsertOffchainWorkflow"` + } + + ctx, cancel := c.CreateServiceContextWithTimeout() + defer cancel() + + if err := c.graphql.Execute(ctx, req, &container); err != nil { + return OffchainWorkflow{}, fmt.Errorf("upsert workflow in registry: %w", err) + } + + c.log.Debug().Str("workflowId", container.UpsertOffchainWorkflow.Workflow.WorkflowID). + Msg("Upserted workflow in private registry") + + return container.UpsertOffchainWorkflow.Workflow, nil +} + +func (c *Client) PauseWorkflowInRegistry(workflowID string) (OffchainWorkflow, error) { + if workflowID == "" { + return OffchainWorkflow{}, fmt.Errorf("workflowId is required") + } + + const mutation = ` +mutation PauseOffchainWorkflow($request: PauseOffchainWorkflowRequest!) { + pauseOffchainWorkflow(request: $request) { + workflow { + workflowId + owner + createdAt + status + workflowName + binaryUrl + configUrl + tag + attributes + donFamily + organizationId + } + } +}` + + req := graphql.NewRequest(mutation) + req.Var("request", PauseOffchainWorkflowRequest{WorkflowID: workflowID}) + + var container struct { + PauseOffchainWorkflow PauseOffchainWorkflowResponse `json:"pauseOffchainWorkflow"` + } + + ctx, cancel := c.CreateServiceContextWithTimeout() + defer cancel() + + if err := c.graphql.Execute(ctx, req, &container); err != nil { + return OffchainWorkflow{}, fmt.Errorf("pause workflow in registry: %w", err) + } + + c.log.Debug().Str("workflowId", workflowID). + Msg("Paused workflow in private registry") + + return container.PauseOffchainWorkflow.Workflow, nil +} + +func (c *Client) ActivateWorkflowInRegistry(workflowID string) (OffchainWorkflow, error) { + if workflowID == "" { + return OffchainWorkflow{}, fmt.Errorf("workflowId is required") + } + + const mutation = ` +mutation ActivateOffchainWorkflow($request: ActivateOffchainWorkflowRequest!) { + activateOffchainWorkflow(request: $request) { + workflow { + workflowId + owner + createdAt + status + workflowName + binaryUrl + configUrl + tag + attributes + donFamily + organizationId + } + } +}` + + req := graphql.NewRequest(mutation) + req.Var("request", ActivateOffchainWorkflowRequest{WorkflowID: workflowID}) + + var container struct { + ActivateOffchainWorkflow ActivateOffchainWorkflowResponse `json:"activateOffchainWorkflow"` + } + + ctx, cancel := c.CreateServiceContextWithTimeout() + defer cancel() + + if err := c.graphql.Execute(ctx, req, &container); err != nil { + return OffchainWorkflow{}, fmt.Errorf("activate workflow in registry: %w", err) + } + + c.log.Debug().Str("workflowId", workflowID). + Msg("Activated workflow in private registry") + + return container.ActivateOffchainWorkflow.Workflow, nil +} + +func (c *Client) DeleteWorkflowInRegistry(workflowID string) (string, error) { + if workflowID == "" { + return "", fmt.Errorf("workflowId is required") + } + + const mutation = ` +mutation DeleteOffchainWorkflow($request: DeleteOffchainWorkflowRequest!) { + deleteOffchainWorkflow(request: $request) { + workflowId + } +}` + + req := graphql.NewRequest(mutation) + req.Var("request", DeleteOffchainWorkflowRequest{WorkflowID: workflowID}) + + var container struct { + DeleteOffchainWorkflow DeleteOffchainWorkflowResponse `json:"deleteOffchainWorkflow"` + } + + ctx, cancel := c.CreateServiceContextWithTimeout() + defer cancel() + + if err := c.graphql.Execute(ctx, req, &container); err != nil { + return "", fmt.Errorf("delete workflow in registry: %w", err) + } + + c.log.Debug().Str("workflowId", workflowID). + Msg("Deleted workflow in private registry") + + return container.DeleteOffchainWorkflow.WorkflowID, nil +} + +func validateUpsertWorkflowInput(input OffchainWorkflowInput) error { + if input.WorkflowID == "" { + return fmt.Errorf("workflowId is required") + } + if input.Status == "" { + return fmt.Errorf("status is required") + } + if input.Status != WorkflowStatusUnspecified && + input.Status != WorkflowStatusActive && + input.Status != WorkflowStatusPaused { + return fmt.Errorf("status must be one of %q, %q, %q", WorkflowStatusUnspecified, WorkflowStatusActive, WorkflowStatusPaused) + } + if input.WorkflowName == "" { + return fmt.Errorf("workflowName is required") + } + if len(input.WorkflowName) > maxWorkflowNameLength { + return fmt.Errorf("workflowName exceeds max length %d", maxWorkflowNameLength) + } + if input.BinaryURL == "" { + return fmt.Errorf("binaryUrl is required") + } + if len(input.BinaryURL) > maxBinaryURLLength { + return fmt.Errorf("binaryUrl exceeds max length %d", maxBinaryURLLength) + } + if err := validateOptionalLength("configUrl", input.ConfigURL, maxConfigURLLength); err != nil { + return err + } + if err := validateOptionalLength("tag", input.Tag, maxTagLength); err != nil { + return err + } + if err := validateOptionalLength("attributes", input.Attributes, maxAttributesLength); err != nil { + return err + } + if input.DonFamily == "" { + return fmt.Errorf("donFamily is required") + } + + return nil +} + +func validateOptionalLength(name string, value *string, maxLen int) error { + if value != nil && len(*value) > maxLen { + return fmt.Errorf("%s exceeds max length %d", name, maxLen) + } + + return nil +} diff --git a/internal/client/privateregistryclient/privateregistryclient_test.go b/internal/client/privateregistryclient/privateregistryclient_test.go new file mode 100644 index 00000000..578fbd06 --- /dev/null +++ b/internal/client/privateregistryclient/privateregistryclient_test.go @@ -0,0 +1,423 @@ +package privateregistryclient + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/testutil" +) + +func TestFormatStatus(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + status OffchainWorkflowStatus + want string + }{ + {name: "active", status: WorkflowStatusActive, want: "Active"}, + {name: "paused", status: WorkflowStatusPaused, want: "Paused"}, + {name: "unspecified", status: WorkflowStatusUnspecified, want: "Unspecified"}, + {name: "empty", status: "", want: ""}, + {name: "unknown passthrough", status: OffchainWorkflowStatus("WORKFLOW_STATUS_FUTURE"), want: "WORKFLOW_STATUS_FUTURE"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tt.want, FormatStatus(tt.status)) + }) + } +} + +func TestValidateUpsertWorkflowInput(t *testing.T) { + t.Run("valid input", func(t *testing.T) { + err := validateUpsertWorkflowInput(OffchainWorkflowInput{ + WorkflowID: "wf-1", + Status: WorkflowStatusActive, + WorkflowName: "test-workflow", + BinaryURL: "s3://binary", + DonFamily: "family-a", + }) + require.NoError(t, err) + }) + + tests := []struct { + name string + input OffchainWorkflowInput + err string + }{ + { + name: "missing workflowId", + input: OffchainWorkflowInput{Status: WorkflowStatusActive, WorkflowName: "w", BinaryURL: "b", DonFamily: "f"}, + err: "workflowId is required", + }, + { + name: "missing status", + input: OffchainWorkflowInput{WorkflowID: "wf", WorkflowName: "w", BinaryURL: "b", DonFamily: "f"}, + err: "status is required", + }, + { + name: "missing workflowName", + input: OffchainWorkflowInput{WorkflowID: "wf", Status: WorkflowStatusActive, BinaryURL: "b", DonFamily: "f"}, + err: "workflowName is required", + }, + { + name: "missing binaryUrl", + input: OffchainWorkflowInput{WorkflowID: "wf", Status: WorkflowStatusActive, WorkflowName: "w", DonFamily: "f"}, + err: "binaryUrl is required", + }, + { + name: "missing donFamily", + input: OffchainWorkflowInput{WorkflowID: "wf", Status: WorkflowStatusActive, WorkflowName: "w", BinaryURL: "b"}, + err: "donFamily is required", + }, + { + name: "invalid status", + input: OffchainWorkflowInput{WorkflowID: "wf", Status: "INVALID", WorkflowName: "w", BinaryURL: "b", DonFamily: "f"}, + err: "status must be one of \"WORKFLOW_STATUS_UNSPECIFIED\", \"WORKFLOW_STATUS_ACTIVE\", \"WORKFLOW_STATUS_PAUSED\"", + }, + { + name: "workflowName exceeds max length", + input: OffchainWorkflowInput{WorkflowID: "wf", Status: WorkflowStatusActive, WorkflowName: strings.Repeat("a", maxWorkflowNameLength+1), BinaryURL: "b", DonFamily: "f"}, + err: "workflowName exceeds max length 64", + }, + { + name: "binaryUrl exceeds max length", + input: OffchainWorkflowInput{WorkflowID: "wf", Status: WorkflowStatusActive, WorkflowName: "w", BinaryURL: strings.Repeat("b", maxBinaryURLLength+1), DonFamily: "f"}, + err: "binaryUrl exceeds max length 200", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := validateUpsertWorkflowInput(tc.input) + require.Error(t, err) + assert.Equal(t, tc.err, err.Error()) + }) + } +} + +func TestUpsertWorkflowInRegistry(t *testing.T) { + var capturedQuery string + var capturedVariables map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + Query string `json:"query"` + Variables map[string]interface{} `json:"variables"` + } + _ = json.NewDecoder(r.Body).Decode(&req) + capturedQuery = req.Query + capturedVariables = req.Variables + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "upsertOffchainWorkflow": map[string]any{ + "workflow": map[string]any{ + "workflowId": "wf-123", + "owner": "owner-1", + "createdAt": "2026-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "registry-workflow", + "binaryUrl": "s3://binary", + "configUrl": "s3://config", + "tag": "v1", + "attributes": "{\"region\":\"us-east-1\"}", + "donFamily": "family-a", + "organizationId": "org-1", + }, + }, + }, + }) + })) + defer srv.Close() + + client := newTestPrivateRegistryClient(t, srv.URL) + configURL := "s3://config" + tag := "v1" + attributes := "{\"region\":\"us-east-1\"}" + result, err := client.UpsertWorkflowInRegistry(OffchainWorkflowInput{ + WorkflowID: "wf-123", + Status: WorkflowStatusActive, + WorkflowName: "registry-workflow", + BinaryURL: "s3://binary", + ConfigURL: &configURL, + Tag: &tag, + Attributes: &attributes, + DonFamily: "family-a", + }) + + require.NoError(t, err) + assert.Contains(t, capturedQuery, "mutation UpsertOffchainWorkflow") + assert.Contains(t, capturedQuery, "upsertOffchainWorkflow") + assert.Equal(t, "wf-123", result.WorkflowID) + assert.Equal(t, WorkflowStatusActive, result.Status) + assert.Equal(t, "family-a", result.DonFamily) + assert.Equal(t, "org-1", result.OrganizationID) + + request, ok := capturedVariables["request"].(map[string]any) + require.True(t, ok) + workflow, ok := request["workflow"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "wf-123", workflow["workflowId"]) + assert.Equal(t, "WORKFLOW_STATUS_ACTIVE", workflow["status"]) + assert.Equal(t, "registry-workflow", workflow["workflowName"]) + assert.Equal(t, "s3://binary", workflow["binaryUrl"]) + assert.Equal(t, "family-a", workflow["donFamily"]) +} + +func TestUpsertWorkflowInRegistry_GQLError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "upsert failed"}}, + }) + })) + defer srv.Close() + + client := newTestPrivateRegistryClient(t, srv.URL) + _, err := client.UpsertWorkflowInRegistry(OffchainWorkflowInput{ + WorkflowID: "wf-123", + Status: WorkflowStatusActive, + WorkflowName: "registry-workflow", + BinaryURL: "s3://binary", + DonFamily: "family-a", + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "upsert workflow in registry") + assert.Contains(t, err.Error(), "cre api error: upsert failed") + assert.NotContains(t, err.Error(), "graphql:") +} + +func TestGetWorkflowByName(t *testing.T) { + var capturedQuery string + var capturedVariables map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + Query string `json:"query"` + Variables map[string]interface{} `json:"variables"` + } + _ = json.NewDecoder(r.Body).Decode(&req) + capturedQuery = req.Query + capturedVariables = req.Variables + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getOffchainWorkflowByName": map[string]any{ + "workflow": map[string]any{ + "workflowId": "wf-123", + "owner": "owner-1", + "createdAt": "2026-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "registry-workflow", + "binaryUrl": "s3://binary", + "configUrl": "s3://config", + "tag": "v1", + "attributes": "{\"region\":\"us-east-1\"}", + "donFamily": "family-a", + "organizationId": "org-1", + }, + }, + }, + }) + })) + defer srv.Close() + + client := newTestPrivateRegistryClient(t, srv.URL) + result, err := client.GetWorkflowByName("registry-workflow") + + require.NoError(t, err) + assert.Contains(t, capturedQuery, "query GetOffchainWorkflowByName") + assert.Contains(t, capturedQuery, "getOffchainWorkflowByName") + assert.Equal(t, "wf-123", result.WorkflowID) + assert.Equal(t, WorkflowStatusActive, result.Status) + assert.Equal(t, "registry-workflow", result.WorkflowName) + assert.Equal(t, "family-a", result.DonFamily) + assert.Equal(t, "org-1", result.OrganizationID) + + request, ok := capturedVariables["request"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "registry-workflow", request["workflowName"]) +} + +func TestGetWorkflowByName_GQLError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "workflow not found"}}, + }) + })) + defer srv.Close() + + client := newTestPrivateRegistryClient(t, srv.URL) + _, err := client.GetWorkflowByName("registry-workflow") + require.Error(t, err) + assert.Contains(t, err.Error(), "get workflow by name in registry") + assert.Contains(t, err.Error(), "cre api error: workflow not found") + assert.NotContains(t, err.Error(), "graphql:") +} + +func TestGetWorkflowByName_EmptyName(t *testing.T) { + logger := testutil.NewTestLogger() + client := New(nil, logger) + + _, err := client.GetWorkflowByName("") + require.EqualError(t, err, "workflowName is required") +} + +func TestPauseWorkflowInRegistry(t *testing.T) { + var capturedVariables map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + Query string `json:"query"` + Variables map[string]interface{} `json:"variables"` + } + _ = json.NewDecoder(r.Body).Decode(&req) + capturedVariables = req.Variables + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "pauseOffchainWorkflow": map[string]any{ + "workflow": map[string]any{ + "workflowId": "wf-123", + "status": "WORKFLOW_STATUS_PAUSED", + "workflowName": "registry-workflow", + "binaryUrl": "s3://binary", + "donFamily": "family-a", + "organizationId": "org-1", + }, + }, + }, + }) + })) + defer srv.Close() + + client := newTestPrivateRegistryClient(t, srv.URL) + result, err := client.PauseWorkflowInRegistry("wf-123") + require.NoError(t, err) + assert.Equal(t, "wf-123", result.WorkflowID) + assert.Equal(t, WorkflowStatusPaused, result.Status) + + request, ok := capturedVariables["request"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "wf-123", request["workflowId"]) +} + +func TestActivateWorkflowInRegistry(t *testing.T) { + var capturedVariables map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + Variables map[string]interface{} `json:"variables"` + } + _ = json.NewDecoder(r.Body).Decode(&req) + capturedVariables = req.Variables + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "activateOffchainWorkflow": map[string]any{ + "workflow": map[string]any{ + "workflowId": "wf-123", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "registry-workflow", + "binaryUrl": "s3://binary", + "donFamily": "family-a", + "organizationId": "org-1", + }, + }, + }, + }) + })) + defer srv.Close() + + client := newTestPrivateRegistryClient(t, srv.URL) + result, err := client.ActivateWorkflowInRegistry("wf-123") + require.NoError(t, err) + assert.Equal(t, WorkflowStatusActive, result.Status) + + request, ok := capturedVariables["request"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "wf-123", request["workflowId"]) +} + +func TestDeleteWorkflowInRegistry(t *testing.T) { + var capturedVariables map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var req struct { + Variables map[string]interface{} `json:"variables"` + } + _ = json.NewDecoder(r.Body).Decode(&req) + capturedVariables = req.Variables + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "deleteOffchainWorkflow": map[string]any{ + "workflowId": "wf-123", + }, + }, + }) + })) + defer srv.Close() + + client := newTestPrivateRegistryClient(t, srv.URL) + deletedWorkflowID, err := client.DeleteWorkflowInRegistry("wf-123") + require.NoError(t, err) + assert.Equal(t, "wf-123", deletedWorkflowID) + + request, ok := capturedVariables["request"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "wf-123", request["workflowId"]) +} + +func TestWorkflowMutations_RequireWorkflowID(t *testing.T) { + logger := testutil.NewTestLogger() + client := New(nil, logger) + + _, pauseErr := client.PauseWorkflowInRegistry("") + require.EqualError(t, pauseErr, "workflowId is required") + + _, activateErr := client.ActivateWorkflowInRegistry("") + require.EqualError(t, activateErr, "workflowId is required") + + _, deleteErr := client.DeleteWorkflowInRegistry("") + require.EqualError(t, deleteErr, "workflowId is required") +} + +func newTestPrivateRegistryClient(t *testing.T, serverURL string) *Client { + t.Helper() + logger := testutil.NewTestLogger() + creds := &credentials.Credentials{ + AuthType: credentials.AuthTypeApiKey, + APIKey: "test-api-key", + } + envSet := &environments.EnvironmentSet{GraphQLURL: serverURL} + gqlClient := graphqlclient.New(creds, envSet, logger) + return New(gqlClient, logger) +} + +func TestCreateServiceContextWithTimeout(t *testing.T) { + logger := zerolog.Nop() + client := New(nil, &logger) + client.SetServiceTimeout(150 * time.Millisecond) + + ctx, cancel := client.CreateServiceContextWithTimeout() + defer cancel() + + deadline, ok := ctx.Deadline() + require.True(t, ok) + assert.WithinDuration(t, time.Now().Add(150*time.Millisecond), deadline, 100*time.Millisecond) +} diff --git a/internal/client/storageclient/storageclient.go b/internal/client/storageclient/storageclient.go index b4bbb03d..5eb9a829 100644 --- a/internal/client/storageclient/storageclient.go +++ b/internal/client/storageclient/storageclient.go @@ -20,24 +20,20 @@ import ( ) type Client struct { - graphql *graphqlclient.Client - workflowRegistryAddress string - workflowOwnerAddress string - chainSelector uint64 - log *zerolog.Logger - serviceTimeout time.Duration - httpTimeout time.Duration + graphql *graphqlclient.Client + workflowOwnerAddress string + log *zerolog.Logger + serviceTimeout time.Duration + httpTimeout time.Duration } -func New(graphql *graphqlclient.Client, workflowRegistryAddress string, workflowOwnerAddress string, chainSelector uint64, log *zerolog.Logger) *Client { +func New(graphql *graphqlclient.Client, workflowOwnerAddress string, log *zerolog.Logger) *Client { return &Client{ - graphql: graphql, - workflowRegistryAddress: workflowRegistryAddress, - workflowOwnerAddress: workflowOwnerAddress, - chainSelector: chainSelector, - log: log, - serviceTimeout: time.Minute * 2, - httpTimeout: time.Minute * 1, + graphql: graphql, + workflowOwnerAddress: workflowOwnerAddress, + log: log, + serviceTimeout: time.Minute * 2, + httpTimeout: time.Minute * 1, } } @@ -95,12 +91,10 @@ mutation GeneratePresignedPostUrlForArtifact($artifact: GeneratePresignedPostUrl contentHash := calculateContentHash(content) req := graphql.NewRequest(mutation) reqVariables := map[string]any{ - "workflowId": workflowId, - "artifactType": artifactType, - "contentHash": contentHash, - "workflowOwnerAddress": c.workflowOwnerAddress, - "workflowRegistryAddress": c.workflowRegistryAddress, - "chainSelector": fmt.Sprintf("%v", c.chainSelector), + "workflowId": workflowId, + "artifactType": artifactType, + "contentHash": contentHash, + "workflowOwnerAddress": c.workflowOwnerAddress, } req.Var("artifact", reqVariables) @@ -131,10 +125,8 @@ mutation GenerateUnsignedGetUrlForArtifact($artifact: GenerateUnsignedGetUrlRequ }` req := graphql.NewRequest(mutation) reqVariables := map[string]any{ - "workflowId": workflowId, - "artifactType": artifactType, - "workflowRegistryAddress": c.workflowRegistryAddress, - "chainSelector": fmt.Sprintf("%v", c.chainSelector), + "workflowId": workflowId, + "artifactType": artifactType, } req.Var("artifact", reqVariables) diff --git a/internal/constants/constants.go b/internal/constants/constants.go index b1f64698..29cb7e92 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -47,8 +47,9 @@ const ( AuthListenAddr = "localhost:53682" CreUiAuthPath = "/auth/cli" - WorkflowOwnerTypeEOA = "EOA" - WorkflowOwnerTypeMSIG = "MSIG" + WorkflowOwnerTypeEOA = "EOA" + WorkflowOwnerTypeMSIG = "MSIG" + WorkflowOwnerTypeOrgDerived = "ORG_DERIVED" WorkflowRegistryV2TypeAndVersion = "WorkflowRegistry 2.0.0" diff --git a/internal/context/project_context.go b/internal/context/project_context.go index 88e79f24..416fa371 100644 --- a/internal/context/project_context.go +++ b/internal/context/project_context.go @@ -77,7 +77,14 @@ func SetProjectContext(projectPath string) error { } if !found { - return fmt.Errorf("no project settings file found in current directory or parent directories") + return fmt.Errorf( + "no CRE project found (could not locate '%s' in '%s' or any parent directory)\n\n"+ + "To fix:\n"+ + " • Run this command from inside a CRE project directory\n"+ + " • Or run 'cre init' to create a new project here\n"+ + " • Or use '--%s ' to specify the project location", + constants.DefaultProjectSettingsFileName, cwd, "project-root", + ) } // Get the directory containing the project settings file (this is the project root) diff --git a/internal/context/project_context_test.go b/internal/context/project_context_test.go index 9382f80e..b4edac62 100644 --- a/internal/context/project_context_test.go +++ b/internal/context/project_context_test.go @@ -151,7 +151,7 @@ func TestSetProjectContext(t *testing.T) { }, projectPath: "", // Empty path should trigger search expectError: true, - errorContains: "no project settings file found", + errorContains: "no CRE project found", }, { name: "fails when project path doesn't exist", diff --git a/internal/credentials/credentials.go b/internal/credentials/credentials.go index 257bc532..3e7f3533 100644 --- a/internal/credentials/credentials.go +++ b/internal/credentials/credentials.go @@ -26,6 +26,7 @@ type Credentials struct { APIKey string `yaml:"api_key"` // #nosec G117 -- credential stored in secure config file AuthType string `yaml:"auth_type"` IsValidated bool `yaml:"-"` + OrgID string `yaml:"-"` log *zerolog.Logger } diff --git a/internal/credentials/credentials_test.go b/internal/credentials/credentials_test.go index 4313bfab..fd5ef8f5 100644 --- a/internal/credentials/credentials_test.go +++ b/internal/credentials/credentials_test.go @@ -1,14 +1,13 @@ package credentials import ( - "encoding/base64" - "encoding/json" "os" "path/filepath" "strings" "testing" "github.com/smartcontractkit/cre-cli/internal/testutil" + "github.com/smartcontractkit/cre-cli/internal/testutil/testjwt" ) func TestNew_Default(t *testing.T) { @@ -86,31 +85,13 @@ TokenType: "file-type" } } -// Helper function to create a JWT token with custom claims -func createTestJWT(t *testing.T, claims map[string]interface{}) string { - t.Helper() - - // JWT header (doesn't matter for our tests) - header := map[string]string{"alg": "HS256", "typ": "JWT"} - headerJSON, _ := json.Marshal(header) - headerEncoded := base64.RawURLEncoding.EncodeToString(headerJSON) - - // JWT payload with claims - claimsJSON, err := json.Marshal(claims) - if err != nil { - t.Fatalf("failed to marshal claims: %v", err) - } - claimsEncoded := base64.RawURLEncoding.EncodeToString(claimsJSON) - - // JWT signature (doesn't need to be valid for our tests) - signature := base64.RawURLEncoding.EncodeToString([]byte("fake-signature")) - - return headerEncoded + "." + claimsEncoded + "." + signature +func createTestJWT(claims map[string]interface{}) string { + return testjwt.CreateTestJWTWithClaims(claims) } func TestGetOrgID_BearerWithOrgID(t *testing.T) { logger := testutil.NewTestLogger() - token := createTestJWT(t, map[string]interface{}{ + token := createTestJWT(map[string]interface{}{ "sub": "user123", "org_id": "org_abc123", }) @@ -132,7 +113,7 @@ func TestGetOrgID_BearerWithOrgID(t *testing.T) { func TestGetOrgID_MissingClaim(t *testing.T) { logger := testutil.NewTestLogger() - token := createTestJWT(t, map[string]interface{}{ + token := createTestJWT(map[string]interface{}{ "sub": "user123", }) @@ -153,7 +134,7 @@ func TestGetOrgID_MissingClaim(t *testing.T) { func TestGetOrgID_EmptyClaim(t *testing.T) { logger := testutil.NewTestLogger() - token := createTestJWT(t, map[string]interface{}{ + token := createTestJWT(map[string]interface{}{ "sub": "user123", "org_id": "", }) @@ -260,7 +241,7 @@ func TestCheckIsUngatedOrganization_JWTWithFullAccess(t *testing.T) { tc.namespace + "organization_roles": "ROOT", } - token := createTestJWT(t, claims) + token := createTestJWT(claims) creds := &Credentials{ AuthType: AuthTypeBearer, @@ -289,7 +270,7 @@ func TestCheckIsUngatedOrganization_JWTWithMissingClaim(t *testing.T) { // organization_status claim is missing } - token := createTestJWT(t, claims) + token := createTestJWT(claims) creds := &Credentials{ AuthType: AuthTypeBearer, @@ -317,7 +298,7 @@ func TestCheckIsUngatedOrganization_JWTWithEmptyStatus(t *testing.T) { "https://api.cre.chain.link/organization_status": "", } - token := createTestJWT(t, claims) + token := createTestJWT(claims) creds := &Credentials{ AuthType: AuthTypeBearer, @@ -345,7 +326,7 @@ func TestCheckIsUngatedOrganization_JWTWithGatedStatus(t *testing.T) { "https://api.cre.chain.link/organization_status": "GATED", } - token := createTestJWT(t, claims) + token := createTestJWT(claims) creds := &Credentials{ AuthType: AuthTypeBearer, @@ -373,7 +354,7 @@ func TestCheckIsUngatedOrganization_JWTWithRestrictedStatus(t *testing.T) { "https://api.cre.chain.link/organization_status": "RESTRICTED", } - token := createTestJWT(t, claims) + token := createTestJWT(claims) creds := &Credentials{ AuthType: AuthTypeBearer, diff --git a/internal/environments/environments.go b/internal/environments/environments.go index 817e628d..ef652ea9 100644 --- a/internal/environments/environments.go +++ b/internal/environments/environments.go @@ -22,6 +22,7 @@ const ( EnvVarWorkflowRegistryChainName = "CRE_CLI_WORKFLOW_REGISTRY_CHAIN_NAME" EnvVarWorkflowRegistryChainExplorerURL = "CRE_CLI_WORKFLOW_REGISTRY_CHAIN_EXPLORER_URL" EnvVarDonFamily = "CRE_CLI_DON_FAMILY" + EnvVarSecretsOrgOwned = "CRE_CLI_SECRETS_ORG_OWNED" DefaultEnv = "PRODUCTION" ) @@ -42,6 +43,7 @@ type EnvironmentSet struct { WorkflowRegistryChainName string `yaml:"CRE_CLI_WORKFLOW_REGISTRY_CHAIN_NAME"` WorkflowRegistryChainExplorerURL string `yaml:"CRE_CLI_WORKFLOW_REGISTRY_CHAIN_EXPLORER_URL"` DonFamily string `yaml:"CRE_CLI_DON_FAMILY"` + SecretsOrgOwned bool `yaml:"CRE_CLI_SECRETS_ORG_OWNED"` } // RequiresVPN returns true if the GraphQL endpoint is on a private network @@ -113,6 +115,10 @@ func NewEnvironmentSet(ff *fileFormat, envName string) *EnvironmentSet { set.DonFamily = v } + if v := os.Getenv(EnvVarSecretsOrgOwned); v != "" { + set.SecretsOrgOwned = strings.EqualFold(v, "true") + } + return &set } diff --git a/internal/environments/environments.yaml b/internal/environments/environments.yaml index d789bf29..c88c0103 100644 --- a/internal/environments/environments.yaml +++ b/internal/environments/environments.yaml @@ -6,6 +6,7 @@ ENVIRONMENTS: CRE_CLI_GRAPHQL_URL: https://graphql-cre-dev.tailf8f749.ts.net/graphql CRE_VAULT_DON_GATEWAY_URL: https://cre-gateway-one-zone-a.main.stage.cldev.sh/ CRE_CLI_DON_FAMILY: "zone-a" + CRE_CLI_SECRETS_ORG_OWNED: false CRE_CLI_WORKFLOW_REGISTRY_ADDRESS: "0x7e69E853D9Ce50C2562a69823c80E01360019Cef" CRE_CLI_WORKFLOW_REGISTRY_CHAIN_NAME: "ethereum-testnet-sepolia" # eth-sepolia @@ -18,6 +19,7 @@ ENVIRONMENTS: CRE_CLI_GRAPHQL_URL: https://graphql-cre-stage.tailf8f749.ts.net/graphql CRE_VAULT_DON_GATEWAY_URL: https://cre-gateway-one-zone-a.main.stage.cldev.sh/ CRE_CLI_DON_FAMILY: "zone-a" + CRE_CLI_SECRETS_ORG_OWNED: false CRE_CLI_WORKFLOW_REGISTRY_ADDRESS: "0xaE55eB3EDAc48a1163EE2cbb1205bE1e90Ea1135" CRE_CLI_WORKFLOW_REGISTRY_CHAIN_NAME: "ethereum-testnet-sepolia" # eth-sepolia @@ -30,6 +32,7 @@ ENVIRONMENTS: CRE_CLI_GRAPHQL_URL: https://api.cre.chain.link/graphql CRE_VAULT_DON_GATEWAY_URL: https://01.gateway.zone-a.cre.chain.link CRE_CLI_DON_FAMILY: "zone-a" + CRE_CLI_SECRETS_ORG_OWNED: false CRE_CLI_WORKFLOW_REGISTRY_ADDRESS: "0x4Ac54353FA4Fa961AfcC5ec4B118596d3305E7e5" CRE_CLI_WORKFLOW_REGISTRY_CHAIN_NAME: "ethereum-mainnet" diff --git a/internal/ethkeys/keys.go b/internal/ethkeys/keys.go index be7b75e2..a19461f4 100644 --- a/internal/ethkeys/keys.go +++ b/internal/ethkeys/keys.go @@ -3,14 +3,42 @@ package ethkeys import ( "crypto/ecdsa" "fmt" + "strings" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) +// FormatWorkflowOwnerAddress trims whitespace, ensures a 0x prefix for hex input, +// and returns the address in EIP-55 checksummed form. Empty input (after trim) +// returns ("", nil). Non-empty input that is not a valid 20-byte hex address returns an error. +func FormatWorkflowOwnerAddress(s string) (string, error) { + s = strings.TrimSpace(s) + if s == "" { + return "", nil + } + if len(s) < 2 || (s[0:2] != "0x" && s[0:2] != "0X") { + s = "0x" + s + } + if !common.IsHexAddress(s) { + return "", fmt.Errorf("invalid owner address %q", s) + } + return common.HexToAddress(s).Hex(), nil +} + func DeriveEthAddressFromPrivateKey(privateKeyHex string) (string, error) { privateKey, err := crypto.HexToECDSA(privateKeyHex) if err != nil { - return "", fmt.Errorf("failed to parse private key. Please check CRE_ETH_PRIVATE_KEY in your .env file or system environment: %w", err) + return "", fmt.Errorf( + "invalid private key: expected 64 hex characters (256 bits), got %d characters.\n\n"+ + "The CLI reads CRE_ETH_PRIVATE_KEY from your .env file or system environment.\n"+ + "The 0x prefix is supported and stripped automatically.\n\n"+ + "Common issues:\n"+ + " • Pasted an Ethereum address (40 chars) instead of a private key (64 chars)\n"+ + " • Value has extra quotes — use CRE_ETH_PRIVATE_KEY=abc123... without wrapping quotes\n"+ + " • Key was truncated during copy-paste", + len(privateKeyHex), + ) } publicKey := privateKey.Public() diff --git a/internal/ethkeys/keys_test.go b/internal/ethkeys/keys_test.go index ba5b30d2..5762ea12 100644 --- a/internal/ethkeys/keys_test.go +++ b/internal/ethkeys/keys_test.go @@ -68,9 +68,64 @@ func TestDeriveEthAddressFromPrivateKey_InvalidInput(t *testing.T) { t.Fatalf("expected error, got nil (addr=%q)", addr) } - if !strings.Contains(strings.ToLower(err.Error()), "failed to parse private key") { + if !strings.Contains(strings.ToLower(err.Error()), "invalid private key") { t.Fatalf("unexpected error message: %v", err) } }) } } + +func TestFormatWorkflowOwnerAddress(t *testing.T) { + t.Parallel() + + addr := "0x1234567890123456789012345678901234567890" + want := common.HexToAddress(addr).Hex() + + t.Run("trims and checksums", func(t *testing.T) { + t.Parallel() + got, err := FormatWorkflowOwnerAddress(" " + addr + " ") + if err != nil { + t.Fatal(err) + } + if got != want { + t.Fatalf("got %q want %q", got, want) + } + }) + + t.Run("adds 0x when missing", func(t *testing.T) { + t.Parallel() + got, err := FormatWorkflowOwnerAddress(strings.TrimPrefix(addr, "0x")) + if err != nil { + t.Fatal(err) + } + if got != want { + t.Fatalf("got %q want %q", got, want) + } + }) + + t.Run("empty", func(t *testing.T) { + t.Parallel() + got, err := FormatWorkflowOwnerAddress("") + if err != nil { + t.Fatal(err) + } + if got != "" { + t.Fatalf("got %q want empty", got) + } + got, err = FormatWorkflowOwnerAddress(" ") + if err != nil { + t.Fatal(err) + } + if got != "" { + t.Fatalf("got %q want empty", got) + } + }) + + t.Run("invalid", func(t *testing.T) { + t.Parallel() + _, err := FormatWorkflowOwnerAddress("not-an-address") + if err == nil { + t.Fatal("expected error") + } + }) +} diff --git a/internal/oauth/browser.go b/internal/oauth/browser.go new file mode 100644 index 00000000..99e13424 --- /dev/null +++ b/internal/oauth/browser.go @@ -0,0 +1,20 @@ +package oauth + +import ( + "fmt" + "os/exec" +) + +// OpenBrowser opens urlStr in the default browser for the given GOOS value. +func OpenBrowser(urlStr string, goos string) error { + switch goos { + case "darwin": + return exec.Command("open", urlStr).Start() + case "linux": + return exec.Command("xdg-open", urlStr).Start() + case "windows": + return exec.Command("rundll32", "url.dll,FileProtocolHandler", urlStr).Start() + default: + return fmt.Errorf("unsupported OS: %s", goos) + } +} diff --git a/internal/oauth/exchange.go b/internal/oauth/exchange.go new file mode 100644 index 00000000..68904d05 --- /dev/null +++ b/internal/oauth/exchange.go @@ -0,0 +1,69 @@ +package oauth + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/environments" +) + +// DefaultHTTPClient is used for token exchange when no client is supplied. +var DefaultHTTPClient = &http.Client{Timeout: 10 * time.Second} + +// ExchangeAuthorizationCode exchanges an OAuth authorization code for tokens (PKCE). +// If oauthClientID is non-empty, it is used as client_id (must match the authorize URL). +// If oauthAuthServerBase is non-empty (scheme + host only), it is used as the token endpoint host; +// otherwise env.AuthBase is used (e.g. cre login builds the authorize URL from env). +func ExchangeAuthorizationCode(ctx context.Context, httpClient *http.Client, env *environments.EnvironmentSet, code, codeVerifier, oauthClientID, oauthAuthServerBase string) (*credentials.CreLoginTokenSet, error) { + if httpClient == nil { + httpClient = DefaultHTTPClient + } + clientID := env.ClientID + if oauthClientID != "" { + clientID = oauthClientID + } + authBase := env.AuthBase + if oauthAuthServerBase != "" { + authBase = oauthAuthServerBase + } + form := url.Values{} + form.Set("grant_type", "authorization_code") + form.Set("client_id", clientID) + form.Set("code", code) + form.Set("redirect_uri", constants.AuthRedirectURI) + form.Set("code_verifier", codeVerifier) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, authBase+constants.AuthTokenPath, strings.NewReader(form.Encode())) + if err != nil { + return nil, fmt.Errorf("create request: %w", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := httpClient.Do(req) // #nosec G704 -- URL is from trusted environment config + if err != nil { + return nil, fmt.Errorf("perform request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return nil, fmt.Errorf("read response: %w", err) + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("status %d: %s", resp.StatusCode, body) + } + + var tokenSet credentials.CreLoginTokenSet + if err := json.Unmarshal(body, &tokenSet); err != nil { + return nil, fmt.Errorf("unmarshal token set: %w", err) + } + return &tokenSet, nil +} diff --git a/internal/oauth/exchange_test.go b/internal/oauth/exchange_test.go new file mode 100644 index 00000000..f05efc2d --- /dev/null +++ b/internal/oauth/exchange_test.go @@ -0,0 +1,76 @@ +package oauth + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/environments" +) + +func TestExchangeAuthorizationCode(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method", http.StatusMethodNotAllowed) + return + } + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + assert.Equal(t, "authorization_code", r.Form.Get("grant_type")) + assert.Equal(t, "cid", r.Form.Get("client_id")) + assert.Equal(t, "auth-code", r.Form.Get("code")) + assert.Equal(t, constants.AuthRedirectURI, r.Form.Get("redirect_uri")) + assert.Equal(t, "verifier", r.Form.Get("code_verifier")) + + _ = json.NewEncoder(w).Encode(credentials.CreLoginTokenSet{ + AccessToken: "a", // #nosec G101 G117 -- test fixture, not a real credential + TokenType: "Bearer", + }) + })) + defer ts.Close() + + env := &environments.EnvironmentSet{ + AuthBase: ts.URL, + ClientID: "cid", + } + + tok, err := ExchangeAuthorizationCode(context.Background(), ts.Client(), env, "auth-code", "verifier", "", "") + require.NoError(t, err) + require.NotNil(t, tok) + assert.Equal(t, "a", tok.AccessToken) +} + +func TestExchangeAuthorizationCode_OAuthOverrides(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, 1<<20) + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + assert.Equal(t, "override-cid", r.Form.Get("client_id")) + _ = json.NewEncoder(w).Encode(credentials.CreLoginTokenSet{ + AccessToken: "b", // #nosec G101 G117 -- test fixture + TokenType: "Bearer", + }) + })) + defer ts.Close() + + env := &environments.EnvironmentSet{ + AuthBase: "https://wrong.example", + ClientID: "wrong", + } + + tok, err := ExchangeAuthorizationCode(context.Background(), ts.Client(), env, "c", "v", "override-cid", ts.URL) + require.NoError(t, err) + assert.Equal(t, "b", tok.AccessToken) +} diff --git a/cmd/login/htmlPages/error.html b/internal/oauth/htmlPages/error.html similarity index 100% rename from cmd/login/htmlPages/error.html rename to internal/oauth/htmlPages/error.html diff --git a/cmd/login/htmlPages/output.css b/internal/oauth/htmlPages/output.css similarity index 100% rename from cmd/login/htmlPages/output.css rename to internal/oauth/htmlPages/output.css diff --git a/internal/oauth/htmlPages/secrets_error.html b/internal/oauth/htmlPages/secrets_error.html new file mode 100644 index 00000000..f0d43412 --- /dev/null +++ b/internal/oauth/htmlPages/secrets_error.html @@ -0,0 +1,63 @@ + + + + + Secrets authorization failed + + + + + +
+ + + + +

CRE

+
+
+ + + + + + + + + + +

+ Secrets authorization was unsuccessful +

+

+ Your vault sign-in step could not be completed. Close this window and try + again from your terminal. +

+
+ + diff --git a/internal/oauth/htmlPages/secrets_success.html b/internal/oauth/htmlPages/secrets_success.html new file mode 100644 index 00000000..0eb515a3 --- /dev/null +++ b/internal/oauth/htmlPages/secrets_success.html @@ -0,0 +1,59 @@ + + + + + Secrets authorization complete + + + + + +
+ + + + +

CRE

+
+
+ + + +

+ Your secrets request was signed successfully +

+

+ Vault authorization is complete. You can close this window; the CLI will + finish in your terminal. +

+
+ + diff --git a/cmd/login/htmlPages/success.html b/internal/oauth/htmlPages/success.html similarity index 100% rename from cmd/login/htmlPages/success.html rename to internal/oauth/htmlPages/success.html diff --git a/cmd/login/htmlPages/waiting.html b/internal/oauth/htmlPages/waiting.html similarity index 100% rename from cmd/login/htmlPages/waiting.html rename to internal/oauth/htmlPages/waiting.html diff --git a/internal/oauth/pages.go b/internal/oauth/pages.go new file mode 100644 index 00000000..040a4ec3 --- /dev/null +++ b/internal/oauth/pages.go @@ -0,0 +1,89 @@ +package oauth + +import ( + "embed" + "fmt" + "net/http" + "strings" + + "github.com/rs/zerolog" +) + +const ( + PageError = "htmlPages/error.html" + PageSuccess = "htmlPages/success.html" + PageSecretsSuccess = "htmlPages/secrets_success.html" + PageSecretsError = "htmlPages/secrets_error.html" + PageWaiting = "htmlPages/waiting.html" + StylePage = "htmlPages/output.css" +) + +//go:embed htmlPages/*.html +//go:embed htmlPages/*.css +var htmlFiles embed.FS + +// ServeEmbeddedHTML serves an embedded HTML page with inline CSS. +func ServeEmbeddedHTML(log *zerolog.Logger, w http.ResponseWriter, filePath string, status int) { + htmlContent, err := htmlFiles.ReadFile(filePath) + if err != nil { + log.Error().Err(err).Str("file", filePath).Msg("failed to read embedded HTML file") + sendHTTPError(w) + return + } + + cssContent, err := htmlFiles.ReadFile(StylePage) + if err != nil { + log.Error().Err(err).Str("file", StylePage).Msg("failed to read embedded CSS file") + sendHTTPError(w) + return + } + + modified := strings.Replace( + string(htmlContent), + ``, + fmt.Sprintf("", string(cssContent)), + 1, + ) + + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(status) + if _, err := w.Write([]byte(modified)); err != nil { + log.Error().Err(err).Msg("failed to write HTML response") + } +} + +// ServeWaitingPage serves the waiting page with the redirect URL injected. +func ServeWaitingPage(log *zerolog.Logger, w http.ResponseWriter, redirectURL string) { + htmlContent, err := htmlFiles.ReadFile(PageWaiting) + if err != nil { + log.Error().Err(err).Str("file", PageWaiting).Msg("failed to read waiting page HTML file") + sendHTTPError(w) + return + } + + cssContent, err := htmlFiles.ReadFile(StylePage) + if err != nil { + log.Error().Err(err).Str("file", StylePage).Msg("failed to read embedded CSS file") + sendHTTPError(w) + return + } + + modified := strings.Replace( + string(htmlContent), + ``, + fmt.Sprintf("", string(cssContent)), + 1, + ) + + modified = strings.Replace(modified, "{{REDIRECT_URL}}", redirectURL, 1) + + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusOK) + if _, err := w.Write([]byte(modified)); err != nil { + log.Error().Err(err).Msg("failed to write waiting page response") + } +} + +func sendHTTPError(w http.ResponseWriter) { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) +} diff --git a/internal/oauth/pkce.go b/internal/oauth/pkce.go new file mode 100644 index 00000000..0ed0e8a0 --- /dev/null +++ b/internal/oauth/pkce.go @@ -0,0 +1,20 @@ +package oauth + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "fmt" +) + +// GeneratePKCE returns an RFC 7636 S256 code verifier and code challenge. +func GeneratePKCE() (verifier, challenge string, err error) { + b := make([]byte, 32) + if _, err = rand.Read(b); err != nil { + return "", "", fmt.Errorf("pkce random: %w", err) + } + verifier = base64.RawURLEncoding.EncodeToString(b) + sum := sha256.Sum256([]byte(verifier)) + challenge = base64.RawURLEncoding.EncodeToString(sum[:]) + return verifier, challenge, nil +} diff --git a/internal/oauth/pkce_test.go b/internal/oauth/pkce_test.go new file mode 100644 index 00000000..50b7c376 --- /dev/null +++ b/internal/oauth/pkce_test.go @@ -0,0 +1,22 @@ +package oauth + +import ( + "crypto/sha256" + "encoding/base64" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGeneratePKCE_S256(t *testing.T) { + verifier, challenge, err := GeneratePKCE() + require.NoError(t, err) + require.NotEmpty(t, verifier) + require.NotEmpty(t, challenge) + + sum := sha256.Sum256([]byte(verifier)) + decoded, err := base64.RawURLEncoding.DecodeString(challenge) + require.NoError(t, err) + assert.Equal(t, sum[:], decoded) +} diff --git a/internal/oauth/secrets_callback.go b/internal/oauth/secrets_callback.go new file mode 100644 index 00000000..cb7f93af --- /dev/null +++ b/internal/oauth/secrets_callback.go @@ -0,0 +1,41 @@ +package oauth + +import ( + "net/http" + + "github.com/rs/zerolog" +) + +// SecretsCallbackHandler handles the OAuth redirect for the browser secrets flow. +// If expectedState is non-empty (parsed from the platform authorize URL), the callback +// must include the same state; otherwise only a non-empty authorization code is required. +func SecretsCallbackHandler(codeCh chan<- string, expectedState string, log *zerolog.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + errorParam := r.URL.Query().Get("error") + errorDesc := r.URL.Query().Get("error_description") + + if errorParam != "" { + log.Error().Str("error", errorParam).Str("description", errorDesc).Msg("auth error in secrets callback") + ServeEmbeddedHTML(log, w, PageSecretsError, http.StatusBadRequest) + return + } + + if expectedState != "" { + if st := r.URL.Query().Get("state"); st != expectedState { + log.Error().Str("got", st).Str("want", expectedState).Msg("invalid state in secrets callback") + ServeEmbeddedHTML(log, w, PageSecretsError, http.StatusBadRequest) + return + } + } + + code := r.URL.Query().Get("code") + if code == "" { + log.Error().Msg("no code in secrets callback") + ServeEmbeddedHTML(log, w, PageSecretsError, http.StatusBadRequest) + return + } + + ServeEmbeddedHTML(log, w, PageSecretsSuccess, http.StatusOK) + codeCh <- code + } +} diff --git a/internal/oauth/secrets_callback_test.go b/internal/oauth/secrets_callback_test.go new file mode 100644 index 00000000..e7071dab --- /dev/null +++ b/internal/oauth/secrets_callback_test.go @@ -0,0 +1,66 @@ +package oauth + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func TestSecretsCallbackHandler_success(t *testing.T) { + log := zerolog.Nop() + codeCh := make(chan string, 1) + h := SecretsCallbackHandler(codeCh, "want-state", &log) + + req := httptest.NewRequest(http.MethodGet, "/callback?code=the-code&state=want-state", nil) + rr := httptest.NewRecorder() + h(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "the-code", <-codeCh) +} + +func TestSecretsCallbackHandler_stateMismatch(t *testing.T) { + log := zerolog.Nop() + codeCh := make(chan string, 1) + h := SecretsCallbackHandler(codeCh, "want", &log) + + req := httptest.NewRequest(http.MethodGet, "/callback?code=c&state=wrong", nil) + rr := httptest.NewRecorder() + h(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + select { + case <-codeCh: + t.Fatal("expected no code") + default: + } +} + +func TestSecretsCallbackHandler_oauthError(t *testing.T) { + log := zerolog.Nop() + codeCh := make(chan string, 1) + h := SecretsCallbackHandler(codeCh, "", &log) + + req := httptest.NewRequest(http.MethodGet, "/callback?error=access_denied", nil) + rr := httptest.NewRecorder() + h(rr, req) + + assert.Equal(t, http.StatusBadRequest, rr.Code) + assert.Len(t, codeCh, 0) +} + +func TestSecretsCallbackHandler_noStateRequired(t *testing.T) { + log := zerolog.Nop() + codeCh := make(chan string, 1) + h := SecretsCallbackHandler(codeCh, "", &log) + + req := httptest.NewRequest(http.MethodGet, "/callback?code=only-code", nil) + rr := httptest.NewRecorder() + h(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, "only-code", <-codeCh) +} diff --git a/internal/oauth/server.go b/internal/oauth/server.go new file mode 100644 index 00000000..4ca2abe5 --- /dev/null +++ b/internal/oauth/server.go @@ -0,0 +1,24 @@ +package oauth + +import ( + "fmt" + "net" + "net/http" + "time" +) + +// NewCallbackHTTPServer listens on listenAddr and serves callback on /callback. +func NewCallbackHTTPServer(listenAddr string, callback http.HandlerFunc) (*http.Server, net.Listener, error) { + mux := http.NewServeMux() + mux.HandleFunc("/callback", callback) + + listener, err := net.Listen("tcp", listenAddr) + if err != nil { + return nil, nil, fmt.Errorf("failed to listen on %s: %w", listenAddr, err) + } + + return &http.Server{ + Handler: mux, + ReadHeaderTimeout: 5 * time.Second, + }, listener, nil +} diff --git a/internal/oauth/state.go b/internal/oauth/state.go new file mode 100644 index 00000000..bf0de0ec --- /dev/null +++ b/internal/oauth/state.go @@ -0,0 +1,49 @@ +package oauth + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "net/url" +) + +// RandomState returns a URL-safe random string suitable for OAuth "state". +func RandomState() (string, error) { + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + return "", fmt.Errorf("oauth: random state: %w", err) + } + return base64.RawURLEncoding.EncodeToString(b), nil +} + +// StateFromAuthorizeURL returns the OAuth "state" query parameter from an authorize URL, if present. +func StateFromAuthorizeURL(raw string) (string, error) { + u, err := url.Parse(raw) + if err != nil { + return "", err + } + return u.Query().Get("state"), nil +} + +// ClientIDFromAuthorizeURL returns the "client_id" query parameter from an authorize URL (if present). +// Token exchange must use the same client_id the IdP bound to the authorization code. +func ClientIDFromAuthorizeURL(raw string) (string, error) { + u, err := url.Parse(raw) + if err != nil { + return "", err + } + return u.Query().Get("client_id"), nil +} + +// OAuthServerBaseFromAuthorizeURL returns the authorization server origin (scheme + host) for the +// given authorize URL. The token endpoint must be on the same host that issued the authorization code. +func OAuthServerBaseFromAuthorizeURL(raw string) (string, error) { + u, err := url.Parse(raw) + if err != nil { + return "", err + } + if u.Scheme == "" || u.Host == "" { + return "", fmt.Errorf("authorize URL missing scheme or host") + } + return u.Scheme + "://" + u.Host, nil +} diff --git a/internal/oauth/state_test.go b/internal/oauth/state_test.go new file mode 100644 index 00000000..fba0e450 --- /dev/null +++ b/internal/oauth/state_test.go @@ -0,0 +1,42 @@ +package oauth + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRandomState(t *testing.T) { + s, err := RandomState() + require.NoError(t, err) + require.NotEmpty(t, s) + s2, err := RandomState() + require.NoError(t, err) + assert.NotEqual(t, s, s2) +} + +func TestStateFromAuthorizeURL(t *testing.T) { + s, err := StateFromAuthorizeURL("https://id.example/authorize?state=abc&client_id=x") + require.NoError(t, err) + assert.Equal(t, "abc", s) + + s, err = StateFromAuthorizeURL("https://id.example/authorize") + require.NoError(t, err) + assert.Equal(t, "", s) +} + +func TestClientIDFromAuthorizeURL(t *testing.T) { + c, err := ClientIDFromAuthorizeURL("https://auth0.example/authorize?client_id=myapp&response_type=code") + require.NoError(t, err) + assert.Equal(t, "myapp", c) +} + +func TestOAuthServerBaseFromAuthorizeURL(t *testing.T) { + base, err := OAuthServerBaseFromAuthorizeURL("https://tenant.auth0.com/authorize?foo=1") + require.NoError(t, err) + assert.Equal(t, "https://tenant.auth0.com", base) + + _, err = OAuthServerBaseFromAuthorizeURL("/relative") + assert.Error(t, err) +} diff --git a/internal/runtime/runtime_context.go b/internal/runtime/runtime_context.go index af367838..255b7674 100644 --- a/internal/runtime/runtime_context.go +++ b/internal/runtime/runtime_context.go @@ -13,6 +13,7 @@ import ( "github.com/smartcontractkit/cre-cli/internal/authvalidation" "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/ethkeys" "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/tenantctx" ) @@ -23,14 +24,21 @@ var ( ) type Context struct { - Logger *zerolog.Logger - Viper *viper.Viper - ClientFactory client.Factory - Settings *settings.Settings - Credentials *credentials.Credentials - EnvironmentSet *environments.EnvironmentSet - TenantContext *tenantctx.EnvironmentContext - Workflow WorkflowRuntime + Logger *zerolog.Logger + Viper *viper.Viper + ClientFactory client.Factory + Settings *settings.Settings + Credentials *credentials.Credentials + EnvironmentSet *environments.EnvironmentSet + TenantContext *tenantctx.EnvironmentContext + ResolvedRegistry settings.ResolvedRegistry + Workflow WorkflowRuntime + + OrgID string + DerivedWorkflowOwner string + // InvocationDir is the working directory at the time the CLI was invoked, + // before any os.Chdir calls made by SetExecutionContext. + InvocationDir string } type WorkflowRuntime struct { @@ -64,6 +72,22 @@ func (ctx *Context) AttachSettings(cmd *cobra.Command, validateDeployRPC bool) e return nil } +// FinalizeDeferredWorkflowOwner fills workflow owner when settings load deferred it +// (non-empty deployment-registry). Call after AttachResolvedRegistry. +func (ctx *Context) FinalizeDeferredWorkflowOwner(cmd *cobra.Command) error { + if ctx.Settings == nil { + return nil + } + return settings.FinalizeWorkflowOwner( + ctx.Viper, + cmd, + &ctx.Settings.Workflow, + ctx.Settings.User.TargetName, + ctx.ResolvedRegistry, + ctx.DerivedWorkflowOwner, + ) +} + func (ctx *Context) AttachCredentials(validationCtx context.Context, skipValidation bool) error { var err error @@ -78,9 +102,19 @@ func (ctx *Context) AttachCredentials(validationCtx context.Context, skipValidat } validator := authvalidation.NewValidator(ctx.Credentials, ctx.EnvironmentSet, ctx.Logger) - if err := validator.ValidateCredentials(validationCtx, ctx.Credentials); err != nil { + result, err := validator.ValidateCredentials(validationCtx, ctx.Credentials) + if err != nil { return fmt.Errorf("%w: %w", ErrValidationFailed, err) } + + if result != nil { + ctx.OrgID = result.OrgID + formatted, err := ethkeys.FormatWorkflowOwnerAddress(result.DerivedWorkflowOwner) + if err != nil { + return fmt.Errorf("%w: %w", ErrValidationFailed, err) + } + ctx.DerivedWorkflowOwner = formatted + } } return nil @@ -111,6 +145,24 @@ func (ctx *Context) AttachTenantContext(validationCtx context.Context) error { return nil } +// AttachResolvedRegistry resolves the deployment-registry from workflow +// settings against the tenant context registries. Must be called after +// AttachSettings and AttachTenantContext. +func (ctx *Context) AttachResolvedRegistry() error { + deploymentRegistry := "" + if ctx.Settings != nil { + deploymentRegistry = ctx.Settings.Workflow.UserWorkflowSettings.DeploymentRegistry + } + + resolved, err := settings.ResolveRegistry(deploymentRegistry, ctx.TenantContext, ctx.EnvironmentSet) + if err != nil { + return fmt.Errorf("failed to resolve deployment registry: %w", err) + } + + ctx.ResolvedRegistry = resolved + return nil +} + func (ctx *Context) AttachEnvironmentSet() error { var err error diff --git a/internal/settings/registry_resolution.go b/internal/settings/registry_resolution.go new file mode 100644 index 00000000..93249881 --- /dev/null +++ b/internal/settings/registry_resolution.go @@ -0,0 +1,173 @@ +package settings + +import ( + "fmt" + "strings" + + "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" +) + +// RegistryType distinguishes between on-chain and off-chain workflow registries. +type RegistryType string + +const ( + RegistryTypeOnChain RegistryType = "on-chain" + RegistryTypeOffChain RegistryType = "off-chain" +) + +// ResolvedRegistry is the interface implemented by both OnChainRegistry and +// OffChainRegistry. Commands type-switch on the concrete type to access +// type-specific fields, which prevents accidental use of fields that don't +// exist for a given registry kind. +type ResolvedRegistry interface { + ID() string + Type() RegistryType + DonFamily() string +} + +// OnChainRegistry holds the resolved configuration for an on-chain workflow +// registry. Address and ChainName are guaranteed non-empty. +type OnChainRegistry struct { + id string + address string + chainName string + donFamily string + explorerURL string +} + +func NewOnChainRegistry(id, address, chainName, donFamily, explorerURL string) *OnChainRegistry { + return &OnChainRegistry{ + id: id, + address: address, + chainName: chainName, + donFamily: donFamily, + explorerURL: explorerURL, + } +} + +func (r *OnChainRegistry) ID() string { return r.id } +func (r *OnChainRegistry) Type() RegistryType { return RegistryTypeOnChain } +func (r *OnChainRegistry) DonFamily() string { return r.donFamily } +func (r *OnChainRegistry) Address() string { return r.address } +func (r *OnChainRegistry) ChainName() string { return r.chainName } +func (r *OnChainRegistry) ExplorerURL() string { return r.explorerURL } + +// OffChainRegistry holds the resolved configuration for an off-chain (private) +// workflow registry. It has no on-chain address or chain. +type OffChainRegistry struct { + id string + donFamily string +} + +func NewOffChainRegistry(id, donFamily string) *OffChainRegistry { + return &OffChainRegistry{id: id, donFamily: donFamily} +} + +func (r *OffChainRegistry) ID() string { return r.id } +func (r *OffChainRegistry) Type() RegistryType { return RegistryTypeOffChain } +func (r *OffChainRegistry) DonFamily() string { return r.donFamily } + +// ResolveRegistry maps an optional deployment-registry value to a concrete +// ResolvedRegistry. When deploymentRegistry is empty the static EnvironmentSet +// values are used (backwards-compatible default). When set, it is looked up in +// tenantCtx.Registries. On-chain entries must have a non-empty address. +// Off-chain registries are rejected in production environments. +func ResolveRegistry( + deploymentRegistry string, + tenantCtx *tenantctx.EnvironmentContext, + envSet *environments.EnvironmentSet, +) (ResolvedRegistry, error) { + if deploymentRegistry == "" { + return defaultFromEnvironmentSet(envSet), nil + } + + if tenantCtx == nil { + return nil, fmt.Errorf("deployment-registry %q is set but user context is not available — run `cre login` and retry", deploymentRegistry) + } + + reg := findRegistry(tenantCtx.Registries, deploymentRegistry) + if reg == nil { + return nil, fmt.Errorf("registry %q not found in context.yaml; available: [%s]", + deploymentRegistry, availableIDs(tenantCtx.Registries)) + } + + if ParseRegistryType(reg.Type) == RegistryTypeOffChain { + if isProduction(envSet) { + return nil, fmt.Errorf("off-chain (private) registries are not yet supported in production") + } + return NewOffChainRegistry(reg.ID, tenantCtx.DefaultDonFamily), nil + } + + if reg.Address == nil || *reg.Address == "" { + return nil, fmt.Errorf("on-chain registry %q has no address in context.yaml", reg.ID) + } + + if reg.ChainSelector == nil { + return nil, fmt.Errorf("on-chain registry %q has no chain_selector in context.yaml", reg.ID) + } + chainName, err := ChainNameFromSelectorString(*reg.ChainSelector) + if err != nil { + return nil, fmt.Errorf("registry %q: %w", reg.ID, err) + } + + return NewOnChainRegistry( + reg.ID, + *reg.Address, + chainName, + tenantCtx.DefaultDonFamily, + envSet.WorkflowRegistryChainExplorerURL, + ), nil +} + +// ParseRegistryType converts a raw type string from context.yaml to a +// RegistryType. Unknown values default to on-chain. +func ParseRegistryType(raw string) RegistryType { + if strings.EqualFold(raw, string(RegistryTypeOffChain)) || strings.EqualFold(raw, "off_chain") { + return RegistryTypeOffChain + } + return RegistryTypeOnChain +} + +func defaultFromEnvironmentSet(envSet *environments.EnvironmentSet) *OnChainRegistry { + return NewOnChainRegistry( + "", + envSet.WorkflowRegistryAddress, + envSet.WorkflowRegistryChainName, + envSet.DonFamily, + envSet.WorkflowRegistryChainExplorerURL, + ) +} + +func findRegistry(registries []*tenantctx.Registry, id string) *tenantctx.Registry { + for _, r := range registries { + if r.ID == id { + return r + } + } + return nil +} + +func availableIDs(registries []*tenantctx.Registry) string { + ids := make([]string, 0, len(registries)) + for _, r := range registries { + ids = append(ids, r.ID) + } + return strings.Join(ids, ", ") +} + +func isProduction(envSet *environments.EnvironmentSet) bool { + return envSet.EnvName == "" || envSet.EnvName == environments.DefaultEnv +} + +// AsOnChain asserts that r is an *OnChainRegistry. If it is not, it returns a +// descriptive error mentioning the command that required on-chain support. +func AsOnChain(r ResolvedRegistry, commandName string) (*OnChainRegistry, error) { + if oc, ok := r.(*OnChainRegistry); ok { + return oc, nil + } + return nil, fmt.Errorf( + "%s currently only supports on-chain registries; deployment-registry %q is %s", + commandName, r.ID(), r.Type(), + ) +} diff --git a/internal/settings/registry_resolution_test.go b/internal/settings/registry_resolution_test.go new file mode 100644 index 00000000..55511952 --- /dev/null +++ b/internal/settings/registry_resolution_test.go @@ -0,0 +1,246 @@ +package settings + +import ( + "strings" + "testing" + + "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" +) + +func strPtr(s string) *string { return &s } + +func stagingEnvSet() *environments.EnvironmentSet { + return &environments.EnvironmentSet{ + EnvName: "STAGING", + WorkflowRegistryAddress: "0xaE55eB3EDAc48a1163EE2cbb1205bE1e90Ea1135", + WorkflowRegistryChainName: "ethereum-testnet-sepolia", + WorkflowRegistryChainExplorerURL: "https://sepolia.etherscan.io", + DonFamily: "zone-a", + } +} + +func prodEnvSet() *environments.EnvironmentSet { + return &environments.EnvironmentSet{ + EnvName: "PRODUCTION", + WorkflowRegistryAddress: "0x4Ac54353FA4Fa961AfcC5ec4B118596d3305E7e5", + WorkflowRegistryChainName: "ethereum-mainnet", + WorkflowRegistryChainExplorerURL: "https://etherscan.io", + DonFamily: "zone-a", + } +} + +func sampleTenantCtx() *tenantctx.EnvironmentContext { + return &tenantctx.EnvironmentContext{ + DefaultDonFamily: "zone-a", + Registries: []*tenantctx.Registry{ + { + ID: "onchain:ethereum-testnet-sepolia", + Label: "ethereum-testnet-sepolia (0xaE55...1135)", + Type: "on-chain", + ChainSelector: strPtr("16015286601757825753"), + Address: strPtr("0xaE55eB3EDAc48a1163EE2cbb1205bE1e90Ea1135"), + }, + { + ID: "private", + Label: "Private (Chainlink-hosted)", + Type: "off-chain", + }, + }, + } +} + +func TestResolveRegistry_EmptyFallsBackToEnvSet(t *testing.T) { + envSet := stagingEnvSet() + resolved, err := ResolveRegistry("", nil, envSet) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + onchain, ok := resolved.(*OnChainRegistry) + if !ok { + t.Fatalf("expected *OnChainRegistry, got %T", resolved) + } + if onchain.Address() != envSet.WorkflowRegistryAddress { + t.Errorf("expected address %s, got %s", envSet.WorkflowRegistryAddress, onchain.Address()) + } + if onchain.ChainName() != envSet.WorkflowRegistryChainName { + t.Errorf("expected chain %s, got %s", envSet.WorkflowRegistryChainName, onchain.ChainName()) + } + if onchain.DonFamily() != envSet.DonFamily { + t.Errorf("expected don %s, got %s", envSet.DonFamily, onchain.DonFamily()) + } + if onchain.ExplorerURL() != envSet.WorkflowRegistryChainExplorerURL { + t.Errorf("expected explorer %s, got %s", envSet.WorkflowRegistryChainExplorerURL, onchain.ExplorerURL()) + } +} + +func TestResolveRegistry_OnChainFromContext(t *testing.T) { + resolved, err := ResolveRegistry("onchain:ethereum-testnet-sepolia", sampleTenantCtx(), stagingEnvSet()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + onchain, ok := resolved.(*OnChainRegistry) + if !ok { + t.Fatalf("expected *OnChainRegistry, got %T", resolved) + } + if onchain.Address() != "0xaE55eB3EDAc48a1163EE2cbb1205bE1e90Ea1135" { + t.Errorf("unexpected address: %s", onchain.Address()) + } + if onchain.ChainName() != "ethereum-testnet-sepolia" { + t.Errorf("unexpected chain name: %s", onchain.ChainName()) + } + if onchain.DonFamily() != "zone-a" { + t.Errorf("unexpected don family: %s", onchain.DonFamily()) + } +} + +func TestResolveRegistry_OffChainFromContext(t *testing.T) { + resolved, err := ResolveRegistry("private", sampleTenantCtx(), stagingEnvSet()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + offchain, ok := resolved.(*OffChainRegistry) + if !ok { + t.Fatalf("expected *OffChainRegistry, got %T", resolved) + } + if offchain.ID() != "private" { + t.Errorf("expected ID %q, got %q", "private", offchain.ID()) + } + if offchain.DonFamily() != "zone-a" { + t.Errorf("unexpected don family: %s", offchain.DonFamily()) + } + if resolved.Type() != RegistryTypeOffChain { + t.Errorf("expected type %s, got %s", RegistryTypeOffChain, resolved.Type()) + } +} + +func TestResolveRegistry_UnknownID(t *testing.T) { + _, err := ResolveRegistry("does-not-exist", sampleTenantCtx(), stagingEnvSet()) + if err == nil { + t.Fatal("expected error for unknown registry ID") + } + if !strings.Contains(err.Error(), "not found in context.yaml") { + t.Errorf("unexpected error: %v", err) + } + if !strings.Contains(err.Error(), "onchain:ethereum-testnet-sepolia") { + t.Errorf("error should list available IDs: %v", err) + } +} + +func TestResolveRegistry_NilTenantContextWithID(t *testing.T) { + _, err := ResolveRegistry("private", nil, stagingEnvSet()) + if err == nil { + t.Fatal("expected error when TenantContext is nil with a registry ID set") + } + if !strings.Contains(err.Error(), "user context is not available") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestResolveRegistry_OffChainBlockedInProduction(t *testing.T) { + _, err := ResolveRegistry("private", sampleTenantCtx(), prodEnvSet()) + if err == nil { + t.Fatal("expected error for off-chain in production") + } + if !strings.Contains(err.Error(), "not yet supported in production") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestResolveRegistry_OffChainBlockedWhenEnvEmpty(t *testing.T) { + envSet := stagingEnvSet() + envSet.EnvName = "" + _, err := ResolveRegistry("private", sampleTenantCtx(), envSet) + if err == nil { + t.Fatal("expected error for off-chain when env name is empty (defaults to production)") + } + if !strings.Contains(err.Error(), "not yet supported in production") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestResolveRegistry_OnChainMissingAddress(t *testing.T) { + ctx := &tenantctx.EnvironmentContext{ + DefaultDonFamily: "zone-a", + Registries: []*tenantctx.Registry{ + { + ID: "onchain:no-addr", + Type: "on-chain", + ChainSelector: strPtr("16015286601757825753"), + }, + }, + } + _, err := ResolveRegistry("onchain:no-addr", ctx, stagingEnvSet()) + if err == nil { + t.Fatal("expected error for on-chain registry without address") + } + if !strings.Contains(err.Error(), "has no address") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestResolveRegistry_OnChainMissingChainSelector(t *testing.T) { + ctx := &tenantctx.EnvironmentContext{ + DefaultDonFamily: "zone-a", + Registries: []*tenantctx.Registry{ + { + ID: "onchain:no-chain", + Type: "on-chain", + Address: strPtr("0x1234"), + }, + }, + } + _, err := ResolveRegistry("onchain:no-chain", ctx, stagingEnvSet()) + if err == nil { + t.Fatal("expected error for on-chain registry without chain selector") + } + if !strings.Contains(err.Error(), "has no chain_selector") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestParseRegistryType(t *testing.T) { + tests := []struct { + input string + want RegistryType + }{ + {"on-chain", RegistryTypeOnChain}, + {"off-chain", RegistryTypeOffChain}, + {"ON-CHAIN", RegistryTypeOnChain}, + {"OFF-CHAIN", RegistryTypeOffChain}, + {"off_chain", RegistryTypeOffChain}, + {"unknown", RegistryTypeOnChain}, + } + for _, tt := range tests { + if got := ParseRegistryType(tt.input); got != tt.want { + t.Errorf("ParseRegistryType(%q) = %q, want %q", tt.input, got, tt.want) + } + } +} + +func TestInterfaceMethods(t *testing.T) { + onchain := NewOnChainRegistry("oc-1", "0x1234", "sepolia", "zone-a", "https://etherscan.io") + if onchain.Type() != RegistryTypeOnChain { + t.Errorf("expected on-chain type") + } + if onchain.ID() != "oc-1" { + t.Errorf("expected ID oc-1, got %s", onchain.ID()) + } + if onchain.DonFamily() != "zone-a" { + t.Errorf("expected don zone-a, got %s", onchain.DonFamily()) + } + + offchain := NewOffChainRegistry("private", "zone-b") + if offchain.Type() != RegistryTypeOffChain { + t.Errorf("expected off-chain type") + } + if offchain.ID() != "private" { + t.Errorf("expected ID private, got %s", offchain.ID()) + } + if offchain.DonFamily() != "zone-b" { + t.Errorf("expected don zone-b, got %s", offchain.DonFamily()) + } +} diff --git a/internal/settings/settings_get.go b/internal/settings/settings_get.go index d96e27e8..9ddb2d28 100644 --- a/internal/settings/settings_get.go +++ b/internal/settings/settings_get.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "strconv" "strings" ethcommon "github.com/ethereum/go-ethereum/common" @@ -250,10 +251,21 @@ func GetChainNameByChainSelector(chainSelector uint64) (string, error) { return chainDetails.ChainName, nil } +// ChainNameFromSelectorString parses a raw chain-selector string and resolves +// it to a chain name. It combines the string-to-uint64 conversion with the +// selector-to-name lookup in a single call. +func ChainNameFromSelectorString(raw string) (string, error) { + sel, err := strconv.ParseUint(raw, 10, 64) + if err != nil { + return "", fmt.Errorf("invalid chain_selector %q: %w", raw, err) + } + return GetChainNameByChainSelector(sel) +} + func GetChainSelectorByChainName(name string) (uint64, error) { chainID, err := chainSelectors.ChainIdFromName(name) if err != nil { - return 0, fmt.Errorf("failed to get chain ID from name %q: %w", name, err) + return 0, fmt.Errorf("failed to get chain ID from name %q: %w\n Run 'cre workflow supported-chains' to see all valid chain names", name, err) } selector, err := chainSelectors.SelectorFromChainId(chainID) diff --git a/internal/settings/settings_load.go b/internal/settings/settings_load.go index ed193351..6a9e394c 100644 --- a/internal/settings/settings_load.go +++ b/internal/settings/settings_load.go @@ -21,6 +21,7 @@ const ( SethConfigPathSettingName = "logging.seth-config-path" RegistriesSettingName = "contracts.registries" KeystoneSettingName = "contracts.keystone" + DeploymentRegistrySettingName = "user-workflow.deployment-registry" RpcsSettingName = "rpcs" ExperimentalChainsSettingName = "experimental-chains" // used by simulator when present in target config ) @@ -103,7 +104,15 @@ func LoadSettingsIntoViper(v *viper.Viper, cmd *cobra.Command) error { if context.IsWorkflowCommand(cmd) { // Step 2: Load workflow settings next (overwrites values from project settings) if err := mergeConfigToViper(v, constants.DefaultWorkflowSettingsFileName); err != nil { - return fmt.Errorf("failed to load workflow settings: %w", err) + cwd, _ := os.Getwd() + return fmt.Errorf( + "workflow settings file not found: no '%s' in '%s'\n\n"+ + "To fix:\n"+ + " • Run 'cre workflow init' to create a properly initialized workflow\n"+ + " • If this workflow was manually created, add a %s with your target configuration\n"+ + " • Check that the workflow folder path argument is correct", + constants.DefaultWorkflowSettingsFileName, cwd, constants.DefaultWorkflowSettingsFileName, + ) } } diff --git a/internal/settings/settings_test.go b/internal/settings/settings_test.go index aa14081f..3b99133f 100644 --- a/internal/settings/settings_test.go +++ b/internal/settings/settings_test.go @@ -14,7 +14,10 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/ethkeys" "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/testutil" ) @@ -66,6 +69,15 @@ func copyFile(src, dest string) error { return err } +// workflowSubcommand returns a cobra command that is a child of "workflow", matching +// how LoadSettingsIntoViper loads workflow.yaml only for workflow commands. +func workflowSubcommand(use string) *cobra.Command { + workflowCmd := &cobra.Command{Use: "workflow"} + sub := &cobra.Command{Use: use} + workflowCmd.AddCommand(sub) + return sub +} + func TestLoadEnvAndSettingsEmptyTarget(t *testing.T) { envVars := map[string]string{ settings.CreTargetEnvVar: "", @@ -275,6 +287,136 @@ func TestLoadEnvAndSettingsInvalidTarget(t *testing.T) { assert.Nil(t, s, "Settings object should be nil on error") } +func TestOffChainDeploymentRegistryUsesDerivedOwnerWithoutPrivateKey(t *testing.T) { + t.Setenv(settings.EthPrivateKeyEnvVar, "") + + envVars := map[string]string{ + settings.CreTargetEnvVar: "staging", + } + + workflowTemplatePath, err := filepath.Abs(filepath.Join("testdata", "workflow_storage", "workflow-private-registry.yaml")) + require.NoError(t, err) + + projectTemplatePath, err := filepath.Abs(TempProjectSettingsFile) + require.NoError(t, err) + + tempDir := t.TempDir() + restoreWorkingDirectory, err := testutil.ChangeWorkingDirectory(tempDir) + require.NoError(t, err) + defer restoreWorkingDirectory() + + v, logger := createTestContext(t, envVars, tempDir) + + setUpTestSettingsFiles(t, v, workflowTemplatePath, projectTemplatePath, tempDir) + + derived, err := ethkeys.FormatWorkflowOwnerAddress(" 0x1234567890123456789012345678901234567890 ") + require.NoError(t, err) + require.NotEmpty(t, derived) + tenantCtx := &tenantctx.EnvironmentContext{ + DefaultDonFamily: "test-don", + Registries: []*tenantctx.Registry{ + {ID: "my-private-registry", Type: "off-chain"}, + }, + } + envSet := &environments.EnvironmentSet{EnvName: "STAGING"} + + cmd := workflowSubcommand("deploy") + s, err := settings.New(logger, v, cmd, "") + require.NoError(t, err) + require.NotNil(t, s) + resolved, err := settings.ResolveRegistry("my-private-registry", tenantCtx, envSet) + require.NoError(t, err) + err = settings.FinalizeWorkflowOwner(v, cmd, &s.Workflow, s.User.TargetName, resolved, derived) + require.NoError(t, err) + assert.Equal(t, derived, s.Workflow.UserWorkflowSettings.WorkflowOwnerAddress) + assert.Equal(t, constants.WorkflowOwnerTypeOrgDerived, s.Workflow.UserWorkflowSettings.WorkflowOwnerType) + assert.Empty(t, s.User.EthPrivateKey) +} + +func TestOffChainDeploymentRegistryMissingDerivedOwnerReturnsError(t *testing.T) { + envVars := map[string]string{ + settings.CreTargetEnvVar: "staging", + } + + workflowTemplatePath, err := filepath.Abs(filepath.Join("testdata", "workflow_storage", "workflow-private-registry.yaml")) + require.NoError(t, err) + + projectTemplatePath, err := filepath.Abs(TempProjectSettingsFile) + require.NoError(t, err) + + tempDir := t.TempDir() + restoreWorkingDirectory, err := testutil.ChangeWorkingDirectory(tempDir) + require.NoError(t, err) + defer restoreWorkingDirectory() + + v, logger := createTestContext(t, envVars, tempDir) + + setUpTestSettingsFiles(t, v, workflowTemplatePath, projectTemplatePath, tempDir) + + tenantCtx := &tenantctx.EnvironmentContext{ + DefaultDonFamily: "test-don", + Registries: []*tenantctx.Registry{ + {ID: "my-private-registry", Type: "off-chain"}, + }, + } + envSet := &environments.EnvironmentSet{EnvName: "STAGING"} + + cmd := workflowSubcommand("deploy") + s, err := settings.New(logger, v, cmd, "") + require.NoError(t, err) + resolved, err := settings.ResolveRegistry("my-private-registry", tenantCtx, envSet) + require.NoError(t, err) + err = settings.FinalizeWorkflowOwner(v, cmd, &s.Workflow, s.User.TargetName, resolved, "") + require.Error(t, err) + assert.Contains(t, err.Error(), "derived workflow owner is not available") +} + +func TestOnChainDeploymentRegistryStillRequiresPrivateKey(t *testing.T) { + t.Setenv(settings.EthPrivateKeyEnvVar, "") + + envVars := map[string]string{ + settings.CreTargetEnvVar: "staging", + } + + workflowTemplatePath, err := filepath.Abs(filepath.Join("testdata", "workflow_storage", "workflow-onchain-named-registry.yaml")) + require.NoError(t, err) + + projectTemplatePath, err := filepath.Abs(TempProjectSettingsFile) + require.NoError(t, err) + + tempDir := t.TempDir() + restoreWorkingDirectory, err := testutil.ChangeWorkingDirectory(tempDir) + require.NoError(t, err) + defer restoreWorkingDirectory() + + v, logger := createTestContext(t, envVars, tempDir) + + setUpTestSettingsFiles(t, v, workflowTemplatePath, projectTemplatePath, tempDir) + + chainSel := "16015286601757825753" + addr := "0xaE55eB3EDAc48a1163EE2cbb1205bE1e90Ea1135" + tenantCtx := &tenantctx.EnvironmentContext{ + DefaultDonFamily: "zone-a", + Registries: []*tenantctx.Registry{ + { + ID: "onchain:ethereum-testnet-sepolia", + Type: "on-chain", + ChainSelector: &chainSel, + Address: &addr, + }, + }, + } + envSet := &environments.EnvironmentSet{EnvName: "STAGING"} + + cmd := workflowSubcommand("deploy") + s, err := settings.New(logger, v, cmd, "") + require.NoError(t, err) + resolved, err := settings.ResolveRegistry("onchain:ethereum-testnet-sepolia", tenantCtx, envSet) + require.NoError(t, err) + err = settings.FinalizeWorkflowOwner(v, cmd, &s.Workflow, s.User.TargetName, resolved, "1234567890123456789012345678901234567890") + require.Error(t, err) +} + func TestShouldSkipGetOwner(t *testing.T) { t.Parallel() diff --git a/internal/settings/testdata/workflow_storage/workflow-onchain-named-registry.yaml b/internal/settings/testdata/workflow_storage/workflow-onchain-named-registry.yaml new file mode 100644 index 00000000..ef4ae892 --- /dev/null +++ b/internal/settings/testdata/workflow_storage/workflow-onchain-named-registry.yaml @@ -0,0 +1,9 @@ +# staging target with named on-chain deployment-registry (owner still from key or MSIG) +staging: + user-workflow: + workflow-name: "workflowTest" + deployment-registry: "onchain:ethereum-testnet-sepolia" + workflow-artifacts: + workflow-language: "go" + workflow-path: "./main.go" + config-path: "./config.json" diff --git a/internal/settings/testdata/workflow_storage/workflow-private-registry.yaml b/internal/settings/testdata/workflow_storage/workflow-private-registry.yaml new file mode 100644 index 00000000..0a22552b --- /dev/null +++ b/internal/settings/testdata/workflow_storage/workflow-private-registry.yaml @@ -0,0 +1,9 @@ +# staging target with private deployment-registry (no workflow-owner-address; owner comes from auth) +staging: + user-workflow: + workflow-name: "workflowTest" + deployment-registry: "my-private-registry" + workflow-artifacts: + workflow-language: "go" + workflow-path: "./main.go" + config-path: "./config.json" diff --git a/internal/settings/workflow_settings.go b/internal/settings/workflow_settings.go index 1ac6665c..db29dbee 100644 --- a/internal/settings/workflow_settings.go +++ b/internal/settings/workflow_settings.go @@ -11,6 +11,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" "sigs.k8s.io/yaml" + + "github.com/smartcontractkit/cre-cli/internal/constants" ) // GetWorkflowPathFromFile reads workflow-path from a workflow.yaml file (same value deploy/simulate get from Settings). @@ -84,6 +86,7 @@ type WorkflowSettings struct { WorkflowOwnerAddress string `mapstructure:"workflow-owner-address" yaml:"workflow-owner-address"` WorkflowOwnerType string `mapstructure:"workflow-owner-type" yaml:"workflow-owner-type"` WorkflowName string `mapstructure:"workflow-name" yaml:"workflow-name"` + DeploymentRegistry string `mapstructure:"deployment-registry" yaml:"deployment-registry"` } `mapstructure:"user-workflow" yaml:"user-workflow"` WorkflowArtifactSettings struct { WorkflowPath string `mapstructure:"workflow-path" yaml:"workflow-path"` @@ -117,14 +120,20 @@ func loadWorkflowSettings(logger *zerolog.Logger, v *viper.Viper, cmd *cobra.Com var workflowSettings WorkflowSettings - // if a command doesn't need private key, skip getting owner here + workflowSettings.UserWorkflowSettings.DeploymentRegistry = getSetting(DeploymentRegistrySettingName) + deploymentRegistry := workflowSettings.UserWorkflowSettings.DeploymentRegistry + + // If deployment-registry is set, owner depends on how that id resolves; defer to + // FinalizeWorkflowOwner (after ResolveRegistry). Otherwise resolve from env/config now. if !ShouldSkipGetOwner(cmd) { - ownerAddress, ownerType, err := GetWorkflowOwner(v) - if err != nil { - return WorkflowSettings{}, err + if deploymentRegistry == "" { + ownerAddress, ownerType, err := GetWorkflowOwner(v) + if err != nil { + return WorkflowSettings{}, err + } + workflowSettings.UserWorkflowSettings.WorkflowOwnerAddress = ownerAddress + workflowSettings.UserWorkflowSettings.WorkflowOwnerType = ownerType } - workflowSettings.UserWorkflowSettings.WorkflowOwnerAddress = ownerAddress - workflowSettings.UserWorkflowSettings.WorkflowOwnerType = ownerType } workflowSettings.UserWorkflowSettings.WorkflowName = getSetting(WorkflowNameSettingName) @@ -150,10 +159,8 @@ func loadWorkflowSettings(logger *zerolog.Logger, v *viper.Viper, cmd *cobra.Com workflowSettings.RPCs[i].Url = resolved } - if registryChainName != "" { - if err := validateDeploymentRPC(&workflowSettings, registryChainName); err != nil { - return WorkflowSettings{}, errors.Wrap(err, "for target "+target) - } + if err := ValidateDeploymentRPC(&workflowSettings, registryChainName); err != nil { + return WorkflowSettings{}, errors.Wrap(err, "for target "+target) } if err := validateSettings(&workflowSettings); err != nil { @@ -163,19 +170,61 @@ func loadWorkflowSettings(logger *zerolog.Logger, v *viper.Viper, cmd *cobra.Com // This is required because some commands still read values directly out of viper // TODO: Remove this function once all access to settings no longer uses viper // DEVSVCS-1561 - if err := flattenWorkflowSettingsToViper(v, target); err != nil { + if err := flattenWorkflowSettingsToViper(v, target, workflowSettings.UserWorkflowSettings.WorkflowOwnerAddress); err != nil { return WorkflowSettings{}, err } return workflowSettings, nil } +// FinalizeWorkflowOwner sets workflow owner when loadWorkflowSettings deferred it because +// user-workflow.deployment-registry was non-empty. Call after ResolveRegistry. +func FinalizeWorkflowOwner( + v *viper.Viper, + cmd *cobra.Command, + workflow *WorkflowSettings, + target string, + resolved ResolvedRegistry, + derivedWorkflowOwner string, +) error { + if ShouldSkipGetOwner(cmd) { + return nil + } + if workflow.UserWorkflowSettings.DeploymentRegistry == "" { + return nil + } + if resolved == nil { + return fmt.Errorf("resolved registry is required to finalize workflow owner") + } + + var ownerAddr, ownerType string + var err error + if resolved.Type() == RegistryTypeOffChain { + ownerAddr = derivedWorkflowOwner + if ownerAddr == "" { + return fmt.Errorf("derived workflow owner is not available; ensure authentication succeeded") + } + ownerType = constants.WorkflowOwnerTypeOrgDerived + } else { + ownerAddr, ownerType, err = GetWorkflowOwner(v) + if err != nil { + return err + } + } + workflow.UserWorkflowSettings.WorkflowOwnerAddress = ownerAddr + workflow.UserWorkflowSettings.WorkflowOwnerType = ownerType + return flattenWorkflowSettingsToViper(v, target, ownerAddr) +} + // TODO: Remove this function once all access to settings no longer uses viper // DEVSVCS-1561 -func flattenWorkflowSettingsToViper(v *viper.Viper, target string) error { - // Manually flatten the workflow owner setting. +func flattenWorkflowSettingsToViper(v *viper.Viper, target string, effectiveWorkflowOwner string) error { + // Manually flatten the workflow owner setting (effective address from settings load, + // including org-derived owner when deployment registry is private). ownerKey := fmt.Sprintf("%s.%s", target, WorkflowOwnerSettingName) - if v.IsSet(ownerKey) { + if effectiveWorkflowOwner != "" { + v.Set(WorkflowOwnerSettingName, effectiveWorkflowOwner) + } else if v.IsSet(ownerKey) { owner := v.GetString(ownerKey) v.Set(WorkflowOwnerSettingName, owner) } @@ -274,7 +323,12 @@ func ShouldSkipGetOwner(cmd *cobra.Command) bool { } } -func validateDeploymentRPC(config *WorkflowSettings, chainName string) error { +// ValidateDeploymentRPC ensures project settings define a valid RPC URL for chainName (e.g. the workflow +// registry chain). It is a no-op when chainName is empty. Used during settings load and from secrets owner-key flows. +func ValidateDeploymentRPC(config *WorkflowSettings, chainName string) error { + if chainName == "" { + return nil + } deploymentRPCFound := false deploymentRPCURL := "" commonError := " - required to deploy CRE workflows" diff --git a/internal/templaterepo/builtin/hello-world-ts/workflow/package.json b/internal/templaterepo/builtin/hello-world-ts/workflow/package.json index 0c2ad22b..dfe126d8 100644 --- a/internal/templaterepo/builtin/hello-world-ts/workflow/package.json +++ b/internal/templaterepo/builtin/hello-world-ts/workflow/package.json @@ -4,12 +4,11 @@ "main": "dist/main.js", "private": true, "scripts": { - "postinstall": "bun x cre-setup", "typecheck": "tsc --noEmit" }, "license": "UNLICENSED", "dependencies": { - "@chainlink/cre-sdk": "^1.4.0" + "@chainlink/cre-sdk": "^1.5.0" }, "devDependencies": { "typescript": "5.9.3" diff --git a/internal/testutil/chainsim/simulated_environment.go b/internal/testutil/chainsim/simulated_environment.go index 4b2d012c..4738e4e0 100644 --- a/internal/testutil/chainsim/simulated_environment.go +++ b/internal/testutil/chainsim/simulated_environment.go @@ -12,14 +12,21 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" - "github.com/smartcontractkit/cre-cli/internal/settings" + settingspkg "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" "github.com/smartcontractkit/cre-cli/internal/testutil" + "github.com/smartcontractkit/cre-cli/internal/testutil/testjwt" + "github.com/smartcontractkit/cre-cli/internal/testutil/testsettings" ) type SimulatedEnvironment struct { Chain *SimulatedChain EthClient *seth.Client Contracts *SimulatedContracts + + tenantID string + donFamily string + jwtToken string } type SimulatedContracts struct { @@ -45,6 +52,17 @@ func NewSimulatedEnvironment(t *testing.T) *SimulatedEnvironment { return &simulatedEnvironment } +func (se *SimulatedEnvironment) WithPrivateRegistry(tenantID, donFamily string) *SimulatedEnvironment { + se.tenantID = tenantID + se.donFamily = donFamily + return se +} + +func (se *SimulatedEnvironment) WithJWT(orgID string) *SimulatedEnvironment { + se.jwtToken = testjwt.CreateTestJWT(orgID) + return se +} + func (se *SimulatedEnvironment) NewRuntimeContext() *runtime.Context { logger := testutil.NewTestLogger() return se.createContextWithLogger(logger) @@ -61,8 +79,8 @@ func (se *SimulatedEnvironment) Close() { func (se *SimulatedEnvironment) createContextWithLogger(logger *zerolog.Logger) *runtime.Context { v := viper.New() - v.Set(settings.EthPrivateKeyEnvVar, TestPrivateKey) - settings, err := testutil.NewTestSettings(v, logger) + v.Set(settingspkg.EthPrivateKeyEnvVar, TestPrivateKey) + settings, err := testsettings.NewTestSettings(v, logger) if err != nil { logger.Warn().Err(err).Msg("failed to create new test settings") } @@ -79,18 +97,42 @@ func (se *SimulatedEnvironment) createContextWithLogger(logger *zerolog.Logger) logger.Warn().Err(err).Msg("failed to create new credentials") } + var resolved settingspkg.ResolvedRegistry + if se.tenantID != "" { + resolved = settingspkg.NewOffChainRegistry("private", se.donFamily) + } else if environmentSet != nil { + resolved = settingspkg.NewOnChainRegistry( + "", + se.Contracts.WorkflowRegistry.Contract.Hex(), + environmentSet.WorkflowRegistryChainName, + environmentSet.DonFamily, + environmentSet.WorkflowRegistryChainExplorerURL, + ) + } + ctx := &runtime.Context{ - Logger: logger, - Viper: v, - ClientFactory: simulatedFactory, - Settings: settings, - EnvironmentSet: environmentSet, - Credentials: creds, + Logger: logger, + Viper: v, + ClientFactory: simulatedFactory, + Settings: settings, + EnvironmentSet: environmentSet, + Credentials: creds, + ResolvedRegistry: resolved, + } + + if se.tenantID != "" { + ctx.TenantContext = &tenantctx.EnvironmentContext{TenantID: se.tenantID} } // Mark credentials as validated for tests to bypass validation if creds != nil { creds.IsValidated = true + if se.jwtToken != "" { + if creds.Tokens == nil { + creds.Tokens = &credentials.CreLoginTokenSet{} + } + creds.Tokens.AccessToken = se.jwtToken + } } return ctx diff --git a/internal/testutil/graphql_mock.go b/internal/testutil/graphql_mock.go index 98bbd188..9f5ff73f 100644 --- a/internal/testutil/graphql_mock.go +++ b/internal/testutil/graphql_mock.go @@ -11,8 +11,8 @@ import ( ) // NewGraphQLMockServerGetOrganization starts an httptest.Server that responds to -// getOrganization with a fixed organizationId. It sets EnvVarGraphQLURL so CLI -// commands use this server. Caller must defer srv.Close(). +// getCreOrganizationInfo with a fixed orgId and derivedWorkflowOwners. +// It sets EnvVarGraphQLURL so CLI commands use this server. Caller must defer srv.Close(). func NewGraphQLMockServerGetOrganization(t *testing.T) *httptest.Server { t.Helper() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -23,10 +23,13 @@ func NewGraphQLMockServerGetOrganization(t *testing.T) *httptest.Server { } _ = json.NewDecoder(r.Body).Decode(&req) w.Header().Set("Content-Type", "application/json") - if strings.Contains(req.Query, "getOrganization") { + if strings.Contains(req.Query, "getCreOrganizationInfo") { _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ - "getOrganization": map[string]any{"organizationId": "test-org-id"}, + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, + }, }, }) return diff --git a/internal/testutil/testjwt/jwt.go b/internal/testutil/testjwt/jwt.go new file mode 100644 index 00000000..50223567 --- /dev/null +++ b/internal/testutil/testjwt/jwt.go @@ -0,0 +1,35 @@ +package testjwt + +import ( + "encoding/base64" + "encoding/json" + "time" +) + +// CreateTestJWTWithClaims creates a JWT token with custom claims for testing. +// The signature is a dummy value. +func CreateTestJWTWithClaims(claims map[string]interface{}) string { + // JWT header (doesn't matter for our tests) + header := map[string]string{"alg": "HS256", "typ": "JWT"} + headerJSON, _ := json.Marshal(header) + headerEncoded := base64.RawURLEncoding.EncodeToString(headerJSON) + + // JWT payload with claims + claimsJSON, _ := json.Marshal(claims) + claimsEncoded := base64.RawURLEncoding.EncodeToString(claimsJSON) + + // JWT signature (doesn't need to be valid for our tests) + signature := base64.RawURLEncoding.EncodeToString([]byte("fake-signature")) + + return headerEncoded + "." + claimsEncoded + "." + signature +} + +// CreateTestJWT creates a JWT token with default claims for a given organization ID. +func CreateTestJWT(orgID string) string { + return CreateTestJWTWithClaims(map[string]interface{}{ + "sub": "test-user", + "org_id": orgID, + "organization_status": "FULL_ACCESS", + "exp": time.Now().Add(2 * time.Hour).Unix(), + }) +} diff --git a/internal/testutil/test_settings.go b/internal/testutil/testsettings/test_settings.go similarity index 98% rename from internal/testutil/test_settings.go rename to internal/testutil/testsettings/test_settings.go index cd3dcc43..d3f4148c 100644 --- a/internal/testutil/test_settings.go +++ b/internal/testutil/testsettings/test_settings.go @@ -1,4 +1,4 @@ -package testutil +package testsettings import ( _ "embed" diff --git a/internal/testutil/testdata/test-project.yaml b/internal/testutil/testsettings/testdata/test-project.yaml similarity index 100% rename from internal/testutil/testdata/test-project.yaml rename to internal/testutil/testsettings/testdata/test-project.yaml diff --git a/internal/testutil/testdata/test-workflow.yaml b/internal/testutil/testsettings/testdata/test-workflow.yaml similarity index 100% rename from internal/testutil/testdata/test-workflow.yaml rename to internal/testutil/testsettings/testdata/test-workflow.yaml diff --git a/internal/ui/prompts.go b/internal/ui/prompts.go index d68ce1eb..c288e835 100644 --- a/internal/ui/prompts.go +++ b/internal/ui/prompts.go @@ -1,9 +1,21 @@ package ui import ( + "context" + "github.com/charmbracelet/huh" ) +// WaitForEnter displays a styled note prompt and blocks until the user presses +// Enter or ctx is cancelled (e.g. because a done signal fired). +// It returns true if Enter was pressed, or false if the context was cancelled. +func WaitForEnter(ctx context.Context, message string) bool { + note := huh.NewNote().Title(message).Next(true) + form := huh.NewForm(huh.NewGroup(note)).WithTheme(ChainlinkTheme()) + err := form.RunWithContext(ctx) + return err == nil +} + // --- Option types for functional options pattern --- // ConfirmOption configures a Confirm prompt. diff --git a/package.json b/package.json new file mode 100644 index 00000000..35c9e853 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "cre-cli", + "private": true, + "version": "1.8.2", + "scripts": { + "ci:changeset:publish": "bash ./scripts/tag-and-push.sh", + "ci:changeset:version": "pnpm changeset version" + }, + "devDependencies": { + "@changesets/changelog-github": "0.6.0", + "@changesets/cli": "2.30.0" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..aee922b0 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,871 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@changesets/changelog-github': + specifier: 0.6.0 + version: 0.6.0 + '@changesets/cli': + specifier: 2.30.0 + version: 2.30.0 + +packages: + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@changesets/apply-release-plan@7.1.0': + resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} + + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/changelog-github@0.6.0': + resolution: {integrity: sha512-wA2/y4hR/A1K411cCT75rz0d46Iezxp1WYRFoFJDIUpkQ6oDBAIUiU7BZkDCmYgz0NBl94X1lgcZO+mHoiHnFg==} + + '@changesets/cli@2.30.0': + resolution: {integrity: sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==} + hasBin: true + + '@changesets/config@3.1.3': + resolution: {integrity: sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + + '@changesets/get-github-info@0.8.0': + resolution: {integrity: sha512-cRnC+xdF0JIik7coko3iUP9qbnfi1iJQ3sAa6dE+Tx3+ET8bjFEm63PA4WEohgjYcmsOikPHWzPsMWWiZmntOQ==} + + '@changesets/get-release-plan@4.0.15': + resolution: {integrity: sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + dataloader@1.4.0: + resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dotenv@8.6.0: + resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} + engines: {node: '>=10'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + human-id@4.1.3: + resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} + hasBin: true + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + +snapshots: + + '@babel/runtime@7.29.2': {} + + '@changesets/apply-release-plan@7.1.0': + dependencies: + '@changesets/config': 3.1.3 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.4 + + '@changesets/assemble-release-plan@6.0.9': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.4 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/changelog-github@0.6.0': + dependencies: + '@changesets/get-github-info': 0.8.0 + '@changesets/types': 6.1.0 + dotenv: 8.6.0 + transitivePeerDependencies: + - encoding + + '@changesets/cli@2.30.0': + dependencies: + '@changesets/apply-release-plan': 7.1.0 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.3 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.15 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3 + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.4 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.3': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.3': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.4 + + '@changesets/get-github-info@0.8.0': + dependencies: + dataloader: 1.4.0 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@changesets/get-release-plan@4.0.15': + dependencies: + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/config': 3.1.3 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.1.1 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.3 + prettier: 2.8.8 + + '@inquirer/external-editor@1.0.3': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.2 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.2 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@types/node@12.20.55': {} + + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + chardet@2.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + dataloader@1.4.0: {} + + detect-indent@6.1.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dotenv@8.6.0: {} + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + esprima@4.0.1: {} + + extendable-error@0.1.7: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + human-id@4.1.3: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mri@1.2.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + pify@4.0.1: {} + + prettier@2.8.8: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.2 + pify: 4.0.1 + strip-bom: 3.0.0 + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + term-size@2.2.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + tr46@0.0.3: {} + + universalify@0.1.2: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 diff --git a/scripts/tag-and-push.sh b/scripts/tag-and-push.sh new file mode 100755 index 00000000..94c4ffc1 --- /dev/null +++ b/scripts/tag-and-push.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -euo pipefail + +version=$(node -p "require('./package.json').version") +tag="v${version}" + +if git rev-parse "$tag" >/dev/null 2>&1; then + echo "Tag $tag already exists. Skipping..." + exit 0 +fi + +sha=$(git rev-parse HEAD) +repo="${GITHUB_REPOSITORY}" + +echo "Creating signed tag: $tag (pointing to $sha)" + +# Create an annotated tag object via the GitHub API. +# Tags created server-side are signed by GitHub and display as "Verified". +tag_sha=$(gh api "repos/${repo}/git/tags" \ + --method POST \ + -f tag="$tag" \ + -f message="CRE CLI $tag" \ + -f object="$sha" \ + -f type="commit" \ + --jq '.sha') + +# Create the ref pointing to the tag object +gh api "repos/${repo}/git/refs" \ + --method POST \ + -f ref="refs/tags/$tag" \ + -f sha="$tag_sha" + +echo "Tag $tag created and verified." diff --git a/test/graphql_mock.go b/test/graphql_mock.go index 8298fe02..f9ee608d 100644 --- a/test/graphql_mock.go +++ b/test/graphql_mock.go @@ -8,7 +8,7 @@ import ( ) // NewGraphQLMockServerGetOrganization starts a mock GraphQL server that responds to -// getOrganization and sets EnvVarGraphQLURL. Caller must defer srv.Close(). +// getCreOrganizationInfo and sets EnvVarGraphQLURL. Caller must defer srv.Close(). func NewGraphQLMockServerGetOrganization(t *testing.T) *httptest.Server { return testutil.NewGraphQLMockServerGetOrganization(t) } diff --git a/test/init_convert_simulate_ts_test.go b/test/init_convert_simulate_ts_test.go index d950aa9e..1a552f7b 100644 --- a/test/init_convert_simulate_ts_test.go +++ b/test/init_convert_simulate_ts_test.go @@ -67,13 +67,19 @@ func TestE2EInit_ConvertToCustomBuild_TS(t *testing.T) { require.NoError(t, err) require.Contains(t, string(mainBefore), `return "Hello world!";`, "convert must not modify workflow source") - // Test-only: copy compile-to-js and workflow-wrapper from SDK, then patch to add define (so FLAG env drives the build). + // Test-only: copy all SDK script sources from cre-sdk (compile-to-js may import helpers like compile-cli-args). scriptsDir := filepath.Join(workflowDirectory, "scripts") require.NoError(t, os.MkdirAll(scriptsDir, 0755)) srcDir := filepath.Join(workflowDirectory, "node_modules", "@chainlink", "cre-sdk", "scripts", "src") - for _, name := range []string{"compile-to-js.ts", "workflow-wrapper.ts", "validate-workflow-runtime-compat.ts"} { - b, err := os.ReadFile(filepath.Join(srcDir, name)) - require.NoError(t, err) + entries, err := os.ReadDir(srcDir) + require.NoError(t, err) + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".ts") { + continue + } + name := e.Name() + b, readErr := os.ReadFile(filepath.Join(srcDir, name)) + require.NoError(t, readErr) require.NoError(t, os.WriteFile(filepath.Join(scriptsDir, name), b, 0600)) //nolint:gosec // G703 -- test paths in temp dir } compileToJSPath := filepath.Join(scriptsDir, "compile-to-js.ts") diff --git a/test/multi_command_flows/account_happy_path.go b/test/multi_command_flows/account_happy_path.go index 03cfde61..e769d834 100644 --- a/test/multi_command_flows/account_happy_path.go +++ b/test/multi_command_flows/account_happy_path.go @@ -57,11 +57,12 @@ func RunAccountHappyPath(t *testing.T, tc TestConfig, testEthURL, chainName stri w.Header().Set("Content-Type", "application/json") // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { + if strings.Contains(req.Query, "getCreOrganizationInfo") { _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, }, }, }) diff --git a/test/multi_command_flows/secrets_happy_path.go b/test/multi_command_flows/secrets_happy_path.go index 462b304a..f37fbfdf 100644 --- a/test/multi_command_flows/secrets_happy_path.go +++ b/test/multi_command_flows/secrets_happy_path.go @@ -62,11 +62,12 @@ func RunSecretsHappyPath(t *testing.T, tc TestConfig, chainName string) { w.Header().Set("Content-Type", "application/json") // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { + if strings.Contains(req.Query, "getCreOrganizationInfo") { _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, }, }, }) @@ -256,11 +257,12 @@ func RunSecretsListMsig(t *testing.T, tc TestConfig, chainName string) { w.Header().Set("Content-Type", "application/json") // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { + if strings.Contains(req.Query, "getCreOrganizationInfo") { _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, }, }, }) diff --git a/test/multi_command_flows/workflow_happy_path_1.go b/test/multi_command_flows/workflow_happy_path_1.go index cc33e2e3..4433ac62 100644 --- a/test/multi_command_flows/workflow_happy_path_1.go +++ b/test/multi_command_flows/workflow_happy_path_1.go @@ -61,11 +61,12 @@ func workflowDeployEoaWithMockStorage(t *testing.T, tc TestConfig) (output strin w.Header().Set("Content-Type", "application/json") // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { + if strings.Contains(req.Query, "getCreOrganizationInfo") { _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, }, }, }) diff --git a/test/multi_command_flows/workflow_happy_path_2.go b/test/multi_command_flows/workflow_happy_path_2.go index b12604c9..42c52bb8 100644 --- a/test/multi_command_flows/workflow_happy_path_2.go +++ b/test/multi_command_flows/workflow_happy_path_2.go @@ -34,11 +34,12 @@ func workflowDeployEoa(t *testing.T, tc TestConfig) string { w.Header().Set("Content-Type", "application/json") // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { + if strings.Contains(req.Query, "getCreOrganizationInfo") { _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, }, }, }) @@ -154,11 +155,12 @@ func workflowDeployUpdateWithConfig(t *testing.T, tc TestConfig) string { w.Header().Set("Content-Type", "application/json") // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { + if strings.Contains(req.Query, "getCreOrganizationInfo") { _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, }, }, }) diff --git a/test/multi_command_flows/workflow_happy_path_3.go b/test/multi_command_flows/workflow_happy_path_3.go index 90b55223..5b890207 100644 --- a/test/multi_command_flows/workflow_happy_path_3.go +++ b/test/multi_command_flows/workflow_happy_path_3.go @@ -31,11 +31,12 @@ func workflowInit(t *testing.T, projectRootFlag, projectName, workflowName strin w.Header().Set("Content-Type", "application/json") // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { + if strings.Contains(req.Query, "getCreOrganizationInfo") { _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, }, }, }) @@ -99,11 +100,12 @@ func workflowDeployUnsigned(t *testing.T, tc TestConfig, projectRootFlag, workfl w.Header().Set("Content-Type", "application/json") // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { + if strings.Contains(req.Query, "getCreOrganizationInfo") { _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, }, }, }) @@ -217,11 +219,12 @@ func workflowDeployWithConfigAndLinkedKey(t *testing.T, tc TestConfig, projectRo w.Header().Set("Content-Type", "application/json") // Handle authentication validation query - if strings.Contains(req.Query, "getOrganization") { + if strings.Contains(req.Query, "getCreOrganizationInfo") { _ = json.NewEncoder(w).Encode(map[string]any{ "data": map[string]any{ - "getOrganization": map[string]any{ - "organizationId": "test-org-id", + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{"ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12"}, }, }, }) diff --git a/test/multi_command_flows/workflow_private_registry.go b/test/multi_command_flows/workflow_private_registry.go new file mode 100644 index 00000000..5ab8c3df --- /dev/null +++ b/test/multi_command_flows/workflow_private_registry.go @@ -0,0 +1,921 @@ +package multi_command_flows + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "sync/atomic" + "testing" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/cre-cli/internal/authvalidation" + "github.com/smartcontractkit/cre-cli/internal/constants" + "github.com/smartcontractkit/cre-cli/internal/credentials" + "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/ethkeys" + "github.com/smartcontractkit/cre-cli/internal/settings" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" + "github.com/smartcontractkit/cre-cli/internal/testutil" + "github.com/smartcontractkit/cre-cli/internal/testutil/testjwt" +) + +// MockDerivedWorkflowOwnerHex is the raw address (no 0x) in getCreOrganizationInfo.derivedWorkflowOwners +// in tests. The canonical checksummed form is privateRegistryOwnerAddress. +const MockDerivedWorkflowOwnerHex = "ab12cd34ef56ab12cd34ef56ab12cd34ef56ab12" + +var privateRegistryOwnerAddress = mustChecksumMockDerivedOwner() + +func mustChecksumMockDerivedOwner() string { + s, err := ethkeys.FormatWorkflowOwnerAddress("0x" + MockDerivedWorkflowOwnerHex) + if err != nil { + panic(err) + } + return s +} + +// mockGetCreOrganizationInfoGraphQLPayload is the full GraphQL JSON body for getCreOrganizationInfo +// (shared by composite mocks and graphQLMockOrgInfoOnly). +func mockGetCreOrganizationInfoGraphQLPayload() map[string]any { + return map[string]any{ + "data": map[string]any{ + "getCreOrganizationInfo": map[string]any{ + "orgId": "test-org-id", + "derivedWorkflowOwners": []string{MockDerivedWorkflowOwnerHex}, + }, + }, + } +} + +// CreateTestBearerCredentialsHome writes JWT bearer credentials under HOME/.cre for subprocess CLI tests. +func CreateTestBearerCredentialsHome(t *testing.T) string { + t.Helper() + + homeDir := t.TempDir() + creDir := filepath.Join(homeDir, ".cre") + require.NoError(t, os.MkdirAll(creDir, 0o700), "failed to create .cre dir") + + jwt := createTestJWT("test-org-id") + creConfig := "AccessToken: " + jwt + "\n" + + "IDToken: test-id-token\n" + + "RefreshToken: test-refresh-token\n" + + "ExpiresIn: 3600\n" + + "TokenType: Bearer\n" + + require.NoError(t, os.WriteFile(filepath.Join(creDir, "cre.yaml"), []byte(creConfig), 0o600), "failed to write test credentials") + + return homeDir +} + +func createTestJWT(orgID string) string { + return testjwt.CreateTestJWT(orgID) +} + +// workflowDeployPrivateRegistry deploys a workflow to the private registry via CLI. +// It starts a single httptest.Server that mocks GraphQL (getCreOrganizationInfo, +// GetTenantConfig, GeneratePresignedPostUrlForArtifact, GenerateUnsignedGetUrlForArtifact, +// listWorkflowOwners, GetOffchainWorkflowByName, UpsertOffchainWorkflow) and POST /upload. +func workflowDeployPrivateRegistry(t *testing.T, tc TestConfig) string { + t.Helper() + + var presignedPostCalled atomic.Bool + var uploadCalled atomic.Bool + var upsertCalled atomic.Bool + var srv *httptest.Server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost: + var req graphQLRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + + if strings.Contains(req.Query, "getCreOrganizationInfo") { + _ = json.NewEncoder(w).Encode(mockGetCreOrganizationInfoGraphQLPayload()) + return + } + + if strings.Contains(req.Query, "GetTenantConfig") || strings.Contains(req.Query, "getTenantConfig") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getTenantConfig": map[string]any{ + "tenantId": "42", + "defaultDonFamily": "test-don", + "vaultGatewayUrl": "https://vault.example.test", + "registries": []map[string]any{ + { + "id": "reg-test", + "label": "reg-test", + "type": "OFF_CHAIN", + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "secretsAuthFlows": []string{"BROWSER"}, + }, + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GeneratePresignedPostUrlForArtifact") { + presignedPostCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "generatePresignedPostUrlForArtifact": map[string]any{ + "presignedPostUrl": srv.URL + "/upload", + "presignedPostFields": []map[string]string{{"key": "k1", "value": "v1"}}, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GenerateUnsignedGetUrlForArtifact") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "generateUnsignedGetUrlForArtifact": map[string]any{ + "unsignedGetUrl": srv.URL + "/get/binary.wasm", + }, + }, + }) + return + } + + if strings.Contains(req.Query, "listWorkflowOwners") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "listWorkflowOwners": map[string]any{ + "linkedOwners": []map[string]string{ + { + "workflowOwnerAddress": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "verificationStatus": "VERIFICATION_STATUS_SUCCESSFULL", //nolint:misspell + }, + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetOffchainWorkflowByName") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]any{ + { + "message": "workflow not found", + "path": []string{"getOffchainWorkflowByName"}, + "extensions": map[string]any{ + "code": "NOT_FOUND", + }, + }, + }, + "data": nil, + }) + return + } + + if strings.Contains(req.Query, "UpsertOffchainWorkflow") { + upsertCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "upsertOffchainWorkflow": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, + }) + return + + case r.URL.Path == "/upload" && r.Method == http.MethodPost: + uploadCalled.Store(true) + w.WriteHeader(http.StatusCreated) + _, _ = w.Write([]byte("OK")) + return + + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + })) + defer srv.Close() + + t.Setenv(environments.EnvVarGraphQLURL, srv.URL+"/graphql") + + args := []string{ + "workflow", "deploy", + "blank_workflow", + tc.GetCliEnvFlag(), + tc.GetProjectRootFlag(), + "--" + settings.Flags.SkipConfirmation.Name, + } + + cmd := exec.Command(CLIPath, args...) + testHome := CreateTestBearerCredentialsHome(t) + + realHome, err := os.UserHomeDir() + require.NoError(t, err, "failed to get real home dir") + + childEnv := make([]string, 0, len(os.Environ())+3) + hasGOPATH := false + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { + continue + } + if strings.HasPrefix(entry, "GOPATH=") { + hasGOPATH = true + } + childEnv = append(childEnv, entry) + } + childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) + // When HOME is overridden, Go defaults GOPATH to $HOME/go which lands + // inside t.TempDir(). Go modules are read-only, so TempDir cleanup + // fails and marks the test as failed. Pin GOPATH to the real home. + if !hasGOPATH { + childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) + } + cmd.Env = childEnv + + var stdout, stderr bytes.Buffer + cmd.Stdout, cmd.Stderr = &stdout, &stderr + + require.NoError( + t, + cmd.Run(), + "cre workflow deploy failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + require.True(t, presignedPostCalled.Load(), "expected GeneratePresignedPostUrlForArtifact to be called") + require.True(t, uploadCalled.Load(), "expected artifact upload endpoint to be called") + require.True(t, upsertCalled.Load(), "expected UpsertOffchainWorkflow to be called") + + return StripANSI(stdout.String() + stderr.String()) +} + +// RunWorkflowPrivateRegistryHappyPath runs the workflow deploy happy path for private registry. +func RunWorkflowPrivateRegistryHappyPath(t *testing.T, tc TestConfig) { + t.Helper() + + out := workflowDeployPrivateRegistry(t, tc) + require.Contains(t, out, "Workflow compiled", "expected workflow to compile.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Uploaded binary", "expected binary upload to succeed.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow registered in private registry", "expected private registry deployment success.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Details:", "expected details block.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Registry: reg-test", "expected registry ID in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow Name: private-registry-happy-path-workflow", "expected workflow name in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow ID:", "expected workflow ID in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Status: Active", "expected active status in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Binary URL:", "expected binary URL in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Owner: "+privateRegistryOwnerAddress, "expected owner in details.\nCLI OUTPUT:\n%s", out) +} + +// workflowPausePrivateRegistry pauses a workflow in the private registry via CLI +// using a mock GraphQL server. +func workflowPausePrivateRegistry(t *testing.T, tc TestConfig) string { + t.Helper() + + var getWorkflowCalled atomic.Bool + var pauseWorkflowCalled atomic.Bool + var srv *httptest.Server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost: + var req graphQLRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + + if strings.Contains(req.Query, "getCreOrganizationInfo") { + _ = json.NewEncoder(w).Encode(mockGetCreOrganizationInfoGraphQLPayload()) + return + } + + if strings.Contains(req.Query, "GetTenantConfig") || strings.Contains(req.Query, "getTenantConfig") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getTenantConfig": map[string]any{ + "tenantId": "42", + "defaultDonFamily": "test-don", + "vaultGatewayUrl": "https://vault.example.test", + "registries": []map[string]any{ + { + "id": "reg-test", + "label": "reg-test", + "type": "OFF_CHAIN", + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "secretsAuthFlows": []string{"BROWSER"}, + }, + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetOffchainWorkflowByName") { + getWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getOffchainWorkflowByName": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "PauseOffchainWorkflow") { + pauseWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "pauseOffchainWorkflow": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_PAUSED", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, + }) + return + + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + })) + defer srv.Close() + + t.Setenv(environments.EnvVarGraphQLURL, srv.URL+"/graphql") + + args := []string{ + "workflow", "pause", + "blank_workflow", + tc.GetCliEnvFlag(), + tc.GetProjectRootFlag(), + "--" + settings.Flags.SkipConfirmation.Name, + } + + cmd := exec.Command(CLIPath, args...) + testHome := CreateTestBearerCredentialsHome(t) + + realHome, err := os.UserHomeDir() + require.NoError(t, err, "failed to get real home dir") + + childEnv := make([]string, 0, len(os.Environ())+3) + hasGOPATH := false + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { + continue + } + if strings.HasPrefix(entry, "GOPATH=") { + hasGOPATH = true + } + childEnv = append(childEnv, entry) + } + childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) + if !hasGOPATH { + childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) + } + cmd.Env = childEnv + + var stdout, stderr bytes.Buffer + cmd.Stdout, cmd.Stderr = &stdout, &stderr + + require.NoError( + t, + cmd.Run(), + "cre workflow pause failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + require.True(t, getWorkflowCalled.Load(), "expected GetOffchainWorkflowByName to be called") + require.True(t, pauseWorkflowCalled.Load(), "expected PauseOffchainWorkflow to be called") + + return StripANSI(stdout.String() + stderr.String()) +} + +// RunWorkflowPausePrivateRegistryHappyPath runs the workflow pause happy path for private registry. +func RunWorkflowPausePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { + t.Helper() + + out := workflowPausePrivateRegistry(t, tc) + require.Contains(t, out, "Workflow paused successfully", "expected private registry pause success.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Details:", "expected details block.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Registry: reg-test", "expected registry ID in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow Name: private-registry-happy-path-workflow", "expected workflow name in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow ID:", "expected workflow ID in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Status: Paused", "expected paused status in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Owner: "+privateRegistryOwnerAddress, "expected owner in details.\nCLI OUTPUT:\n%s", out) +} + +// workflowActivatePrivateRegistry activates a workflow in the private registry via CLI +// using a mock GraphQL server. +func workflowActivatePrivateRegistry(t *testing.T, tc TestConfig) string { + t.Helper() + + var getWorkflowCalled atomic.Bool + var activateWorkflowCalled atomic.Bool + var srv *httptest.Server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost: + var req graphQLRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + + if strings.Contains(req.Query, "getCreOrganizationInfo") { + _ = json.NewEncoder(w).Encode(mockGetCreOrganizationInfoGraphQLPayload()) + return + } + + if strings.Contains(req.Query, "GetTenantConfig") || strings.Contains(req.Query, "getTenantConfig") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getTenantConfig": map[string]any{ + "tenantId": "42", + "defaultDonFamily": "test-don", + "vaultGatewayUrl": "https://vault.example.test", + "registries": []map[string]any{ + { + "id": "reg-test", + "label": "reg-test", + "type": "OFF_CHAIN", + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "secretsAuthFlows": []string{"BROWSER"}, + }, + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetOffchainWorkflowByName") { + getWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getOffchainWorkflowByName": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_PAUSED", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "ActivateOffchainWorkflow") { + activateWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "activateOffchainWorkflow": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, + }) + return + + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + })) + defer srv.Close() + + t.Setenv(environments.EnvVarGraphQLURL, srv.URL+"/graphql") + + args := []string{ + "workflow", "activate", + "blank_workflow", + tc.GetCliEnvFlag(), + tc.GetProjectRootFlag(), + "--" + settings.Flags.SkipConfirmation.Name, + } + + cmd := exec.Command(CLIPath, args...) + testHome := CreateTestBearerCredentialsHome(t) + + realHome, err := os.UserHomeDir() + require.NoError(t, err, "failed to get real home dir") + + childEnv := make([]string, 0, len(os.Environ())+3) + hasGOPATH := false + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { + continue + } + if strings.HasPrefix(entry, "GOPATH=") { + hasGOPATH = true + } + childEnv = append(childEnv, entry) + } + childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) + if !hasGOPATH { + childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) + } + cmd.Env = childEnv + + var stdout, stderr bytes.Buffer + cmd.Stdout, cmd.Stderr = &stdout, &stderr + + require.NoError( + t, + cmd.Run(), + "cre workflow activate failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + require.True(t, getWorkflowCalled.Load(), "expected GetOffchainWorkflowByName to be called") + require.True(t, activateWorkflowCalled.Load(), "expected ActivateOffchainWorkflow to be called") + + return StripANSI(stdout.String() + stderr.String()) +} + +// RunWorkflowActivatePrivateRegistryHappyPath runs the workflow activate happy path for private registry. +func RunWorkflowActivatePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { + t.Helper() + + out := workflowActivatePrivateRegistry(t, tc) + require.Contains(t, out, "Workflow activated successfully", "expected private registry activate success.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Details:", "expected details block.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Registry: reg-test", "expected registry ID in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow Name: private-registry-happy-path-workflow", "expected workflow name in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow ID:", "expected workflow ID in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Status: Active", "expected active status in details.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Owner: "+privateRegistryOwnerAddress, "expected owner in details.\nCLI OUTPUT:\n%s", out) +} + +// workflowDeletePrivateRegistry deletes a workflow in the private registry via CLI +// using a mock GraphQL server. +func workflowDeletePrivateRegistry(t *testing.T, tc TestConfig) string { + t.Helper() + + var getWorkflowCalled atomic.Bool + var deleteWorkflowCalled atomic.Bool + var srv *httptest.Server + srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch { + case strings.HasPrefix(r.URL.Path, "/graphql") && r.Method == http.MethodPost: + var req graphQLRequest + _ = json.NewDecoder(r.Body).Decode(&req) + + w.Header().Set("Content-Type", "application/json") + + if strings.Contains(req.Query, "getCreOrganizationInfo") { + _ = json.NewEncoder(w).Encode(mockGetCreOrganizationInfoGraphQLPayload()) + return + } + + if strings.Contains(req.Query, "GetTenantConfig") || strings.Contains(req.Query, "getTenantConfig") { + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getTenantConfig": map[string]any{ + "tenantId": "42", + "defaultDonFamily": "test-don", + "vaultGatewayUrl": "https://vault.example.test", + "registries": []map[string]any{ + { + "id": "reg-test", + "label": "reg-test", + "type": "OFF_CHAIN", + "chainSelector": "6433500567565415381", + "address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "secretsAuthFlows": []string{"BROWSER"}, + }, + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "GetOffchainWorkflowByName") { + getWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "getOffchainWorkflowByName": map[string]any{ + "workflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "owner": privateRegistryOwnerAddress, + "createdAt": "2025-01-01T00:00:00Z", + "status": "WORKFLOW_STATUS_ACTIVE", + "workflowName": "private-registry-happy-path-workflow", + "binaryUrl": srv.URL + "/get/binary.wasm", + "configUrl": "", + "tag": "private-registry-happy-path-workflow", + "attributes": "", + "donFamily": "test-don", + "organizationId": "test-org-id", + }, + }, + }, + }) + return + } + + if strings.Contains(req.Query, "DeleteOffchainWorkflow") { + deleteWorkflowCalled.Store(true) + _ = json.NewEncoder(w).Encode(map[string]any{ + "data": map[string]any{ + "deleteOffchainWorkflow": map[string]any{ + "workflowId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + }, + }, + }) + return + } + + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(map[string]any{ + "errors": []map[string]string{{"message": "Unsupported GraphQL query"}}, + }) + return + + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + return + } + })) + defer srv.Close() + + t.Setenv(environments.EnvVarGraphQLURL, srv.URL+"/graphql") + + args := []string{ + "workflow", "delete", + "blank_workflow", + tc.GetCliEnvFlag(), + tc.GetProjectRootFlag(), + "--" + settings.Flags.SkipConfirmation.Name, + } + + cmd := exec.Command(CLIPath, args...) + testHome := CreateTestBearerCredentialsHome(t) + + realHome, err := os.UserHomeDir() + require.NoError(t, err, "failed to get real home dir") + + childEnv := make([]string, 0, len(os.Environ())+3) + hasGOPATH := false + for _, entry := range os.Environ() { + if strings.HasPrefix(entry, "HOME=") || strings.HasPrefix(entry, "USERPROFILE=") { + continue + } + if strings.HasPrefix(entry, "GOPATH=") { + hasGOPATH = true + } + childEnv = append(childEnv, entry) + } + childEnv = append(childEnv, "HOME="+testHome, "USERPROFILE="+testHome) + if !hasGOPATH { + childEnv = append(childEnv, "GOPATH="+filepath.Join(realHome, "go")) + } + cmd.Env = childEnv + + var stdout, stderr bytes.Buffer + cmd.Stdout, cmd.Stderr = &stdout, &stderr + + require.NoError( + t, + cmd.Run(), + "cre workflow delete failed:\nSTDOUT:\n%s\nSTDERR:\n%s", + stdout.String(), + stderr.String(), + ) + require.True(t, getWorkflowCalled.Load(), "expected GetOffchainWorkflowByName to be called") + require.True(t, deleteWorkflowCalled.Load(), "expected DeleteOffchainWorkflow to be called") + + return StripANSI(stdout.String() + stderr.String()) +} + +// RunWorkflowDeletePrivateRegistryHappyPath runs the workflow delete happy path for private registry. +func RunWorkflowDeletePrivateRegistryHappyPath(t *testing.T, tc TestConfig) { + t.Helper() + + out := workflowDeletePrivateRegistry(t, tc) + require.Contains(t, out, "Workflows deleted successfully", "expected private registry delete success.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Registry: reg-test", "expected registry ID in delete preview.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Workflow Status: Active", "expected formatted status in delete preview.\nCLI OUTPUT:\n%s", out) + require.Contains(t, out, "Deleted workflow ID: 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "expected workflow ID in details.\nCLI OUTPUT:\n%s", out) +} + +func workflowDeployCmd() *cobra.Command { + w := &cobra.Command{Use: "workflow"} + d := &cobra.Command{Use: "deploy"} + w.AddCommand(d) + return d +} + +func assertEnvFileHasNoEthPrivateKey(t *testing.T, envPath string) { + t.Helper() + b, err := os.ReadFile(envPath) + require.NoError(t, err) + for _, line := range strings.Split(string(b), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + key, val, ok := strings.Cut(line, "=") + if !ok { + continue + } + if strings.EqualFold(strings.TrimSpace(key), settings.EthPrivateKeyEnvVar) { + if strings.TrimSpace(val) != "" { + t.Fatalf("expected %s to be unset in %s, got a non-empty value", settings.EthPrivateKeyEnvVar, envPath) + } + } + } +} + +func graphQLMockOrgInfoOnly(t *testing.T) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, "/graphql") || r.Method != http.MethodPost { + http.NotFound(w, r) + return + } + var req struct { + Query string `json:"query"` + } + _ = json.NewDecoder(r.Body).Decode(&req) + w.Header().Set("Content-Type", "application/json") + if strings.Contains(req.Query, "getCreOrganizationInfo") { + require.NoError(t, json.NewEncoder(w).Encode(mockGetCreOrganizationInfoGraphQLPayload())) + return + } + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"errors":[{"message":"unsupported"}]}`)) + })) +} + +// RunPrivateRegistryAuthAndSettingsFinalize asserts .env has no private key, runs auth +// validation against a minimal GraphQL mock (getCreOrganizationInfo), then loads settings +// from disk and finalizes org-derived workflow owner for deployment-registry reg-test. +func RunPrivateRegistryAuthAndSettingsFinalize(t *testing.T, envPath, blankWorkflowDir string) { + t.Helper() + assertEnvFileHasNoEthPrivateKey(t, envPath) + + orgSrv := graphQLMockOrgInfoOnly(t) + defer orgSrv.Close() + t.Setenv(environments.EnvVarGraphQLURL, orgSrv.URL+"/graphql") + + bearerHome := CreateTestBearerCredentialsHome(t) + t.Setenv("HOME", bearerHome) + t.Setenv("USERPROFILE", bearerHome) + + logger := testutil.NewTestLogger() + creds, err := credentials.New(logger) + require.NoError(t, err) + require.False(t, creds.IsValidated) + + envSet, err := environments.New() + require.NoError(t, err) + require.NotEmpty(t, envSet.GraphQLURL) + + val := authvalidation.NewValidator(creds, envSet, logger) + result, err := val.ValidateCredentials(context.Background(), creds) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, "test-org-id", result.OrgID) + + derivedFormatted, err := ethkeys.FormatWorkflowOwnerAddress(result.DerivedWorkflowOwner) + require.NoError(t, err) + wantDerived, err := ethkeys.FormatWorkflowOwnerAddress("0x" + MockDerivedWorkflowOwnerHex) + require.NoError(t, err) + require.Equal(t, wantDerived, derivedFormatted, "derived owner from mock GQL must match formatted MockDerivedWorkflowOwnerHex") + + restoreWD, err := testutil.ChangeWorkingDirectory(blankWorkflowDir) + require.NoError(t, err) + defer restoreWD() + + v := viper.New() + settings.LoadEnv(logger, v, envPath) + cmd := workflowDeployCmd() + s, err := settings.New(logger, v, cmd, "") + require.NoError(t, err) + require.NotNil(t, s) + require.Empty(t, s.User.EthPrivateKey, "CRE_ETH_PRIVATE_KEY must be absent") + require.Equal(t, "reg-test", s.Workflow.UserWorkflowSettings.DeploymentRegistry) + require.Empty(t, s.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, "owner is deferred until finalize when deployment-registry is set") + require.Empty(t, s.Workflow.UserWorkflowSettings.WorkflowOwnerType) + + tenantCtx := &tenantctx.EnvironmentContext{ + DefaultDonFamily: "test-don", + Registries: []*tenantctx.Registry{ + {ID: "reg-test", Type: "OFF_CHAIN"}, + }, + } + resolved, err := settings.ResolveRegistry("reg-test", tenantCtx, envSet) + require.NoError(t, err) + require.NotNil(t, resolved) + require.Equal(t, settings.RegistryTypeOffChain, resolved.Type()) + + err = settings.FinalizeWorkflowOwner(v, cmd, &s.Workflow, s.User.TargetName, resolved, derivedFormatted) + require.NoError(t, err) + require.Equal(t, derivedFormatted, s.Workflow.UserWorkflowSettings.WorkflowOwnerAddress) + require.Equal(t, constants.WorkflowOwnerTypeOrgDerived, s.Workflow.UserWorkflowSettings.WorkflowOwnerType) +} + +// RunPrivateRegistryE2E runs auth/settings finalize (no private key) then the full CLI +// private-registry lifecycle (deploy, pause, activate, delete) with httptest GraphQL mocks. +func RunPrivateRegistryE2E(t *testing.T, tc TestConfig, envPath, blankWorkflowDir string) { + t.Helper() + t.Run("auth_and_settings_finalize_without_private_key", func(t *testing.T) { + RunPrivateRegistryAuthAndSettingsFinalize(t, envPath, blankWorkflowDir) + }) + t.Run("cli_private_registry_lifecycle", func(t *testing.T) { + RunWorkflowPrivateRegistryHappyPath(t, tc) + RunWorkflowPausePrivateRegistryHappyPath(t, tc) + RunWorkflowActivatePrivateRegistryHappyPath(t, tc) + RunWorkflowDeletePrivateRegistryHappyPath(t, tc) + }) +} diff --git a/test/multi_command_test.go b/test/multi_command_test.go index f03f6fbb..c316d3b6 100644 --- a/test/multi_command_test.go +++ b/test/multi_command_test.go @@ -1,9 +1,12 @@ package test import ( + "fmt" + "path/filepath" "sync" "testing" + "github.com/spf13/viper" "github.com/stretchr/testify/require" chainselectors "github.com/smartcontractkit/chain-selectors" @@ -120,6 +123,35 @@ func TestMultiCommandHappyPaths(t *testing.T) { multi_command_flows.RunHappyPath3bWorkflow(t, tc) }) + // Private registry (off-chain): no CRE_ETH_PRIVATE_KEY; org-derived owner from mock GQL, + // settings load + finalize, then full CLI lifecycle (see multi_command_flows.RunPrivateRegistryE2E). + t.Run("WorkflowPrivateRegistry_E2E", func(t *testing.T) { + anvilProc, testEthUrl := initTestEnv(t, "anvil-state.json") + defer StopAnvil(anvilProc) + + t.Setenv(environments.EnvVarEnv, "STAGING") + + t.Setenv(environments.EnvVarWorkflowRegistryAddress, "0x5FbDB2315678afecb367f032d93F642f64180aa3") + t.Setenv(environments.EnvVarWorkflowRegistryChainName, chainselectors.ANVIL_DEVNET.Name) + t.Setenv(environments.EnvVarDonFamily, "test-don") + + tc := NewTestConfig(t) + + require.NoError(t, createCliEnvFile(tc.EnvFile, ""), "failed to create env file without private key") + require.NoError(t, createProjectSettingsFile(tc.ProjectDirectory+"project.yaml", "", testEthUrl), "failed to create project.yaml") + require.NoError(t, createWorkflowDirectory(tc.ProjectDirectory, "private-registry-happy-path-workflow", "", "blank_workflow"), "failed to create workflow directory") + + v := viper.New() + v.SetConfigFile(filepath.Join(tc.ProjectDirectory, "blank_workflow", constants.DefaultWorkflowSettingsFileName)) + require.NoError(t, v.ReadInConfig()) + v.Set(fmt.Sprintf("%s.user-workflow.deployment-registry", SettingsTarget), "reg-test") + require.NoError(t, v.WriteConfig()) + + t.Cleanup(tc.Cleanup(t)) + + multi_command_flows.RunPrivateRegistryE2E(t, tc, tc.EnvFile, filepath.Join(tc.ProjectDirectory, "blank_workflow")) + }) + // Run Account Happy Path: Link -> List -> Unlink -> List (verify unlinked) t.Run("AccountHappyPath_LinkListUnlinkList", func(t *testing.T) { anvilProc, testEthUrl := initTestEnv(t, "anvil-state.json")