Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,40 @@
## [Unreleased]

### Added

- Webhook handling spec helpers (CHA-2961): `UnknownEvent` class for forward-compat;
`gunzip_payload`, `decode_sqs_payload`, `decode_sns_payload` primitives;
`parse_event` (returns typed event or `UnknownEvent` for unrecognized discriminators);
`verify_and_parse_webhook` HTTP composite; `parse_sqs` / `parse_sns`
queue composites (no signature; backend emits no HMAC for queue messages today).
Security for queue-delivered payloads is enforced via AWS IAM on the SQS/SNS
subscription, not in-SDK.
- New `Stream::Webhook` module alias (preferred). `StreamChat::Webhook` retained as
backward-compat alias for one minor-version cycle.
- New unified error class: `StreamChat::Webhook::InvalidWebhookError` covering signature
mismatch, invalid JSON, missing/non-string `type` field, gzip decompression failure,
invalid base64 in a queue body, and malformed SNS envelopes. Distinguish failure modes
via the message substring or `cause` chain rather than the class.
- New instance methods on `GetStreamRuby::Client`: `verify_signature(body, signature)` and
`verify_and_parse_webhook(body, signature)` — drop the `api_secret` parameter in favor
of the client's stored secret. Dual API: module-level methods remain available.
- New instance methods on `GetStreamRuby::Client`: `parse_sqs(message_body)` and
`parse_sns(notification_body)` (no signature; AWS IAM).
- Conformance fixture suite under `test/fixtures/webhooks/` (14 event-type buckets plus
`_invalid/` negative cases).

### Changed

- No breaking changes.

### Fixed

- `event_class_for_type` now references `GetStream::Generated::Models::*Event`
(was `StreamChat::*Event`, which raised `NameError` at runtime). `parse_event`
resolves known event types correctly.

