noresponse 5.1.1 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/lib/no_response/accounts.rb +663 -71
  3. data/lib/no_response/late_results.rb +10 -8
  4. data/lib/no_response/models/operations/listaccounts_request.rb +52 -0
  5. data/lib/no_response/models/operations/listaccounts_request.rbi +26 -0
  6. data/lib/no_response/models/operations/listaccounts_response.rb +32 -0
  7. data/lib/no_response/models/operations/listaccounts_response.rbi +20 -0
  8. data/lib/no_response/models/operations/recordenforcement_request.rb +33 -0
  9. data/lib/no_response/models/operations/recordenforcement_request.rbi +20 -0
  10. data/lib/no_response/models/operations/recordenforcement_response.rb +32 -0
  11. data/lib/no_response/models/operations/recordenforcement_response.rbi +20 -0
  12. data/lib/no_response/models/operations.rb +4 -0
  13. data/lib/no_response/models/shared/accountdeleted.rb +49 -0
  14. data/lib/no_response/models/shared/accountdeleted.rbi +26 -0
  15. data/lib/no_response/models/shared/accountdeletedevent.rb +70 -0
  16. data/lib/no_response/models/shared/accountdeletedevent.rbi +38 -0
  17. data/lib/no_response/models/shared/accountlist.rb +40 -0
  18. data/lib/no_response/models/shared/accountlist.rbi +20 -0
  19. data/lib/no_response/models/shared/accountriskchanged.rb +44 -0
  20. data/lib/no_response/models/shared/accountriskchanged.rbi +20 -0
  21. data/lib/no_response/models/shared/accountriskchangedevent.rb +70 -0
  22. data/lib/no_response/models/shared/accountriskchangedevent.rbi +38 -0
  23. data/lib/no_response/models/shared/assessmentexpired.rb +82 -0
  24. data/lib/no_response/models/shared/assessmentexpired.rbi +38 -0
  25. data/lib/no_response/models/shared/assessmentupdateddata.rb +23 -0
  26. data/lib/no_response/models/shared/assessmentupdateddata.rbi +9 -0
  27. data/lib/no_response/models/shared/assessmentupdatedevent.rb +79 -0
  28. data/lib/no_response/models/shared/assessmentupdatedevent.rbi +38 -0
  29. data/lib/no_response/models/shared/conflicterrorcode.rb +1 -0
  30. data/lib/no_response/models/shared/endpointverification.rb +41 -0
  31. data/lib/no_response/models/shared/endpointverification.rbi +20 -0
  32. data/lib/no_response/models/shared/endpointverificationevent.rb +70 -0
  33. data/lib/no_response/models/shared/endpointverificationevent.rbi +38 -0
  34. data/lib/no_response/models/shared/enforcement.rb +156 -0
  35. data/lib/no_response/models/shared/enforcement.rbi +68 -0
  36. data/lib/no_response/models/shared/enforcementaction.rb +23 -0
  37. data/lib/no_response/models/shared/enforcementaction.rbi +9 -0
  38. data/lib/no_response/models/shared/enforcementprovider.rb +21 -0
  39. data/lib/no_response/models/shared/enforcementprovider.rbi +9 -0
  40. data/lib/no_response/models/shared/enforcementrequest.rb +122 -0
  41. data/lib/no_response/models/shared/enforcementrequest.rbi +50 -0
  42. data/lib/no_response/models/shared/enforcementstate.rb +22 -0
  43. data/lib/no_response/models/shared/enforcementstate.rbi +9 -0
  44. data/lib/no_response/models/shared/enforcementstatus.rb +19 -0
  45. data/lib/no_response/models/shared/enforcementstatus.rbi +9 -0
  46. data/lib/no_response/models/shared/environment.rb +16 -0
  47. data/lib/no_response/models/shared/environment.rbi +9 -0
  48. data/lib/no_response/models/shared/webhookendpointdisabled.rb +49 -0
  49. data/lib/no_response/models/shared/webhookendpointdisabled.rbi +26 -0
  50. data/lib/no_response/models/shared/webhookendpointdisabledevent.rb +70 -0
  51. data/lib/no_response/models/shared/webhookendpointdisabledevent.rbi +38 -0
  52. data/lib/no_response/models/shared/webhookevent.rb +23 -0
  53. data/lib/no_response/models/shared/webhookevent.rbi +9 -0
  54. data/lib/no_response/models/shared/webhookverificationresponse.rb +33 -0
  55. data/lib/no_response/models/shared/webhookverificationresponse.rbi +14 -0
  56. data/lib/no_response/models/shared/workspacedeleted.rb +55 -0
  57. data/lib/no_response/models/shared/workspacedeleted.rbi +26 -0
  58. data/lib/no_response/models/shared/workspacedeletedevent.rb +70 -0
  59. data/lib/no_response/models/shared/workspacedeletedevent.rbi +38 -0
  60. data/lib/no_response/models/shared.rb +23 -0
  61. data/lib/no_response/sdk_hooks/registration.rb +0 -1
  62. data/lib/no_response/sdkconfiguration.rb +2 -2
  63. data/lib/no_response/webhook_verification.rb +170 -0
  64. data/lib/noresponse.rb +2 -0
  65. metadata +56 -2
  66. data/lib/no_response/webhooks.rb +0 -264
