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.
- checksums.yaml +7 -0
- data/README.md +170 -0
- data/lib/pushizo/base_resource.rb +14 -0
- data/lib/pushizo/client.rb +264 -0
- data/lib/pushizo/errors.rb +96 -0
- data/lib/pushizo/file_upload.rb +30 -0
- data/lib/pushizo/generated/models.rb +9125 -0
- data/lib/pushizo/generated/resources.rb +4221 -0
- data/lib/pushizo/identity_token.rb +40 -0
- data/lib/pushizo/model.rb +117 -0
- data/lib/pushizo/transport.rb +26 -0
- data/lib/pushizo/version.rb +5 -0
- data/lib/pushizo/webhook.rb +92 -0
- data/lib/pushizo.rb +27 -0
- metadata +62 -0
|
@@ -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,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: []
|