Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c6b43e0
feat: implement webhook signing and delivery
niteshpurohit Apr 30, 2026
d0ae1c3
feat: enhance outbound event serialization and attributes
niteshpurohit Apr 30, 2026
db0b38e
feat: enhance outbound event schema definition
niteshpurohit Apr 30, 2026
2192309
feat: enhance webhook signing and validation
niteshpurohit Apr 30, 2026
80e02f9
feat: update outbound event handling and documentation
niteshpurohit Apr 30, 2026
d5bde0f
feat: enhance outbound event delivery and validation
niteshpurohit Apr 30, 2026
0c33665
feat: improve outbound event handling and serialization
niteshpurohit Apr 30, 2026
6d3de32
feat: enhance delivery and dispatcher normalization
niteshpurohit Apr 30, 2026
e68e8d0
feat: implement versioned outbound event schemas and webhook signing
niteshpurohit Apr 30, 2026
0c8895b
feat: add error classes for outbound events and webhooks
niteshpurohit Apr 30, 2026
477e748
feat: implement optional outbound event dispatcher
niteshpurohit Apr 30, 2026
97474ad
feat: enhance webhook verification and dispatcher classes
niteshpurohit Apr 30, 2026
90b3a1f
feat: update logger usage in runtime specs
niteshpurohit Apr 30, 2026
f07dbef
feat: update callable types for forker and normalization
niteshpurohit Apr 30, 2026
60204e9
feat: enhance webhook verifier and tests
niteshpurohit Apr 30, 2026
6d7b946
chore: update private constants in webhook verifier
niteshpurohit Apr 30, 2026
e58e743
feat: update outbound event payload types
niteshpurohit Apr 30, 2026
249baf2
Merge branch 'main' into feat/versioned-outbound-event-schemas-and-we…
niteshpurohit Apr 30, 2026
71d359f
feat: add forker normalization and update types
niteshpurohit Apr 30, 2026
a0ae5a3
feat: deep-freeze schema definitions and required keys
niteshpurohit Apr 30, 2026
7e16481
feat: implement immutable hook payload snapshots
niteshpurohit Apr 30, 2026
8a481b7
feat: implement forker validation and enhance webhook signing
niteshpurohit Apr 30, 2026
6a9aedb
feat: enhance PresentString normalization logic
niteshpurohit May 1, 2026
63f4520
feat: update context_payload type for enhanced flexibility
niteshpurohit May 1, 2026
cc407ec
feat: enhance webhook timestamp validation and error handling
niteshpurohit May 1, 2026
2910921
feat: enhance outbound event delivery and verification
niteshpurohit May 1, 2026
3064612
feat: enhance snapshot_key method and add tests
niteshpurohit May 1, 2026
28d4ea4
feat: enhance outbound event handling and validation
niteshpurohit May 1, 2026
6f01aff
feat: update instrumentation to use hash syntax
niteshpurohit May 1, 2026
533f652
feat: implement PayloadInput for enhanced event handling
niteshpurohit May 1, 2026
b7ab3be
feat: implement versioned outbound event schemas and webhook signing
niteshpurohit May 1, 2026
c973e7f
feat: enhance PayloadInput and runtime instrumentation
niteshpurohit May 1, 2026
ff05546
feat: refactor ABSENT constant in PayloadInput
niteshpurohit May 1, 2026
9ce270a
feat: enhance validation for outbound event payloads
niteshpurohit May 1, 2026
028571f
feat: enhance webhook signature verification
niteshpurohit May 1, 2026
9f37357
feat: update webhook verifier to support signing scheme
niteshpurohit May 1, 2026
e709048
feat: enhance ImmutableHookPayload for error handling
niteshpurohit May 1, 2026
f518039
feat: implement unique sentinel for omitted payloads
niteshpurohit May 1, 2026
ae4f1c2
feat: add string key normalization for outbound events
niteshpurohit May 1, 2026
f64a281
feat: enhance event handling and payload validation
niteshpurohit May 1, 2026
e3fda28
feat: update time handling in event serialization
niteshpurohit May 1, 2026
d0be948
feat: enhance webhook signing for non-UTF-8 bodies
niteshpurohit May 2, 2026
816e66c
feat: update outbound events and workers documentation
niteshpurohit May 2, 2026
1a35e74
feat: implement hook dispatch for event instrumentation
niteshpurohit May 2, 2026
4b02008
feat: enhance snapshot pair creation and testing
niteshpurohit May 2, 2026
cdb0d5d
feat: add direct require support for worker and supervisor runtimes
niteshpurohit May 2, 2026
d750ae2
feat: remove unused methods from forker and dispatcher classes
niteshpurohit May 2, 2026
613f020
feat: enhance outbound event dispatcher behavior
niteshpurohit May 2, 2026
951532e
feat: implement versioned outbound event schemas and webhook signing
niteshpurohit May 2, 2026
edbf0f4
feat: add outbound events module dependencies
niteshpurohit May 2, 2026
6d2e5f7
feat: implement generic callable and dispatcher classes
niteshpurohit May 2, 2026
689cba3
feat: update outbound event delivery handler types
niteshpurohit May 2, 2026
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
1 change: 1 addition & 0 deletions core/karya/lib/karya.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
require_relative 'karya/job'
require_relative 'karya/retry_policy'
require_relative 'karya/retry_policy_set'
require_relative 'karya/outbound_events'
require_relative 'karya/reservation'
require_relative 'karya/queue_store'
require_relative 'karya/workflow'
Expand Down
16 changes: 15 additions & 1 deletion core/karya/lib/karya/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,28 @@ class Error < StandardError; end
# Raised when runtime code requires a configured queue store but none has been set.
class MissingQueueStoreConfigurationError < Error; end