@@ -0,0 +1,70 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module NoResponse
5
+ module Models
6
+ module Shared
7
+ # The webhook_endpoint.disabled event.
8
+ class WebhookEndpointDisabledEvent
9
+ extend T::Sig
10
+ include Crystalline::MetadataFields
11
+
12
+ # The event id, equal to the NR-Webhook-Id header and across redeliveries and every endpoint the event fans out to.
13
+ field :id, ::String, {'format_json': {'letter_case': ::NoResponse::Utils.field_name("id"), required: true}}
14
+ # When the event was created. Equal across redeliveries; the signing timestamp header is per delivery.
15
+ field(
16
+ :created_at,
17
+ ::String,
18
+ {'format_json': {'letter_case': ::NoResponse::Utils.field_name("created_at"), required: true}}
19
+ )
20
+ # One of the environment's endpoints stopped receiving events: every delivery attempt to it failed from failing_since until disabled_at, 72 hours or more. Its queued deliveries are dropped. Fix the receiver and verify it again with POST /v1/webhooks/{endpoint_id}/test (type endpoint.verification) to resume delivery.
21
+ field(
22
+ :data,
23
+ Models::Shared::WebhookEndpointDisabled,
24
+ {'format_json': {'letter_case': ::NoResponse::Utils.field_name("data"), required: true}}
25
+ )
26
+ # Always webhook_endpoint.disabled.
27
+ field :type, ::String, {'format_json': {'letter_case': ::NoResponse::Utils.field_name("type"), required: true}}
28
+ # Present, and true, only on an event sent by POST /v1/webhooks/{endpoint_id}/test: its data is a fixed example assessment, not one of the workspace's, so a receiver acknowledges it and applies nothing. Real events have no test member.
29
+ field(
30
+ :test,
31
+ Crystalline::Nilable.new(Crystalline::Boolean.new),
32
+ {'format_json': {'letter_case': ::NoResponse::Utils.field_name("test")}}
33
+ )
34
+
35
+ sig {
36
+ params(
37
+ id: ::String,
38
+ created_at: ::String,
39
+ data: Models::Shared::WebhookEndpointDisabled,
40
+ type: ::String,
41
+ test: T.nilable(T::Boolean)
42
+ )
43
+ .void
44
+ }
45
+ def initialize(id:, created_at:, data:, type: "webhook_endpoint.disabled", test: nil)
46
+ @id = id
47
+ @created_at = created_at
48
+ @data = data
49
+ unless type == "webhook_endpoint.disabled"
50
+ raise ArgumentError, "Invalid value for type"
51
+ end
52
+
53
+ @type = "webhook_endpoint.disabled"
54
+ @test = test
55
+ end
56
+
57
+ sig { params(other: T.untyped).returns(T::Boolean) }
58
+ def ==(other)
59
+ return false unless other.is_a?(self.class)
60
+ return false unless @id == other.id
61
+ return false unless @created_at == other.created_at
62
+ return false unless @data == other.data
63
+ return false unless @type == other.type
64
+ return false unless @test == other.test
65
+ true
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,38 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class NoResponse::Models::Shared::WebhookEndpointDisabledEvent
5
+ extend ::Crystalline::MetadataFields::ClassMethods
6
+ end
7
+
8
+ class NoResponse::Models::Shared::WebhookEndpointDisabledEvent
9
+ def id
10
+ end
11
+
12
+ def id=(str_)
13
+ end
14
+
15
+ def created_at
16
+ end
17
+
18
+ def created_at=(str_)
19
+ end
20
+
21
+ def data
22
+ end
23
+
24
+ def data=(str_)
25
+ end
26
+
27
+ def type
28
+ end
29
+
30
+ def type=(str_)
31
+ end
32
+
33
+ def test
34
+ end
35
+
36
+ def test=(str_)
37
+ end
38
+ end
@@ -0,0 +1,23 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module NoResponse
5
+ module Models
6
+ module Shared
7
+ # The body of every webhook delivery: {id, type, created_at, data}, where type chooses the shape of data. Deliveries are at least once and unordered: deduplicate on id, and keep an assessment only when its detail.assessment_version is newer than the one you hold. GET /v1/assessments/{assessment_id} stays authoritative.
8
+ class WebhookEvent
9
+ extend T::Sig
10
+ include Crystalline::MetadataFields
11
+
12
+ def initialize
13
+ end
14
+
15
+ sig { params(other: T.untyped).returns(T::Boolean) }
16
+ def ==(other)
17
+ return false unless other.is_a?(self.class)
18
+ true
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class NoResponse::Models::Shared::WebhookEvent
5
+ extend ::Crystalline::MetadataFields::ClassMethods
6
+ end
7
+
8
+ class NoResponse::Models::Shared::WebhookEvent
9
+ end
@@ -0,0 +1,33 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module NoResponse
5
+ module Models
6
+ module Shared
7
+ # The body an endpoint returns, with status 200 and Content-Type application/json within 5 seconds, to an endpoint.verification event. A 2xx without the echoed challenge does not verify the endpoint.
8
+ class WebhookVerificationResponse
9
+ extend T::Sig
10
+ include Crystalline::MetadataFields
11
+
12
+ # Endpoint verification nonce: 32 random bytes, unpadded base64url (43 characters). Fresh per verification.
13
+ field(
14
+ :challenge,
15
+ ::String,
16
+ {'format_json': {'letter_case': ::NoResponse::Utils.field_name("challenge"), required: true}}
17
+ )
18
+
19
+ sig { params(challenge: ::String).void }
20
+ def initialize(challenge:)
21
+ @challenge = challenge
22
+ end
23
+
24
+ sig { params(other: T.untyped).returns(T::Boolean) }
25
+ def ==(other)
26
+ return false unless other.is_a?(self.class)
27
+ return false unless @challenge == other.challenge
28
+ true
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class NoResponse::Models::Shared::WebhookVerificationResponse
5
+ extend ::Crystalline::MetadataFields::ClassMethods
6
+ end
7
+
8
+ class NoResponse::Models::Shared::WebhookVerificationResponse
9
+ def challenge
10
+ end
11
+
12
+ def challenge=(str_)
13
+ end
14
+ end
@@ -0,0 +1,55 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module NoResponse
5
+ module Models
6
+ module Shared
7
+ # Terminal for the receiving endpoint's environment: its workspace was deleted. Every key of the environment already answers 401, its assessments and accounts are gone, and no further event follows. The workspace id is not reused, so a later event naming it cannot arrive.
8
+ class WorkspaceDeleted
9
+ extend T::Sig
10
+ include Crystalline::MetadataFields
11
+
12
+ # The workspace id, which is also its live environment's workspace id; the test environment's is the same id with :test appended. A workspace created through the API is tnt_ and a 26-character ULID.
13
+ field(
14
+ :workspace_id,
15
+ ::String,
16
+ {'format_json': {'letter_case': ::NoResponse::Utils.field_name("workspace_id"), required: true}}
17
+ )
18
+ # The environment whose endpoints receive this event: live, or test for the workspace's test environment.
19
+ field(
20
+ :environment,
21
+ Models::Shared::Environment,
22
+ {
23
+ 'format_json': {
24
+ 'letter_case': ::NoResponse::Utils.field_name("environment"),
25
+ required: true,
26
+ 'decoder': ::NoResponse::Utils.enum_from_string(Models::Shared::Environment, false)
27
+ }
28
+ }
29
+ )
30
+ # When the deletion took effect, equal to deleted_at in the DELETE answer.
31
+ field(
32
+ :deleted_at,
33
+ ::String,
34
+ {'format_json': {'letter_case': ::NoResponse::Utils.field_name("deleted_at"), required: true}}
35
+ )
36
+
37
+ sig { params(workspace_id: ::String, environment: Models::Shared::Environment, deleted_at: ::String).void }
38
+ def initialize(workspace_id:, environment:, deleted_at:)
39
+ @workspace_id = workspace_id
40
+ @environment = environment
41
+ @deleted_at = deleted_at
42
+ end
43
+
44
+ sig { params(other: T.untyped).returns(T::Boolean) }
45
+ def ==(other)
46
+ return false unless other.is_a?(self.class)
47
+ return false unless @workspace_id == other.workspace_id
48
+ return false unless @environment == other.environment
49
+ return false unless @deleted_at == other.deleted_at
50
+ true
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,26 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class NoResponse::Models::Shared::WorkspaceDeleted
5
+ extend ::Crystalline::MetadataFields::ClassMethods
6
+ end
7
+
8
+ class NoResponse::Models::Shared::WorkspaceDeleted
9
+ def workspace_id
10
+ end
11
+
12
+ def workspace_id=(str_)
13
+ end
14
+
15
+ def environment
16
+ end
17
+
18
+ def environment=(str_)
19
+ end
20
+
21
+ def deleted_at
22
+ end
23
+
24
+ def deleted_at=(str_)
25
+ end
26
+ end
@@ -0,0 +1,70 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module NoResponse
5
+ module Models
6
+ module Shared
7
+ # The workspace.deleted event.
8
+ class WorkspaceDeletedEvent
9
+ extend T::Sig
10
+ include Crystalline::MetadataFields
11
+
12
+ # The event id, equal to the NR-Webhook-Id header and across redeliveries and every endpoint the event fans out to.
13
+ field :id, ::String, {'format_json': {'letter_case': ::NoResponse::Utils.field_name("id"), required: true}}
14
+ # When the event was created. Equal across redeliveries; the signing timestamp header is per delivery.
15
+ field(
16
+ :created_at,
17
+ ::String,
18
+ {'format_json': {'letter_case': ::NoResponse::Utils.field_name("created_at"), required: true}}
19
+ )
20
+ # Terminal for the receiving endpoint's environment: its workspace was deleted. Every key of the environment already answers 401, its assessments and accounts are gone, and no further event follows. The workspace id is not reused, so a later event naming it cannot arrive.
21
+ field(
22
+ :data,
23
+ Models::Shared::WorkspaceDeleted,
24
+ {'format_json': {'letter_case': ::NoResponse::Utils.field_name("data"), required: true}}
25
+ )
26
+ # Always workspace.deleted.
27
+ field :type, ::String, {'format_json': {'letter_case': ::NoResponse::Utils.field_name("type"), required: true}}
28
+ # Present, and true, only on an event sent by POST /v1/webhooks/{endpoint_id}/test: its data is a fixed example assessment, not one of the workspace's, so a receiver acknowledges it and applies nothing. Real events have no test member.
29
+ field(
30
+ :test,
31
+ Crystalline::Nilable.new(Crystalline::Boolean.new),
32
+ {'format_json': {'letter_case': ::NoResponse::Utils.field_name("test")}}
33
+ )
34
+
35
+ sig {
36
+ params(
37
+ id: ::String,
38
+ created_at: ::String,
39
+ data: Models::Shared::WorkspaceDeleted,
40
+ type: ::String,
41
+ test: T.nilable(T::Boolean)
42
+ )
43
+ .void
44
+ }
45
+ def initialize(id:, created_at:, data:, type: "workspace.deleted", test: nil)
46
+ @id = id
47
+ @created_at = created_at
48
+ @data = data
49
+ unless type == "workspace.deleted"
50
+ raise ArgumentError, "Invalid value for type"
51
+ end
52
+
53
+ @type = "workspace.deleted"
54
+ @test = test
55
+ end
56
+
57
+ sig { params(other: T.untyped).returns(T::Boolean) }
58
+ def ==(other)
59
+ return false unless other.is_a?(self.class)
60
+ return false unless @id == other.id
61
+ return false unless @created_at == other.created_at
62
+ return false unless @data == other.data
63
+ return false unless @type == other.type
64
+ return false unless @test == other.test
65
+ true
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,38 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class NoResponse::Models::Shared::WorkspaceDeletedEvent
5
+ extend ::Crystalline::MetadataFields::ClassMethods
6
+ end
7
+
8
+ class NoResponse::Models::Shared::WorkspaceDeletedEvent
9
+ def id
10
+ end
11
+
12
+ def id=(str_)
13
+ end
14
+
15
+ def created_at
16
+ end
17
+
18
+ def created_at=(str_)
19
+ end
20
+
21
+ def data
22
+ end
23
+
24
+ def data=(str_)
25
+ end
26
+
27
+ def type
28
+ end
29
+
30
+ def type=(str_)
31
+ end
32
+
33
+ def test
34
+ end
35
+
36
+ def test=(str_)
37
+ end
38
+ end
@@ -7,8 +7,13 @@ module NoResponse
7
7
  autoload :Account, "no_response/models/shared/account.rb"
