lunixi-sdk 0.1.2

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: dda2f615f12541201a6f330dbbdc4329f467a33ac7e5aea8aec2d4db0794fe88
4
+ data.tar.gz: c5c9c52a55000090157e7afcdd1fbefc3e96dd79112b65adc581c7e6664e2a4b
5
+ SHA512:
6
+ metadata.gz: 25e95b8ed3a4b7f22e745cc83b5b1c21a73faa669dddc11bdfd1c92cd69d656ebff7f402c75e1462b0c00b13cca25c71a7f51fd57c397edb019f53e00f0df06b
7
+ data.tar.gz: 7b888abc11c519dd5144dcc7b83f05fd37db37bb50e8907a7ff737db382f177a1cc26d4da311fa1eeaae59ab037604eb6b50e01700329291d9c46ad855bc1581
@@ -0,0 +1,38 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="katman_2" data-name="katman 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3617.13 1245.41">
3
+ <defs>
4
+ <style>
5
+ .cls-1 {
6
+ fill: #ff6b35;
7
+ }
8
+
9
+ .cls-2 {
10
+ fill: #fff;
11
+ }
12
+ </style>
13
+ </defs>
14
+ <g id="katman_1" data-name="katman 1">
15
+ <g>
16
+ <g>
17
+ <g>
18
+ <path class="cls-1" d="M551.17,0c-161.25,13.44-283.24,136.17-291.61,274.88-6.24,103.33,69.2,191.64,145.81,264.58,23.55,25.2,47.11,50.4,70.66,75.59-72.31,71.01-144.62,142.02-216.93,213.04-36.61,27.35-85.58,26.3-116.29,0-40.82-34.96-38.79-105.45,2.59-137.96,29.24-22.98,73.8-23.57,109.17,0,25.88-22.77,51.76-45.53,77.64-68.3-7.82-8.6-56.38-60.22-136.11-61.06-76.38-.8-125.07,45.98-137.12,57.95C1.96,675.4.08,746.23,0,760.49c-.08,14.46,1.09,89.82,62.09,146.95,8.36,7.82,60.46,55.1,137.12,52.78,27.54-.84,49.6-7.81,63.64-13.45,94-87.96,187.99-175.92,281.99-263.88l7.37-7.37v-139.58c-45.92-44.84-91.84-89.69-137.76-134.53-7.99-9.31-46.99-56.69-42.43-126.77.87-13.36,6.38-74.53,58.99-120.56,49.77-43.55,107.15-43.32,120.16-42.95"/>
19
+ <path class="cls-1" d="M551.09.02c161.25,13.44,283.24,136.17,291.61,274.88,6.24,103.33-69.2,191.64-145.81,264.58-23.55,25.2-47.11,50.4-70.66,75.59,72.31,71.01,144.62,142.02,216.93,213.04,36.61,27.35,85.58,26.3,116.29,0,40.82-34.96,38.79-105.45-2.59-137.96-29.24-22.98-73.8-23.57-109.17,0-25.88-22.77-51.76-45.53-77.64-68.3,7.82-8.6,56.38-60.22,136.11-61.06,76.38-.8,125.07,45.98,137.12,57.95,57.03,56.68,58.91,127.51,58.99,141.77.08,14.46-1.09,89.82-62.09,146.95-8.36,7.82-60.46,55.1-137.12,52.78-27.54-.84-49.6-7.81-63.64-13.45-94-87.96-187.99-175.92-281.99-263.88-2.46-2.46-4.91-4.91-7.37-7.37v-139.58c45.92-44.84,91.84-89.69,137.76-134.53,7.99-9.31,46.99-56.69,42.43-126.77-.87-13.36-6.38-74.53-58.99-120.56-49.77-43.55-107.15-43.32-120.16-42.95"/>
20
+ </g>
21
+ <g>
22
+ <path class="cls-1" d="M551.17,820.59c-103.18,100.13-206.35,200.26-309.53,300.39-7.21,4.19-22.66,11.87-43.62,12.27-26.16.5-44.58-10.65-50.44-14.31-33.19-20.73-41.88-55.64-43.62-63.39H0c.51,13.43,4.67,90.04,67.84,143.81,48.7,41.45,103.64,44.81,122,45.67,17.99.84,59.34,2.23,101.55-21.81,13.88-7.91,24.2-16.48,31.01-22.83,76.6-75.31,153.2-150.63,229.8-225.94-.35-51.28-.69-102.57-1.04-153.85Z"/>
23
+ <path class="cls-1" d="M551.09,820.62c103.18,100.13,206.35,200.26,309.53,300.39,7.21,4.19,22.66,11.87,43.62,12.27,26.16.5,44.58-10.65,50.44-14.31,33.19-20.73,41.88-55.64,43.62-63.39h103.97c-.51,13.43-4.67,90.04-67.84,143.81-48.7,41.45-103.64,44.81-122,45.67-17.99.84-59.34,2.23-101.55-21.81-13.88-7.91-24.2-16.48-31.01-22.83-76.6-75.31-153.2-150.63-229.8-225.94.35-51.28.69-102.57,1.04-153.85Z"/>
24
+ </g>
25
+ </g>
26
+ <g>
27
+ <path class="cls-2" d="M1512.66,1000.14V382h101.34v618.13h-101.34Z"/>
28
+ <path class="cls-2" d="M1911.28,1013.17c-33.64,0-61.39-5.61-83.26-16.82-21.87-11.21-39.25-25.51-52.14-42.89-12.9-17.38-22.43-35.88-28.59-55.51-6.17-19.62-10.16-38.19-11.98-55.72-1.83-17.52-2.73-31.75-2.73-42.68v-253.56h102.6v218.66c0,14.02.98,29.79,2.94,47.31,1.96,17.52,6.52,34.34,13.67,50.46,7.15,16.12,17.8,29.37,31.96,39.74,14.15,10.38,33.29,15.56,57.4,15.56,12.89,0,25.65-2.1,38.27-6.31,12.61-4.21,24.03-11.42,34.27-21.66,10.23-10.23,18.43-24.32,24.6-42.26,6.16-17.94,9.25-40.64,9.25-68.12l60.13,25.65c0,38.69-7.5,73.73-22.5,105.12-15,31.4-37.08,56.42-66.23,75.06-29.16,18.64-65.04,27.96-107.65,27.96ZM2059.72,1000.14v-140.87h-12.2v-313.27h101.76v454.14h-89.57Z"/>
29
+ <path class="cls-2" d="M2267.01,1000.14v-454.14h90.41v140.87h12.19v313.27h-102.6ZM2581.97,1000.14v-218.66c0-14.3-.99-30.13-2.94-47.52-1.96-17.38-6.52-34.13-13.67-50.25-7.15-16.12-17.81-29.36-31.96-39.74-14.16-10.37-33.29-15.56-57.4-15.56-12.9,0-25.65,2.1-38.27,6.31s-24.04,11.43-34.27,21.66c-10.24,10.24-18.44,24.25-24.6,42.05-6.17,17.81-9.25,40.58-9.25,68.33l-60.13-25.65c0-38.69,7.5-73.73,22.5-105.12,14.99-31.39,37.07-56.41,66.23-75.06,29.15-18.64,65.03-27.96,107.65-27.96,33.64,0,61.39,5.61,83.26,16.82,21.87,11.22,39.25,25.51,52.14,42.89,12.89,17.38,22.42,35.89,28.59,55.51,6.16,19.63,10.16,38.2,11.98,55.72,1.82,17.52,2.73,31.75,2.73,42.68v253.56h-102.6Z"/>
30
+ <path class="cls-2" d="M2802.79,1000.14v-454.14h101.34v454.14h-101.34Z"/>
31
+ <path class="cls-2" d="M2981,1000.14l166.1-229.59-162.73-224.55h119.42l102.18,145.07,100.5-145.07h119.42l-162.73,224.55,166.94,229.59h-119.42l-104.7-150.12-105.55,150.12h-119.42Z"/>
32
+ <path class="cls-1" d="M3506.97,1000.14v-454.14h101.34v454.14h-101.34Z"/>
33
+ <circle class="cls-2" cx="2853.46" cy="415.33" r="59.49"/>
34
+ <circle class="cls-1" cx="3557.64" cy="415.33" r="59.49"/>
35
+ </g>
36
+ </g>
37
+ </g>
38
+ </svg>
@@ -0,0 +1,38 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="katman_2" data-name="katman 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3617.13 1245.41">
3
+ <defs>
4
+ <style>
5
+ .cls-1 {
6
+ fill: #ff6b35;
7
+ }
8
+
9
+ .cls-2 {
10
+ fill: #1e3a5f;
11
+ }
12
+ </style>
13
+ </defs>
14
+ <g id="katman_1" data-name="katman 1">
15
+ <g>
16
+ <g>
17
+ <g>
18
+ <path class="cls-1" d="M551.17,0c-161.25,13.44-283.24,136.17-291.61,274.88-6.24,103.33,69.2,191.64,145.81,264.58,23.55,25.2,47.11,50.4,70.66,75.59-72.31,71.01-144.62,142.02-216.93,213.04-36.61,27.35-85.58,26.3-116.29,0-40.82-34.96-38.79-105.45,2.59-137.96,29.24-22.98,73.8-23.57,109.17,0,25.88-22.77,51.76-45.53,77.64-68.3-7.82-8.6-56.38-60.22-136.11-61.06-76.38-.8-125.07,45.98-137.12,57.95C1.96,675.4.08,746.23,0,760.49c-.08,14.46,1.09,89.82,62.09,146.95,8.36,7.82,60.46,55.1,137.12,52.78,27.54-.84,49.6-7.81,63.64-13.45,94-87.96,187.99-175.92,281.99-263.88,2.46-2.46,4.91-4.91,7.37-7.37v-139.58c-45.92-44.84-91.84-89.69-137.76-134.53-7.99-9.31-46.99-56.69-42.43-126.77.87-13.36,6.38-74.53,58.99-120.56,49.77-43.55,107.15-43.32,120.16-42.95"/>
19
+ <path class="cls-1" d="M551.09.02c161.25,13.44,283.24,136.17,291.61,274.88,6.24,103.33-69.2,191.64-145.81,264.58-23.55,25.2-47.11,50.4-70.66,75.59,72.31,71.01,144.62,142.02,216.93,213.04,36.61,27.35,85.58,26.3,116.29,0,40.82-34.96,38.79-105.45-2.59-137.96-29.24-22.98-73.8-23.57-109.17,0-25.88-22.77-51.76-45.53-77.64-68.3,7.82-8.6,56.38-60.22,136.11-61.06,76.38-.8,125.07,45.98,137.12,57.95,57.03,56.68,58.91,127.51,58.99,141.77.08,14.46-1.09,89.82-62.09,146.95-8.36,7.82-60.46,55.1-137.12,52.78-27.54-.84-49.6-7.81-63.64-13.45-94-87.96-187.99-175.92-281.99-263.88l-7.37-7.37v-139.58c45.92-44.84,91.84-89.69,137.76-134.53,7.99-9.31,46.99-56.69,42.43-126.77-.87-13.36-6.38-74.53-58.99-120.56-49.77-43.55-107.15-43.32-120.16-42.95"/>
20
+ </g>
21
+ <g>
22
+ <path class="cls-1" d="M551.17,820.59c-103.18,100.13-206.35,200.26-309.53,300.39-7.21,4.19-22.66,11.87-43.62,12.27-26.16.5-44.58-10.65-50.44-14.31-33.19-20.73-41.88-55.64-43.62-63.39H0c.51,13.43,4.67,90.04,67.84,143.81,48.7,41.45,103.64,44.81,122,45.67,17.99.84,59.34,2.23,101.55-21.81,13.88-7.91,24.2-16.48,31.01-22.83,76.6-75.31,153.2-150.63,229.8-225.94-.35-51.28-.69-102.57-1.04-153.85Z"/>
23
+ <path class="cls-1" d="M551.09,820.62c103.18,100.13,206.35,200.26,309.53,300.39,7.21,4.19,22.66,11.87,43.62,12.27,26.16.5,44.58-10.65,50.44-14.31,33.19-20.73,41.88-55.64,43.62-63.39h103.97c-.51,13.43-4.67,90.04-67.84,143.81-48.7,41.45-103.64,44.81-122,45.67-17.99.84-59.34,2.23-101.55-21.81-13.88-7.91-24.2-16.48-31.01-22.83-76.6-75.31-153.2-150.63-229.8-225.94.35-51.28.69-102.57,1.04-153.85Z"/>
24
+ </g>
25
+ </g>
26
+ <g>
27
+ <path class="cls-2" d="M1512.66,1000.14V382h101.34v618.13h-101.34Z"/>
28
+ <path class="cls-2" d="M1911.28,1013.17c-33.64,0-61.39-5.61-83.26-16.82-21.87-11.21-39.25-25.51-52.14-42.89-12.9-17.38-22.43-35.88-28.59-55.51-6.17-19.62-10.16-38.19-11.98-55.72-1.83-17.52-2.73-31.75-2.73-42.68v-253.56h102.6v218.66c0,14.02.98,29.79,2.94,47.31,1.96,17.52,6.52,34.34,13.67,50.46,7.15,16.12,17.8,29.37,31.96,39.74,14.15,10.38,33.29,15.56,57.4,15.56,12.89,0,25.65-2.1,38.27-6.31,12.61-4.21,24.03-11.42,34.27-21.66,10.23-10.23,18.43-24.32,24.6-42.26,6.16-17.94,9.25-40.64,9.25-68.12l60.13,25.65c0,38.69-7.5,73.73-22.5,105.12-15,31.4-37.08,56.42-66.23,75.06-29.16,18.64-65.04,27.96-107.65,27.96ZM2059.72,1000.14v-140.87h-12.2v-313.27h101.76v454.14h-89.57Z"/>
29
+ <path class="cls-2" d="M2267.01,1000.14v-454.14h90.41v140.87h12.19v313.27h-102.6ZM2581.97,1000.14v-218.66c0-14.3-.99-30.13-2.94-47.52-1.96-17.38-6.52-34.13-13.67-50.25-7.15-16.12-17.81-29.36-31.96-39.74-14.16-10.37-33.29-15.56-57.4-15.56-12.9,0-25.65,2.1-38.27,6.31s-24.04,11.43-34.27,21.66c-10.24,10.24-18.44,24.25-24.6,42.05-6.17,17.81-9.25,40.58-9.25,68.33l-60.13-25.65c0-38.69,7.5-73.73,22.5-105.12,14.99-31.39,37.07-56.41,66.23-75.06,29.15-18.64,65.03-27.96,107.65-27.96,33.64,0,61.39,5.61,83.26,16.82,21.87,11.22,39.25,25.51,52.14,42.89,12.89,17.38,22.42,35.89,28.59,55.51,6.16,19.63,10.16,38.2,11.98,55.72,1.82,17.52,2.73,31.75,2.73,42.68v253.56h-102.6Z"/>
30
+ <path class="cls-2" d="M2802.79,1000.14v-454.14h101.34v454.14h-101.34Z"/>
31
+ <path class="cls-2" d="M2981,1000.14l166.1-229.59-162.73-224.55h119.42l102.18,145.07,100.5-145.07h119.42l-162.73,224.55,166.94,229.59h-119.42l-104.7-150.12-105.55,150.12h-119.42Z"/>
32
+ <path class="cls-1" d="M3506.97,1000.14v-454.14h101.34v454.14h-101.34Z"/>
33
+ <circle class="cls-2" cx="2853.46" cy="415.33" r="59.49"/>
34
+ <circle class="cls-1" cx="3557.64" cy="415.33" r="59.49"/>
35
+ </g>
36
+ </g>
37
+ </g>
38
+ </svg>
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## 0.1.2 — 2026-10-31
4
+
5
+ - Add strict backend payment-page URL and official embedded-form configuration helpers, without rebuilding hosted URLs from tokens.
6
+ - Default the Rails example to an on-site CDN-mounted form with escaped JSON, nonce CSP and optional explicit hosted mode.
7
+ - Freeze and encrypt request/mode before network dispatch, cache encrypted backend URLs, retain original uncertain keys/bodies and fence replay at 23 hours.
8
+ - Add an explicit migration preserving pre-existing orders as legacy hosted attempts; unknown legacy requests or missing URLs require reconciliation rather than a new charge.
9
+ - Preserve nonce CSP while allowing exact operator-approved HTTPS form-action origins for provider/legacy POSTs; reject unsafe origin configuration before use.
10
+
11
+
12
+ ## 0.1.1 — 2026-10-31
13
+
14
+ - Add a bounded stateless Rails POST return adapter before parameter/session processing. It ignores gateway browser fields and redirects to the existing authenticated GET; checkout CSRF remains required.
15
+ - Add cross-site, empty-body, oversize and session-resume integration regressions. Browser returns never mark an order paid.
16
+
17
+ ## 0.1.0 — 2026-10-31
18
+
19
+ - Initial server-side Ruby SDK: OpenSSL Ed25519 signing, JavaScript-compatible compact JSON, concurrent token cache and bounded HTTPS transport.
20
+ - Payment intent/status/list/methods/capture/refund/void/split helpers and fraud decision/event helpers.
21
+ - Caller-owned persistent idempotency keys, safe same-key retries, safe typed errors and redirect/path injection protection.
22
+ - Raw-body v2 webhooks with timestamp validation, rotation, constant-time comparison and a transactional replay-store contract.
23
+ - Executable Rails 8.1 reference integration with encrypted checkout tokens, CSRF, EN/TR UI and atomic database webhook handling.
24
+ - Offline cryptographic, transport/TLS and Rails integration tests; lint, dependency audit, Rails security scanning and CI.
25
+ - English and Turkish SDK/Rails guides. Source distribution; RubyGems publication is a separate release step.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lunixi
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,161 @@
1
+ <p align="center"><img src=".github/assets/lunixi-logo.svg" width="300" alt="Lunixi"></p>
2
+
3
+ # Lunixi Ruby SDK
4
+
5
+ [English](README.md) · [Türkçe](README.tr.md) · [Documentation](https://docs.lunixi.com/sdks/ruby) · [Rails sample](samples/rails)
6
+
7
+ Server-side Ruby client for Lunixi payments, fraud and signed gateway requests. It signs requests with OpenSSL Ed25519, caches tokens across threads, preserves integer minor-unit amounts, retries safe operations with the same idempotency key, and verifies raw-body v2 webhooks. Private keys and card details stay out of the browser and application logs.
8
+
9
+ ## Requirements and installation
10
+
11
+ Ruby **3.3+**, OpenSSL with **Ed25519** support, and Bundler. The runnable Rails sample uses **Rails 8.1** and SQLite. The core gem does not depend on Rails.
12
+
13
+ Version **0.1.2** is distributed as source here. RubyGems publication is a separate release step; this guide does not assume a published registry package.
14
+
15
+ ```bash
16
+ git clone https://github.com/Lunixi-Payment/lunixi-ruby.git
17
+ cd lunixi-ruby
18
+ bundle install
19
+ gem build lunixi-sdk.gemspec
20
+ gem install ./lunixi-sdk-0.1.2.gem
21
+ ```
22
+
23
+ Inside this checkout, `bundle exec ruby …` loads the local gem. An application can use a reviewed checkout with `gem 'lunixi-sdk', path: '/absolute/path/to/lunixi-ruby'`. Pin an audited commit when using a Bundler Git source. Never use an unpinned moving branch for a production deployment.
24
+
25
+ ## Create a client
26
+
27
+ ```ruby
28
+ require 'lunixi'
29
+
30
+ lunixi = Lunixi::Client.new(
31
+ base_url: ENV.fetch('LUNIXI_BASE_URL'), # https://api-gateway.lunixi.com
32
+ key_id: ENV.fetch('LUNIXI_KEY_ID'),
33
+ private_key: File.read(ENV.fetch('LUNIXI_PRIVATE_KEY_PATH')),
34
+ timeout: 30,
35
+ max_retries: 2
36
+ )
37
+ ```
38
+
39
+ Create one client per merchant connection and reuse it across requests. The key determines TEST/LIVE behavior; there is no fake local sandbox switch. Keep the private key outside the public web root and source control. Register its public key in the Lunixi dashboard. Accepted private-key encodings are Ed25519 PKCS#8 PEM, base64 PKCS#8 DER, or a base64 32-byte seed / 64-byte secret key. OpenSSL performs all curve operations.
40
+
41
+ Generate a new key pair securely on your own server:
42
+
43
+ ```bash
44
+ umask 077
45
+ openssl genpkey -algorithm ED25519 -out lunixi-private.pem
46
+ openssl pkey -in lunixi-private.pem -pubout -out lunixi-public.pem
47
+ ```
48
+
49
+ Only register `lunixi-public.pem` in the dashboard. Keep the private key in your secret store; the SDK never prints it.
50
+
51
+ ## Start checkout
52
+
53
+ Calculate the total on the server and persist a UUID idempotency key with the payment attempt **before** calling Lunixi. Reuse that key after a timeout or process restart; create a different key only for a genuinely new operation.
54
+
55
+ ```ruby
56
+ result = lunixi.payments.create_intent(
57
+ {
58
+ amount: 10_050, # 100.50 TRY; integer kuruş, never 100.50
59
+ currency: 'TRY',
60
+ orderId: order.public_id,
61
+ callbackUrl: 'https://merchant.example/payments/return',
62
+ basketItems: [{ id: 'item-1', name: 'Product', itemType: 'PHYSICAL', price: 10_050 }]
63
+ },
64
+ idempotency_key: order.persisted_payment_attempt_key
65
+ )
66
+
67
+ intent_id = result.fetch('data').fetch('paymentId')
68
+ checkout_token = result.fetch('data').fetch('token')
69
+ # Persist the intent id and encrypt the checkout token at rest.
70
+ # The Rails sample validates the token and builds a fixed-origin redirect:
71
+ # Optional hosted checkout uses the validated backend paymentPageUrl, never a URL built from this token.
72
+ ```
73
+
74
+ Response envelopes are retained as received. An HTTP 2xx response, an intent/token, a return URL or an intermediate split state **does not mean funds were captured**. Confirm a verified payment webhook and reconcile the persisted intent, order reference, currency, amount and final status before fulfillment.
75
+
76
+ The shared Lunixi form owns enabled payment methods and supports the methods configured for the merchant, including eligible alternative methods and multi-card flows. Availability remains merchant/account dependent. Do not collect PAN/CVV through this SDK. The Rails example demonstrates fixed-total single-intent checkout; adapt accounting and final settlement checks before enabling split settlement, customer-paid insurance or surcharges in that example.
77
+
78
+ ## API coverage
79
+
80
+ | Operation | Ruby API |
81
+ | --- | --- |
82
+ | Create intent | `payments.create_intent(body, idempotency_key:)` |
83
+ | Get/list payment | `payments.get(id)`, `payments.list(**filters)` |
84
+ | Enabled methods | `payments.methods(payment_method: nil)` |
85
+ | Capture/preauthorization | `payments.capture(id, idempotency_key:, amount: nil)` |
86
+ | Full/partial refund | `payments.refund(id, idempotency_key:, amount: nil, reason: nil)` |
87
+ | Void authorization | `payments.void(id, idempotency_key:)` |
88
+ | Split payment (1–5 legs) | `payments.split(body, idempotency_key:)` |
89
+ | Split status/refund | `payments.get_split(id)`, `payments.refund_split(id, amount:, idempotency_key:, reason: nil)` |
90
+ | Fraud decision | `fraud.evaluate(body)` |
91
+ | Fraud transaction event | `fraud.record_event(body, idempotency_key:)` |
92
+ | Other documented endpoints | `request(method, path, body: nil, query: {}, idempotency_key: nil, signed: true, binary: false)` |
93
+
94
+ `amount`, split `totalAmount` and each leg/item price must be positive integers within JavaScript's safe integer range. Root `price`/`paidPrice` are removed fields and rejected even when null. Basket and split totals must agree. Intent installments belong to the checkout form; `installment` is rejected on intent creation. Set `preauth: true` for authorization, then capture separately with a new durable operation key. A nil capture/refund amount asks the gateway to use the remaining amount; zero, false, fractional and string amounts are rejected.
95
+
96
+ ```ruby
97
+ lunixi.payments.refund('intent_123', amount: 2_500,
98
+ reason: 'Customer request', idempotency_key: refund.persisted_operation_key)
99
+
100
+ # Advanced transport: a documented existing endpoint, not a second HTTP stack.
101
+ links = lunixi.request('GET', '/api/v1/payments/links', query: { limit: 20 })
102
+ ```
103
+
104
+ Payment, fraud and webhook helpers are explicit. Other products use the signed transport with the published gateway schema; version 0.1.2 does not claim full typed service-client parity with the PHP SDK. JSON is compact UTF-8 with JavaScript integer-index key ordering and compatible numeric formatting, so the signed bytes are exactly the transmitted bytes.
105
+
106
+ ## Raw-body webhooks and durable deduplication
107
+
108
+ ```ruby
109
+ verifier = Lunixi::WebhookVerifier.new(
110
+ secret: ENV.fetch('LUNIXI_WEBHOOK_SECRET'),
111
+ previous_secrets: [] # Up to two retained secrets during rotation.
112
+ )
113
+
114
+ verifier.process(raw_request_bytes, signature_headers, store: durable_transactional_store) do |event|
115
+ # Correlate event.data with your persisted intent and order.
116
+ # Query payment status, check amount/currency/final status, update atomically.
117
+ end
118
+ ```
119
+
120
+ The verifier accepts only v2 HMAC-SHA256 over `eventId.timestamp.rawBody`, compares signatures in constant time, binds signed ids/types to the body, rejects malformed/duplicate JSON keys, checks both future and stale timestamps (default **300 seconds**), and supports rotated secrets/signatures. Supply the exact raw request bytes before JSON parsing. Header names are case-insensitive; duplicate headers are rejected. Verification alone never consumes an event id.
121
+
122
+ The durable store implements `process_once(id:, digest:) { … }`: insert a unique event receipt and apply business changes in **one database transaction**, return `:processed` or `:duplicate`, and raise `Lunixi::ReplayConflict` if an id arrives with different bytes. Roll back the receipt on any handler failure so redelivery can recover. An in-memory set is insufficient across processes/restarts. The [Rails sample](samples/rails/app/services/rails_replay_store.rb) provides a real database implementation with a unique `(connection_id, event_id)` index.
123
+
124
+ ## Errors, retries and transport
125
+
126
+ `Lunixi::Error` is the base error. `ConfigurationError`, `SignatureError`, `TransportError`, `RedirectError`, `WebhookError`/`ReplayConflict`, and `ApiError` subclasses distinguish local, network and gateway failures. Gateway errors expose safe `status`, `code`, `request_id` and optional `retry_after`; they omit bodies, credentials and remote error text. Treat timeouts as uncertain outcomes: query/reconcile the original operation, never retry using a new key.
127
+
128
+ GET/HEAD and keyed mutations may retry network failures or HTTP 429/502/503/504, at most `max_retries` (default 2, maximum 3). Each retry keeps the same body/key and creates a fresh nonce/signature. A safe 401 refreshes credentials once. Unkeyed mutations are never automatically retried. HTTP redirects are rejected, certificate verification cannot be disabled, and URL traversal/credential injection is blocked. Requests are bounded to 1 MiB and responses to 8 MiB. Net::HTTP does not inherit ambient proxy credentials; a custom enterprise transport is injectable.
129
+
130
+ For deterministic tests, inject `transport:`, `clock:` and `sleeper:`. A transport implements `call(method:, uri:, headers:, body:)` and returns `Lunixi::Response.new(status:, headers:, body:)`. `ca_file:` permits an explicit trusted CA bundle without disabling TLS verification. Reusing the SDK is thread-safe; your custom transport must also be thread-safe.
131
+
132
+ ## Rails and validation
133
+
134
+ Follow [samples/rails/README.md](samples/rails/README.md) for the executable Rails integration, environment setup, migration and tests. It protects buyer routes with CSRF and demo authentication, encrypts checkout tokens, persists operation keys, and applies verified webhooks atomically. It is a reference integration, not a complete storefront or multitenant authorization system.
135
+
136
+ The informational return POST is handled by a separate bounded middleware: it discards at most 1 MiB, ignores all form fields and performs no order/session lookup, gateway call or financial write. It returns an empty fixed same-origin 303 to the authenticated GET; checkout CSRF stays enabled.
137
+
138
+ ```bash
139
+ bundle exec rake test
140
+ bundle exec rubocop
141
+ bundle exec bundle-audit check --update
142
+ bundle exec brakeman --force --rails8 --path samples/rails --no-pager
143
+ bundle exec rake rails:test
144
+ gem build lunixi-sdk.gemspec
145
+ ```
146
+
147
+ The SDK suite includes RFC 8032 Ed25519 and shared webhook vectors, tampering, replay/rotation, money validation, concurrent token caching, exact transmitted signatures, safe retries, and real local TLS certificate/hostname/redirect/size checks. JSON compatibility tests use Node.js as an independent implementation. Rails tests use synthetic transport fixtures and a real SQLite database; they move no funds. CI runs these checks on Ruby 3.3 and 3.4. Brakeman has one narrowly documented fixed-origin redirect false positive in `samples/rails/config/brakeman.ignore`, covered by redirect-injection tests; no broad rule is disabled.
148
+
149
+ Security: [SECURITY.md](SECURITY.md) · Contributions: [CONTRIBUTING.md](CONTRIBUTING.md) · Changes: [CHANGELOG.md](CHANGELOG.md) · MIT [license](LICENSE). Support: developer@lunixi.com.
150
+
151
+ ## On-site form and optional hosted checkout
152
+
153
+ The Rails example defaults to `LUNIXI_CHECKOUT_MODE=embedded`. It renders the existing Lunixi form on the merchant page using the fixed official CDN, HTML-escaped JSON data attributes and a per-response CSP nonce. `Lunixi::Checkout.browser_config(token)` provides only the official object/mount configuration. No gateway HTML or merchant secrets are rendered.
154
+
155
+ Set `LUNIXI_CHECKOUT_MODE=hosted` explicitly for optional redirect. `Lunixi::Checkout.payment_page_url(data.fetch('paymentPageUrl'))` and the Rails redirect wrapper validate the exact backend URL: HTTPS `pay.lunixi.com/payment/` followed by a 16–4096-character ASCII opaque value, with no userinfo, port, query, fragment or controls. It can differ from the embedded token. Do not construct a URL from that token. Both modes expose the same merchant-enabled form methods, multi-card, installments and insurance; these features do not require redirecting. The existing fixed-total single-intent settlement limitation still applies.
156
+
157
+ Run `rails db:migrate` before deploying 0.1.2. New request/mode/callback and start time are persisted before network I/O; request/token/backend URL use Active Record Encryption. Retries reuse the saved body and key and stop financial dispatch after 23 hours. Existing rows are marked legacy hosted; unknown legacy requests or missing backend URLs remain reconciliation-only, without rewriting origin, resetting a key or inventing a URL. Global mode changes affect new reservations; existing requests retain their saved presentation. CSRF/demo authorization, raw webhooks, native receipt transactions and the bounded 3DS/split return adapter remain unchanged.
158
+
159
+ Focused 0.1.2 verification: 24 Rails form/return/URL/CSP security tests with 225 assertions, 2 SDK helper tests with 5 assertions, RuboCop and Brakeman (one exact documented strict-URL redirect false positive). Prior full 0.1.1 results remain the baseline; live/merchant acceptance is deferred.
160
+
161
+ The checkout page’s CSP permits form POSTs only to `'self'` and the configured gateway origin (default `https://api-gateway.lunixi.com`). This preserves wallet/provider and legacy POST handoffs supported by the browser SDK. For another reviewed provider, set server-only `LUNIXI_FORM_ACTION_ORIGINS` to a comma-separated list of exact HTTPS origins, without spaces, paths, trailing slashes, credentials, query, fragment or wildcards. Invalid configuration fails closed; gateway responses and browser fields never expand this list. Script nonces, frame/connect restrictions and informational return handling remain in force.
data/README.tr.md ADDED
@@ -0,0 +1,134 @@
1
+ <p align="center"><img src=".github/assets/lunixi-logo.svg" width="300" alt="Lunixi"></p>
2
+
3
+ # Lunixi Ruby SDK
4
+
5
+ [English](README.md) · [Türkçe](README.tr.md) · [Dokümantasyon](https://docs.lunixi.com/tr/sdks/ruby) · [Rails örneği](samples/rails)
6
+
7
+ Lunixi ödemeleri, fraud işlemleri ve imzalı gateway çağrıları için sunucu tarafı Ruby SDK’sı. Ed25519 imzasını OpenSSL ile üretir; token’ı eşzamanlı istekler arasında önbelleğe alır, tutarları tam sayı alt birimde korur ve güvenli tekrar denemelerde aynı idempotency anahtarını kullanır. v2 webhook doğrulaması özgün istek baytları üzerinden yapılır.
8
+
9
+ ## Gereksinimler ve kurulum
10
+
11
+ Ruby **3.3+**, **Ed25519** destekli OpenSSL ve Bundler gerekir. Çalıştırılabilir örnek **Rails 8.1** ve SQLite kullanır; ana gem Rails’e bağımlı değildir.
12
+
13
+ **0.1.2** sürümü burada kaynak kod olarak dağıtılır. RubyGems yayını ayrı bir sürüm adımıdır; aşağıdaki kurulum kayıt deposunda yayımlanmış paket olduğunu varsaymaz.
14
+
15
+ ```bash
16
+ git clone https://github.com/Lunixi-Payment/lunixi-ruby.git
17
+ cd lunixi-ruby
18
+ bundle install
19
+ gem build lunixi-sdk.gemspec
20
+ gem install ./lunixi-sdk-0.1.2.gem
21
+ ```
22
+
23
+ Bu dizinde `bundle exec ruby …` yerel SDK’yı yükler. Uygulamanızın Gemfile’ında incelenmiş bir kopyayı `gem 'lunixi-sdk', path: '/tam/yol/lunixi-ruby'` ile kullanabilirsiniz. Bundler Git kaynağı kullanırken denetlenmiş commit’i sabitleyin.
24
+
25
+ ## İstemci
26
+
27
+ ```ruby
28
+ require 'lunixi'
29
+
30
+ lunixi = Lunixi::Client.new(
31
+ base_url: ENV.fetch('LUNIXI_BASE_URL'),
32
+ key_id: ENV.fetch('LUNIXI_KEY_ID'),
33
+ private_key: File.read(ENV.fetch('LUNIXI_PRIVATE_KEY_PATH')),
34
+ timeout: 30, max_retries: 2
35
+ )
36
+ ```
37
+
38
+ Gateway adresi `https://api-gateway.lunixi.com` şeklinde yalnız HTTPS origin olmalıdır. TEST/LIVE ortamını anahtar belirler. Her üye işyeri bağlantısı için bir istemci oluşturup paylaşın. Özel anahtarı kaynak kontrolünden ve public web dizininden uzak tutun; yalnız public anahtarı Lunixi paneline kaydedin. PKCS#8 PEM, base64 PKCS#8 DER, base64 32 bayt seed ve 64 bayt secret key desteklenir. Anahtar üretimi:
39
+
40
+ ```bash
41
+ umask 077
42
+ openssl genpkey -algorithm ED25519 -out lunixi-private.pem
43
+ openssl pkey -in lunixi-private.pem -pubout -out lunixi-public.pem
44
+ ```
45
+
46
+ ## Ödeme başlatma
47
+
48
+ Toplamı sunucuda hesaplayın. Çağrıdan **önce** ödeme denemesine bir UUID idempotency anahtarı kaydedin. Zaman aşımı ve yeniden başlatma sonrası aynı denemede aynı anahtarı kullanın.
49
+
50
+ ```ruby
51
+ result = lunixi.payments.create_intent(
52
+ { amount: 10_050, currency: 'TRY', orderId: order.public_id,
53
+ callbackUrl: 'https://merchant.example/payments/return',
54
+ basketItems: [{ id: 'urun-1', name: 'Product', itemType: 'PHYSICAL', price: 10_050 }] },
55
+ idempotency_key: order.persisted_payment_attempt_key
56
+ )
57
+ intent_id = result.fetch('data').fetch('paymentId')
58
+ checkout_token = result.fetch('data').fetch('token')
59
+ ```
60
+
61
+ `10_050`, 100,50 TRY’nin kuruş karşılığıdır. Tutarlar pozitif ve JavaScript güvenli tam sayı aralığında olmalıdır; float/string kabul edilmez. Eski kök `price`/`paidPrice` alanları null olsa bile reddedilir. Sepet toplamı ve split bacak toplamı ana tutarla aynı olmalıdır. Intent’te `installment` gönderilmez; taksiti ödeme formu seçtirir. `preauth: true` ile provizyon alınabilir; capture ayrı işlem ve anahtarla yapılır.
62
+
63
+ Varsayılan form mağaza içinde açılır; isteğe bağlı yönlendirme yalnız doğrulanmış backend `paymentPageUrl` adresini kullanır. Token ve adres şifrelenir; iki mod aynı Lunixi form ayarlarını paylaşır. PAN/CVV toplamayın.
64
+
65
+ HTTP 2xx, token, dönüş sayfası veya devam eden split durumu tahsilat kanıtı değildir. Doğrulanmış webhook ve sunucudan durum sorgusunda intent, sipariş, para birimi, toplam ve nihai durum uyuşmadan siparişi ödenmiş saymayın. Rails örneği sabit toplamlı tek intent’i gösterir; split tahsilat, müşteriye yansıtılan sigorta veya ek ücret açmadan önce mutabakat muhasebesini genişletin.
66
+
67
+ ## API kapsamı
68
+
69
+ | İşlem | API |
70
+ | --- | --- |
71
+ | Intent oluşturma | `payments.create_intent(body, idempotency_key:)` |
72
+ | Ödeme okuma/listeleme | `payments.get(id)`, `payments.list(**filters)` |
73
+ | Açık yöntemler | `payments.methods(payment_method: nil)` |
74
+ | Capture | `payments.capture(id, idempotency_key:, amount: nil)` |
75
+ | Tam/kısmi iade | `payments.refund(id, idempotency_key:, amount: nil, reason: nil)` |
76
+ | Provizyon iptali | `payments.void(id, idempotency_key:)` |
77
+ | Çoklu ödeme | `payments.split(body, idempotency_key:)` |
78
+ | Split durum/iade | `payments.get_split(id)`, `payments.refund_split(id, amount:, idempotency_key:, reason: nil)` |
79
+ | Fraud | `fraud.evaluate(body)`, `fraud.record_event(body, idempotency_key:)` |
80
+ | Diğer dokümante uçlar | `request(method, path, body: nil, query: {}, idempotency_key: nil, signed: true, binary: false)` |
81
+
82
+ Capture/iade için `amount: nil`, gateway’in kalan tutarı kullanmasını ister. `0`, `false`, float ve string tutar reddedilir. Her capture/iade/iptal için ayrı ve kalıcı işlem anahtarı gerekir. Yanıt zarfı korunur. Payment, fraud ve webhook yardımcıları açıktır; diğer servisler dokümante şema ile imzalı transport üzerinden çağrılır. Bu sürüm PHP SDK’sındaki tüm servisler için ayrı tipli istemci sunduğunu iddia etmez.
83
+
84
+ ## Webhook güvenliği
85
+
86
+ ```ruby
87
+ verifier = Lunixi::WebhookVerifier.new(
88
+ secret: ENV.fetch('LUNIXI_WEBHOOK_SECRET'), previous_secrets: []
89
+ )
90
+ verifier.process(raw_request_bytes, signature_headers, store: durable_transactional_store) do |event|
91
+ # Sunucu sorgusuyla doğrulayın; sipariş değişikliğini aynı transaction’da yapın.
92
+ end
93
+ ```
94
+
95
+ JSON parse işleminden önce **özgün baytları** alın. v2 HMAC-SHA256 imzası `eventId.timestamp.rawBody` üzerinde constant-time karşılaştırılır. Geçmiş/gelecek timestamp toleransı varsayılan 300 saniyedir. En fazla iki önceki secret ile rotasyon desteklenir; yinelenmiş JSON anahtarları ve tutarsız olay kimliği/tipi reddedilir.
96
+
97
+ Store, `process_once(id:, digest:) { … }` uygular: benzersiz olay kaydı ile sipariş değişikliği **aynı veritabanı transaction’ında** commit olur. Aynı olay `:duplicate`, yeni olay `:processed` döndürür. Aynı kimliğin farklı gövdesi `Lunixi::ReplayConflict` üretir. İşleyici hata verirse olay kaydı geri alınır; yeniden teslim işlenebilir. Bellek içi set yeterli değildir. [RailsReplayStore](samples/rails/app/services/rails_replay_store.rb) gerçek SQLite/veritabanı örneğidir.
98
+
99
+ ## Hatalar ve tekrar deneme
100
+
101
+ `Lunixi::Error` taban sınıftır. `ConfigurationError`, `SignatureError`, `TransportError`, `RedirectError`, `WebhookError`/`ReplayConflict`, `ApiError`, `AuthenticationError`, `ValidationError` ve `RateLimitError` ayrıştırılır. API hatası yalnız `status`, güvenli `code`, `request_id` ve `retry_after` sunar; gövde, özel anahtar ve karşı tarafın hata metni eklenmez.
102
+
103
+ GET/HEAD ve anahtarlı işlemler ağ hatası veya 429/502/503/504 için varsayılan 2, en çok 3 kez denenebilir. Aynı gövde/anahtar korunur; her çağrı yeni nonce/imza alır. Güvenli çağrı 401 alırsa token bir kez yenilenir. Anahtarsız mutation otomatik tekrarlanmaz. Belirsiz ağ sonucunda yeni anahtarla tekrar tahsilat başlatmayın; özgün işlemi sorgulayın.
104
+
105
+ TLS sertifika/hostname doğrulaması zorunludur; yönlendirmeler ve path/credential enjeksiyonu reddedilir. İstek 1 MiB, yanıt 8 MiB ile sınırlıdır. `transport:`, `clock:`, `sleeper:` testlerde enjekte edilebilir; transport `Lunixi::Response` döndürür. `ca_file:` ile güvenilen CA verilebilir; TLS doğrulaması kapatılamaz. Ortam proxy kimlik bilgileri otomatik taşınmaz.
106
+
107
+ ## Rails ve kontroller
108
+
109
+ Çalıştırma, ortam değişkenleri ve migration için [Rails rehberi](samples/rails/README.md). Örnek; CSRF, demo kimlik doğrulama, şifreli token, kalıcı idempotency, imzalı webhook ve atomik dedupe içerir; tamamlanmış mağaza veya çok kiracılı yetkilendirme sistemi değildir.
110
+
111
+ ```bash
112
+ bundle exec rake test
113
+ bundle exec rake rails:test
114
+ bundle exec rubocop
115
+ bundle exec bundle-audit check --update
116
+ bundle exec brakeman --force --rails8 --path samples/rails --no-pager
117
+ gem build lunixi-sdk.gemspec
118
+ ```
119
+
120
+ Testler gerçek para taşımaz. RFC 8032 ve ortak webhook vektörleri, bozulmuş imza, replay/rotasyon, tutar doğruluğu, eşzamanlı token, aynı anahtarla retry ve gerçek yerel TLS sertifika/hostname/yönlendirme/boyut kontrolleri kapsanır. JSON uyumu bağımsız Node.js ile karşılaştırılır. Rails testi gerçek SQLite transaction’larını kullanır. Brakeman’da sabit host yönlendirmesi için tek, gerekçeli ve test edilmiş false positive `samples/rails/config/brakeman.ignore` içinde yer alır.
121
+
122
+ [Güvenlik](SECURITY.md) · [Katkı](CONTRIBUTING.md) · [Değişiklikler](CHANGELOG.md) · [MIT lisansı](LICENSE) · developer@lunixi.com
123
+
124
+ ## Mağaza içi form ve isteğe bağlı yönlendirme
125
+
126
+ Rails örneğinde varsayılan `LUNIXI_CHECKOUT_MODE=embedded` değeridir. Mevcut Lunixi formu sabit resmi CDN, HTML için kaçırılmış JSON data attribute ve yanıta özgü CSP nonce ile mağaza sayfasında açılır. `Lunixi::Checkout.browser_config(token)` yalnız resmi object/mount yapılandırmasını verir; gateway HTML'i veya merchant secret'ı basılmaz.
127
+
128
+ İsteğe bağlı yönlendirme için açıkça `LUNIXI_CHECKOUT_MODE=hosted` seçin. `Lunixi::Checkout.payment_page_url(data.fetch('paymentPageUrl'))` backend adresini birebir doğrular: HTTPS `pay.lunixi.com/payment/` ve 16–4096 karakter ASCII opak değer; userinfo, port, sorgu, fragment veya kontrol karakteri kabul edilmez. Bu değer gömülü token'dan farklı olabilir; token'dan URL üretilmez. İki mod aynı açık yöntemleri, çoklu kartı, taksiti ve sigortayı sunar; yönlendirme şart değildir. Sabit toplamlı tek-intent tahsilat sınırlaması korunur.
129
+
130
+ 0.1.2'yi açmadan önce `rails db:migrate` çalıştırın. Yeni istek/mod/dönüş adresi/zaman ağdan önce kaydedilir; gövde, token ve backend adresi Active Record Encryption ile şifrelenir. Tekrar aynı gövde/anahtarla gider; 23 saatten eski finansal gönderim durur. Mevcut satırlar eski hosted akışı olarak işaretlenir; bilinmeyen eski istek veya eksik backend adresi origin/anahtar değiştirilmeden ve adres üretilmeden mutabakat bekler. Genel mod değişikliği yeni rezervasyonlara uygulanır; mevcut istek kayıtlı modunu korur. CSRF/demo yetkisi, ham webhook, atomik receipt ve sınırlı 3DS/çoklu kart dönüşü korunur.
131
+
132
+ 0.1.2 odaklı kontrolü: 24 Rails form/dönüş/URL/CSP güvenlik testi ve 225 assertion, 2 SDK yardımcı testi ve 5 assertion, RuboCop ve Brakeman (belgeli tek strict-URL yönlendirme false positive'i). Önceki tam 0.1.1 sonuçları temel kanıttır; canlı/merchant kabulü ertelenmiştir.
133
+
134
+ Checkout sayfasının CSP’si form POST’u için yalnız `'self'` ve yapılandırılan gateway origin’ine (varsayılan `https://api-gateway.lunixi.com`) izin verir. Böylece tarayıcı SDK’sının desteklediği cüzdan/sağlayıcı ve eski POST geçişleri korunur. İncelenmiş başka bir sağlayıcı gerekiyorsa yalnız sunucuda `LUNIXI_FORM_ACTION_ORIGINS` tanımlayın: virgülle ayrılmış tam HTTPS origin’leri; boşluk, yol, sondaki slash, kullanıcı bilgisi, sorgu, fragment ve wildcard kabul edilmez. Geçersiz ayar reddedilir; gateway yanıtı veya tarayıcı alanı listeyi genişletemez. Script nonce, frame/connect sınırları ve bilgilendirici dönüş güvenliği korunur.
data/SECURITY.md ADDED
@@ -0,0 +1,20 @@
1
+ # Security policy
2
+
3
+ Report suspected vulnerabilities privately to **developer@lunixi.com**. Include affected version, reproducible synthetic inputs, expected/actual behavior and impact. Do not send private keys, access/checkout tokens, PAN/CVV or customer information. Do not post exploitable details publicly before coordination.
4
+
5
+ Security fixes target the latest released version. Review CHANGELOG.md and dependency advisories before deployment. Version 0.1.0 is the initial source release; it is not a claim of certification or a live merchant acceptance test.
6
+
7
+ ## Integration guarantees and responsibilities
8
+
9
+ - OpenSSL provides Ed25519, HMAC-SHA256, TLS verification and constant-time comparison. No custom curve implementation exists.
10
+ - SDK HTTP redirects are rejected; the gateway origin must use HTTPS. Ambient proxy configuration is not inherited. Use an injected transport for a reviewed enterprise proxy, retaining TLS/hostname verification and redirect restrictions.
11
+ - Persist one idempotency key per financial operation before the first request; reuse it after uncertain outcomes. SDK retries never create a replacement key.
12
+ - Webhooks must be verified over raw bytes. Commit event receipts and payment changes together, and roll back both on failure. Retain receipts according to the gateway's redelivery/reconciliation policy; never blindly prune them at the timestamp-tolerance boundary.
13
+ - Reconcile order/intent identity, amount, currency and final status. Browser returns, pending/unknown split states, or successful token creation are insufficient for fulfillment.
14
+ - Never log credentials, raw request/response/webhook bodies, card data or customer PII. SDK errors omit payloads. Application logging must preserve this boundary.
15
+ - The Rails example disables request info logging because Rails includes redirect destinations in those logs. Checkout tokens are encrypted at rest. Demo HTTP Basic authentication is not a customer authorization system; integrate your own session/tenant/order authorization before deployment.
16
+ - The fixed-origin hosted redirect produces one weak Brakeman warning. Its precise fingerprint, rationale and injection tests are recorded in samples/rails/config/brakeman.ignore. No security category is globally disabled.
17
+
18
+ ## Required checks
19
+
20
+ Run `bundle exec rake test`, `bundle exec rake rails:test`, `bundle exec rubocop`, `bundle exec bundle-audit check --update`, and `bundle exec brakeman --force --rails8 --path samples/rails --no-pager`. CI runs all checks with synthetic credentials. Live TEST-merchant acceptance and credentials provisioning are separate operational checks; automated tests move no money.
@@ -0,0 +1,20 @@
1
+ # RubyGems publication
2
+
3
+ `publish-rubygems.yml` is a manual, main-branch-only workflow for a tested version tag. It checks the exact tag's successful CI, gem identity/version and package contents before requesting a short-lived registry credential. No permanent API key is stored in the repository.
4
+
5
+ For the first `lunixi-sdk` release, sign in with the intended confirmed RubyGems account and configure its pending trusted publisher. The account must satisfy the registry MFA requirement and becomes the gem's initial owner.
6
+
7
+ | Setting | Value |
8
+ | --- | --- |
9
+ | Gem | `lunixi-sdk` |
10
+ | Repository owner / repository | `Lunixi-Payment` / `lunixi-ruby` |
11
+ | Workflow filename | `publish-rubygems.yml` |
12
+ | Environment | `release` |
13
+ | Reusable workflow fields | Blank |
14
+
15
+ The account owner must review and authorize this package-specific publishing delegation. The workflow alone does not create the publisher or publish a gem. After authorization, run it from `main` with version `0.1.1`; it refuses a version that already exists. Verify the resulting version and downloaded gem digest from RubyGems.
16
+
17
+ The build does not repeat the functional suite. Ephemeral runner credentials are removed after the upload.
18
+
19
+ - [Trusted Publishing and pending publishers](https://guides.rubygems.org/trusted-publishing/)
20
+ - [RubyGems publishing](https://guides.rubygems.org/publishing/)
data/docs/rails.en.md ADDED
@@ -0,0 +1,112 @@
1
+ ---
2
+ title: Rails
3
+ description: Runnable Rails integration for the Lunixi Ruby SDK — hosted checkout, encrypted tokens, CSRF, durable webhook deduplication and payment reconciliation.
4
+ ---
5
+
6
+
7
+ # Rails
8
+
9
+ The [Ruby SDK](/sdks/ruby) includes an executable Rails 8.1 reference application at `samples/rails`. It creates a hosted checkout with server-calculated totals, stores its token encrypted, protects buyer routes with CSRF and demo authentication, and commits verified webhook receipts and order changes atomically.
10
+
11
+ This is a **single-merchant fixed-total TRY** example. It is a source integration, not a separately published Rails gem or a complete storefront. Request signing, token caching, retries and webhook cryptography remain in `lunixi-sdk`.
12
+
13
+ ## Install
14
+
15
+ Ruby 3.3+, Bundler, OpenSSL with Ed25519, and SQLite are required. Node.js is needed for the SDK's independent JSON compatibility tests.
16
+
17
+ ```bash
18
+ git clone https://github.com/Lunixi-Payment/lunixi-ruby.git
19
+ cd lunixi-ruby
20
+ bundle install
21
+ ```
22
+
23
+ The repository Gemfile loads the local SDK and installs Rails, Puma and SQLite. No RubyGems publication of `lunixi-sdk` is assumed. See the [sample source](https://github.com/Lunixi-Payment/lunixi-ruby/tree/main/samples/rails).
24
+
25
+ ## Configure secrets
26
+
27
+ Export the following through your shell or secret manager before boot. The application does not auto-load dotenv; the repository includes `samples/rails/.env.example` as a checklist.
28
+
29
+ | Variable | Purpose |
30
+ | --- | --- |
31
+ | `LUNIXI_BASE_URL` | HTTPS gateway origin, normally `https://api-gateway.lunixi.com` |
32
+ | `LUNIXI_KEY_ID` | Registered merchant TEST/LIVE key id |
33
+ | `LUNIXI_MERCHANT_ID` | Merchant UUID that must match the authenticated payment response |
34
+ | `LUNIXI_PRIVATE_KEY_PATH` | Private Ed25519 key file outside the public web root |
35
+ | `LUNIXI_WEBHOOK_SECRET` | Secret matching the registered webhook connection |
36
+ | `PUBLIC_ORIGIN` | Public HTTPS origin of this Rails app, without a path |
37
+ | `DEMO_USERNAME`, `DEMO_PASSWORD` | Demo HTTP Basic authentication; password at least 16 bytes |
38
+ | `SECRET_KEY_BASE` | Stable Rails session secret |
39
+ | `ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY` | Stable primary encryption key |
40
+ | `ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY` | Independent deterministic encryption key |
41
+ | `ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT` | Independent derivation salt |
42
+
43
+ Generate a session secret locally with `ruby -rsecurerandom -e 'puts SecureRandom.hex(64)'`. Generate three additional independent 32-byte random values for encryption configuration. Save them securely and retain them across restarts. Plan Rails encryption key rotation before replacing any key; discarding an old key makes saved tokens unreadable.
44
+
45
+ A local HTTPS tunnel can supply `PUBLIC_ORIGIN`. The origin is used for the return URL and allowed-host validation. Configure it before launching the app. Do not publish a development server without authentication.
46
+
47
+ ## Start a payment
48
+
49
+ ```bash
50
+ cd samples/rails
51
+ bundle exec rails db:migrate db:seed
52
+ bundle exec rails server -b 127.0.0.1
53
+ ```
54
+
55
+ Visit `http://localhost:3000/orders/00000000-0000-4000-8000-000000000001`, authenticate with the demo credentials and select Pay. The synthetic seeded order is **100.50 TRY**. Add `?locale=tr` for Turkish UI.
56
+
57
+ The checkout POST requires a CSRF token and reads amount/currency from the database. A UUID `checkout_key` is saved on the order before contacting Lunixi. The request uses that key, stores `intent_id`, encrypts `checkout_token` with Active Record Encryption, then redirects with HTTP 303 to the fixed host `pay.lunixi.com`. Fresh and reused tokens must pass an anchored ASCII allowlist; no browser/gateway value can supply another host or scheme.
58
+
59
+ If the network fails, the order stays pending and retains its attempt key. Repeating checkout uses the same key. A persisted token is reused; the example never clears an uncertain attempt and silently creates a new charge. Expired or unknown attempts need explicit reconciliation.
60
+
61
+ The Lunixi hosted form determines the methods available to that merchant, including eligible alternative methods and multi-card flows. The merchant application never handles PAN/CVV.
62
+
63
+ ## Register the webhook
64
+
65
+ Register this URL in the Lunixi dashboard and configure the matching webhook secret:
66
+
67
+ ```text
68
+ https://<your-public-host>/webhooks/lunixi
69
+ ```
70
+
71
+ Subscribe to the documented payment events. The external `WebhooksController < ActionController::API` uses exact `request.raw_post` bytes and no browser session. A separate middleware also accepts only `POST /orders/{public_id}/return`: it bounds the discarded raw body to 1 MiB, ignores every field, performs no order/session lookup, gateway request or financial write, and returns an empty fixed same-origin 303 to the existing authenticated GET. This informational adapter does not use the webhook secret. Checkout retains Rails CSRF and authentication; the normal GET still requires demo authentication, to be replaced with your customer/order authorization before production.
72
+
73
+ The SDK verifies v2 signature, timestamp, event id/type and rotation. `RailsReplayStore` then inserts a receipt with a unique `(connection_id, event_id)` constraint inside the **same database transaction** as order changes. An identical duplicate returns 200 without applying fulfillment twice. Different bytes under the same id return 400. Any handler/status-query failure rolls back the receipt and returns 503 so redelivery can recover.
74
+
75
+ ## Confirm settlement
76
+
77
+ `WebhookProcessor` acts on `payment.captured` only when the event identifies the stored order and intent. It authenticates `payments.get(intent_id)` and requires:
78
+
79
+ - final status `CAPTURED`;
80
+ - the same persisted order reference and intent;
81
+ - the same currency;
82
+ - the exact integer order amount.
83
+
84
+ Only then is the order marked `paid`. A failed, authorized, unknown or inconsistent result remains pending. Other subscribed events are recorded without inferring payment success. The browser return route only redirects to the persisted order state, ignoring client `status` or `paid` parameters.
85
+
86
+ >
87
+ The SDK supports split, capture and refund requests. This sample fulfils fixed-total single-intent TRY orders. Split aggregate settlement, partial fulfillment, customer-paid insurance and surcharges require an explicit accounting/reconciliation extension before they are enabled here. A mismatched total fails closed.
88
+
89
+
90
+ ## Adapt to your application
91
+
92
+ Replace demo HTTP Basic with your customer/session/tenant/order ownership checks. Preserve server-owned totals, persistent per-operation keys and atomic receipt/business transactions. Add a durable reconciliation worker for missing webhooks and uncertain outcomes, retaining the same correlation data.
93
+
94
+ The sample uses SQLite. To change the production database, install its Rails adapter and repeat transaction/concurrency checks. Long gateway reads inside database transactions simplify the example but limit throughput; a production outbox/state machine may use shorter transactions while preserving atomic event receipt and business updates.
95
+
96
+ HTTPS is forced outside local/test environments. Request info logging is disabled because Rails includes redirect URLs, which contain checkout credentials. Never log raw payment/webhook payloads or secrets. Use stable session/encryption secrets and a persistent database.
97
+
98
+ ## Tests and security
99
+
100
+ From the repository root:
101
+
102
+ ```bash
103
+ bundle exec rake test rails:test
104
+ bundle exec rubocop
105
+ bundle exec bundle-audit check --update
106
+ bundle exec brakeman --force --rails8 --path samples/rails --no-pager
107
+ gem build lunixi-sdk.gemspec
108
+ ```
109
+
110
+ Rails tests use synthetic transport responses and a real SQLite database; no merchant credentials or fund movement are required. They cover authentication, CSRF, encrypted tokens, durable keys, redirect injection, browser-return distrust, signatures, deduplication, wrong totals/order references and rollback/redelivery. Brakeman's single weak fixed-origin redirect false positive has a precise fingerprint, review rationale and attack regression tests in `samples/rails/config/brakeman.ignore`. No security category is disabled globally.
111
+
112
+ Before launch, run an authorized TEST-merchant end-to-end payment, webhook and refund acceptance test. See the repository [README](https://github.com/Lunixi-Payment/lunixi-ruby/tree/main/samples/rails) and [security policy](https://github.com/Lunixi-Payment/lunixi-ruby/blob/main/SECURITY.md).