[Spec](https://www.notion.so/stream-wiki/Server-Side-SDK-Webhook-Handling-Spec-34b6a5d7f9f681e78003c443f227493c)

## [6.0.0] - 2026-04-17

### major^2 changes
Expand Down
3 changes: 3 additions & 0 deletions generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ set -ex
# cd in API repo, generate new spec and then generate code from it
( cd $SOURCE_PATH ; make openapi ; go run ./cmd/chat-manager openapi generate-client --language ruby --spec ./releases/v2/serverside-api.yaml --output $DST_PATH )

# Generate webhook conformance fixtures (CHA-2961)
( cd $SOURCE_PATH ; go run ./cmd/chat-manager openapi generate-webhook-fixtures --output $DST_PATH/test/fixtures/webhooks )

# Fix any potential issues in generated code
echo "Applying Ruby-specific fixes..."

Expand Down
47 changes: 47 additions & 0 deletions lib/getstream_ruby/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
require_relative 'generated/video_client'
require_relative 'extensions/moderation_extensions'
require_relative 'generated/feed'
require_relative 'generated/webhook'
require_relative 'stream_response'

module GetStreamRuby
Expand Down Expand Up @@ -76,6 +77,52 @@ def feed(feed_group_id, feed_id)
GetStream::Generated::Feed.new(self, feed_group_id, feed_id)
end

# Verify a webhook signature using this client's API secret (CHA-2961).
#
# Convenience wrapper around StreamChat::Webhook.verify_signature that
# supplies the secret automatically. The module-level method is still
# available for callers that need to verify with an arbitrary secret.
#
# @param body [String] The raw request body (already-decompressed)
# @param signature [String] The signature from the X-Signature header
# @return [Boolean] true if the signature is valid, false otherwise
def verify_signature(body, signature)
StreamChat::Webhook.verify_signature(body, signature, @configuration.api_secret)
end

# Verify and parse a webhook payload in one call, using this client's API
# secret (CHA-2961).
#
# Handles gzip-compressed bodies transparently. Raises
# StreamChat::Webhook::InvalidWebhookError on signature mismatch or parse
# failures; distinguish failure modes via the message substring.
#
# @param body [String] raw request body (possibly gzip-compressed)
# @param signature [String] X-Signature header value
# @return [Object] the typed event class instance or
# StreamChat::Webhook::UnknownEvent
# @raise [StreamChat::Webhook::InvalidWebhookError]
def verify_and_parse_webhook(body, signature)
StreamChat::Webhook.verify_and_parse_webhook(body, signature, @configuration.api_secret)
end

# Decode + parse a Stream-delivered SQS message body.
#
# Convenience wrapper around StreamChat::Webhook.parse_sqs. No signature is
# required; SQS deliveries are authenticated via AWS IAM.
def parse_sqs(message_body)
StreamChat::Webhook.parse_sqs(message_body)
end

# Decode + parse a Stream-delivered SNS notification body.
#
# Accepts either the raw SNS HTTP envelope JSON or the pre-extracted Message
# string. Convenience wrapper around StreamChat::Webhook.parse_sns. No signature
# is required; SNS deliveries are authenticated via AWS IAM.
def parse_sns(notification_body)
StreamChat::Webhook.parse_sns(notification_body)
end

# @param path [String] The API path
# @param body [Hash] The request body
# @return [GetStreamRuby::StreamResponse] The API response
Expand Down
4 changes: 3 additions & 1 deletion lib/getstream_ruby/generated/feed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ def initialize(client, feed_group_id, feed_id)
# Delete a single feed by its ID
#
# @param hard_delete [Boolean]
# @param purge_user_activities [Boolean]
# @return [Models::DeleteFeedResponse]
def delete_feed(hard_delete = nil)
def delete_feed(hard_delete = nil, purge_user_activities = nil)
# Build query parameters
query_params = {}
query_params['hard_delete'] = hard_delete unless hard_delete.nil?
query_params['purge_user_activities'] = purge_user_activities unless purge_user_activities.nil?

# Delegate to the FeedsClient
@client.feeds.delete_feed(@feed_group_id, @feed_id, query_params)
Expand Down
4 changes: 3 additions & 1 deletion lib/getstream_ruby/generated/feeds_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -992,15 +992,17 @@ def create_feed_group(create_feed_group_request)
# @param feed_group_id [String]
# @param feed_id [String]
# @param hard_delete [Boolean]
# @param purge_user_activities [Boolean]
# @return [Models::DeleteFeedResponse]
def delete_feed(feed_group_id, feed_id, hard_delete = nil)
def delete_feed(feed_group_id, feed_id, hard_delete = nil, purge_user_activities = nil)
path = '/api/v2/feeds/feed_groups/{feed_group_id}/feeds/{feed_id}'
# Replace path parameters
path = path.gsub('{feed_group_id}', feed_group_id.to_s)
path = path.gsub('{feed_id}', feed_id.to_s)
# Build query parameters
query_params = {}
query_params['hard_delete'] = hard_delete unless hard_delete.nil?
query_params['purge_user_activities'] = purge_user_activities unless purge_user_activities.nil?

# Make the API request
@client.make_request(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,24 @@ class DeleteFeedsBatchRequest < GetStream::BaseModel
# @!attribute hard_delete
# @return [Boolean] Whether to permanently delete the feeds instead of soft delete
attr_accessor :hard_delete
# @!attribute purge_user_activities
# @return [Boolean] When hard-deleting, also fully delete activities authored by each feed's owner from every other feed those activities were fanned out to. Default false preserves existing fan-out. Requires 'hard_delete' to be true; the request is rejected otherwise. Feeds with no recorded owner (created_by_id is empty) are silently skipped for the purge step — owner-matching against an empty string is a safety guard, not a wildcard.
attr_accessor :purge_user_activities

# Initialize with attributes
def initialize(attributes = {})
super(attributes)
@feeds = attributes[:feeds] || attributes['feeds']
@hard_delete = attributes[:hard_delete] || attributes['hard_delete'] || nil
@purge_user_activities = attributes[:purge_user_activities] || attributes['purge_user_activities'] || nil
end

# Override field mappings for JSON serialization
def self.json_field_mappings
{
feeds: 'feeds',
hard_delete: 'hard_delete'
hard_delete: 'hard_delete',
purge_user_activities: 'purge_user_activities'
}
end
end
Expand Down
5 changes: 5 additions & 0 deletions lib/getstream_ruby/generated/models/labels_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class LabelsRequest < GetStream::BaseModel
# @!attribute content_type
# @return [String] Type of content: 'text' (default), 'message', or 'username'. Stored as-sent; only 'username' routes to the username moderation API.
attr_accessor :content_type
# @!attribute dry_run
# @return [Boolean] When true, run moderation and return labels without persisting the result. Useful for one-off checks (e.g. UI testers) that should not be recorded in the stored history.
attr_accessor :dry_run
# @!attribute policy
# @return [String] Optional moderation policy key (max 128 chars)
attr_accessor :policy
Expand All @@ -35,6 +38,7 @@ def initialize(attributes = {})
@category = attributes[:category] || attributes['category'] || nil
@content_id = attributes[:content_id] || attributes['content_id'] || nil
@content_type = attributes[:content_type] || attributes['content_type'] || nil
@dry_run = attributes[:dry_run] || attributes['dry_run'] || nil
@policy = attributes[:policy] || attributes['policy'] || nil
@user_id = attributes[:user_id] || attributes['user_id'] || nil
end
Expand All @@ -46,6 +50,7 @@ def self.json_field_mappings
category: 'category',
content_id: 'content_id',
content_type: 'content_type',
dry_run: 'dry_run',
policy: 'policy',
user_id: 'user_id'
}
Expand Down
12 changes: 11 additions & 1 deletion lib/getstream_ruby/generated/models/query_bookmarks_request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ class QueryBookmarksRequest < GetStream::BaseModel
# @!attribute prev
# @return [String]
attr_accessor :prev
# @!attribute user_id
# @return [String]
attr_accessor :user_id
# @!attribute sort
# @return [Array<SortParamRequest>] Sorting parameters for the query
attr_accessor :sort
# @!attribute filter
# @return [Object] Filters to apply to the query
attr_accessor :filter
# @!attribute user
# @return [UserRequest]
attr_accessor :user

# Initialize with attributes
def initialize(attributes = {})
Expand All @@ -35,8 +41,10 @@ def initialize(attributes = {})
@limit = attributes[:limit] || attributes['limit'] || nil
@next = attributes[:next] || attributes['next'] || nil
@prev = attributes[:prev] || attributes['prev'] || nil
@user_id = attributes[:user_id] || attributes['user_id'] || nil
@sort = attributes[:sort] || attributes['sort'] || nil
@filter = attributes[:filter] || attributes['filter'] || nil
@user = attributes[:user] || attributes['user'] || nil
end

# Override field mappings for JSON serialization
Expand All @@ -46,8 +54,10 @@ def self.json_field_mappings
limit: 'limit',
next: 'next',
prev: 'prev',
user_id: 'user_id',
sort: 'sort',
filter: 'filter'
filter: 'filter',
user: 'user'
}
end
end
Expand Down
Loading
Loading