8
8
  autoload :AccountDecision, "no_response/models/shared/accountdecision.rb"
9
9
  autoload :AccountDecisionsByFlow, "no_response/models/shared/accountdecisionsbyflow.rb"
10
+ autoload :AccountDeleted, "no_response/models/shared/accountdeleted.rb"
11
+ autoload :AccountDeletedEvent, "no_response/models/shared/accountdeletedevent.rb"
10
12
  autoload :AccountDeletion, "no_response/models/shared/accountdeletion.rb"
13
+ autoload :AccountList, "no_response/models/shared/accountlist.rb"
11
14
  autoload :AccountRisk, "no_response/models/shared/accountrisk.rb"
15
+ autoload :AccountRiskChanged, "no_response/models/shared/accountriskchanged.rb"
16
+ autoload :AccountRiskChangedEvent, "no_response/models/shared/accountriskchangedevent.rb"
12
17
  autoload :AccountRiskReason, "no_response/models/shared/accountriskreason.rb"
13
18
  autoload :Assessment, "no_response/models/shared/assessment.rb"
14
19
  autoload :AssessmentDeletion, "no_response/models/shared/assessmentdeletion.rb"
@@ -16,6 +21,7 @@ module NoResponse
16
21
  autoload :AssessmentDetail, "no_response/models/shared/assessmentdetail.rb"
