coffrify 0.2.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7045ff71ac4b8f5fd52ef0bff58665f4b68041d352c0cff4c02d7309f3e7fe3b
4
+ data.tar.gz: fd2dd9783511a0c72928ff9c1ec6ee9885c500f8ce698f62b289138ac174add8
5
+ SHA512:
6
+ metadata.gz: 5b12face4179600ad371f25e4590e964ef7de5d9d1704931f39db2452341aa35e23a696d2af1501625340b084f073aea6d4397dec73effd2e5a049d1ae364a30
7
+ data.tar.gz: c196306ce5ed50b891afb73c87b6486e2e601450af04da2ef395b3b6a5e1884198bfe4bcb6040bfae8ec19ca8f2ffcd427ad53cea5209d2fd4316748f9933d80
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Coffrify
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # Coffrify — Ruby SDK
2
+
3
+ Official Ruby SDK for [Coffrify](https://coffrify.com), encrypted file transfer infrastructure.
4
+
5
+ Pure stdlib (`net/http`, `openssl`, `json`). **Zero runtime dependencies.** Requires Ruby 2.7+.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ gem install coffrify
11
+ # or in a Gemfile:
12
+ # gem "coffrify", "~> 0.1"
13
+ ```
14
+
15
+ ## Quickstart
16
+
17
+ ```ruby
18
+ require "coffrify"
19
+
20
+ coffrify = Coffrify::Client.new(api_key: ENV.fetch("COFFRIFY_API_KEY"))
21
+
22
+ # Create a transfer
23
+ transfer = coffrify.transfers.create(
24
+ [{ name: "rapport.pdf", size: 1_240_000, mime_type: "application/pdf" }],
25
+ expires_in_hours: 72,
26
+ max_downloads: 10,
27
+ password: "s3cret!",
28
+ )
29
+ puts transfer["share_url"]
30
+
31
+ # List
32
+ puts coffrify.transfers.list(limit: 5)
33
+
34
+ # Webhooks
35
+ hook = coffrify.webhooks.create(
36
+ name: "Production hook",
37
+ url: "https://app.example.com/hooks/coffrify",
38
+ events: ["transfer.created", "transfer.downloaded", "transfer.scan_infected"],
39
+ )
40
+ puts "Save this secret: #{hook['secret']}"
41
+
42
+ # Catalog of every supported event
43
+ coffrify.webhooks.events["data"].each { |e| puts "#{e['type']} (#{e['family']})" }
44
+ ```
45
+
46
+ ## Webhook signature verification
47
+
48
+ ```ruby
49
+ require "sinatra"
50
+ require "coffrify"
51
+
52
+ post "/hooks/coffrify" do
53
+ raw = request.body.read
54
+ sig = request.env["HTTP_X_COFFRIFY_SIGNATURE"]
55
+ v = Coffrify::Webhook.verify(raw, sig, ENV.fetch("COFFRIFY_WEBHOOK_SECRET"))
56
+ halt 400, v[:reason] unless v[:valid]
57
+
58
+ case v[:event]["type"]
59
+ when "transfer.downloaded" then handle_download(v[:event])
60
+ when "transfer.scan_infected" then quarantine!(v[:event])
61
+ end
62
+ status 200
63
+ end
64
+ ```
65
+
66
+ The header has the form `t=<timestamp>,v1=<hmac_hex>`. Verification:
67
+
68
+ 1. Confirms the timestamp is within ±5 minutes (replay protection).
69
+ 2. Recomputes `HMAC-SHA256(secret, "<timestamp>.<raw_body>")` in constant time.
70
+
71
+ ## API key rotation
72
+
73
+ ```ruby
74
+ result = coffrify.api_keys.rotate("ak_…", grace_days: 7)
75
+ puts "New key (save now): #{result['new_key']}"
76
+ puts "Old key auto-revokes at: #{result['grace_until']}"
77
+ ```
78
+
79
+ ## License
80
+
81
+ MIT.
@@ -0,0 +1,154 @@
1
+ module Coffrify
2
+ class Client
3
+ DEFAULT_API_URL = "https://api.coffrify.com".freeze
4
+ DEFAULT_TIMEOUT = 30
5
+
6
+ attr_reader :api_key, :api_url, :workspace_id, :timeout
7
+
8
+ def initialize(api_key:, api_url: DEFAULT_API_URL, workspace_id: nil, timeout: DEFAULT_TIMEOUT)
9
+ raise ArgumentError, "api_key must start with 'cfy_'" unless api_key.is_a?(String) && api_key.start_with?("cfy_")
10
+ @api_key = api_key
11
+ @api_url = api_url
12
+ @workspace_id = workspace_id
13
+ @timeout = timeout
14
+ end
15
+
16
+ # ── Resources ────────────────────────────────────────────────────────
17
+ def transfers; @transfers ||= Transfers.new(self); end
18
+ def webhooks; @webhooks ||= Webhooks.new(self); end
19
+ def api_keys; @api_keys ||= ApiKeys.new(self); end
20
+ def audit; @audit ||= Audit.new(self); end
21
+
22
+ # ── HTTP plumbing ────────────────────────────────────────────────────
23
+ def request(method, path, body: nil, query: nil, headers: {})
24
+ uri = URI.parse("#{api_url}/v1#{path}")
25
+ uri.query = URI.encode_www_form(query) if query && !query.empty?
26
+
27
+ http = Net::HTTP.new(uri.host, uri.port)
28
+ http.use_ssl = uri.scheme == "https"
29
+ http.read_timeout = timeout
30
+ http.open_timeout = timeout
31
+
32
+ req = build_request(method, uri, body, headers)
33
+ attempts = 0
34
+ begin
35
+ attempts += 1
36
+ resp = http.request(req)
37
+ handle_response(resp)
38
+ rescue Net::OpenTimeout, Net::ReadTimeout, IOError, Errno::ECONNRESET => e
39
+ if attempts < 3
40
+ sleep(0.5 * (2 ** (attempts - 1)))
41
+ retry
42
+ end
43
+ raise Error, "Network error after #{attempts} attempts: #{e.message}"
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def build_request(method, uri, body, extra_headers)
50
+ cls = case method.to_s.upcase
51
+ when "GET" then Net::HTTP::Get
52
+ when "POST" then Net::HTTP::Post
53
+ when "PATCH" then Net::HTTP::Patch
54
+ when "DELETE" then Net::HTTP::Delete
55
+ else raise ArgumentError, "Unsupported method: #{method}"
56
+ end
57
+ req = cls.new(uri.request_uri)
58
+ req["Authorization"] = "Bearer #{api_key}"
59
+ req["User-Agent"] = "coffrify-ruby/#{VERSION}"
60
+ req["Content-Type"] = "application/json"
61
+ req["Accept"] = "application/json"
62
+ req["X-Coffrify-Workspace-Id"] = workspace_id if workspace_id
63
+ # Idempotency for non-GET
64
+ req["Idempotency-Key"] = "rb_#{SecureRandom.uuid}" unless method.to_s.upcase == "GET"
65
+ extra_headers.each { |k, v| req[k] = v }
66
+ req.body = body.to_json if body
67
+ req
68
+ end
69
+
70
+ def handle_response(resp)
71
+ status = resp.code.to_i
72
+ body = resp.body && !resp.body.empty? ? (JSON.parse(resp.body) rescue resp.body) : nil
73
+ return body if status >= 200 && status < 300
74
+
75
+ message = body.is_a?(Hash) ? (body["message"] || body["error"] || resp.message) : resp.message
76
+ code = body.is_a?(Hash) ? body["code"] : nil
77
+ klass = case status
78
+ when 401, 403 then AuthError
79
+ when 404 then NotFoundError
80
+ when 422, 400 then ValidationError
81
+ when 429 then RateLimitError
82
+ else ApiError
83
+ end
84
+ raise klass.new(status: status, message: message.to_s, code: code, details: body)
85
+ end
86
+ end
87
+
88
+ # ── Resources ──────────────────────────────────────────────────────────
89
+ class Transfers
90
+ def initialize(client); @c = client; end
91
+
92
+ def list(limit: 20, offset: 0, status: nil)
93
+ q = { limit: limit, offset: offset }
94
+ q[:status] = status if status
95
+ @c.request(:get, "/transfers", query: q)
96
+ end
97
+
98
+ def get(id); @c.request(:get, "/transfers/#{escape(id)}"); end
99
+ def delete(id); @c.request(:delete, "/transfers/#{escape(id)}"); end
100
+
101
+ def create(files, **options)
102
+ @c.request(:post, "/transfers", body: { files: files, **options })
103
+ end
104
+
105
+ private
106
+ def escape(s); URI.encode_www_form_component(s); end
107
+ end
108
+
109
+ class Webhooks
110
+ def initialize(client); @c = client; end
111
+
112
+ def list; @c.request(:get, "/webhooks"); end
113
+ def get(id); @c.request(:get, "/webhooks/#{escape(id)}"); end
114
+ def create(**body); @c.request(:post, "/webhooks", body: body); end
115
+ def update(id, **b); @c.request(:patch, "/webhooks", body: b.merge(id: id)); end
116
+ def delete(id); @c.request(:delete, "/webhooks", body: { id: id }); end
117
+ def test(id); @c.request(:post, "/webhooks/#{escape(id)}/test"); end
118
+ def deliveries(id, limit: 20)
119
+ @c.request(:get, "/webhooks/#{escape(id)}/deliveries", query: { limit: limit })
120
+ end
121
+ def replay(delivery_id)
122
+ @c.request(:post, "/webhooks/deliveries/#{escape(delivery_id)}/replay")
123
+ end
124
+ def events
125
+ @c.request(:get, "/webhooks/events")
126
+ end
127
+
128
+ private
129
+ def escape(s); URI.encode_www_form_component(s); end
130
+ end
131
+
132
+ class ApiKeys
133
+ def initialize(client); @c = client; end
134
+ def list; @c.request(:get, "/api-keys"); end
135
+ def create(**body); @c.request(:post, "/api-keys", body: body); end
136
+ def revoke(id); @c.request(:delete, "/api-keys/#{escape(id)}"); end
137
+ def rotate(id, grace_days: 7)
138
+ @c.request(:post, "/api-keys/#{escape(id)}/rotate", body: { grace_days: grace_days })
139
+ end
140
+ private
141
+ def escape(s); URI.encode_www_form_component(s); end
142
+ end
143
+
144
+ class Audit
145
+ def initialize(client); @c = client; end
146
+ def list(action: nil, since: nil, until_: nil, limit: 100)
147
+ q = { limit: limit }
148
+ q[:action] = action if action
149
+ q[:since] = since if since
150
+ q[:until] = until_ if until_
151
+ @c.request(:get, "/audit", query: q)
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,18 @@
1
+ module Coffrify
2
+ class Error < StandardError; end
3
+
4
+ class ApiError < Error
5
+ attr_reader :status, :code, :details
6
+ def initialize(status:, message:, code: nil, details: nil)
7
+ super(message)
8
+ @status = status
9
+ @code = code
10
+ @details = details
11
+ end
12
+ end
13
+
14
+ class AuthError < ApiError; end
15
+ class RateLimitError < ApiError; end
16
+ class NotFoundError < ApiError; end
17
+ class ValidationError < ApiError; end
18
+ end
@@ -0,0 +1,56 @@
1
+ module Coffrify
2
+ # Static event catalog mirroring the JS SDK / REST `GET /v1/webhooks/events`.
3
+ EVENT_CATALOG = [
4
+ # Transfer
5
+ { type: "transfer.created", family: "transfer", stability: "stable" },
6
+ { type: "transfer.downloaded", family: "transfer", stability: "stable" },
7
+ { type: "transfer.expired", family: "transfer", stability: "stable" },
8
+ { type: "transfer.deleted", family: "transfer", stability: "stable" },
9
+ { type: "transfer.cloned", family: "transfer", stability: "stable" },
10
+ { type: "transfer.scanned", family: "transfer", stability: "stable" },
11
+ { type: "transfer.scan_clean", family: "transfer", stability: "stable", required_plan: "pro" },
12
+ { type: "transfer.scan_infected", family: "transfer", stability: "stable", required_plan: "pro" },
13
+ { type: "transfer.scan_quarantined", family: "transfer", stability: "stable", required_plan: "pro" },
14
+ { type: "transfer.limit_reached", family: "transfer", stability: "stable" },
15
+ { type: "transfer.password_failed", family: "transfer", stability: "stable" },
16
+ { type: "transfer.geo_blocked", family: "transfer", stability: "stable", required_plan: "ultra" },
17
+ { type: "transfer.preview_opened", family: "transfer", stability: "beta" },
18
+ { type: "transfer.email_sent", family: "transfer", stability: "stable" },
19
+ { type: "transfer.e2e_created", family: "transfer", stability: "stable" },
20
+ # Workspace
21
+ { type: "workspace.created", family: "workspace", stability: "stable" },
22
+ { type: "workspace.plan_changed", family: "workspace", stability: "stable" },
23
+ { type: "workspace.payment_succeeded", family: "workspace", stability: "stable" },
24
+ { type: "workspace.payment_failed", family: "workspace", stability: "stable" },
25
+ { type: "workspace.usage_limit_warning", family: "workspace", stability: "stable" },
26
+ { type: "workspace.usage_limit_reached", family: "workspace", stability: "stable" },
27
+ # Members
28
+ { type: "member.invited", family: "member", stability: "stable" },
29
+ { type: "member.accepted", family: "member", stability: "stable" },
30
+ { type: "member.removed", family: "member", stability: "stable" },
31
+ # API keys / tokens
32
+ { type: "api_key.created", family: "api_key", stability: "stable" },
33
+ { type: "api_key.revoked", family: "api_key", stability: "stable" },
34
+ { type: "api_key.rotated", family: "api_key", stability: "stable" },
35
+ { type: "api_key.expired", family: "api_key", stability: "stable" },
36
+ { type: "api_key.suspicious_usage", family: "api_key", stability: "beta", required_plan: "ultra" },
37
+ { type: "api_token.created", family: "api_token", stability: "stable" },
38
+ { type: "api_token.used", family: "api_token", stability: "beta" },
39
+ { type: "api_token.expired", family: "api_token", stability: "stable" },
40
+ # Webhook self
41
+ { type: "webhook.delivery_failed_final", family: "webhook", stability: "stable" },
42
+ { type: "webhook.endpoint_disabled", family: "webhook", stability: "stable" },
43
+ # SCIM/SAML
44
+ { type: "scim.user_provisioned", family: "scim", stability: "stable", required_plan: "enterprise" },
45
+ { type: "scim.user_deprovisioned", family: "scim", stability: "stable", required_plan: "enterprise" },
46
+ { type: "saml.login_succeeded", family: "saml", stability: "stable", required_plan: "enterprise" },
47
+ { type: "saml.login_failed", family: "saml", stability: "stable", required_plan: "enterprise" },
48
+ # Audit/GDPR
49
+ { type: "audit.exported", family: "audit", stability: "stable" },
50
+ { type: "audit.policy_violated", family: "audit", stability: "beta", required_plan: "ultra" },
51
+ { type: "gdpr.deletion_requested", family: "gdpr", stability: "stable", required_plan: "pro" },
52
+ { type: "gdpr.export_requested", family: "gdpr", stability: "stable", required_plan: "pro" },
53
+ # System
54
+ { type: "ping", family: "system", stability: "stable" },
55
+ ].freeze
56
+ end
@@ -0,0 +1,3 @@
1
+ module Coffrify
2
+ VERSION = "0.2.0".freeze
3
+ end
@@ -0,0 +1,130 @@
1
+ require "openssl"
2
+ require "json"
3
+ require "base64"
4
+
5
+ module Coffrify
6
+ # Webhook signature verification.
7
+ #
8
+ # Two formats are auto-detected by `verify_from_headers`:
9
+ #
10
+ # * **Standard Webhooks** (https://www.standardwebhooks.com/) — preferred.
11
+ # headers: webhook-id / webhook-timestamp / webhook-signature
12
+ # signed payload: "<id>.<ts>.<raw_body>"
13
+ # signature : "v1,<base64>" (space-separated for rotation).
14
+ #
15
+ # * **Coffrify legacy** — single header `X-Coffrify-Signature: t=<ts>,v1=<hex>`.
16
+ # signed payload : "<ts>.<raw_body>"
17
+ #
18
+ # `secret` accepts a String OR an Array of Strings — pass multiple values to
19
+ # support rotation grace windows. Returns
20
+ # { valid:, event:, reason:, matched_secret_index: }
21
+ module Webhook
22
+ DEFAULT_TOLERANCE_SECONDS = 300 # 5 minutes
23
+ SIGNATURE_VERSION = "v1".freeze
24
+
25
+ # Resolve a Coffrify secret to its raw key bytes.
26
+ # "whsec_<hex>" → bytes via hex decode (Standard Webhooks convention).
27
+ # Anything else returned as-is.
28
+ def self.resolve_key(secret)
29
+ if secret.is_a?(String) && secret.start_with?("whsec_") && secret.length > 8
30
+ hex = secret[6..]
31
+ return [hex].pack("H*") if hex =~ /\A[0-9a-f]+\z/i && hex.length.even?
32
+ end
33
+ secret
34
+ end
35
+
36
+ def self.normalise_secrets(secret)
37
+ arr = secret.is_a?(Array) ? secret : [secret]
38
+ arr.compact.reject { |s| !s.is_a?(String) || s.empty? }
39
+ end
40
+
41
+ # Legacy Coffrify header verification.
42
+ def self.verify(raw_body, signature_header, secret, tolerance_seconds: DEFAULT_TOLERANCE_SECONDS)
43
+ secrets = normalise_secrets(secret)
44
+ return result(false, reason: "missing signature") if signature_header.nil? || signature_header.empty?
45
+ return result(false, reason: "missing secret") if secrets.empty?
46
+
47
+ parts = signature_header.split(",").map { |p| p.strip.split("=", 2) }.to_h
48
+ timestamp = parts["t"]
49
+ sig_hex = parts[SIGNATURE_VERSION]
50
+ return result(false, reason: "malformed signature header") if timestamp.nil? || sig_hex.nil?
51
+
52
+ ts = timestamp.to_i
53
+ return result(false, reason: "timestamp out of tolerance") if (Time.now.to_i - ts).abs > tolerance_seconds
54
+
55
+ signed = "#{timestamp}.#{raw_body}"
56
+ provided = sig_hex.downcase
57
+ secrets.each_with_index do |s, i|
58
+ expected = OpenSSL::HMAC.hexdigest("SHA256", resolve_key(s), signed)
59
+ if secure_compare(expected, provided)
60
+ event = JSON.parse(raw_body) rescue nil
61
+ return result(true, event: event, matched_secret_index: i)
62
+ end
63
+ end
64
+ result(false, reason: "signature mismatch")
65
+ end
66
+
67
+ # Auto-detect verification from a request headers hash (case-insensitive).
68
+ # Standard Webhooks is preferred when its three headers are present.
69
+ def self.verify_from_headers(raw_body, headers, secret, tolerance_seconds: DEFAULT_TOLERANCE_SECONDS)
70
+ secrets = normalise_secrets(secret)
71
+ return result(false, reason: "missing secret") if secrets.empty?
72
+
73
+ lookup = lambda do |name|
74
+ target = name.downcase
75
+ pair = headers.find { |k, _| k.to_s.downcase == target }
76
+ pair && pair[1].is_a?(Array) ? pair[1].first : (pair && pair[1])
77
+ end
78
+
79
+ std_id = lookup.call("webhook-id")
80
+ std_ts = lookup.call("webhook-timestamp")
81
+ std_sig = lookup.call("webhook-signature")
82
+
83
+ if std_id && std_ts && std_sig
84
+ return result(false, reason: "malformed timestamp") unless std_ts.to_s =~ /\A\d+\z/
85
+ ts = std_ts.to_i
86
+ return result(false, reason: "timestamp out of tolerance") if (Time.now.to_i - ts).abs > tolerance_seconds
87
+
88
+ candidates = std_sig.to_s.split(/\s+/).filter_map do |piece|
89
+ next nil unless piece.start_with?("#{SIGNATURE_VERSION},")
90
+ begin
91
+ Base64.strict_decode64(piece[(SIGNATURE_VERSION.length + 1)..])
92
+ rescue ArgumentError
93
+ nil
94
+ end
95
+ end
96
+ return result(false, reason: "malformed signature") if candidates.empty?
97
+
98
+ message = "#{std_id}.#{ts}.#{raw_body}"
99
+ secrets.each_with_index do |s, i|
100
+ expected = OpenSSL::HMAC.digest("SHA256", resolve_key(s), message)
101
+ candidates.each do |c|
102
+ if secure_compare(expected, c)
103
+ event = JSON.parse(raw_body) rescue nil
104
+ return result(true, event: event, matched_secret_index: i)
105
+ end
106
+ end
107
+ end
108
+ return result(false, reason: "signature mismatch")
109
+ end
110
+
111
+ # Fall back to legacy.
112
+ legacy = lookup.call("x-coffrify-signature")
113
+ return verify(raw_body, legacy, secrets, tolerance_seconds: tolerance_seconds) if legacy
114
+ result(false, reason: "malformed")
115
+ end
116
+
117
+ def self.result(valid, event: nil, reason: nil, matched_secret_index: nil)
118
+ { valid: valid, event: event, reason: reason, matched_secret_index: matched_secret_index }
119
+ end
120
+
121
+ # Constant-time comparison to thwart timing attacks
122
+ def self.secure_compare(a, b)
123
+ return false if a.bytesize != b.bytesize
124
+ l = a.unpack("C*")
125
+ res = 0
126
+ b.each_byte { |byte| res |= byte ^ l.shift }
127
+ res == 0
128
+ end
129
+ end
130
+ end
data/lib/coffrify.rb ADDED
@@ -0,0 +1,28 @@
1
+ # Coffrify Ruby SDK
2
+ # https://coffrify.com | https://docs.coffrify.dev
3
+ #
4
+ # Pure stdlib — no runtime dependencies. Requires Ruby 2.7+.
5
+ #
6
+ # Quickstart:
7
+ # require "coffrify"
8
+ # coffrify = Coffrify::Client.new(api_key: ENV.fetch("COFFRIFY_API_KEY"))
9
+ # transfer = coffrify.transfers.create(
10
+ # [{ name: "report.pdf", size: 1_240_000, mime_type: "application/pdf" }],
11
+ # expires_in_hours: 72, max_downloads: 10
12
+ # )
13
+ # puts transfer["share_url"]
14
+
15
+ require "net/http"
16
+ require "uri"
17
+ require "json"
18
+ require "openssl"
19
+ require "securerandom"
20
+
21
+ require_relative "coffrify/version"
22
+ require_relative "coffrify/errors"
23
+ require_relative "coffrify/client"
24
+ require_relative "coffrify/webhook"
25
+ require_relative "coffrify/event_catalog"
26
+
27
+ module Coffrify
28
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: coffrify
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Coffrify
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-05-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.20'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.20'
27
+ description: Send transfers, manage webhooks, verify signatures, query audit logs
28
+ from Ruby. Webhooks signed with HMAC-SHA256, retries 72h.
29
+ email: support@coffrify.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.md
36
+ - lib/coffrify.rb
37
+ - lib/coffrify/client.rb
38
+ - lib/coffrify/errors.rb
39
+ - lib/coffrify/event_catalog.rb
40
+ - lib/coffrify/version.rb
41
+ - lib/coffrify/webhook.rb
42
+ homepage: https://coffrify.com
43
+ licenses:
44
+ - MIT
45
+ metadata:
46
+ documentation_uri: https://docs.coffrify.dev
47
+ source_code_uri: https://github.com/coffrify/coffrify-ruby
48
+ bug_tracker_uri: https://github.com/coffrify/coffrify-ruby/issues
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '2.7'
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.4.19
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: Official Ruby SDK for Coffrify — encrypted file transfer infrastructure.
68
+ test_files: []