pushizo 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,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "json"
5
+ require "openssl"
6
+
7
+ module Pushizo
8
+ # SDK JWT identity verification belirteci (ES256; ADR-0023 §9): sub = external_id, aud = app_id, ömür en çok
9
+ # 24 saat. Uygulamanız belirteci kendi sunucusundan alır ve istemci SDK'sının login(external_id, jwt) çağrısına verir.
10
+ module IdentityToken
11
+ MAX_LIFETIME_SECONDS = 86_400
12
+
13
+ module_function
14
+
15
+ def create(private_key_pem, kid:, app_id:, external_id:, expires_in: 3600, now: nil)
16
+ raise ArgumentError, "expires_in 1…86400 aralığında olmalı" unless expires_in.is_a?(Integer) && expires_in.between?(1, MAX_LIFETIME_SECONDS)
17
+
18
+ key = OpenSSL::PKey.read(private_key_pem)
19
+ raise ArgumentError, "ES256 için P-256 EC özel anahtarı gerekli" unless key.is_a?(OpenSSL::PKey::EC) && key.group.curve_name == "prime256v1"
20
+
21
+ iat = (now || Time.now).to_i
22
+ header = b64url(JSON.generate({ alg: "ES256", typ: "JWT", kid: kid }))
23
+ payload = b64url(JSON.generate({ sub: external_id, aud: app_id, iat: iat, exp: iat + expires_in }))
24
+ signing_input = "#{header}.#{payload}"
25
+ der = key.sign(OpenSSL::Digest.new("SHA256"), signing_input)
26
+ # JWS ES256 imzası DER değil r||s (her biri 32 bayt).
27
+ r, s = OpenSSL::ASN1.decode(der).value.map { |i| i.value.to_s(2).rjust(32, "\0".b) }
28
+ "#{signing_input}.#{b64url(r + s)}"
29
+ end
30
+
31
+ def b64url(data)
32
+ Base64.urlsafe_encode64(data, padding: false)
33
+ end
34
+ end
35
+
36
+ # Kısayol: Pushizo.identity_token(pem, kid:, app_id:, external_id:).
37
+ def self.identity_token(private_key_pem, **kwargs)
38
+ IdentityToken.create(private_key_pem, **kwargs)
39
+ end
40
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Pushizo
6
+ # Çalışma zamanı tip dönüşümü: API yanıtlarını model sınıflarına, istek gövdelerini JSON uyumlu hash'e çevirir.
7
+ # Tip tanımı: :string, :integer, :number, :boolean, :binary, :any, [:array, T], [:map, T] veya model sınıf adı (String).
8
+ module Types
9
+ module_function
10
+
11
+ def coerce(value, type)
12
+ return value if value.nil?
13
+
14
+ case type
15
+ when String
16
+ klass = Pushizo::Models.const_get(type)
17
+ value.is_a?(Hash) ? klass.new(value) : value
18
+ when Array
19
+ kind, inner = type
20
+ if kind == :array && value.is_a?(Array)
21
+ value.map { |v| coerce(v, inner) }
22
+ elsif kind == :map && value.is_a?(Hash)
23
+ value.each_with_object({}) { |(k, v), h| h[k.to_s] = coerce(v, inner) }
24
+ else
25
+ value
26
+ end
27
+ else
28
+ value
29
+ end
30
+ end
31
+
32
+ # Model, sembol anahtarlı hash ve iç içe yapıları JSON uyumlu (string anahtarlı) değere çevirir.
33
+ def serialize(value)
34
+ case value
35
+ when BaseModel then serialize(value.to_h)
36
+ when Hash then value.each_with_object({}) { |(k, v), h| h[k.to_s] = serialize(v) }
37
+ when Array then value.map { |v| serialize(v) }
38
+ when Symbol then value.to_s
39
+ else value
40
+ end
41
+ end
42
+ end
43
+
44
+ # Hash tabanlı model: alanlara snake_case okuyucu/yazıcı ve [] ile erişilir; bilinmeyen alanlar korunur.
45
+ #
46
+ # msg = hs.notifications.get("msg_1")
47
+ # msg.status # => "completed"
48
+ # msg["status"] # => "completed"
49
+ # msg.to_h # => { "id" => …, "status" => … }
50
+ class BaseModel
51
+ class << self
52
+ # JSON adı => [metot adı, tip]
53
+ def fields
54
+ @fields ||= {}
55
+ end
56
+
57
+ def field(json_name, method_name, type)
58
+ fields[json_name] = [method_name, type]
59
+ define_method(method_name) { Pushizo::Types.coerce(@attributes[json_name], type) }
60
+ define_method(:"#{method_name}=") { |v| @attributes[json_name] = Pushizo::Types.serialize(v) }
61
+ end
62
+
63
+ # JSON adına veya metot adına göre JSON anahtarı.
64
+ def json_key(key)
65
+ name = key.to_s
66
+ return name if fields.key?(name)
67
+
68
+ found = fields.find { |_json, (method, _type)| method.to_s == name }
69
+ found ? found[0] : name
70
+ end
71
+ end
72
+
73
+ def initialize(attributes = {})
74
+ @attributes = {}
75
+ (attributes || {}).each { |k, v| @attributes[self.class.json_key(k)] = Pushizo::Types.serialize(v) }
76
+ end
77
+
78
+ # JSON veya metot adıyla okuma (model alanları model nesnesine çevrilir).
79
+ def [](key)
80
+ json = self.class.json_key(key)
81
+ entry = self.class.fields[json]
82
+ entry ? Pushizo::Types.coerce(@attributes[json], entry[1]) : @attributes[json]
83
+ end
84
+
85
+ def []=(key, value)
86
+ @attributes[self.class.json_key(key)] = Pushizo::Types.serialize(value)
87
+ end
88
+
89
+ def key?(key)
90
+ @attributes.key?(self.class.json_key(key))
91
+ end
92
+
93
+ # JSON adlarıyla ham hash (derin kopya).
94
+ def to_h
95
+ Marshal.load(Marshal.dump(@attributes))
96
+ end
97
+ alias to_hash to_h
98
+
99
+ def to_json(*args)
100
+ @attributes.to_json(*args)
101
+ end
102
+
103
+ def ==(other)
104
+ other.class == self.class && other.to_h == to_h
105
+ end
106
+ alias eql? ==
107
+
108
+ def hash
109
+ [self.class, @attributes].hash
110
+ end
111
+
112
+ def inspect
113
+ "#<#{self.class.name} #{@attributes.inspect}>"
114
+ end
115
+ alias to_s inspect
116
+ end
117
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "openssl"
5
+ require "uri"
6
+
7
+ module Pushizo
8
+ # Net::HTTP tabanlı varsayılan taşıma. Özel taşıma aynı imzalı #call(method, url, headers, body, timeout)
9
+ # metodu sağlar ve [durum, başlıklar (küçük harf anahtar), gövde] döndürür; ağ hatasında istisna fırlatır.
10
+ class NetHttpTransport
11
+ def call(method, url, headers, body, timeout)
12
+ uri = URI.parse(url)
13
+ http = Net::HTTP.new(uri.host, uri.port)
14
+ http.use_ssl = uri.scheme == "https"
15
+ http.open_timeout = timeout
16
+ http.read_timeout = timeout
17
+ http.write_timeout = timeout if http.respond_to?(:write_timeout=)
18
+ request = Net::HTTPGenericRequest.new(method.to_s.upcase, !body.nil?, true, uri.request_uri, headers)
19
+ request.body = body unless body.nil?
20
+ response = http.start { |conn| conn.request(request) }
21
+ response_headers = {}
22
+ response.each_header { |k, v| response_headers[k.downcase] = v }
23
+ [response.code.to_i, response_headers, response.body.to_s]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pushizo
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "openssl"
5
+
6
+ module Pushizo
7
+ # Giden webhook imza doğrulaması (alıcı tarafı).
8
+ #
9
+ # Başlık: Pushizo-Signature: t=<unix>,v1=<hex> — v1 = HMAC-SHA256(secret, "#{t}.#{ham_gövde}").
10
+ # Secret rotasyonunda başlıkta iki v1 bulunabilir; herhangi biri eşleşirse imza geçerlidir.
11
+ #
12
+ # payload = request.body.read # ham gövde (JSON.parse etmeden)
13
+ # header = request.get_header("HTTP_PUSHIZO_SIGNATURE")
14
+ # events = Pushizo::Webhook.construct_events(payload, header, ENV.fetch("HS_WEBHOOK_SECRET"))
15
+ # # Pushizo-Event-Id ile tekrarları ayıklayın; 2xx ile hızlı yanıt verin.
16
+ module Webhook
17
+ SIGNATURE_HEADER = "Pushizo-Signature"
18
+ EVENT_ID_HEADER = "Pushizo-Event-Id"
19
+ # Varsayılan zaman damgası toleransı (sn); daha eski t tekrar (replay) sayılır.
20
+ DEFAULT_TOLERANCE = 300
21
+
22
+ # Doğrulama sonucu: ok? ise timestamp, değilse reason doludur.
23
+ Result = Struct.new(:ok, :timestamp, :reason) do
24
+ def ok?
25
+ ok
26
+ end
27
+ end
28
+
29
+ module_function
30
+
31
+ # İmzayı doğrular. payload HAM istek gövdesidir; secret tek değer veya dizi (rotasyon) olabilir.
32
+ # now test için Unix zamanıdır (sn). Karşılaştırma sabit zamanlıdır.
33
+ def verify_signature(payload, header, secret, tolerance: DEFAULT_TOLERANCE, now: nil)
34
+ raw = header.is_a?(Array) ? header.first : header
35
+ return Result.new(false, nil, "missing_header") if raw.nil? || raw.to_s.empty?
36
+
37
+ timestamp = nil
38
+ signatures = []
39
+ raw.to_s.split(",").each do |part|
40
+ idx = part.index("=")
41
+ next if idx.nil? || idx.zero?
42
+
43
+ key = part[0...idx].strip
44
+ value = part[(idx + 1)..].strip
45
+ if key == "t" && value.match?(/\A[0-9]+\z/)
46
+ timestamp = value.to_i
47
+ elsif key == "v1" && value.match?(/\A[0-9a-f]{64}\z/)
48
+ signatures << [value].pack("H*")
49
+ end
50
+ end
51
+ return Result.new(false, nil, "malformed_header") if timestamp.nil? || signatures.empty?
52
+
53
+ current = (now || Time.now.to_i).to_i
54
+ return Result.new(false, nil, "timestamp_out_of_tolerance") if (current - timestamp).abs > tolerance
55
+
56
+ body = payload.to_s.b
57
+ Array(secret).each do |s|
58
+ expected = OpenSSL::HMAC.digest("SHA256", s.to_s, "#{timestamp}.".b + body)
59
+ return Result.new(true, timestamp, nil) if signatures.any? { |sig| secure_compare(sig, expected) }
60
+ end
61
+ Result.new(false, nil, "signature_mismatch")
62
+ end
63
+
64
+ # İmzayı doğrular ve gövdeyi olay listesine (Hash dizisi) çevirir: tekil gövde veya {"events": [...]} batch.
65
+ # Geçersiz imzada WebhookSignatureError fırlatır.
66
+ def construct_events(payload, header, secret, tolerance: DEFAULT_TOLERANCE, now: nil)
67
+ result = verify_signature(payload, header, secret, tolerance: tolerance, now: now)
68
+ raise WebhookSignatureError, result.reason unless result.ok?
69
+
70
+ body = begin
71
+ JSON.parse(payload.to_s)
72
+ rescue JSON::ParserError
73
+ raise WebhookSignatureError, "malformed_body"
74
+ end
75
+ raise WebhookSignatureError, "malformed_body" unless body.is_a?(Hash)
76
+
77
+ body["events"].is_a?(Array) ? body["events"] : [body]
78
+ end
79
+
80
+ def secure_compare(a, b)
81
+ return false unless a.bytesize == b.bytesize
82
+
83
+ if OpenSSL.respond_to?(:fixed_length_secure_compare)
84
+ OpenSSL.fixed_length_secure_compare(a, b)
85
+ else
86
+ result = 0
87
+ a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
88
+ result.zero?
89
+ end
90
+ end
91
+ end
92
+ end
data/lib/pushizo.rb ADDED
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Pushizo Ruby sunucu SDK'sı (Net::HTTP, bağımlılıksız).
4
+ #
5
+ # hs = Pushizo::Client.new(api_key: ENV.fetch("HS_API_KEY"), app_id: "app_…")
6
+ # hs.notifications.create(
7
+ # { target: { include_aliases: { external_id: [user_id] } }, contents: { en: "Shipped" } },
8
+ # idempotency_key: "order-#{order_id}"
9
+ # )
10
+ require_relative "pushizo/version"
11
+ require_relative "pushizo/errors"
12
+ require_relative "pushizo/model"
13
+ require_relative "pushizo/file_upload"
14
+ require_relative "pushizo/generated/models"
15
+ require_relative "pushizo/base_resource"
16
+ require_relative "pushizo/generated/resources"
17
+ require_relative "pushizo/transport"
18
+ require_relative "pushizo/client"
19
+ require_relative "pushizo/webhook"
20
+ require_relative "pushizo/identity_token"
21
+
22
+ module Pushizo
23
+ # SDK identity verification için identity_hash = HMAC-SHA256(sdk_secret, external_id) (hex).
24
+ def self.identity_hash(sdk_secret, external_id)
25
+ OpenSSL::HMAC.hexdigest("SHA256", sdk_secret, external_id.to_s)
26
+ end
27
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pushizo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Pushizo
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-10-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'Pushizo public API istemcisi: bildirim gönderme, kullanıcı/abonelik
14
+ yönetimi, segment/şablon/kampanya ve webhook imza doğrulaması (Net::HTTP, bağımlılıksız).'
15
+ email:
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - README.md
21
+ - lib/pushizo.rb
22
+ - lib/pushizo/base_resource.rb
23
+ - lib/pushizo/client.rb
24
+ - lib/pushizo/errors.rb
25
+ - lib/pushizo/file_upload.rb
26
+ - lib/pushizo/generated/models.rb
27
+ - lib/pushizo/generated/resources.rb
28
+ - lib/pushizo/identity_token.rb
29
+ - lib/pushizo/model.rb
30
+ - lib/pushizo/transport.rb
31
+ - lib/pushizo/version.rb
32
+ - lib/pushizo/webhook.rb
33
+ homepage: https://docs.pushizo.com/docs/sdk/ruby
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ rubygems_mfa_required: 'true'
38
+ homepage_uri: https://docs.pushizo.com/docs/sdk/ruby
39
+ changelog_uri: https://docs.pushizo.com/docs/changelog
40
+ documentation_uri: https://docs.pushizo.com/docs/sdk/ruby
41
+ source_code_uri: https://github.com/pushizo/pushizo-ruby
42
+ bug_tracker_uri: https://github.com/pushizo/pushizo-ruby/issues
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '3.0'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.5.22
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Pushizo sunucu SDK'sı
62
+ test_files: []