17
22
  autoload :AssessmentDetailAuthContext, "no_response/models/shared/assessmentdetailauthcontext.rb"
18
23
  autoload :AssessmentDetailTiming, "no_response/models/shared/assessmentdetailtiming.rb"
24
+ autoload :AssessmentExpired, "no_response/models/shared/assessmentexpired.rb"
19
25
  autoload :AssessmentList, "no_response/models/shared/assessmentlist.rb"
20
26
  autoload :AssessmentOutcome, "no_response/models/shared/assessmentoutcome.rb"
21
27
  autoload :AssessmentOutcomeRequest, "no_response/models/shared/assessmentoutcomerequest.rb"
@@ -25,6 +31,8 @@ module NoResponse
25
31
  autoload :AssessmentRequestIdentifiers, "no_response/models/shared/assessmentrequestidentifiers.rb"
26
32
  autoload :AssessmentRequestIssuedCredential, "no_response/models/shared/assessmentrequestissuedcredential.rb"
27
33
  autoload :AssessmentRequestPolicyContext, "no_response/models/shared/assessmentrequestpolicycontext.rb"
34
+ autoload :AssessmentUpdatedData, "no_response/models/shared/assessmentupdateddata.rb"
35
+ autoload :AssessmentUpdatedEvent, "no_response/models/shared/assessmentupdatedevent.rb"
28
36
  autoload :AssuranceLevel, "no_response/models/shared/assurancelevel.rb"