# Raised when outbound event input cannot be normalized into a supported contract.
class InvalidOutboundEventError < Error; end

# Raised when a caller asks for an outbound event that is not part of the supported contract.
class UnsupportedOutboundEventError < Error; end

# Raised when a webhook signature cannot be parsed or verified.
class InvalidWebhookSignatureError < Error; end

class << self
attr_reader :instrumenter
attr_reader :instrumenter, :outbound_event_dispatcher

def configure_instrumenter(instrumenter)
# Process-wide default used when a runtime does not receive an explicit instrumenter.
@instrumenter = instrumenter
end

def configure_outbound_event_dispatcher(outbound_event_dispatcher)
# Process-wide default used when a runtime does not receive an explicit outbound event dispatcher.
@outbound_event_dispatcher = outbound_event_dispatcher
end

def configure_logger(logger)
# Process-wide default used when a runtime does not receive an explicit logger.
@logger = logger
Expand Down
51 changes: 51 additions & 0 deletions core/karya/lib/karya/internal/hook_dispatch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

# Copyright Codevedas Inc. 2025-present
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

module Karya
module Internal
# Shares runtime hook payload normalization and dispatch flow.
class HookDispatch
def self.instrument(
event:,
payload:,
payload_keywords:,
payload_given:,
instrumenter:,
dispatch_outbound:,
error_class:,
mixed_payload_message:,
emit_instrumentation:,
emit_outbound_event:
)
return nil unless instrumenter || dispatch_outbound

normalized_payload = PayloadInput.new(
payload,
payload_keywords,
payload_given:,
error_class:,
mixed_payload_message:
).to_h

if instrumenter && dispatch_outbound
instrumentation_payload, outbound_payload = ImmutableHookPayload.snapshot_pair(
normalized_payload,
error_class:
)
emit_instrumentation.call(event, instrumentation_payload)
emit_outbound_event.call(event, outbound_payload)
return nil
end

snapshot = ImmutableHookPayload.snapshot(normalized_payload, error_class:)
emit_instrumentation.call(event, snapshot) if instrumenter
emit_outbound_event.call(event, snapshot) if dispatch_outbound
nil
end
end
end
end
77 changes: 77 additions & 0 deletions core/karya/lib/karya/internal/immutable_hook_payload.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

