diff --git a/lib/privy/public_api/services/policies.rb b/lib/privy/public_api/services/policies.rb index f369713..c9ea4f8 100644 --- a/lib/privy/public_api/services/policies.rb +++ b/lib/privy/public_api/services/policies.rb @@ -66,16 +66,20 @@ def create(policy_create_params:, idempotency_key: nil, request_options: nil) # @option policy_update_params [String, nil] :owner_id Key quorum ID to set as owner. # @option policy_update_params [Array, nil] :rules New rules for the policy. # @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned policies. + # @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the + # request expires. Defaults to the value computed by the client's + # PrivyRequestExpiryOptions. # @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries). # # @return [Privy::Models::Policy] - def update(policy_id, policy_update_params:, authorization_context: nil, request_options: nil) + def update(policy_id, policy_update_params:, authorization_context: nil, request_expiry: nil, request_options: nil) prepared = Privy::Authorization.prepare_request( privy_client, method: :patch, url: Privy::Authorization.signed_url(privy_client, "v1/policies/#{policy_id}"), body: policy_update_params, - authorization_context: authorization_context + authorization_context: authorization_context, + request_expiry: privy_client.compute_request_expiry(request_expiry) ) combined_params = policy_update_params.merge(request_options: request_options) Privy::Authorization.merge_prepared_headers!(combined_params, prepared.headers) @@ -92,16 +96,20 @@ def update(policy_id, policy_update_params:, authorization_context: nil, request # # @param policy_id [String] ID of the policy to delete. # @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned policies. + # @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the + # request expires. Defaults to the value computed by the client's + # PrivyRequestExpiryOptions. # @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries). # # @return [Privy::Models::SuccessResponse] - def delete(policy_id, authorization_context: nil, request_options: nil) + def delete(policy_id, authorization_context: nil, request_expiry: nil, request_options: nil) prepared = Privy::Authorization.prepare_request( privy_client, method: :delete, url: Privy::Authorization.signed_url(privy_client, "v1/policies/#{policy_id}"), body: "", - authorization_context: authorization_context + authorization_context: authorization_context, + request_expiry: privy_client.compute_request_expiry(request_expiry) ) # Workaround: the Stainless-generated Ruby client sends Content-Type: application/json on # every request (lib/privy/internal/transport/base_client.rb:204), even bodyless DELETEs. @@ -136,16 +144,20 @@ def delete(policy_id, authorization_context: nil, request_options: nil) # @option policy_create_rule_params [String] :action Action when the rule matches ("ALLOW" or "DENY", required). # @option policy_create_rule_params [Array] :conditions Array of condition objects (required). # @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned policies. + # @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the + # request expires. Defaults to the value computed by the client's + # PrivyRequestExpiryOptions. # @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries). # # @return [Privy::Models::PolicyRuleResponse] - def create_rule(policy_id, policy_create_rule_params:, authorization_context: nil, request_options: nil) + def create_rule(policy_id, policy_create_rule_params:, authorization_context: nil, request_expiry: nil, request_options: nil) prepared = Privy::Authorization.prepare_request( privy_client, method: :post, url: Privy::Authorization.signed_url(privy_client, "v1/policies/#{policy_id}/rules"), body: policy_create_rule_params, - authorization_context: authorization_context + authorization_context: authorization_context, + request_expiry: privy_client.compute_request_expiry(request_expiry) ) combined_params = policy_create_rule_params.merge(request_options: request_options) Privy::Authorization.merge_prepared_headers!(combined_params, prepared.headers) @@ -176,6 +188,9 @@ def create_rule(policy_id, policy_create_rule_params:, authorization_context: ni # @option policy_update_rule_params [String] :action Action when the rule matches ("ALLOW" or "DENY", required). # @option policy_update_rule_params [Array] :conditions Array of condition objects (required). # @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned policies. + # @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the + # request expires. Defaults to the value computed by the client's + # PrivyRequestExpiryOptions. # @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries). # # @return [Privy::Models::PolicyRuleResponse] @@ -184,6 +199,7 @@ def update_rule( policy_id:, policy_update_rule_params:, authorization_context: nil, + request_expiry: nil, request_options: nil ) prepared = Privy::Authorization.prepare_request( @@ -191,7 +207,8 @@ def update_rule( method: :patch, url: Privy::Authorization.signed_url(privy_client, "v1/policies/#{policy_id}/rules/#{rule_id}"), body: policy_update_rule_params, - authorization_context: authorization_context + authorization_context: authorization_context, + request_expiry: privy_client.compute_request_expiry(request_expiry) ) combined_params = policy_update_rule_params.merge( policy_id: policy_id, @@ -210,16 +227,20 @@ def update_rule( # @param rule_id [String] ID of the rule to delete. # @param policy_id [String] ID of the policy the rule belongs to. # @param authorization_context [Privy::Authorization::AuthorizationContext, nil] Authorization context for owned policies. + # @param request_expiry [Integer, nil] Absolute Unix-ms timestamp at which the + # request expires. Defaults to the value computed by the client's + # PrivyRequestExpiryOptions. # @param request_options [Privy::RequestOptions, Hash, nil] Transport-level config (timeouts, retries). # # @return [Privy::Models::SuccessResponse] - def delete_rule(rule_id, policy_id:, authorization_context: nil, request_options: nil) + def delete_rule(rule_id, policy_id:, authorization_context: nil, request_expiry: nil, request_options: nil) prepared = Privy::Authorization.prepare_request( privy_client, method: :delete, url: Privy::Authorization.signed_url(privy_client, "v1/policies/#{policy_id}/rules/#{rule_id}"), body: "", - authorization_context: authorization_context + authorization_context: authorization_context, + request_expiry: privy_client.compute_request_expiry(request_expiry) ) # Workaround: same Content-Type issue as delete — see comment there. opts = (request_options || {}).merge(extra_headers: {"Content-Type" => nil}) diff --git a/test/privy/public_api/services/policies_test.rb b/test/privy/public_api/services/policies_test.rb new file mode 100644 index 0000000..ab3e74a --- /dev/null +++ b/test/privy/public_api/services/policies_test.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require_relative "../../test_helper" + +class Privy::Services::PoliciesTest < Minitest::Test + extend Minitest::Serial + include WebMock::API + + BASE_URL = "https://api.staging.privy.io" + + def before_all + super + WebMock.enable! + end + + def after_all + WebMock.disable! + super + end + + def teardown + WebMock.reset! + super + end + + def build_client(**opts) + Privy::PrivyClient.new(app_id: "app-abc", app_secret: "secret", environment: :staging, **opts) + end + + def policy_response_body(**overrides) + { + id: "p-1", + chain_type: "ethereum", + created_at: 0, + name: "test policy", + owner_id: nil, + rules: [], + version: "1.0" + }.merge(overrides).to_json + end + + def stub_json(method, url, body:) + stub_request(method, url).to_return( + status: 200, + body: body, + headers: {"content-type" => "application/json"} + ) + end + + # --- request_expiry on update ----------------------------------------------- + + def test_update_default_request_expiry_is_auto_set + stub_json(:patch, "#{BASE_URL}/v1/policies/p-1", body: policy_response_body(name: "n")) + before = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) + build_client.policies.update("p-1", policy_update_params: {name: "n"}) + assert_requested(:patch, "#{BASE_URL}/v1/policies/p-1") do |req| + sent = req.headers["Privy-Request-Expiry"].to_i + assert_in_delta(before + (15 * 60 * 1_000), sent, 5_000) + end + end + + def test_update_per_call_request_expiry_overrides_default + stub_json(:patch, "#{BASE_URL}/v1/policies/p-1", body: policy_response_body(name: "n")) + build_client.policies.update( + "p-1", + policy_update_params: {name: "n"}, + request_expiry: 1_750_000_000_000 + ) + assert_requested(:patch, "#{BASE_URL}/v1/policies/p-1") do |req| + assert_equal("1750000000000", req.headers["Privy-Request-Expiry"]) + end + end + + def test_update_disabled_options_omit_header + stub_json(:patch, "#{BASE_URL}/v1/policies/p-1", body: policy_response_body(name: "n")) + build_client(request_expiry: Privy::PrivyRequestExpiryOptions.build(disabled: true)) + .policies.update("p-1", policy_update_params: {name: "n"}) + assert_requested(:patch, "#{BASE_URL}/v1/policies/p-1") do |req| + refute(req.headers.key?("Privy-Request-Expiry")) + end + end + + def test_update_disabled_options_still_honor_explicit_override + stub_json(:patch, "#{BASE_URL}/v1/policies/p-1", body: policy_response_body(name: "n")) + build_client(request_expiry: Privy::PrivyRequestExpiryOptions.build(disabled: true)) + .policies.update("p-1", policy_update_params: {name: "n"}, request_expiry: 1_750_000_000_000) + assert_requested(:patch, "#{BASE_URL}/v1/policies/p-1") do |req| + assert_equal("1750000000000", req.headers["Privy-Request-Expiry"]) + end + end + + # --- request_expiry on delete / create_rule / update_rule / delete_rule ---- + + def test_delete_per_call_request_expiry_overrides_default + stub_request(:delete, "#{BASE_URL}/v1/policies/p-1").to_return( + status: 200, body: '{"success":true}', headers: {"content-type" => "application/json"} + ) + build_client.policies.delete("p-1", request_expiry: 1_750_000_000_000) + assert_requested(:delete, "#{BASE_URL}/v1/policies/p-1") do |req| + assert_equal("1750000000000", req.headers["Privy-Request-Expiry"]) + end + end + + def test_create_rule_per_call_request_expiry_overrides_default + stub_json( + :post, + "#{BASE_URL}/v1/policies/p-1/rules", + body: '{"id":"r-1","name":"n","method":"eth_sendTransaction","action":"ALLOW","conditions":[]}' + ) + build_client.policies.create_rule( + "p-1", + policy_create_rule_params: { + name: "n", method: "eth_sendTransaction", action: "ALLOW", conditions: [] + }, + request_expiry: 1_750_000_000_000 + ) + assert_requested(:post, "#{BASE_URL}/v1/policies/p-1/rules") do |req| + assert_equal("1750000000000", req.headers["Privy-Request-Expiry"]) + end + end + + def test_update_rule_per_call_request_expiry_overrides_default + stub_json( + :patch, + "#{BASE_URL}/v1/policies/p-1/rules/r-1", + body: '{"id":"r-1","name":"n","method":"eth_sendTransaction","action":"ALLOW","conditions":[]}' + ) + build_client.policies.update_rule( + "r-1", + policy_id: "p-1", + policy_update_rule_params: { + name: "n", method: "eth_sendTransaction", action: "ALLOW", conditions: [] + }, + request_expiry: 1_750_000_000_000 + ) + assert_requested(:patch, "#{BASE_URL}/v1/policies/p-1/rules/r-1") do |req| + assert_equal("1750000000000", req.headers["Privy-Request-Expiry"]) + end + end + + def test_delete_rule_per_call_request_expiry_overrides_default + stub_request(:delete, "#{BASE_URL}/v1/policies/p-1/rules/r-1").to_return( + status: 200, body: '{"success":true}', headers: {"content-type" => "application/json"} + ) + build_client.policies.delete_rule("r-1", policy_id: "p-1", request_expiry: 1_750_000_000_000) + assert_requested(:delete, "#{BASE_URL}/v1/policies/p-1/rules/r-1") do |req| + assert_equal("1750000000000", req.headers["Privy-Request-Expiry"]) + end + end +end