29
37
  autoload :AuthContext, "no_response/models/shared/authcontext.rb"
30
38
  autoload :AuthContextMfa, "no_response/models/shared/authcontextmfa.rb"
@@ -42,6 +50,15 @@ module NoResponse
42
50
  autoload :ConsistencyValue, "no_response/models/shared/consistencyvalue.rb"
43
51
  autoload :Country, "no_response/models/shared/country.rb"
44
52
  autoload :Depth, "no_response/models/shared/depth.rb"
53
+ autoload :EndpointVerification, "no_response/models/shared/endpointverification.rb"
54
+ autoload :EndpointVerificationEvent, "no_response/models/shared/endpointverificationevent.rb"
55
+ autoload :Enforcement, "no_response/models/shared/enforcement.rb"
56
+ autoload :EnforcementAction, "no_response/models/shared/enforcementaction.rb"
57
+ autoload :EnforcementProvider, "no_response/models/shared/enforcementprovider.rb"
58
+ autoload :EnforcementRequest, "no_response/models/shared/enforcementrequest.rb"
59
+ autoload :EnforcementState, "no_response/models/shared/enforcementstate.rb"
60
+ autoload :EnforcementStatus, "no_response/models/shared/enforcementstatus.rb"
61
+ autoload :Environment, "no_response/models/shared/environment.rb"
45
62
  autoload :Failure, "no_response/models/shared/failure.rb"