# Copyright Codevedas Inc. 2025-present
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

module Karya
module Internal
# Builds immutable snapshots for runtime hook payloads.
class ImmutableHookPayload
def self.snapshot(payload, error_class:)
new(payload, error_class:).snapshot
end

def self.snapshot_pair(payload, error_class:)
snapshot = snapshot(payload, error_class:)
[snapshot, shallow_snapshot(snapshot)].freeze
end

def self.snapshot_key(value)
return value if value.is_a?(Symbol)
return value.frozen? ? value : value.dup.freeze if value.is_a?(String)

raise ArgumentError, 'payload keys must be Symbols or Strings'
end
private_class_method :snapshot_key

def initialize(payload, error_class:)
@payload = payload
@error_class = error_class
end

def snapshot
snapshot_hash(payload)
end

private

attr_reader :error_class, :payload

def self.shallow_snapshot(snapshot)
snapshot.each_with_object({}) do |(key, value), duplicated|
duplicated[key] = value
end.freeze
end
private_class_method :shallow_snapshot

def snapshot_hash(value)
value.each_with_object({}) do |(key, item), duplicated|
duplicated[self.class.send(:snapshot_key, key)] = snapshot_value(item)
rescue ArgumentError => e
raise error_class, e.message
end.freeze
end
Comment thread
niteshpurohit marked this conversation as resolved.

def snapshot_array(value)
value.map { |item| snapshot_value(item) }.freeze
end

def snapshot_value(value)
case value
when Hash
snapshot_hash(value)
when Array
snapshot_array(value)
when String, Time
value.frozen? ? value : value.dup.freeze
when NilClass, TrueClass, FalseClass, Numeric, Symbol
value
else
raise error_class, 'payload values must be nil, booleans, numerics, strings, symbols, times, arrays, or hashes'
end
end
end
end
end
53 changes: 53 additions & 0 deletions core/karya/lib/karya/internal/payload_input.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

# Copyright Codevedas Inc. 2025-present
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

module Karya
module Internal
# Normalizes positional and keyword payload inputs into one Hash.
class PayloadInput
# Unique sentinel for omitted positional payload arguments.
class Absent
def self.instance
@instance ||= new.freeze
end

private_class_method :new
end
private_constant :Absent

ABSENT = Absent.instance

def initialize(payload, payload_keywords, payload_given:, error_class:, mixed_payload_message:)
@payload = payload
@payload_keywords = payload_keywords
@payload_given = payload_given
@error_class = error_class
@mixed_payload_message = mixed_payload_message
end

def to_h
return payload_keywords unless payload_given

payload_is_hash = payload.is_a?(Hash)

if payload_keywords.empty?
raise error_class, 'payload must be a Hash' unless payload_is_hash

return payload
end

raise error_class, mixed_payload_message unless payload_is_hash

payload.merge(payload_keywords)
end
Comment thread
niteshpurohit marked this conversation as resolved.

private

attr_reader :error_class, :mixed_payload_message, :payload, :payload_given, :payload_keywords
end
end
end
23 changes: 23 additions & 0 deletions core/karya/lib/karya/outbound_events.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

# Copyright Codevedas Inc. 2025-present
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

require_relative 'base'
require_relative 'outbound_events/values'
require_relative 'outbound_events/delivery'
require_relative 'outbound_events/dispatcher'
require_relative 'outbound_events/event'
require_relative 'outbound_events/schema'
require_relative 'outbound_events/schema_catalog'
require_relative 'outbound_events/webhook_signature'
require_relative 'outbound_events/webhook_signer'
require_relative 'outbound_events/webhook_verifier'
Comment thread
niteshpurohit marked this conversation as resolved.

module Karya
# Shared outbound event contracts for external delivery and verification.
module OutboundEvents
end
end
75 changes: 75 additions & 0 deletions core/karya/lib/karya/outbound_events/delivery.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

require_relative 'event'
require_relative 'webhook_signature'

# Copyright Codevedas Inc. 2025-present
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

