diff --git a/.release-please-manifest.json b/.release-please-manifest.json index d2d60a3..a36746b 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.24.0" + ".": "0.25.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 99bb6c4..434275e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 89 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-13214b99e392aab631aa1ca99b6a51a58df81e34156d21b8d639bea779566123.yml -openapi_spec_hash: a88d175fc3980de3097ac1411d8dcbff -config_hash: 179f33af31ece83563163d5b3d751d13 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/kernel%2Fkernel-8d66dbedea5b240936b338809f272568ca84a452fc13dbda835479f2ec068b41.yml +openapi_spec_hash: 7c499bfce2e996f1fff5e7791cea390e +config_hash: fcc2db3ed48ab4e8d1b588d31d394a23 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f01456..fdeefc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 0.25.0 (2026-01-07) + +Full Changelog: [v0.24.0...v0.25.0](https://github.com/onkernel/kernel-go-sdk/compare/v0.24.0...v0.25.0) + +### Features + +* **api:** add health check endpoint for proxies ([c7b8728](https://github.com/onkernel/kernel-go-sdk/commit/c7b8728369482de76bdf143a59f16b0de8bc03bb)) +* **auth:** add auto_login credential flow ([2eec1a1](https://github.com/onkernel/kernel-go-sdk/commit/2eec1a147279fe64a060c2066b30a2f0a13e74ab)) +* Enhance AuthAgentInvocation with step and last activity tracking ([ccb1425](https://github.com/onkernel/kernel-go-sdk/commit/ccb1425ea205fbb18129f341ae36bfc55c70ca64)) + + +### Bug Fixes + +* skip usage tests that don't work with Prism ([8872193](https://github.com/onkernel/kernel-go-sdk/commit/88721930052bff16639f371a9a7e46e7e34e7ff4)) + + +### Chores + +* add float64 to valid types for RegisterFieldValidator ([1e23b39](https://github.com/onkernel/kernel-go-sdk/commit/1e23b39aca94dd1a98d6ec6669d2c8b87f8ccf02)) +* **internal:** codegen related update ([e07718d](https://github.com/onkernel/kernel-go-sdk/commit/e07718dbecbbc00c19e9d0725c04297fe48f2121)) +* **internal:** codegen related update ([0320876](https://github.com/onkernel/kernel-go-sdk/commit/0320876c8f64185768a3ad562b9656f96ab7e935)) + ## 0.24.0 (2025-12-17) Full Changelog: [v0.23.0...v0.24.0](https://github.com/onkernel/kernel-go-sdk/compare/v0.23.0...v0.24.0) diff --git a/LICENSE b/LICENSE index b32a077..3b7d20d 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2025 Kernel + Copyright 2026 Kernel Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 3ff9520..124904d 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Or to pin the version: ```sh -go get -u 'github.com/onkernel/kernel-go-sdk@v0.24.0' +go get -u 'github.com/onkernel/kernel-go-sdk@v0.25.0' ``` diff --git a/agentauth.go b/agentauth.go index 9a61ac5..e9a7e79 100644 --- a/agentauth.go +++ b/agentauth.go @@ -20,7 +20,6 @@ import ( "github.com/onkernel/kernel-go-sdk/packages/pagination" "github.com/onkernel/kernel-go-sdk/packages/param" "github.com/onkernel/kernel-go-sdk/packages/respjson" - "github.com/onkernel/kernel-go-sdk/shared/constant" ) // AgentAuthService contains methods and other services that help with interacting @@ -68,7 +67,7 @@ func (r *AgentAuthService) Get(ctx context.Context, id string, opts ...option.Re return } -// List auth agents with optional filters for profile_name and target_domain. +// List auth agents with optional filters for profile_name and domain. func (r *AgentAuthService) List(ctx context.Context, query AgentAuthListParams, opts ...option.RequestOption) (res *pagination.OffsetPagination[AuthAgent], err error) { var raw *http.Response opts = slices.Concat(r.Options, opts) @@ -86,7 +85,7 @@ func (r *AgentAuthService) List(ctx context.Context, query AgentAuthListParams, return res, nil } -// List auth agents with optional filters for profile_name and target_domain. +// List auth agents with optional filters for profile_name and domain. func (r *AgentAuthService) ListAutoPaging(ctx context.Context, query AgentAuthListParams, opts ...option.RequestOption) *pagination.OffsetPaginationAutoPager[AuthAgent] { return pagination.NewOffsetPaginationAutoPager(r.List(ctx, query, opts...)) } @@ -108,74 +107,60 @@ func (r *AgentAuthService) Delete(ctx context.Context, id string, opts ...option return } -// Triggers automatic re-authentication for an auth agent using stored credentials. -// Requires the auth agent to have a linked credential, stored selectors, and -// login_url. Returns immediately with status indicating whether re-auth was -// started. -func (r *AgentAuthService) Reauth(ctx context.Context, id string, opts ...option.RequestOption) (res *ReauthResponse, err error) { - opts = slices.Concat(r.Options, opts) - if id == "" { - err = errors.New("missing required id parameter") - return - } - path := fmt.Sprintf("agents/auth/%s/reauth", id) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) - return -} - -// Response from discover endpoint matching AuthBlueprint schema -type AgentAuthDiscoverResponse struct { - // Whether discovery succeeded - Success bool `json:"success,required"` - // Error message if discovery failed - ErrorMessage string `json:"error_message"` - // Discovered form fields (present when success is true) - Fields []DiscoveredField `json:"fields"` - // Whether user is already logged in - LoggedIn bool `json:"logged_in"` - // URL of the discovered login page - LoginURL string `json:"login_url" format:"uri"` - // Title of the login page - PageTitle string `json:"page_title"` - // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. - JSON struct { - Success respjson.Field - ErrorMessage respjson.Field - Fields respjson.Field - LoggedIn respjson.Field - LoginURL respjson.Field - PageTitle respjson.Field - ExtraFields map[string]respjson.Field - raw string - } `json:"-"` -} - -// Returns the unmodified JSON received from the API -func (r AgentAuthDiscoverResponse) RawJSON() string { return r.JSON.raw } -func (r *AgentAuthDiscoverResponse) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - // Response from get invocation endpoint type AgentAuthInvocationResponse struct { // App name (org name at time of invocation creation) AppName string `json:"app_name,required"` + // Domain for authentication + Domain string `json:"domain,required"` // When the handoff code expires ExpiresAt time.Time `json:"expires_at,required" format:"date-time"` // Invocation status // - // Any of "IN_PROGRESS", "SUCCESS", "EXPIRED", "CANCELED". + // Any of "IN_PROGRESS", "SUCCESS", "EXPIRED", "CANCELED", "FAILED". Status AgentAuthInvocationResponseStatus `json:"status,required"` - // Target domain for authentication - TargetDomain string `json:"target_domain,required"` + // Current step in the invocation workflow + // + // Any of "initialized", "discovering", "awaiting_input", + // "awaiting_external_action", "submitting", "completed", "expired". + Step AgentAuthInvocationResponseStep `json:"step,required"` + // The invocation type: + // + // - login: First-time authentication + // - reauth: Re-authentication for previously authenticated agents + // - auto_login: Legacy type (no longer created, kept for backward compatibility) + // + // Any of "login", "auto_login", "reauth". + Type AgentAuthInvocationResponseType `json:"type,required"` + // Error message explaining why the invocation failed (present when status=FAILED) + ErrorMessage string `json:"error_message,nullable"` + // Instructions for user when external action is required (present when + // step=awaiting_external_action) + ExternalActionMessage string `json:"external_action_message,nullable"` + // Browser live view URL for debugging the invocation + LiveViewURL string `json:"live_view_url,nullable"` + // Fields currently awaiting input (present when step=awaiting_input) + PendingFields []DiscoveredField `json:"pending_fields,nullable"` + // SSO buttons available on the page (present when step=awaiting_input) + PendingSSOButtons []AgentAuthInvocationResponsePendingSSOButton `json:"pending_sso_buttons,nullable"` + // Names of fields that have been submitted (present when step=submitting or later) + SubmittedFields []string `json:"submitted_fields,nullable"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - AppName respjson.Field - ExpiresAt respjson.Field - Status respjson.Field - TargetDomain respjson.Field - ExtraFields map[string]respjson.Field - raw string + AppName respjson.Field + Domain respjson.Field + ExpiresAt respjson.Field + Status respjson.Field + Step respjson.Field + Type respjson.Field + ErrorMessage respjson.Field + ExternalActionMessage respjson.Field + LiveViewURL respjson.Field + PendingFields respjson.Field + PendingSSOButtons respjson.Field + SubmittedFields respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -193,36 +178,68 @@ const ( AgentAuthInvocationResponseStatusSuccess AgentAuthInvocationResponseStatus = "SUCCESS" AgentAuthInvocationResponseStatusExpired AgentAuthInvocationResponseStatus = "EXPIRED" AgentAuthInvocationResponseStatusCanceled AgentAuthInvocationResponseStatus = "CANCELED" + AgentAuthInvocationResponseStatusFailed AgentAuthInvocationResponseStatus = "FAILED" +) + +// Current step in the invocation workflow +type AgentAuthInvocationResponseStep string + +const ( + AgentAuthInvocationResponseStepInitialized AgentAuthInvocationResponseStep = "initialized" + AgentAuthInvocationResponseStepDiscovering AgentAuthInvocationResponseStep = "discovering" + AgentAuthInvocationResponseStepAwaitingInput AgentAuthInvocationResponseStep = "awaiting_input" + AgentAuthInvocationResponseStepAwaitingExternalAction AgentAuthInvocationResponseStep = "awaiting_external_action" + AgentAuthInvocationResponseStepSubmitting AgentAuthInvocationResponseStep = "submitting" + AgentAuthInvocationResponseStepCompleted AgentAuthInvocationResponseStep = "completed" + AgentAuthInvocationResponseStepExpired AgentAuthInvocationResponseStep = "expired" +) + +// The invocation type: +// +// - login: First-time authentication +// - reauth: Re-authentication for previously authenticated agents +// - auto_login: Legacy type (no longer created, kept for backward compatibility) +type AgentAuthInvocationResponseType string + +const ( + AgentAuthInvocationResponseTypeLogin AgentAuthInvocationResponseType = "login" + AgentAuthInvocationResponseTypeAutoLogin AgentAuthInvocationResponseType = "auto_login" + AgentAuthInvocationResponseTypeReauth AgentAuthInvocationResponseType = "reauth" ) -// Response from submit endpoint matching SubmitResult schema +// An SSO button for signing in with an external identity provider +type AgentAuthInvocationResponsePendingSSOButton struct { + // Visible button text + Label string `json:"label,required"` + // Identity provider name + Provider string `json:"provider,required"` + // XPath selector for the button + Selector string `json:"selector,required"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Label respjson.Field + Provider respjson.Field + Selector respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r AgentAuthInvocationResponsePendingSSOButton) RawJSON() string { return r.JSON.raw } +func (r *AgentAuthInvocationResponsePendingSSOButton) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Response from submit endpoint - returns immediately after submission is accepted type AgentAuthSubmitResponse struct { - // Whether submission succeeded - Success bool `json:"success,required"` - // Additional fields needed (e.g., OTP) - present when needs_additional_auth is - // true - AdditionalFields []DiscoveredField `json:"additional_fields"` - // App name (only present when logged_in is true) - AppName string `json:"app_name"` - // Error message if submission failed - ErrorMessage string `json:"error_message"` - // Whether user is now logged in - LoggedIn bool `json:"logged_in"` - // Whether additional authentication fields are needed - NeedsAdditionalAuth bool `json:"needs_additional_auth"` - // Target domain (only present when logged_in is true) - TargetDomain string `json:"target_domain"` + // Whether the submission was accepted for processing + Accepted bool `json:"accepted,required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - Success respjson.Field - AdditionalFields respjson.Field - AppName respjson.Field - ErrorMessage respjson.Field - LoggedIn respjson.Field - NeedsAdditionalAuth respjson.Field - TargetDomain respjson.Field - ExtraFields map[string]respjson.Field - raw string + Accepted respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -245,6 +262,10 @@ type AuthAgent struct { // // Any of "AUTHENTICATED", "NEEDS_AUTH". Status AuthAgentStatus `json:"status,required"` + // Additional domains that are valid for this auth agent's authentication flow + // (besides the primary domain). Useful when login pages redirect to different + // domains. + AllowedDomains []string `json:"allowed_domains"` // Whether automatic re-authentication is possible (has credential_id, selectors, // and login_url) CanReauth bool `json:"can_reauth"` @@ -262,6 +283,7 @@ type AuthAgent struct { Domain respjson.Field ProfileName respjson.Field Status respjson.Field + AllowedDomains respjson.Field CanReauth respjson.Field CredentialID respjson.Field CredentialName respjson.Field @@ -288,12 +310,12 @@ const ( // Request to create or find an auth agent // -// The properties ProfileName, TargetDomain are required. +// The properties Domain, ProfileName are required. type AuthAgentCreateRequestParam struct { + // Domain for authentication + Domain string `json:"domain,required"` // Name of the profile to use for this auth agent ProfileName string `json:"profile_name,required"` - // Target domain for authentication - TargetDomain string `json:"target_domain,required"` // Optional name of an existing credential to use for this auth agent. If provided, // the credential will be linked to the agent and its values will be used to // auto-fill the login form on invocation. @@ -301,6 +323,10 @@ type AuthAgentCreateRequestParam struct { // Optional login page URL. If provided, will be stored on the agent and used to // skip discovery in future invocations. LoginURL param.Opt[string] `json:"login_url,omitzero" format:"uri"` + // Additional domains that are valid for this auth agent's authentication flow + // (besides the primary domain). Useful when login pages redirect to different + // domains. + AllowedDomains []string `json:"allowed_domains,omitzero"` // Optional proxy configuration Proxy AuthAgentCreateRequestProxyParam `json:"proxy,omitzero"` paramObj @@ -350,102 +376,8 @@ func (r *AuthAgentInvocationCreateRequestParam) UnmarshalJSON(data []byte) error return apijson.UnmarshalRoot(data, r) } -// AuthAgentInvocationCreateResponseUnion contains all possible properties and -// values from [AuthAgentInvocationCreateResponseAlreadyAuthenticated], -// [AuthAgentInvocationCreateResponseInvocationCreated]. -// -// Use the [AuthAgentInvocationCreateResponseUnion.AsAny] method to switch on the -// variant. -// -// Use the methods beginning with 'As' to cast the union to one of its variants. -type AuthAgentInvocationCreateResponseUnion struct { - // Any of "already_authenticated", "invocation_created". - Status string `json:"status"` - // This field is from variant [AuthAgentInvocationCreateResponseInvocationCreated]. - ExpiresAt time.Time `json:"expires_at"` - // This field is from variant [AuthAgentInvocationCreateResponseInvocationCreated]. - HandoffCode string `json:"handoff_code"` - // This field is from variant [AuthAgentInvocationCreateResponseInvocationCreated]. - HostedURL string `json:"hosted_url"` - // This field is from variant [AuthAgentInvocationCreateResponseInvocationCreated]. - InvocationID string `json:"invocation_id"` - JSON struct { - Status respjson.Field - ExpiresAt respjson.Field - HandoffCode respjson.Field - HostedURL respjson.Field - InvocationID respjson.Field - raw string - } `json:"-"` -} - -// anyAuthAgentInvocationCreateResponse is implemented by each variant of -// [AuthAgentInvocationCreateResponseUnion] to add type safety for the return type -// of [AuthAgentInvocationCreateResponseUnion.AsAny] -type anyAuthAgentInvocationCreateResponse interface { - implAuthAgentInvocationCreateResponseUnion() -} - -func (AuthAgentInvocationCreateResponseAlreadyAuthenticated) implAuthAgentInvocationCreateResponseUnion() { -} -func (AuthAgentInvocationCreateResponseInvocationCreated) implAuthAgentInvocationCreateResponseUnion() { -} - -// Use the following switch statement to find the correct variant -// -// switch variant := AuthAgentInvocationCreateResponseUnion.AsAny().(type) { -// case kernel.AuthAgentInvocationCreateResponseAlreadyAuthenticated: -// case kernel.AuthAgentInvocationCreateResponseInvocationCreated: -// default: -// fmt.Errorf("no variant present") -// } -func (u AuthAgentInvocationCreateResponseUnion) AsAny() anyAuthAgentInvocationCreateResponse { - switch u.Status { - case "already_authenticated": - return u.AsAlreadyAuthenticated() - case "invocation_created": - return u.AsInvocationCreated() - } - return nil -} - -func (u AuthAgentInvocationCreateResponseUnion) AsAlreadyAuthenticated() (v AuthAgentInvocationCreateResponseAlreadyAuthenticated) { - apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) - return -} - -func (u AuthAgentInvocationCreateResponseUnion) AsInvocationCreated() (v AuthAgentInvocationCreateResponseInvocationCreated) { - apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) - return -} - -// Returns the unmodified JSON received from the API -func (u AuthAgentInvocationCreateResponseUnion) RawJSON() string { return u.JSON.raw } - -func (r *AuthAgentInvocationCreateResponseUnion) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - -// Response when the agent is already authenticated. -type AuthAgentInvocationCreateResponseAlreadyAuthenticated struct { - // Indicates the agent is already authenticated and no invocation was created. - Status constant.AlreadyAuthenticated `json:"status,required"` - // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. - JSON struct { - Status respjson.Field - ExtraFields map[string]respjson.Field - raw string - } `json:"-"` -} - -// Returns the unmodified JSON received from the API -func (r AuthAgentInvocationCreateResponseAlreadyAuthenticated) RawJSON() string { return r.JSON.raw } -func (r *AuthAgentInvocationCreateResponseAlreadyAuthenticated) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - -// Response when a new invocation was created. -type AuthAgentInvocationCreateResponseInvocationCreated struct { +// Response from creating an invocation. Always returns an invocation_id. +type AuthAgentInvocationCreateResponse struct { // When the handoff code expires. ExpiresAt time.Time `json:"expires_at,required" format:"date-time"` // One-time code for handoff. @@ -454,26 +386,45 @@ type AuthAgentInvocationCreateResponseInvocationCreated struct { HostedURL string `json:"hosted_url,required" format:"uri"` // Unique identifier for the invocation. InvocationID string `json:"invocation_id,required"` - // Indicates an invocation was created. - Status constant.InvocationCreated `json:"status,required"` + // The invocation type: + // + // - login: First-time authentication + // - reauth: Re-authentication for previously authenticated agents + // - auto_login: Legacy type (no longer created, kept for backward compatibility) + // + // Any of "login", "auto_login", "reauth". + Type AuthAgentInvocationCreateResponseType `json:"type,required"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { ExpiresAt respjson.Field HandoffCode respjson.Field HostedURL respjson.Field InvocationID respjson.Field - Status respjson.Field + Type respjson.Field ExtraFields map[string]respjson.Field raw string } `json:"-"` } // Returns the unmodified JSON received from the API -func (r AuthAgentInvocationCreateResponseInvocationCreated) RawJSON() string { return r.JSON.raw } -func (r *AuthAgentInvocationCreateResponseInvocationCreated) UnmarshalJSON(data []byte) error { +func (r AuthAgentInvocationCreateResponse) RawJSON() string { return r.JSON.raw } +func (r *AuthAgentInvocationCreateResponse) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +// The invocation type: +// +// - login: First-time authentication +// - reauth: Re-authentication for previously authenticated agents +// - auto_login: Legacy type (no longer created, kept for backward compatibility) +type AuthAgentInvocationCreateResponseType string + +const ( + AuthAgentInvocationCreateResponseTypeLogin AuthAgentInvocationCreateResponseType = "login" + AuthAgentInvocationCreateResponseTypeAutoLogin AuthAgentInvocationCreateResponseType = "auto_login" + AuthAgentInvocationCreateResponseTypeReauth AuthAgentInvocationCreateResponseType = "reauth" +) + // A discovered form field type DiscoveredField struct { // Field label @@ -484,7 +435,7 @@ type DiscoveredField struct { Selector string `json:"selector,required"` // Field type // - // Any of "text", "email", "password", "tel", "number", "url", "code". + // Any of "text", "email", "password", "tel", "number", "url", "code", "totp". Type DiscoveredFieldType `json:"type,required"` // Field placeholder Placeholder string `json:"placeholder"` @@ -520,41 +471,7 @@ const ( DiscoveredFieldTypeNumber DiscoveredFieldType = "number" DiscoveredFieldTypeURL DiscoveredFieldType = "url" DiscoveredFieldTypeCode DiscoveredFieldType = "code" -) - -// Response from triggering re-authentication -type ReauthResponse struct { - // Result of the re-authentication attempt - // - // Any of "reauth_started", "already_authenticated", "cannot_reauth". - Status ReauthResponseStatus `json:"status,required"` - // ID of the re-auth invocation if one was created - InvocationID string `json:"invocation_id"` - // Human-readable description of the result - Message string `json:"message"` - // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. - JSON struct { - Status respjson.Field - InvocationID respjson.Field - Message respjson.Field - ExtraFields map[string]respjson.Field - raw string - } `json:"-"` -} - -// Returns the unmodified JSON received from the API -func (r ReauthResponse) RawJSON() string { return r.JSON.raw } -func (r *ReauthResponse) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - -// Result of the re-authentication attempt -type ReauthResponseStatus string - -const ( - ReauthResponseStatusReauthStarted ReauthResponseStatus = "reauth_started" - ReauthResponseStatusAlreadyAuthenticated ReauthResponseStatus = "already_authenticated" - ReauthResponseStatusCannotReauth ReauthResponseStatus = "cannot_reauth" + DiscoveredFieldTypeTotp DiscoveredFieldType = "totp" ) type AgentAuthNewParams struct { @@ -571,14 +488,14 @@ func (r *AgentAuthNewParams) UnmarshalJSON(data []byte) error { } type AgentAuthListParams struct { + // Filter by domain + Domain param.Opt[string] `query:"domain,omitzero" json:"-"` // Maximum number of results to return Limit param.Opt[int64] `query:"limit,omitzero" json:"-"` // Number of results to skip Offset param.Opt[int64] `query:"offset,omitzero" json:"-"` // Filter by profile name ProfileName param.Opt[string] `query:"profile_name,omitzero" json:"-"` - // Filter by target domain - TargetDomain param.Opt[string] `query:"target_domain,omitzero" json:"-"` paramObj } diff --git a/agentauth_test.go b/agentauth_test.go index 6368098..72a1a95 100644 --- a/agentauth_test.go +++ b/agentauth_test.go @@ -28,8 +28,9 @@ func TestAgentAuthNewWithOptionalParams(t *testing.T) { ) _, err := client.Agents.Auth.New(context.TODO(), kernel.AgentAuthNewParams{ AuthAgentCreateRequest: kernel.AuthAgentCreateRequestParam{ + Domain: "netflix.com", ProfileName: "user-123", - TargetDomain: "netflix.com", + AllowedDomains: []string{"login.netflix.com", "auth.netflix.com"}, CredentialName: kernel.String("my-netflix-login"), LoginURL: kernel.String("https://netflix.com/login"), Proxy: kernel.AuthAgentCreateRequestProxyParam{ @@ -83,10 +84,10 @@ func TestAgentAuthListWithOptionalParams(t *testing.T) { option.WithAPIKey("My API Key"), ) _, err := client.Agents.Auth.List(context.TODO(), kernel.AgentAuthListParams{ - Limit: kernel.Int(100), - Offset: kernel.Int(0), - ProfileName: kernel.String("profile_name"), - TargetDomain: kernel.String("target_domain"), + Domain: kernel.String("domain"), + Limit: kernel.Int(100), + Offset: kernel.Int(0), + ProfileName: kernel.String("profile_name"), }) if err != nil { var apierr *kernel.Error @@ -119,26 +120,3 @@ func TestAgentAuthDelete(t *testing.T) { t.Fatalf("err should be nil: %s", err.Error()) } } - -func TestAgentAuthReauth(t *testing.T) { - t.Skip("Prism tests are disabled") - baseURL := "http://localhost:4010" - if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { - baseURL = envURL - } - if !testutil.CheckTestServer(t, baseURL) { - return - } - client := kernel.NewClient( - option.WithBaseURL(baseURL), - option.WithAPIKey("My API Key"), - ) - _, err := client.Agents.Auth.Reauth(context.TODO(), "id") - if err != nil { - var apierr *kernel.Error - if errors.As(err, &apierr) { - t.Log(string(apierr.DumpRequest(true))) - } - t.Fatalf("err should be nil: %s", err.Error()) - } -} diff --git a/agentauthinvocation.go b/agentauthinvocation.go index 878146f..c8d5ec8 100644 --- a/agentauthinvocation.go +++ b/agentauthinvocation.go @@ -40,15 +40,15 @@ func NewAgentAuthInvocationService(opts ...option.RequestOption) (r AgentAuthInv // Creates a new authentication invocation for the specified auth agent. This // starts the auth flow and returns a hosted URL for the user to complete // authentication. -func (r *AgentAuthInvocationService) New(ctx context.Context, body AgentAuthInvocationNewParams, opts ...option.RequestOption) (res *AuthAgentInvocationCreateResponseUnion, err error) { +func (r *AgentAuthInvocationService) New(ctx context.Context, body AgentAuthInvocationNewParams, opts ...option.RequestOption) (res *AuthAgentInvocationCreateResponse, err error) { opts = slices.Concat(r.Options, opts) path := "agents/auth/invocations" err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) return } -// Returns invocation details including app_name and target_domain. Uses the JWT -// returned by the exchange endpoint, or standard API key or JWT authentication. +// Returns invocation details including status, app_name, and domain. Supports both +// API key and JWT (from exchange endpoint) authentication. func (r *AgentAuthInvocationService) Get(ctx context.Context, invocationID string, opts ...option.RequestOption) (res *AgentAuthInvocationResponse, err error) { opts = slices.Concat(r.Options, opts) if invocationID == "" { @@ -60,20 +60,6 @@ func (r *AgentAuthInvocationService) Get(ctx context.Context, invocationID strin return } -// Inspects the target site to detect logged-in state or discover required fields. -// Returns 200 with success: true when fields are found, or 4xx/5xx for failures. -// Requires the JWT returned by the exchange endpoint. -func (r *AgentAuthInvocationService) Discover(ctx context.Context, invocationID string, body AgentAuthInvocationDiscoverParams, opts ...option.RequestOption) (res *AgentAuthDiscoverResponse, err error) { - opts = slices.Concat(r.Options, opts) - if invocationID == "" { - err = errors.New("missing required invocation_id parameter") - return - } - path := fmt.Sprintf("agents/auth/invocations/%s/discover", invocationID) - err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, body, &res, opts...) - return -} - // Validates the handoff code and returns a JWT token for subsequent requests. No // authentication required (the handoff code serves as the credential). func (r *AgentAuthInvocationService) Exchange(ctx context.Context, invocationID string, body AgentAuthInvocationExchangeParams, opts ...option.RequestOption) (res *AgentAuthInvocationExchangeResponse, err error) { @@ -87,8 +73,9 @@ func (r *AgentAuthInvocationService) Exchange(ctx context.Context, invocationID return } -// Submits field values for the discovered login form and may return additional -// auth fields or success. Requires the JWT returned by the exchange endpoint. +// Submits field values for the discovered login form. Returns immediately after +// submission is accepted. Poll the invocation endpoint to track progress and get +// results. func (r *AgentAuthInvocationService) Submit(ctx context.Context, invocationID string, body AgentAuthInvocationSubmitParams, opts ...option.RequestOption) (res *AgentAuthSubmitResponse, err error) { opts = slices.Concat(r.Options, opts) if invocationID == "" { @@ -134,21 +121,6 @@ func (r *AgentAuthInvocationNewParams) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &r.AuthAgentInvocationCreateRequest) } -type AgentAuthInvocationDiscoverParams struct { - // Optional login page URL. If provided, will override the stored login URL for - // this discovery invocation and skip Phase 1 discovery. - LoginURL param.Opt[string] `json:"login_url,omitzero" format:"uri"` - paramObj -} - -func (r AgentAuthInvocationDiscoverParams) MarshalJSON() (data []byte, err error) { - type shadow AgentAuthInvocationDiscoverParams - return param.MarshalObject(r, (*shadow)(&r)) -} -func (r *AgentAuthInvocationDiscoverParams) UnmarshalJSON(data []byte) error { - return apijson.UnmarshalRoot(data, r) -} - type AgentAuthInvocationExchangeParams struct { // Handoff code from start endpoint Code string `json:"code,required"` @@ -164,15 +136,52 @@ func (r *AgentAuthInvocationExchangeParams) UnmarshalJSON(data []byte) error { } type AgentAuthInvocationSubmitParams struct { + + // + // Request body variants + // + + // This field is a request body variant, only one variant field can be set. + OfFieldValues *AgentAuthInvocationSubmitParamsBodyFieldValues `json:",inline"` + // This field is a request body variant, only one variant field can be set. + OfSSOButton *AgentAuthInvocationSubmitParamsBodySSOButton `json:",inline"` + + paramObj +} + +func (u AgentAuthInvocationSubmitParams) MarshalJSON() ([]byte, error) { + return param.MarshalUnion(u, u.OfFieldValues, u.OfSSOButton) +} +func (r *AgentAuthInvocationSubmitParams) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The property FieldValues is required. +type AgentAuthInvocationSubmitParamsBodyFieldValues struct { // Values for the discovered login fields FieldValues map[string]string `json:"field_values,omitzero,required"` paramObj } -func (r AgentAuthInvocationSubmitParams) MarshalJSON() (data []byte, err error) { - type shadow AgentAuthInvocationSubmitParams +func (r AgentAuthInvocationSubmitParamsBodyFieldValues) MarshalJSON() (data []byte, err error) { + type shadow AgentAuthInvocationSubmitParamsBodyFieldValues return param.MarshalObject(r, (*shadow)(&r)) } -func (r *AgentAuthInvocationSubmitParams) UnmarshalJSON(data []byte) error { +func (r *AgentAuthInvocationSubmitParamsBodyFieldValues) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// The property SSOButton is required. +type AgentAuthInvocationSubmitParamsBodySSOButton struct { + // Selector of SSO button to click + SSOButton string `json:"sso_button,required"` + paramObj +} + +func (r AgentAuthInvocationSubmitParamsBodySSOButton) MarshalJSON() (data []byte, err error) { + type shadow AgentAuthInvocationSubmitParamsBodySSOButton + return param.MarshalObject(r, (*shadow)(&r)) +} +func (r *AgentAuthInvocationSubmitParamsBodySSOButton) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } diff --git a/agentauthinvocation_test.go b/agentauthinvocation_test.go index c0f6090..1e7186a 100644 --- a/agentauthinvocation_test.go +++ b/agentauthinvocation_test.go @@ -64,35 +64,6 @@ func TestAgentAuthInvocationGet(t *testing.T) { } } -func TestAgentAuthInvocationDiscoverWithOptionalParams(t *testing.T) { - t.Skip("Prism tests are disabled") - baseURL := "http://localhost:4010" - if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { - baseURL = envURL - } - if !testutil.CheckTestServer(t, baseURL) { - return - } - client := kernel.NewClient( - option.WithBaseURL(baseURL), - option.WithAPIKey("My API Key"), - ) - _, err := client.Agents.Auth.Invocations.Discover( - context.TODO(), - "invocation_id", - kernel.AgentAuthInvocationDiscoverParams{ - LoginURL: kernel.String("https://doordash.com/account/login"), - }, - ) - if err != nil { - var apierr *kernel.Error - if errors.As(err, &apierr) { - t.Log(string(apierr.DumpRequest(true))) - } - t.Fatalf("err should be nil: %s", err.Error()) - } -} - func TestAgentAuthInvocationExchange(t *testing.T) { t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" @@ -139,9 +110,11 @@ func TestAgentAuthInvocationSubmit(t *testing.T) { context.TODO(), "invocation_id", kernel.AgentAuthInvocationSubmitParams{ - FieldValues: map[string]string{ - "email": "user@example.com", - "password": "********", + OfFieldValues: &kernel.AgentAuthInvocationSubmitParamsBodyFieldValues{ + FieldValues: map[string]string{ + "email": "user@example.com", + "password": "********", + }, }, }, ) diff --git a/api.md b/api.md index 44374eb..e849839 100644 --- a/api.md +++ b/api.md @@ -205,6 +205,7 @@ Response Types: - kernel.ProxyNewResponse - kernel.ProxyGetResponse - kernel.ProxyListResponse +- kernel.ProxyCheckResponse Methods: @@ -212,6 +213,7 @@ Methods: - client.Proxies.Get(ctx context.Context, id string) (kernel.ProxyGetResponse, error) - client.Proxies.List(ctx context.Context) ([]kernel.ProxyListResponse, error) - client.Proxies.Delete(ctx context.Context, id string) error +- client.Proxies.Check(ctx context.Context, id string) (kernel.ProxyCheckResponse, error) # Extensions @@ -257,13 +259,11 @@ Params Types: Response Types: -- kernel.AgentAuthDiscoverResponse - kernel.AgentAuthInvocationResponse - kernel.AgentAuthSubmitResponse - kernel.AuthAgent -- kernel.AuthAgentInvocationCreateResponseUnion +- kernel.AuthAgentInvocationCreateResponse - kernel.DiscoveredField -- kernel.ReauthResponse Methods: @@ -271,7 +271,6 @@ Methods: - client.Agents.Auth.Get(ctx context.Context, id string) (kernel.AuthAgent, error) - client.Agents.Auth.List(ctx context.Context, query kernel.AgentAuthListParams) (pagination.OffsetPagination[kernel.AuthAgent], error) - client.Agents.Auth.Delete(ctx context.Context, id string) error -- client.Agents.Auth.Reauth(ctx context.Context, id string) (kernel.ReauthResponse, error) ### Invocations @@ -281,9 +280,8 @@ Response Types: Methods: -- client.Agents.Auth.Invocations.New(ctx context.Context, body kernel.AgentAuthInvocationNewParams) (kernel.AuthAgentInvocationCreateResponseUnion, error) +- client.Agents.Auth.Invocations.New(ctx context.Context, body kernel.AgentAuthInvocationNewParams) (kernel.AuthAgentInvocationCreateResponse, error) - client.Agents.Auth.Invocations.Get(ctx context.Context, invocationID string) (kernel.AgentAuthInvocationResponse, error) -- client.Agents.Auth.Invocations.Discover(ctx context.Context, invocationID string, body kernel.AgentAuthInvocationDiscoverParams) (kernel.AgentAuthDiscoverResponse, error) - client.Agents.Auth.Invocations.Exchange(ctx context.Context, invocationID string, body kernel.AgentAuthInvocationExchangeParams) (kernel.AgentAuthInvocationExchangeResponse, error) - client.Agents.Auth.Invocations.Submit(ctx context.Context, invocationID string, body kernel.AgentAuthInvocationSubmitParams) (kernel.AgentAuthSubmitResponse, error) @@ -297,11 +295,13 @@ Params Types: Response Types: - kernel.Credential +- kernel.CredentialTotpCodeResponse Methods: - client.Credentials.New(ctx context.Context, body kernel.CredentialNewParams) (kernel.Credential, error) -- client.Credentials.Get(ctx context.Context, id string) (kernel.Credential, error) -- client.Credentials.Update(ctx context.Context, id string, body kernel.CredentialUpdateParams) (kernel.Credential, error) +- client.Credentials.Get(ctx context.Context, idOrName string) (kernel.Credential, error) +- client.Credentials.Update(ctx context.Context, idOrName string, body kernel.CredentialUpdateParams) (kernel.Credential, error) - client.Credentials.List(ctx context.Context, query kernel.CredentialListParams) (pagination.OffsetPagination[kernel.Credential], error) -- client.Credentials.Delete(ctx context.Context, id string) error +- client.Credentials.Delete(ctx context.Context, idOrName string) error +- client.Credentials.TotpCode(ctx context.Context, idOrName string) (kernel.CredentialTotpCodeResponse, error) diff --git a/credential.go b/credential.go index 2eb2c0c..774a7d2 100644 --- a/credential.go +++ b/credential.go @@ -41,8 +41,7 @@ func NewCredentialService(opts ...option.RequestOption) (r CredentialService) { return } -// Create a new credential for storing login information. Values are encrypted at -// rest. +// Create a new credential for storing login information. func (r *CredentialService) New(ctx context.Context, body CredentialNewParams, opts ...option.RequestOption) (res *Credential, err error) { opts = slices.Concat(r.Options, opts) path := "credentials" @@ -50,26 +49,27 @@ func (r *CredentialService) New(ctx context.Context, body CredentialNewParams, o return } -// Retrieve a credential by its ID. Credential values are not returned. -func (r *CredentialService) Get(ctx context.Context, id string, opts ...option.RequestOption) (res *Credential, err error) { +// Retrieve a credential by its ID or name. Credential values are not returned. +func (r *CredentialService) Get(ctx context.Context, idOrName string, opts ...option.RequestOption) (res *Credential, err error) { opts = slices.Concat(r.Options, opts) - if id == "" { - err = errors.New("missing required id parameter") + if idOrName == "" { + err = errors.New("missing required id_or_name parameter") return } - path := fmt.Sprintf("credentials/%s", id) + path := fmt.Sprintf("credentials/%s", idOrName) err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) return } -// Update a credential's name or values. Values are encrypted at rest. -func (r *CredentialService) Update(ctx context.Context, id string, body CredentialUpdateParams, opts ...option.RequestOption) (res *Credential, err error) { +// Update a credential's name or values. When values are provided, they are merged +// with existing values (new keys are added, existing keys are overwritten). +func (r *CredentialService) Update(ctx context.Context, idOrName string, body CredentialUpdateParams, opts ...option.RequestOption) (res *Credential, err error) { opts = slices.Concat(r.Options, opts) - if id == "" { - err = errors.New("missing required id parameter") + if idOrName == "" { + err = errors.New("missing required id_or_name parameter") return } - path := fmt.Sprintf("credentials/%s", id) + path := fmt.Sprintf("credentials/%s", idOrName) err = requestconfig.ExecuteNewRequest(ctx, http.MethodPatch, path, body, &res, opts...) return } @@ -99,19 +99,33 @@ func (r *CredentialService) ListAutoPaging(ctx context.Context, query Credential return pagination.NewOffsetPaginationAutoPager(r.List(ctx, query, opts...)) } -// Delete a credential by its ID. -func (r *CredentialService) Delete(ctx context.Context, id string, opts ...option.RequestOption) (err error) { +// Delete a credential by its ID or name. +func (r *CredentialService) Delete(ctx context.Context, idOrName string, opts ...option.RequestOption) (err error) { opts = slices.Concat(r.Options, opts) opts = append([]option.RequestOption{option.WithHeader("Accept", "*/*")}, opts...) - if id == "" { - err = errors.New("missing required id parameter") + if idOrName == "" { + err = errors.New("missing required id_or_name parameter") return } - path := fmt.Sprintf("credentials/%s", id) + path := fmt.Sprintf("credentials/%s", idOrName) err = requestconfig.ExecuteNewRequest(ctx, http.MethodDelete, path, nil, nil, opts...) return } +// Returns the current 6-digit TOTP code for a credential with a configured +// totp_secret. Use this to complete 2FA setup on sites or when you need a fresh +// code. +func (r *CredentialService) TotpCode(ctx context.Context, idOrName string, opts ...option.RequestOption) (res *CredentialTotpCodeResponse, err error) { + opts = slices.Concat(r.Options, opts) + if idOrName == "" { + err = errors.New("missing required id_or_name parameter") + return + } + path := fmt.Sprintf("credentials/%s/totp-code", idOrName) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodGet, path, nil, &res, opts...) + return +} + // Request to create a new credential // // The properties Domain, Name, Values are required. @@ -122,6 +136,14 @@ type CreateCredentialRequestParam struct { Name string `json:"name,required"` // Field name to value mapping (e.g., username, password) Values map[string]string `json:"values,omitzero,required"` + // If set, indicates this credential should be used with the specified SSO provider + // (e.g., google, github, microsoft). When the target site has a matching SSO + // button, it will be clicked first before filling credential values on the + // identity provider's login page. + SSOProvider param.Opt[string] `json:"sso_provider,omitzero"` + // Base32-encoded TOTP secret for generating one-time passwords. Used for automatic + // 2FA during login. + TotpSecret param.Opt[string] `json:"totp_secret,omitzero"` paramObj } @@ -145,15 +167,31 @@ type Credential struct { Name string `json:"name,required"` // When the credential was last updated UpdatedAt time.Time `json:"updated_at,required" format:"date-time"` + // Whether this credential has a TOTP secret configured for automatic 2FA + HasTotpSecret bool `json:"has_totp_secret"` + // If set, indicates this credential should be used with the specified SSO provider + // (e.g., google, github, microsoft). When the target site has a matching SSO + // button, it will be clicked first before filling credential values on the + // identity provider's login page. + SSOProvider string `json:"sso_provider,nullable"` + // Current 6-digit TOTP code. Only included in create/update responses when + // totp_secret was just set. + TotpCode string `json:"totp_code"` + // When the totp_code expires. Only included when totp_code is present. + TotpCodeExpiresAt time.Time `json:"totp_code_expires_at" format:"date-time"` // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. JSON struct { - ID respjson.Field - CreatedAt respjson.Field - Domain respjson.Field - Name respjson.Field - UpdatedAt respjson.Field - ExtraFields map[string]respjson.Field - raw string + ID respjson.Field + CreatedAt respjson.Field + Domain respjson.Field + Name respjson.Field + UpdatedAt respjson.Field + HasTotpSecret respjson.Field + SSOProvider respjson.Field + TotpCode respjson.Field + TotpCodeExpiresAt respjson.Field + ExtraFields map[string]respjson.Field + raw string } `json:"-"` } @@ -165,10 +203,16 @@ func (r *Credential) UnmarshalJSON(data []byte) error { // Request to update an existing credential type UpdateCredentialRequestParam struct { + // If set, indicates this credential should be used with the specified SSO + // provider. Set to empty string or null to remove. + SSOProvider param.Opt[string] `json:"sso_provider,omitzero"` // New name for the credential Name param.Opt[string] `json:"name,omitzero"` - // Field name to value mapping (e.g., username, password). Replaces all existing - // values. + // Base32-encoded TOTP secret for generating one-time passwords. Spaces and + // formatting are automatically normalized. Set to empty string to remove. + TotpSecret param.Opt[string] `json:"totp_secret,omitzero"` + // Field name to value mapping. Values are merged with existing values (new keys + // added, existing keys overwritten). Values map[string]string `json:"values,omitzero"` paramObj } @@ -181,6 +225,26 @@ func (r *UpdateCredentialRequestParam) UnmarshalJSON(data []byte) error { return apijson.UnmarshalRoot(data, r) } +type CredentialTotpCodeResponse struct { + // Current 6-digit TOTP code + Code string `json:"code,required"` + // When this code expires (ISO 8601 timestamp) + ExpiresAt time.Time `json:"expires_at,required" format:"date-time"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Code respjson.Field + ExpiresAt respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r CredentialTotpCodeResponse) RawJSON() string { return r.JSON.raw } +func (r *CredentialTotpCodeResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + type CredentialNewParams struct { // Request to create a new credential CreateCredentialRequest CreateCredentialRequestParam diff --git a/credential_test.go b/credential_test.go index 810805f..3e66392 100644 --- a/credential_test.go +++ b/credential_test.go @@ -13,7 +13,7 @@ import ( "github.com/onkernel/kernel-go-sdk/option" ) -func TestCredentialNew(t *testing.T) { +func TestCredentialNewWithOptionalParams(t *testing.T) { t.Skip("Prism tests are disabled") baseURL := "http://localhost:4010" if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { @@ -34,6 +34,8 @@ func TestCredentialNew(t *testing.T) { "username": "user@example.com", "password": "mysecretpassword", }, + SSOProvider: kernel.String("google"), + TotpSecret: kernel.String("JBSWY3DPEHPK3PXP"), }, }) if err != nil { @@ -58,7 +60,7 @@ func TestCredentialGet(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - _, err := client.Credentials.Get(context.TODO(), "id") + _, err := client.Credentials.Get(context.TODO(), "id_or_name") if err != nil { var apierr *kernel.Error if errors.As(err, &apierr) { @@ -83,10 +85,12 @@ func TestCredentialUpdateWithOptionalParams(t *testing.T) { ) _, err := client.Credentials.Update( context.TODO(), - "id", + "id_or_name", kernel.CredentialUpdateParams{ UpdateCredentialRequest: kernel.UpdateCredentialRequestParam{ - Name: kernel.String("my-updated-login"), + Name: kernel.String("my-updated-login"), + SSOProvider: kernel.String("google"), + TotpSecret: kernel.String("JBSWY3DPEHPK3PXP"), Values: map[string]string{ "username": "user@example.com", "password": "newpassword", @@ -143,7 +147,30 @@ func TestCredentialDelete(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) - err := client.Credentials.Delete(context.TODO(), "id") + err := client.Credentials.Delete(context.TODO(), "id_or_name") + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} + +func TestCredentialTotpCode(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Credentials.TotpCode(context.TODO(), "id_or_name") if err != nil { var apierr *kernel.Error if errors.As(err, &apierr) { diff --git a/internal/apijson/enum.go b/internal/apijson/enum.go index 18b218a..5bef11c 100644 --- a/internal/apijson/enum.go +++ b/internal/apijson/enum.go @@ -29,7 +29,7 @@ type validatorFunc func(reflect.Value) exactness var validators sync.Map var validationRegistry = map[reflect.Type][]validationEntry{} -func RegisterFieldValidator[T any, V string | bool | int](fieldName string, values ...V) { +func RegisterFieldValidator[T any, V string | bool | int | float64](fieldName string, values ...V) { var t T parentType := reflect.TypeOf(t) diff --git a/internal/version.go b/internal/version.go index 5f694dc..de6cef8 100644 --- a/internal/version.go +++ b/internal/version.go @@ -2,4 +2,4 @@ package internal -const PackageVersion = "0.24.0" // x-release-please-version +const PackageVersion = "0.25.0" // x-release-please-version diff --git a/proxy.go b/proxy.go index 5d254e6..dfb074e 100644 --- a/proxy.go +++ b/proxy.go @@ -78,6 +78,18 @@ func (r *ProxyService) Delete(ctx context.Context, id string, opts ...option.Req return } +// Run a health check on the proxy to verify it's working. +func (r *ProxyService) Check(ctx context.Context, id string, opts ...option.RequestOption) (res *ProxyCheckResponse, err error) { + opts = slices.Concat(r.Options, opts) + if id == "" { + err = errors.New("missing required id parameter") + return + } + path := fmt.Sprintf("proxies/%s/check", id) + err = requestconfig.ExecuteNewRequest(ctx, http.MethodPost, path, nil, &res, opts...) + return +} + // Configuration for routing traffic through a proxy. type ProxyNewResponse struct { // Proxy type to use. In terms of quality for avoiding bot-detection, from best to @@ -942,6 +954,294 @@ const ( ProxyListResponseStatusUnavailable ProxyListResponseStatus = "unavailable" ) +// Configuration for routing traffic through a proxy. +type ProxyCheckResponse struct { + // Proxy type to use. In terms of quality for avoiding bot-detection, from best to + // worst: `mobile` > `residential` > `isp` > `datacenter`. + // + // Any of "datacenter", "isp", "residential", "mobile", "custom". + Type ProxyCheckResponseType `json:"type,required"` + ID string `json:"id"` + // Configuration specific to the selected proxy `type`. + Config ProxyCheckResponseConfigUnion `json:"config"` + // Timestamp of the last health check performed on this proxy. + LastChecked time.Time `json:"last_checked" format:"date-time"` + // Readable name of the proxy. + Name string `json:"name"` + // Protocol to use for the proxy connection. + // + // Any of "http", "https". + Protocol ProxyCheckResponseProtocol `json:"protocol"` + // Current health status of the proxy. + // + // Any of "available", "unavailable". + Status ProxyCheckResponseStatus `json:"status"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Type respjson.Field + ID respjson.Field + Config respjson.Field + LastChecked respjson.Field + Name respjson.Field + Protocol respjson.Field + Status respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ProxyCheckResponse) RawJSON() string { return r.JSON.raw } +func (r *ProxyCheckResponse) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Proxy type to use. In terms of quality for avoiding bot-detection, from best to +// worst: `mobile` > `residential` > `isp` > `datacenter`. +type ProxyCheckResponseType string + +const ( + ProxyCheckResponseTypeDatacenter ProxyCheckResponseType = "datacenter" + ProxyCheckResponseTypeIsp ProxyCheckResponseType = "isp" + ProxyCheckResponseTypeResidential ProxyCheckResponseType = "residential" + ProxyCheckResponseTypeMobile ProxyCheckResponseType = "mobile" + ProxyCheckResponseTypeCustom ProxyCheckResponseType = "custom" +) + +// ProxyCheckResponseConfigUnion contains all possible properties and values from +// [ProxyCheckResponseConfigDatacenterProxyConfig], +// [ProxyCheckResponseConfigIspProxyConfig], +// [ProxyCheckResponseConfigResidentialProxyConfig], +// [ProxyCheckResponseConfigMobileProxyConfig], +// [ProxyCheckResponseConfigCustomProxyConfig]. +// +// Use the methods beginning with 'As' to cast the union to one of its variants. +type ProxyCheckResponseConfigUnion struct { + Country string `json:"country"` + Asn string `json:"asn"` + City string `json:"city"` + // This field is from variant [ProxyCheckResponseConfigResidentialProxyConfig]. + Os string `json:"os"` + State string `json:"state"` + Zip string `json:"zip"` + // This field is from variant [ProxyCheckResponseConfigMobileProxyConfig]. + Carrier string `json:"carrier"` + // This field is from variant [ProxyCheckResponseConfigCustomProxyConfig]. + Host string `json:"host"` + // This field is from variant [ProxyCheckResponseConfigCustomProxyConfig]. + Port int64 `json:"port"` + // This field is from variant [ProxyCheckResponseConfigCustomProxyConfig]. + HasPassword bool `json:"has_password"` + // This field is from variant [ProxyCheckResponseConfigCustomProxyConfig]. + Username string `json:"username"` + JSON struct { + Country respjson.Field + Asn respjson.Field + City respjson.Field + Os respjson.Field + State respjson.Field + Zip respjson.Field + Carrier respjson.Field + Host respjson.Field + Port respjson.Field + HasPassword respjson.Field + Username respjson.Field + raw string + } `json:"-"` +} + +func (u ProxyCheckResponseConfigUnion) AsProxyCheckResponseConfigDatacenterProxyConfig() (v ProxyCheckResponseConfigDatacenterProxyConfig) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u ProxyCheckResponseConfigUnion) AsProxyCheckResponseConfigIspProxyConfig() (v ProxyCheckResponseConfigIspProxyConfig) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u ProxyCheckResponseConfigUnion) AsProxyCheckResponseConfigResidentialProxyConfig() (v ProxyCheckResponseConfigResidentialProxyConfig) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u ProxyCheckResponseConfigUnion) AsProxyCheckResponseConfigMobileProxyConfig() (v ProxyCheckResponseConfigMobileProxyConfig) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +func (u ProxyCheckResponseConfigUnion) AsProxyCheckResponseConfigCustomProxyConfig() (v ProxyCheckResponseConfigCustomProxyConfig) { + apijson.UnmarshalRoot(json.RawMessage(u.JSON.raw), &v) + return +} + +// Returns the unmodified JSON received from the API +func (u ProxyCheckResponseConfigUnion) RawJSON() string { return u.JSON.raw } + +func (r *ProxyCheckResponseConfigUnion) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Configuration for a datacenter proxy. +type ProxyCheckResponseConfigDatacenterProxyConfig struct { + // ISO 3166 country code. Defaults to US if not provided. + Country string `json:"country"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Country respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ProxyCheckResponseConfigDatacenterProxyConfig) RawJSON() string { return r.JSON.raw } +func (r *ProxyCheckResponseConfigDatacenterProxyConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Configuration for an ISP proxy. +type ProxyCheckResponseConfigIspProxyConfig struct { + // ISO 3166 country code. Defaults to US if not provided. + Country string `json:"country"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Country respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ProxyCheckResponseConfigIspProxyConfig) RawJSON() string { return r.JSON.raw } +func (r *ProxyCheckResponseConfigIspProxyConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Configuration for residential proxies. +type ProxyCheckResponseConfigResidentialProxyConfig struct { + // Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html + Asn string `json:"asn"` + // City name (no spaces, e.g. `sanfrancisco`). If provided, `country` must also be + // provided. + City string `json:"city"` + // ISO 3166 country code. + Country string `json:"country"` + // Operating system of the residential device. + // + // Any of "windows", "macos", "android". + // + // Deprecated: deprecated + Os string `json:"os"` + // Two-letter state code. + State string `json:"state"` + // US ZIP code. + Zip string `json:"zip"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Asn respjson.Field + City respjson.Field + Country respjson.Field + Os respjson.Field + State respjson.Field + Zip respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ProxyCheckResponseConfigResidentialProxyConfig) RawJSON() string { return r.JSON.raw } +func (r *ProxyCheckResponseConfigResidentialProxyConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Configuration for mobile proxies. +type ProxyCheckResponseConfigMobileProxyConfig struct { + // Autonomous system number. See https://bgp.potaroo.net/cidr/autnums.html + Asn string `json:"asn"` + // Mobile carrier. + // + // Any of "a1", "aircel", "airtel", "att", "celcom", "chinamobile", "claro", + // "comcast", "cox", "digi", "dt", "docomo", "dtac", "etisalat", "idea", + // "kyivstar", "meo", "megafon", "mtn", "mtnza", "mts", "optus", "orange", "qwest", + // "reliance_jio", "robi", "sprint", "telefonica", "telstra", "tmobile", "tigo", + // "tim", "verizon", "vimpelcom", "vodacomza", "vodafone", "vivo", "zain", + // "vivabo", "telenormyanmar", "kcelljsc", "swisscom", "singtel", "asiacell", + // "windit", "cellc", "ooredoo", "drei", "umobile", "cableone", "proximus", + // "tele2", "mobitel", "o2", "bouygues", "free", "sfr", "digicel". + Carrier string `json:"carrier"` + // City name (no spaces, e.g. `sanfrancisco`). If provided, `country` must also be + // provided. + City string `json:"city"` + // ISO 3166 country code + Country string `json:"country"` + // Two-letter state code. + State string `json:"state"` + // US ZIP code. + Zip string `json:"zip"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Asn respjson.Field + Carrier respjson.Field + City respjson.Field + Country respjson.Field + State respjson.Field + Zip respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ProxyCheckResponseConfigMobileProxyConfig) RawJSON() string { return r.JSON.raw } +func (r *ProxyCheckResponseConfigMobileProxyConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Configuration for a custom proxy (e.g., private proxy server). +type ProxyCheckResponseConfigCustomProxyConfig struct { + // Proxy host address or IP. + Host string `json:"host,required"` + // Proxy port. + Port int64 `json:"port,required"` + // Whether the proxy has a password. + HasPassword bool `json:"has_password"` + // Username for proxy authentication. + Username string `json:"username"` + // JSON contains metadata for fields, check presence with [respjson.Field.Valid]. + JSON struct { + Host respjson.Field + Port respjson.Field + HasPassword respjson.Field + Username respjson.Field + ExtraFields map[string]respjson.Field + raw string + } `json:"-"` +} + +// Returns the unmodified JSON received from the API +func (r ProxyCheckResponseConfigCustomProxyConfig) RawJSON() string { return r.JSON.raw } +func (r *ProxyCheckResponseConfigCustomProxyConfig) UnmarshalJSON(data []byte) error { + return apijson.UnmarshalRoot(data, r) +} + +// Protocol to use for the proxy connection. +type ProxyCheckResponseProtocol string + +const ( + ProxyCheckResponseProtocolHTTP ProxyCheckResponseProtocol = "http" + ProxyCheckResponseProtocolHTTPS ProxyCheckResponseProtocol = "https" +) + +// Current health status of the proxy. +type ProxyCheckResponseStatus string + +const ( + ProxyCheckResponseStatusAvailable ProxyCheckResponseStatus = "available" + ProxyCheckResponseStatusUnavailable ProxyCheckResponseStatus = "unavailable" +) + type ProxyNewParams struct { // Proxy type to use. In terms of quality for avoiding bot-detection, from best to // worst: `mobile` > `residential` > `isp` > `datacenter`. diff --git a/proxy_test.go b/proxy_test.go index 31d5f96..b8ffa5a 100644 --- a/proxy_test.go +++ b/proxy_test.go @@ -113,3 +113,26 @@ func TestProxyDelete(t *testing.T) { t.Fatalf("err should be nil: %s", err.Error()) } } + +func TestProxyCheck(t *testing.T) { + t.Skip("Prism tests are disabled") + baseURL := "http://localhost:4010" + if envURL, ok := os.LookupEnv("TEST_API_BASE_URL"); ok { + baseURL = envURL + } + if !testutil.CheckTestServer(t, baseURL) { + return + } + client := kernel.NewClient( + option.WithBaseURL(baseURL), + option.WithAPIKey("My API Key"), + ) + _, err := client.Proxies.Check(context.TODO(), "id") + if err != nil { + var apierr *kernel.Error + if errors.As(err, &apierr) { + t.Log(string(apierr.DumpRequest(true))) + } + t.Fatalf("err should be nil: %s", err.Error()) + } +} diff --git a/shared/constant/constants.go b/shared/constant/constants.go index c2d9711..7909c29 100644 --- a/shared/constant/constants.go +++ b/shared/constant/constants.go @@ -18,35 +18,29 @@ func ValueOf[T Constant[T]]() T { return t.Default() } -type AlreadyAuthenticated string // Always "already_authenticated" -type AppVersionSummary string // Always "app_version_summary" -type AwsUsEast1a string // Always "aws.us-east-1a" -type DeploymentState string // Always "deployment_state" -type Error string // Always "error" -type InvocationCreated string // Always "invocation_created" -type InvocationState string // Always "invocation_state" -type Log string // Always "log" -type SseHeartbeat string // Always "sse_heartbeat" - -func (c AlreadyAuthenticated) Default() AlreadyAuthenticated { return "already_authenticated" } -func (c AppVersionSummary) Default() AppVersionSummary { return "app_version_summary" } -func (c AwsUsEast1a) Default() AwsUsEast1a { return "aws.us-east-1a" } -func (c DeploymentState) Default() DeploymentState { return "deployment_state" } -func (c Error) Default() Error { return "error" } -func (c InvocationCreated) Default() InvocationCreated { return "invocation_created" } -func (c InvocationState) Default() InvocationState { return "invocation_state" } -func (c Log) Default() Log { return "log" } -func (c SseHeartbeat) Default() SseHeartbeat { return "sse_heartbeat" } - -func (c AlreadyAuthenticated) MarshalJSON() ([]byte, error) { return marshalString(c) } -func (c AppVersionSummary) MarshalJSON() ([]byte, error) { return marshalString(c) } -func (c AwsUsEast1a) MarshalJSON() ([]byte, error) { return marshalString(c) } -func (c DeploymentState) MarshalJSON() ([]byte, error) { return marshalString(c) } -func (c Error) MarshalJSON() ([]byte, error) { return marshalString(c) } -func (c InvocationCreated) MarshalJSON() ([]byte, error) { return marshalString(c) } -func (c InvocationState) MarshalJSON() ([]byte, error) { return marshalString(c) } -func (c Log) MarshalJSON() ([]byte, error) { return marshalString(c) } -func (c SseHeartbeat) MarshalJSON() ([]byte, error) { return marshalString(c) } +type AppVersionSummary string // Always "app_version_summary" +type AwsUsEast1a string // Always "aws.us-east-1a" +type DeploymentState string // Always "deployment_state" +type Error string // Always "error" +type InvocationState string // Always "invocation_state" +type Log string // Always "log" +type SseHeartbeat string // Always "sse_heartbeat" + +func (c AppVersionSummary) Default() AppVersionSummary { return "app_version_summary" } +func (c AwsUsEast1a) Default() AwsUsEast1a { return "aws.us-east-1a" } +func (c DeploymentState) Default() DeploymentState { return "deployment_state" } +func (c Error) Default() Error { return "error" } +func (c InvocationState) Default() InvocationState { return "invocation_state" } +func (c Log) Default() Log { return "log" } +func (c SseHeartbeat) Default() SseHeartbeat { return "sse_heartbeat" } + +func (c AppVersionSummary) MarshalJSON() ([]byte, error) { return marshalString(c) } +func (c AwsUsEast1a) MarshalJSON() ([]byte, error) { return marshalString(c) } +func (c DeploymentState) MarshalJSON() ([]byte, error) { return marshalString(c) } +func (c Error) MarshalJSON() ([]byte, error) { return marshalString(c) } +func (c InvocationState) MarshalJSON() ([]byte, error) { return marshalString(c) } +func (c Log) MarshalJSON() ([]byte, error) { return marshalString(c) } +func (c SseHeartbeat) MarshalJSON() ([]byte, error) { return marshalString(c) } type constant[T any] interface { Constant[T] diff --git a/usage_test.go b/usage_test.go index 5be681f..bab979d 100644 --- a/usage_test.go +++ b/usage_test.go @@ -24,6 +24,7 @@ func TestUsage(t *testing.T) { option.WithBaseURL(baseURL), option.WithAPIKey("My API Key"), ) + t.Skip("Prism tests are disabled") browser, err := client.Browsers.New(context.TODO(), kernel.BrowserNewParams{ Stealth: kernel.Bool(true), })