46
63
  autoload :FailureReason, "no_response/models/shared/failurereason.rb"
47
64
  autoload :Flow, "no_response/models/shared/flow.rb"
@@ -100,9 +117,12 @@ module NoResponse
100
117
  autoload :WebhookDeliveryList, "no_response/models/shared/webhookdeliverylist.rb"
101
118
  autoload :WebhookEndpoint, "no_response/models/shared/webhookendpoint.rb"
102
119
  autoload :WebhookEndpointCreated, "no_response/models/shared/webhookendpointcreated.rb"
120
+ autoload :WebhookEndpointDisabled, "no_response/models/shared/webhookendpointdisabled.rb"
121
+ autoload :WebhookEndpointDisabledEvent, "no_response/models/shared/webhookendpointdisabledevent.rb"
103
122
  autoload :WebhookEndpointList, "no_response/models/shared/webhookendpointlist.rb"
104
123
  autoload :WebhookEndpointRequest, "no_response/models/shared/webhookendpointrequest.rb"
105
124
  autoload :WebhookEndpointState, "no_response/models/shared/webhookendpointstate.rb"
125
+ autoload :WebhookEvent, "no_response/models/shared/webhookevent.rb"
106
126
  autoload :WebhookKeyRotation, "no_response/models/shared/webhookkeyrotation.rb"
107
127
  autoload :WebhookKeyRotationRequest, "no_response/models/shared/webhookkeyrotationrequest.rb"
108
128
  autoload :WebhookReplay, "no_response/models/shared/webhookreplay.rb"
@@ -110,6 +130,9 @@ module NoResponse
110
130
  autoload :WebhookTest, "no_response/models/shared/webhooktest.rb"
111
131
  autoload :WebhookTestRequest, "no_response/models/shared/webhooktestrequest.rb"
112
132
  autoload :WebhookTestRequestEvent, "no_response/models/shared/webhooktestrequestevent.rb"
133
+ autoload :WebhookVerificationResponse, "no_response/models/shared/webhookverificationresponse.rb"
134
+ autoload :WorkspaceDeleted, "no_response/models/shared/workspacedeleted.rb"
135
+ autoload :WorkspaceDeletedEvent, "no_response/models/shared/workspacedeletedevent.rb"
113
136
  autoload :AlternateName, "no_response/models/shared/alternate_name.rb"
114
137
  autoload :AlternateNameUnion, "no_response/models/shared/alternate_name_union.rb"
115
138
  autoload :Email, "no_response/models/shared/email.rb"
@@ -10,7 +10,6 @@
10
10
 
11
11
  require_relative "./types"
12
12
  # Hand-written helpers beside the generated client, loaded with the gem.
13
- require_relative "../webhooks"
14
13
  require_relative "../wait"
15
14
  require_relative "../late_results"
16
15
  require_relative "idempotency_key_hook"
@@ -95,9 +95,9 @@ module NoResponse
95
95
 
96
96
  @language = "ruby"
97
97
  @openapi_doc_version = "1"