module Karya
module OutboundEvents
# Immutable serialized outbound delivery with canonical headers and body.
class Delivery
CONTENT_TYPE = 'application/cloudevents+json'

attr_reader :body, :event, :headers, :signature

def initialize(event:, signature: nil, body: nil)
@event = normalize_event(event)
@body = normalize_body(body)
@signature = normalize_signature(signature)
@headers = build_headers.freeze
freeze
end

private

def normalize_event(value)
return value if value.is_a?(Event)

raise InvalidOutboundEventError, 'event must be Karya::OutboundEvents::Event'
end

def normalize_signature(value)
return nil if [nil].include?(value)
return value if value.is_a?(WebhookSignature)

raise InvalidOutboundEventError, 'signature must be Karya::OutboundEvents::WebhookSignature'
end

def normalize_body(value)
Body.new(value, event: @event).normalize
end
Comment thread
niteshpurohit marked this conversation as resolved.

def build_headers
{ 'Content-Type' => CONTENT_TYPE }.merge(signature&.headers || {})
end

# Normalizes one optional serialized body value for an outbound delivery.
class Body
def initialize(value, event:)
@value = value
@event = event
end

def normalize
return event.to_json.freeze if [nil].include?(value)

string_value = value if value.is_a?(String)
return string_value if string_value&.frozen?
return string_value.dup.freeze if string_value

raise InvalidOutboundEventError, 'body must be a String'
end

private

attr_reader :event, :value
end

private_constant :Body
end
end
end
69 changes: 69 additions & 0 deletions core/karya/lib/karya/outbound_events/dispatcher.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# frozen_string_literal: true

require 'securerandom'
require_relative '../internal/payload_input'
require_relative '../primitives/callable'
Comment thread
niteshpurohit marked this conversation as resolved.
require_relative 'delivery'
require_relative 'schema_catalog'
require_relative 'webhook_signer'

# Copyright Codevedas Inc. 2025-present
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

module Karya
module OutboundEvents
# Builds canonical outbound deliveries from runtime instrumentation events.
class Dispatcher
def initialize(delivery_handler:, signer: nil, clock: -> { Time.now.utc }, event_id_generator: -> { SecureRandom.uuid })
@delivery_handler = Primitives::Callable.new(:delivery_handler, delivery_handler, error_class: InvalidOutboundEventError).normalize
Comment thread
niteshpurohit marked this conversation as resolved.
@signer = normalize_signer(signer)
@clock = Primitives::Callable.new(:clock, clock, error_class: InvalidOutboundEventError).normalize
@event_id_generator = Primitives::Callable.new(
:event_id_generator,
Comment thread
niteshpurohit marked this conversation as resolved.
event_id_generator,
error_class: InvalidOutboundEventError
).normalize
end

def call(event_name, payload = Internal::PayloadInput::ABSENT, **payload_keywords)
return nil unless SchemaCatalog.supported?(event_name)

occurred_at = clock.call
raise InvalidOutboundEventError, 'clock must return a Time' unless occurred_at.is_a?(Time)

payload_given = !payload.equal?(Internal::PayloadInput::ABSENT)

event = SchemaCatalog.build_event(
event_name:,
payload: Internal::PayloadInput.new(
payload_given ? payload : nil,
payload_keywords,
payload_given:,
error_class: InvalidOutboundEventError,
mixed_payload_message: 'payload must be a Hash when keyword payload is also given'
).to_h,
occurred_at:,
event_id: event_id_generator.call
)
body = event.to_json.freeze
signature = signer&.sign(body:, now: occurred_at)
delivery = Delivery.new(event:, signature:, body:)
delivery_handler.call(delivery)
delivery
end

private

attr_reader :clock, :delivery_handler, :event_id_generator, :signer

def normalize_signer(value)
return nil if [nil].include?(value)
return value if value.is_a?(WebhookSigner)

raise InvalidOutboundEventError, 'signer must be Karya::OutboundEvents::WebhookSigner'
end
Comment thread
niteshpurohit marked this conversation as resolved.
end
end
end
Loading
Loading