omnisocials 0.1.0

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.
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniSocials
4
+ module Resources
5
+ # Webhooks resource: manage event subscriptions (post.scheduled,
6
+ # post.published, post.failed).
7
+ #
8
+ # For verifying incoming deliveries, see OmniSocials::Webhooks.verify.
9
+ class Webhooks
10
+ def initialize(client)
11
+ @client = client
12
+ end
13
+
14
+ # GET /webhooks - list webhook subscriptions.
15
+ def list
16
+ @client.request("GET", "/webhooks")
17
+ end
18
+
19
+ # GET /webhooks/{id} - fetch a single webhook subscription.
20
+ def get(webhook_id)
21
+ @client.request("GET", "/webhooks/#{webhook_id}")
22
+ end
23
+
24
+ # POST /webhooks - create a webhook subscription.
25
+ #
26
+ # `url` must be HTTPS. `events` is a non-empty subset of
27
+ # post.scheduled, post.published, post.failed. The signing `secret` is
28
+ # only returned once, in this response - store it.
29
+ def create(url:, events:)
30
+ @client.request(
31
+ "POST", "/webhooks",
32
+ json: { "url" => url, "events" => Array(events) }
33
+ )
34
+ end
35
+
36
+ # PATCH /webhooks/{id} - update url, events, or active state.
37
+ def update(webhook_id, url: nil, events: nil, is_active: nil)
38
+ body = Internal.drop_nil(
39
+ {
40
+ "url" => url,
41
+ "events" => events.nil? ? nil : Array(events),
42
+ "is_active" => is_active
43
+ }
44
+ )
45
+ @client.request("PATCH", "/webhooks/#{webhook_id}", json: body)
46
+ end
47
+
48
+ # DELETE /webhooks/{id} - delete a webhook. Returns nil (204).
49
+ def delete(webhook_id)
50
+ @client.request("DELETE", "/webhooks/#{webhook_id}")
51
+ end
52
+
53
+ # POST /webhooks/{id}/rotate-secret - rotate the signing secret.
54
+ #
55
+ # The new secret is only returned once, in this response.
56
+ def rotate_secret(webhook_id)
57
+ @client.request("POST", "/webhooks/#{webhook_id}/rotate-secret")
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OmniSocials
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+
6
+ module OmniSocials
7
+ # Webhook signature verification.
8
+ #
9
+ # OmniSocials signs every webhook delivery with HMAC-SHA256 (Stripe-style):
10
+ #
11
+ # - Header: X-OmniSocials-Signature: t=<unix>,v1=<hex>
12
+ # - Signed value: "{timestamp}.{raw_body}" (UTF-8), keyed with the
13
+ # webhook's secret, hex digest.
14
+ module Webhooks
15
+ class << self
16
+ # Verify an OmniSocials webhook delivery and return the parsed event.
17
+ #
18
+ # payload - the raw request body String, exactly as received. Do not
19
+ # parse and re-serialize it first; the signature is over the
20
+ # raw bytes.
21
+ # signature - the X-OmniSocials-Signature header value, of the form
22
+ # t=<unix>,v1=<hex>.
23
+ # secret - the webhook's signing secret (returned once when the
24
+ # webhook is created, or via rotate_secret).
25
+ # tolerance - maximum allowed age (and future skew) of the signed
26
+ # timestamp, in seconds. Defaults to 300. Pass nil to skip
27
+ # the timestamp check (not recommended).
28
+ #
29
+ # Returns the parsed event as a Hash (string keys).
30
+ #
31
+ # Raises OmniSocials::WebhookVerificationError if the header is
32
+ # malformed, the timestamp is outside the tolerance window, or the
33
+ # signature does not match. Uses a constant-time comparison.
34
+ def verify(payload:, signature:, secret:, tolerance: 300)
35
+ unless payload.is_a?(String)
36
+ raise WebhookVerificationError,
37
+ "payload must be a String (the raw request body), got #{payload.class}."
38
+ end
39
+ unless signature.is_a?(String) && !signature.empty?
40
+ raise WebhookVerificationError, "Missing signature header."
41
+ end
42
+
43
+ timestamp = nil
44
+ candidates = []
45
+ signature.split(",").each do |pair|
46
+ key, _, value = pair.strip.partition("=")
47
+ case key
48
+ when "t"
49
+ begin
50
+ timestamp = Integer(value, 10)
51
+ rescue ArgumentError, TypeError
52
+ raise WebhookVerificationError,
53
+ "Malformed signature header: non-integer timestamp."
54
+ end
55
+ when "v1"
56
+ candidates << value
57
+ end
58
+ end
59
+
60
+ if timestamp.nil?
61
+ raise WebhookVerificationError,
62
+ "Malformed signature header: missing 't=' timestamp."
63
+ end
64
+ if candidates.empty?
65
+ raise WebhookVerificationError,
66
+ "Malformed signature header: missing 'v1=' signature."
67
+ end
68
+
69
+ unless tolerance.nil?
70
+ drift = (Time.now.to_i - timestamp).abs
71
+ if drift > tolerance
72
+ raise WebhookVerificationError,
73
+ "Timestamp outside the tolerance window (#{drift}s > #{tolerance}s). " \
74
+ "The delivery may be stale or replayed."
75
+ end
76
+ end
77
+
78
+ signed_value = "#{timestamp}.".b + payload.b
79
+ expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_value)
80
+
81
+ unless candidates.any? { |candidate| secure_compare(expected, candidate) }
82
+ raise WebhookVerificationError,
83
+ "Signature mismatch: the payload was not signed with this secret, " \
84
+ "or the body was modified in transit."
85
+ end
86
+
87
+ begin
88
+ event = JSON.parse(payload)
89
+ rescue JSON::ParserError
90
+ raise WebhookVerificationError, "Payload is not valid JSON."
91
+ end
92
+ unless event.is_a?(Hash)
93
+ raise WebhookVerificationError, "Payload is not a JSON object."
94
+ end
95
+ event
96
+ end
97
+
98
+ private
99
+
100
+ # Constant-time string comparison.
101
+ def secure_compare(expected, candidate)
102
+ return false unless candidate.is_a?(String)
103
+
104
+ if OpenSSL.respond_to?(:secure_compare)
105
+ OpenSSL.secure_compare(expected, candidate)
106
+ else
107
+ expected.bytesize == candidate.bytesize &&
108
+ OpenSSL.fixed_length_secure_compare(expected, candidate)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Official Ruby SDK for the OmniSocials Public API.
4
+ #
5
+ # Docs: https://docs.omnisocials.com
6
+ #
7
+ # require "omnisocials"
8
+ #
9
+ # client = OmniSocials::Client.new # reads OMNISOCIALS_API_KEY from env
10
+ # post = client.posts.create(
11
+ # content: "Hello from Ruby",
12
+ # channels: ["instagram", "linkedin"]
13
+ # )
14
+
15
+ require_relative "omnisocials/version"
16
+ require_relative "omnisocials/errors"
17
+ require_relative "omnisocials/internal"
18
+ require_relative "omnisocials/webhooks"
19
+ require_relative "omnisocials/resources/posts"
20
+ require_relative "omnisocials/resources/media"
21
+ require_relative "omnisocials/resources/folders"
22
+ require_relative "omnisocials/resources/accounts"
23
+ require_relative "omnisocials/resources/analytics"
24
+ require_relative "omnisocials/resources/locations"
25
+ require_relative "omnisocials/resources/webhooks"
26
+ require_relative "omnisocials/client"
27
+
28
+ module OmniSocials
29
+ end
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: omnisocials
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - OmniSocials
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-07-15 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Schedule and publish social media posts, upload media, and read analytics
14
+ across Instagram, Facebook, LinkedIn, YouTube, TikTok, X, Pinterest, Bluesky, Threads,
15
+ Mastodon, and Google Business via the OmniSocials Public API. Zero runtime dependencies
16
+ (Net::HTTP, OpenSSL, JSON).
17
+ email:
18
+ - hello@omnisocials.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - README.md
24
+ - lib/omnisocials.rb
25
+ - lib/omnisocials/client.rb
26
+ - lib/omnisocials/errors.rb
27
+ - lib/omnisocials/internal.rb
28
+ - lib/omnisocials/resources/accounts.rb
29
+ - lib/omnisocials/resources/analytics.rb
30
+ - lib/omnisocials/resources/folders.rb
31
+ - lib/omnisocials/resources/locations.rb
32
+ - lib/omnisocials/resources/media.rb
33
+ - lib/omnisocials/resources/posts.rb
34
+ - lib/omnisocials/resources/webhooks.rb
35
+ - lib/omnisocials/version.rb
36
+ - lib/omnisocials/webhooks.rb
37
+ homepage: https://omnisocials.com
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ homepage_uri: https://omnisocials.com
42
+ documentation_uri: https://docs.omnisocials.com
43
+ source_code_uri: https://github.com/OmniSocials/omnisocials-ruby
44
+ bug_tracker_uri: https://github.com/OmniSocials/omnisocials-ruby/issues
45
+ rubygems_mfa_required: 'true'
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.4.19
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Official Ruby SDK for the OmniSocials Public API
65
+ test_files: []