98
- @sdk_version = "5.1.1"
98
+ @sdk_version = "6.0.1"
99
99
  @gen_version = "internal"
100
- @user_agent = "noresponse-sdk/ruby 5.1.1 internal 1 noresponse"
100
+ @user_agent = "noresponse-sdk/ruby 6.0.1 internal 1 noresponse"
101
101
  end
102
102
 
103
103
  sig { returns([String, T::Hash[Symbol, String]]) }
@@ -0,0 +1,170 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "openssl"
6
+
7
+ module NoResponse
8
+ # Verifies webhook deliveries and parses them into typed events.
9
+ module WebhookVerification
10
+ extend T::Sig
11
+
12
+ # How far the NR-Webhook-Timestamp header may be from the receiver's clock, in seconds.
13
+ DEFAULT_TOLERANCE_SECONDS = 300
14
+
15
+ # Raised by construct_event for a delivery that must not be processed:
16
+ # answer the delivery with a 400 and do not process it. code is one of
17
+ # "missing_header", "duplicate_header", "invalid_timestamp", "unknown_key",
18
+ # "invalid_signature" or "invalid_payload".
19
+ class VerificationError < StandardError
20
+ extend T::Sig
21
+
22
+ sig { returns(String) }
23
+ attr_reader :code
24
+
25
+ sig { params(code: String, message: String).void }
26
+ def initialize(code, message)
27
+ super("webhook verification failed: #{message} (#{code})")
28
+ @code = code
29
+ end
30
+ end
31
+
32
+ HEADER_NAMES = [
33
+ "NR-Webhook-Id",
34
+ "NR-Webhook-Timestamp",
35
+ "NR-Webhook-Key-Id",
36
+ "NR-Webhook-Signature"
37
+ ].freeze
38
+ private_constant :HEADER_NAMES
39
+ EVENT_TYPE = Crystalline::DiscriminatedUnion.new(
40
+ "type",
41
+ {
42
+ "assessment.updated" => Models::Shared::AssessmentUpdatedEvent,
43
+ "endpoint.verification" => Models::Shared::EndpointVerificationEvent,
44
+ "account.deleted" => Models::Shared::AccountDeletedEvent,
45
+ "workspace.deleted" => Models::Shared::WorkspaceDeletedEvent,
46
+ "webhook_endpoint.disabled" => Models::Shared::WebhookEndpointDisabledEvent,
47
+ "account.risk_changed" => Models::Shared::AccountRiskChangedEvent
48
+ }
49
+ )
50
+ private_constant :EVENT_TYPE
51
+
52
+ class << self
53
+ extend T::Sig
54
+
55
+ # Verifies a webhook delivery and parses its body.
56
+ #
57
+ # payload is the raw request body exactly as received: re-encoding parsed JSON breaks the signature.
58
+ # headers is a Hash, or an Array of name/value pairs, whose values are Strings or Arrays of Strings.
59
+ # A header repeated in any letter case, or sent as a comma-joined list, is refused.
60
+ # secrets maps each key id, sent in NR-Webhook-Key-Id, to its secret, so a receiver can hold two during a rotation.
61
+ # now is the receiver's clock (a Time or Unix seconds) used for the tolerance check.
62
+ sig do
63
+ params(
64
+ payload: String,
65
+ headers: T::Enumerable[T.untyped],
66
+ secrets: T::Hash[String, String],
67
+ tolerance_seconds: Integer,
68
+ now: T.nilable(T.any(Time, Integer))
69
+ )
70
+ .returns(
71
+ T.any(
72
+ Models::Shared::AssessmentUpdatedEvent,
73
+ Models::Shared::EndpointVerificationEvent,
74
+ Models::Shared::AccountDeletedEvent,
75
+ Models::Shared::WorkspaceDeletedEvent,
76
+ Models::Shared::WebhookEndpointDisabledEvent,
77
+ Models::Shared::AccountRiskChangedEvent
78
+ )
79
+ )
80
+ end
81
+ def construct_event(payload, headers, secrets, tolerance_seconds: DEFAULT_TOLERANCE_SECONDS, now: nil)
82
+ raw = payload.b
83
+ values = header_values(headers)
84
+ id = values.fetch("NR-Webhook-Id")
85
+ timestamp = values.fetch("NR-Webhook-Timestamp")
86
+ key_id = values.fetch("NR-Webhook-Key-Id")
87
+ signature = values.fetch("NR-Webhook-Signature")
88
+
89
+ unless timestamp.match?(/\A[0-9]{1,12}\z/)
90
+ raise VerificationError.new("invalid_timestamp", "NR-Webhook-Timestamp is not a Unix time in seconds")
91
+ end
92
+
93
+ if ((now || Time.now).to_i - timestamp.to_i).abs > tolerance_seconds
94
+ raise VerificationError.new("invalid_timestamp", "NR-Webhook-Timestamp is outside the tolerance")
95
+ end
96
+
97
+ unless secrets.key?(key_id)
98
+ raise VerificationError.new("unknown_key", "NR-Webhook-Key-Id names a key this receiver does not hold")
99
+ end
100
+
101
+ secret = secrets.fetch(key_id)
102
+ raise VerificationError.new("invalid_signature", "the webhook secret is empty") if secret.empty?
103
+
104
+ received = decode_signature(signature)
105
+ substitutions = {"{timestamp}" => timestamp, "{id}" => id}
106
+ signed = "{timestamp}.".gsub(/\{(?:timestamp|id)\}/, substitutions).b + raw
107
+ expected = OpenSSL::HMAC.digest("SHA256", secret, signed)
108
+ unless received.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(expected, received)
109
+ raise VerificationError.new("invalid_signature", "NR-Webhook-Signature does not match")
110
+ end
111
+
112
+ body = begin
113
+ JSON.parse(raw)
114
+ rescue JSON::ParserError, EncodingError
115
+ raise VerificationError.new("invalid_payload", "the body is not JSON")
116
+ end
117
+
118
+ unless body.is_a?(::Hash) && body["id"].is_a?(::String) && body["id"] == id
119
+ raise VerificationError.new("invalid_payload", "the body's id does not equal NR-Webhook-Id")
120
+ end
121
+
122
+ event = begin
123
+ Crystalline.unmarshal_json(body, EVENT_TYPE)
124
+ rescue StandardError
125
+ nil
126
+ end
127
+
128
+ if event.nil? || Crystalline.unknown?(event)
129
+ raise VerificationError.new("invalid_payload", "the body is not a known webhook event")
130
+ end
131
+
132
+ event
133
+ end
134
+
135
+ private
136
+
137
+ sig { params(headers: T::Enumerable[T.untyped]).returns(T::Hash[String, String]) }
138
+ def header_values(headers)
139
+ HEADER_NAMES.to_h do |name|
140
+ found = T.let([], T::Array[String])
141
+ headers.each do |key, value|
142
+ found.concat(Array(value).map(&:to_s)) if key.to_s.casecmp?(name)
143
+ end
144
+
145
+ if found.length > 1 || found.first&.include?(",")
146
+ raise VerificationError.new("duplicate_header", "#{name} is repeated")
147
+ end
148
+
149
+ raise VerificationError.new("missing_header", "#{name} is missing") if found.first.to_s.empty?
150
+
151
+ [name, found.first.to_s]
152
+ end
153
+ end
154
+
155
+ sig { params(signature: String).returns(String) }
156
+ def decode_signature(signature)
157
+ encoded = signature.delete_prefix("v1=")
158
+ if encoded.length == signature.length
159
+ raise VerificationError.new("invalid_signature", "NR-Webhook-Signature is malformed")
160
+ end
161
+
162
+ unless encoded.match?(/\A(?:[0-9a-fA-F]{2})*\z/)
163
+ raise VerificationError.new("invalid_signature", "NR-Webhook-Signature is malformed")
164
+ end
165
+
166
+ [encoded].pack("H*")
167
+ end
168
+ end
169
+ end
170
+ end
data/lib/noresponse.rb CHANGED
@@ -16,6 +16,8 @@ module NoResponse
16
16
  autoload :Errors, "no_response/models/errors"
17
17
  autoload :Callbacks, "no_response/models/callbacks"
18
18
  end
19
+
20
+ autoload :WebhookVerification, "no_response/webhook_verification"
19
21
  end
20
22
 
21
23
  require_relative "no_response/utils/utils"