open-banking-io 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 +66 -0
- data/lib/open_banking_io/client.rb +266 -0
- data/lib/open_banking_io/envelope.rb +74 -0
- data/lib/open_banking_io/models.rb +86 -0
- data/lib/open_banking_io/version.rb +5 -0
- data/lib/open_banking_io.rb +10 -0
- metadata +106 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6d7f5d02a8a4a808faa00f6c78558b2e11c90a7c5b852062682746c1ab2a3da1
|
|
4
|
+
data.tar.gz: c24bf4296b70c0b5ffc6af71faae40d4c559301f59654358c6c34e94130bbef3
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d4870301dfb82caf2456f2b24a7ae8e6b469375da2bf6ff86821196eaf620f3e1b734deae2a391884e7eaa6dd3eea150c6714962d1f77247e950b5e4b2bef8db
|
|
7
|
+
data.tar.gz: 24788e31dd5f8deb079a4c3bd2dd411ab6f93e1928eb4de6ed6bf0b398d241b22c8ab8e7009be3f758c286a3131ab3d123dc16cc46ce6c0449f01eb88a4c181c
|
data/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# open-banking-io (Ruby)
|
|
2
|
+
|
|
3
|
+
Server-to-server client for [open-banking.io](https://open-banking.io). It authenticates with your
|
|
4
|
+
**API key** and decrypts the **zero-knowledge** data envelopes locally with your exported **private
|
|
5
|
+
key** — the service only ever returns ciphertext it cannot read.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install open-banking-io
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
require "open_banking_io"
|
|
13
|
+
|
|
14
|
+
# Load the credentials .json you exported from the app (API key + private key).
|
|
15
|
+
client = OpenBankingIO::Client.from_credentials("credentials.json")
|
|
16
|
+
|
|
17
|
+
client.get_accounts.each do |account|
|
|
18
|
+
booked = account.balances.find { |b| b.type == "ITBD" }
|
|
19
|
+
label = account.display_name || account.owner_name
|
|
20
|
+
puts "#{label} #{account.iban}: #{booked&.amount} #{account.currency}"
|
|
21
|
+
|
|
22
|
+
page = client.get_transactions(account.id, limit: 50)
|
|
23
|
+
page.items.each do |t|
|
|
24
|
+
puts " #{t.booking_date} #{t.creditor_name || t.debtor_name} #{t.amount} #{t.currency}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Trigger an online sync (decrypts the account uid locally and posts it):
|
|
28
|
+
client.sync(account.id)
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or construct it explicitly:
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
client = OpenBankingIO::Client.new(
|
|
36
|
+
api_base_url: api_base_url,
|
|
37
|
+
api_key: api_key,
|
|
38
|
+
private_key_pkcs8: private_key_pkcs8
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
- `get_accounts` → `Array<Account>` — decrypts each account's envelope, display name and balances.
|
|
45
|
+
- `get_transactions(account_id, from: nil, to: nil, limit: nil, offset: nil)` → `TransactionPage`
|
|
46
|
+
- `get_connections` → `Array<Connection>`
|
|
47
|
+
- `sync(account_id)` → `SyncResult` — decrypts the account uid locally and posts it.
|
|
48
|
+
- `sync_all` → `SyncAllResult` — syncs every account that has an active session.
|
|
49
|
+
|
|
50
|
+
Amounts are exposed as `BigDecimal`. Models are immutable keyword-initialised `Struct`s.
|
|
51
|
+
|
|
52
|
+
## Encryption
|
|
53
|
+
|
|
54
|
+
Envelopes use **ECDH P-256 → HKDF-SHA256 → AES-256-GCM**, implemented entirely with Ruby's OpenSSL
|
|
55
|
+
standard library. Decryption requires the private key from your credentials bundle and happens fully
|
|
56
|
+
in-process. See the [repo README](https://github.com/open-banking-io/clients) for the full scheme and
|
|
57
|
+
the other language clients (.NET, Node, Python, Rust, Go, Java).
|
|
58
|
+
|
|
59
|
+
## Development
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bundle install
|
|
63
|
+
bundle exec rspec
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
MIT licensed.
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "bigdecimal"
|
|
7
|
+
|
|
8
|
+
require_relative "envelope"
|
|
9
|
+
require_relative "models"
|
|
10
|
+
|
|
11
|
+
module OpenBankingIO
|
|
12
|
+
# Raised when the API returns a non-success HTTP status.
|
|
13
|
+
class HTTPError < StandardError
|
|
14
|
+
attr_reader :status, :body
|
|
15
|
+
|
|
16
|
+
def initialize(status, body)
|
|
17
|
+
@status = status
|
|
18
|
+
@body = body
|
|
19
|
+
super("open-banking.io request failed with HTTP #{status}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Server-to-server client for open-banking.io.
|
|
24
|
+
#
|
|
25
|
+
# Authenticates with an API key (+X-Api-Key+) and decrypts the zero-knowledge data
|
|
26
|
+
# envelopes locally with the exported private key -- the service only ever returns
|
|
27
|
+
# ciphertext it cannot read.
|
|
28
|
+
class Client
|
|
29
|
+
DEFAULT_OPEN_TIMEOUT = 15
|
|
30
|
+
DEFAULT_READ_TIMEOUT = 60
|
|
31
|
+
|
|
32
|
+
# Builds a client from a credentials-bundle JSON string or a path to a bundle file.
|
|
33
|
+
def self.from_credentials(path_or_json)
|
|
34
|
+
raw = if File.file?(path_or_json.to_s)
|
|
35
|
+
File.read(path_or_json)
|
|
36
|
+
else
|
|
37
|
+
path_or_json
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
bundle = JSON.parse(raw)
|
|
41
|
+
api_base_url = bundle["apiBaseUrl"].to_s
|
|
42
|
+
api_key = bundle["apiKey"]
|
|
43
|
+
raise ArgumentError, "The credentials bundle has no apiKey" if api_key.nil? || api_key.empty?
|
|
44
|
+
|
|
45
|
+
enc_key = bundle["encryptionKey"] || {}
|
|
46
|
+
private_key = enc_key["privateKey"] || enc_key["privateKeyPkcs8B64"]
|
|
47
|
+
if private_key.nil? || private_key.to_s.empty?
|
|
48
|
+
raise ArgumentError, "The credentials bundle has no encryption private key"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
new(api_base_url: api_base_url, api_key: api_key, private_key_pkcs8: private_key)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize(api_base_url:, api_key:, private_key_pkcs8:)
|
|
55
|
+
raise ArgumentError, "api_base_url is required" if blank?(api_base_url)
|
|
56
|
+
raise ArgumentError, "api_key is required" if blank?(api_key)
|
|
57
|
+
raise ArgumentError, "private_key_pkcs8 is required" if blank?(private_key_pkcs8)
|
|
58
|
+
|
|
59
|
+
@base_uri = URI.parse(api_base_url.to_s.sub(%r{/+\z}, "") + "/")
|
|
60
|
+
@api_key = api_key
|
|
61
|
+
@private_key = Envelope.load_private_key(private_key_pkcs8)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Lists the user's accounts with all sensitive fields decrypted.
|
|
65
|
+
def get_accounts
|
|
66
|
+
account_wires.map { |w| map_account(w) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns a page of an account's statement, newest first, with decrypted fields.
|
|
70
|
+
def get_transactions(account_id, from: nil, to: nil, limit: nil, offset: nil)
|
|
71
|
+
params = {}
|
|
72
|
+
params["from"] = from unless from.nil?
|
|
73
|
+
params["to"] = to unless to.nil?
|
|
74
|
+
params["limit"] = limit unless limit.nil?
|
|
75
|
+
params["offset"] = offset unless offset.nil?
|
|
76
|
+
|
|
77
|
+
page = get_json("api/accounts/#{account_id}/transactions", params)
|
|
78
|
+
items = (page["items"] || []).map { |t| map_transaction(t) }
|
|
79
|
+
TransactionPage.new(items: items, total: page["total"] || 0)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Lists the user's bank connections.
|
|
83
|
+
def get_connections
|
|
84
|
+
get_json("api/connections").map do |c|
|
|
85
|
+
Connection.new(
|
|
86
|
+
session_id: c["sessionId"] || "",
|
|
87
|
+
aspsp_name: c["aspspName"] || "",
|
|
88
|
+
aspsp_country: c["aspspCountry"] || "",
|
|
89
|
+
valid_until: c["validUntil"],
|
|
90
|
+
status: c["status"] || "",
|
|
91
|
+
account_count: c["accountCount"] || 0,
|
|
92
|
+
last_synced_at: c["lastSyncedAt"],
|
|
93
|
+
psu_type: c["psuType"]
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Triggers an online sync of one account.
|
|
99
|
+
#
|
|
100
|
+
# Decrypts that account's Enable Banking uid and posts it, so the service can fetch
|
|
101
|
+
# fresh data without ever holding the uid in plaintext.
|
|
102
|
+
def sync(account_id)
|
|
103
|
+
account = account_wires.find { |a| a["id"] == account_id }
|
|
104
|
+
raise ArgumentError, "Account #{account_id} not found" if account.nil?
|
|
105
|
+
|
|
106
|
+
uid = decrypt_uid(account)
|
|
107
|
+
if uid.nil?
|
|
108
|
+
raise ArgumentError, "Account has no active session (reconnect required) -- cannot sync"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
result = post_json("api/accounts/#{account_id}/sync", { "uid" => uid })
|
|
112
|
+
SyncResult.new(
|
|
113
|
+
new_transactions: result["newTransactions"] || 0,
|
|
114
|
+
total_fetched: result["totalFetched"] || 0
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Triggers an online sync of every account that has an active session.
|
|
119
|
+
def sync_all
|
|
120
|
+
items = []
|
|
121
|
+
account_wires.each do |a|
|
|
122
|
+
uid = decrypt_uid(a)
|
|
123
|
+
items << { "accountId" => a["id"], "uid" => uid } unless uid.nil?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
result = post_json("api/sync", { "items" => items })
|
|
127
|
+
SyncAllResult.new(
|
|
128
|
+
accounts: result["accounts"] || 0,
|
|
129
|
+
new_transactions: result["newTransactions"] || 0
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
private
|
|
134
|
+
|
|
135
|
+
def blank?(value)
|
|
136
|
+
value.nil? || value.to_s.strip.empty?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def account_wires
|
|
140
|
+
get_json("api/accounts")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def decrypt_uid(account)
|
|
144
|
+
payload = Envelope.decrypt_to_json(@private_key, account["uidEnc"])
|
|
145
|
+
payload && payload["uid"]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def map_account(a)
|
|
149
|
+
acc = Envelope.decrypt_to_json(@private_key, a["enc"]) || {}
|
|
150
|
+
name = Envelope.decrypt_to_json(@private_key, a["displayNameEnc"]) || {}
|
|
151
|
+
|
|
152
|
+
balances = (a["balances"] || []).map do |b|
|
|
153
|
+
dec = Envelope.decrypt_to_json(@private_key, b["enc"]) || {}
|
|
154
|
+
Balance.new(
|
|
155
|
+
type: b["type"] || "",
|
|
156
|
+
currency: b["currency"] || "",
|
|
157
|
+
reference_date: b["referenceDate"],
|
|
158
|
+
name: dec["name"],
|
|
159
|
+
amount: parse_decimal(dec["amount"])
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
Account.new(
|
|
164
|
+
id: a["id"] || "",
|
|
165
|
+
aspsp_name: a["aspspName"] || "",
|
|
166
|
+
aspsp_country: a["aspspCountry"] || "",
|
|
167
|
+
currency: a["currency"] || "",
|
|
168
|
+
account_type: a["accountType"],
|
|
169
|
+
bic: a["bic"],
|
|
170
|
+
needs_reconnect: a["needsReconnect"] || false,
|
|
171
|
+
iban: acc["iban"],
|
|
172
|
+
bban: acc["bban"],
|
|
173
|
+
owner_name: acc["ownerName"],
|
|
174
|
+
account_name: acc["accountName"],
|
|
175
|
+
product: acc["product"],
|
|
176
|
+
display_name: name["displayName"],
|
|
177
|
+
balances: balances
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def map_transaction(t)
|
|
182
|
+
d = Envelope.decrypt_to_json(@private_key, t["enc"]) || {}
|
|
183
|
+
Transaction.new(
|
|
184
|
+
id: t["id"] || "",
|
|
185
|
+
currency: t["currency"] || "",
|
|
186
|
+
credit_debit_indicator: t["creditDebitIndicator"] || "",
|
|
187
|
+
status: t["status"],
|
|
188
|
+
booking_date: t["bookingDate"],
|
|
189
|
+
value_date: t["valueDate"],
|
|
190
|
+
transaction_date: t["transactionDate"],
|
|
191
|
+
bank_transaction_code: t["bankTransactionCode"],
|
|
192
|
+
amount: parse_decimal(d["amount"]),
|
|
193
|
+
creditor_name: d["creditorName"],
|
|
194
|
+
creditor_iban: d["creditorIban"],
|
|
195
|
+
creditor_bban: d["creditorBban"],
|
|
196
|
+
creditor_agent_bic: d["creditorAgentBic"],
|
|
197
|
+
debtor_name: d["debtorName"],
|
|
198
|
+
debtor_iban: d["debtorIban"],
|
|
199
|
+
debtor_bban: d["debtorBban"],
|
|
200
|
+
debtor_agent_bic: d["debtorAgentBic"],
|
|
201
|
+
remittance_information: d["remittanceInformation"],
|
|
202
|
+
note: d["note"],
|
|
203
|
+
reference_number: d["referenceNumber"],
|
|
204
|
+
exchange_rate: d["exchangeRate"],
|
|
205
|
+
merchant_category_code: d["merchantCategoryCode"],
|
|
206
|
+
balance_after_transaction: parse_decimal_nullable(d["balanceAfter"]),
|
|
207
|
+
balance_after_currency: d["balanceAfterCurrency"]
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def parse_decimal(value)
|
|
212
|
+
return BigDecimal(0) if value.nil? || value == ""
|
|
213
|
+
|
|
214
|
+
BigDecimal(value.to_s)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def parse_decimal_nullable(value)
|
|
218
|
+
return nil if value.nil? || value == ""
|
|
219
|
+
|
|
220
|
+
BigDecimal(value.to_s)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# -- HTTP ------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
def get_json(path, params = {})
|
|
226
|
+
uri = resolve(path)
|
|
227
|
+
unless params.empty?
|
|
228
|
+
uri.query = URI.encode_www_form(params)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
request = Net::HTTP::Get.new(uri)
|
|
232
|
+
send_request(uri, request)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def post_json(path, body)
|
|
236
|
+
uri = resolve(path)
|
|
237
|
+
request = Net::HTTP::Post.new(uri)
|
|
238
|
+
request["Content-Type"] = "application/json"
|
|
239
|
+
request.body = JSON.generate(body)
|
|
240
|
+
send_request(uri, request)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def resolve(path)
|
|
244
|
+
(@base_uri + path.sub(%r{\A/+}, "")).dup
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def send_request(uri, request)
|
|
248
|
+
request["X-Api-Key"] = @api_key
|
|
249
|
+
request["Accept"] = "application/json"
|
|
250
|
+
|
|
251
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
252
|
+
http.use_ssl = (uri.scheme == "https")
|
|
253
|
+
http.open_timeout = DEFAULT_OPEN_TIMEOUT
|
|
254
|
+
http.read_timeout = DEFAULT_READ_TIMEOUT
|
|
255
|
+
|
|
256
|
+
response = http.request(request)
|
|
257
|
+
code = response.code.to_i
|
|
258
|
+
raise HTTPError.new(code, response.body) unless code.between?(200, 299)
|
|
259
|
+
|
|
260
|
+
body = response.body
|
|
261
|
+
return nil if body.nil? || body.empty?
|
|
262
|
+
|
|
263
|
+
JSON.parse(body)
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module OpenBankingIO
|
|
8
|
+
# Decrypts open-banking.io's zero-knowledge data envelopes.
|
|
9
|
+
#
|
|
10
|
+
# Scheme: ephemeral ECDH on NIST P-256 -> HKDF-SHA256 -> AES-256-GCM.
|
|
11
|
+
# Wire: +version(1)=0x01 | ephemeralPublicKeyRaw(65) | nonce(12) | tag(16) | ciphertext+.
|
|
12
|
+
# Only the user's private key can decrypt -- the service stores ciphertext it cannot read.
|
|
13
|
+
module Envelope
|
|
14
|
+
VERSION_BYTE = 0x01
|
|
15
|
+
POINT_LEN = 65
|
|
16
|
+
NONCE_LEN = 12
|
|
17
|
+
TAG_LEN = 16
|
|
18
|
+
HKDF_SALT = ("\x00".b * 32).freeze
|
|
19
|
+
HKDF_INFO = "bank.core.ci/zk/v1".b.freeze
|
|
20
|
+
GROUP = OpenSSL::PKey::EC::Group.new("prime256v1")
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# Loads a base64 PKCS#8 EC (P-256) private key.
|
|
25
|
+
def load_private_key(private_key_pkcs8_b64)
|
|
26
|
+
key = OpenSSL::PKey.read(Base64.decode64(private_key_pkcs8_b64))
|
|
27
|
+
unless key.is_a?(OpenSSL::PKey::EC)
|
|
28
|
+
raise ArgumentError, "Private key is not an EC key"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
key
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Decrypts the raw bytes of a zero-knowledge envelope, returning the plaintext bytes.
|
|
35
|
+
def decrypt(private_key, envelope_bytes)
|
|
36
|
+
min_len = 1 + POINT_LEN + NONCE_LEN + TAG_LEN
|
|
37
|
+
if envelope_bytes.bytesize < min_len || envelope_bytes.getbyte(0) != VERSION_BYTE
|
|
38
|
+
raise ArgumentError, "Invalid or unsupported envelope"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
eph_pub_bytes = envelope_bytes.byteslice(1, POINT_LEN)
|
|
42
|
+
nonce = envelope_bytes.byteslice(1 + POINT_LEN, NONCE_LEN)
|
|
43
|
+
tag = envelope_bytes.byteslice(1 + POINT_LEN + NONCE_LEN, TAG_LEN)
|
|
44
|
+
ciphertext = envelope_bytes.byteslice((1 + POINT_LEN + NONCE_LEN + TAG_LEN)..) || "".b
|
|
45
|
+
|
|
46
|
+
pub = OpenSSL::PKey::EC::Point.new(GROUP, OpenSSL::BN.new(eph_pub_bytes, 2))
|
|
47
|
+
shared = private_key.dh_compute_key(pub)
|
|
48
|
+
|
|
49
|
+
key = OpenSSL::KDF.hkdf(
|
|
50
|
+
shared,
|
|
51
|
+
salt: HKDF_SALT,
|
|
52
|
+
info: HKDF_INFO,
|
|
53
|
+
length: 32,
|
|
54
|
+
hash: "SHA256"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
58
|
+
cipher.decrypt
|
|
59
|
+
cipher.key = key
|
|
60
|
+
cipher.iv = nonce
|
|
61
|
+
cipher.auth_tag = tag
|
|
62
|
+
cipher.auth_data = ""
|
|
63
|
+
cipher.update(ciphertext) + cipher.final
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Decrypts a base64 envelope and parses its JSON payload. +nil+ in -> +nil+ out.
|
|
67
|
+
def decrypt_to_json(private_key, envelope_b64)
|
|
68
|
+
return nil if envelope_b64.nil?
|
|
69
|
+
|
|
70
|
+
plaintext = decrypt(private_key, Base64.decode64(envelope_b64))
|
|
71
|
+
JSON.parse(plaintext)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenBankingIO
|
|
4
|
+
# Public, decrypted models for the open-banking.io client.
|
|
5
|
+
#
|
|
6
|
+
# These are immutable keyword-initialised value objects (Struct). Amounts are exposed
|
|
7
|
+
# as BigDecimal; dates as String (ISO-8601) as returned by the service.
|
|
8
|
+
|
|
9
|
+
# A balance snapshot. +type+ is the ISO 20022 code (ITBD booked, ITAV available, ...).
|
|
10
|
+
Balance = Struct.new(
|
|
11
|
+
:type,
|
|
12
|
+
:name,
|
|
13
|
+
:amount, # BigDecimal
|
|
14
|
+
:currency,
|
|
15
|
+
:reference_date,
|
|
16
|
+
keyword_init: true
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# A bank account with its sensitive fields decrypted.
|
|
20
|
+
Account = Struct.new(
|
|
21
|
+
:id,
|
|
22
|
+
:aspsp_name,
|
|
23
|
+
:aspsp_country,
|
|
24
|
+
:currency,
|
|
25
|
+
:account_type,
|
|
26
|
+
:bic,
|
|
27
|
+
:needs_reconnect,
|
|
28
|
+
:iban,
|
|
29
|
+
:bban,
|
|
30
|
+
:owner_name,
|
|
31
|
+
:account_name,
|
|
32
|
+
:product,
|
|
33
|
+
:display_name,
|
|
34
|
+
:balances, # Array<Balance>
|
|
35
|
+
keyword_init: true
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# A statement transaction with its sensitive fields decrypted.
|
|
39
|
+
Transaction = Struct.new(
|
|
40
|
+
:id,
|
|
41
|
+
:currency,
|
|
42
|
+
:credit_debit_indicator,
|
|
43
|
+
:status,
|
|
44
|
+
:booking_date,
|
|
45
|
+
:value_date,
|
|
46
|
+
:transaction_date,
|
|
47
|
+
:bank_transaction_code,
|
|
48
|
+
:amount, # BigDecimal
|
|
49
|
+
:creditor_name,
|
|
50
|
+
:creditor_iban,
|
|
51
|
+
:creditor_bban,
|
|
52
|
+
:creditor_agent_bic,
|
|
53
|
+
:debtor_name,
|
|
54
|
+
:debtor_iban,
|
|
55
|
+
:debtor_bban,
|
|
56
|
+
:debtor_agent_bic,
|
|
57
|
+
:remittance_information,
|
|
58
|
+
:note,
|
|
59
|
+
:reference_number,
|
|
60
|
+
:exchange_rate,
|
|
61
|
+
:merchant_category_code,
|
|
62
|
+
:balance_after_transaction, # BigDecimal or nil
|
|
63
|
+
:balance_after_currency,
|
|
64
|
+
keyword_init: true
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# A page of transactions, newest first.
|
|
68
|
+
TransactionPage = Struct.new(:items, :total, keyword_init: true)
|
|
69
|
+
|
|
70
|
+
# A bank connection (consent).
|
|
71
|
+
Connection = Struct.new(
|
|
72
|
+
:session_id,
|
|
73
|
+
:aspsp_name,
|
|
74
|
+
:aspsp_country,
|
|
75
|
+
:valid_until,
|
|
76
|
+
:status,
|
|
77
|
+
:account_count,
|
|
78
|
+
:last_synced_at,
|
|
79
|
+
:psu_type,
|
|
80
|
+
keyword_init: true
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
SyncResult = Struct.new(:new_transactions, :total_fetched, keyword_init: true)
|
|
84
|
+
|
|
85
|
+
SyncAllResult = Struct.new(:accounts, :new_transactions, keyword_init: true)
|
|
86
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "open_banking_io/version"
|
|
4
|
+
require_relative "open_banking_io/envelope"
|
|
5
|
+
require_relative "open_banking_io/models"
|
|
6
|
+
require_relative "open_banking_io/client"
|
|
7
|
+
|
|
8
|
+
# Server-to-server client for open-banking.io with local zero-knowledge envelope decryption.
|
|
9
|
+
module OpenBankingIO
|
|
10
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: open-banking-io
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- open-banking.io
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: bigdecimal
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
68
|
+
description: Authenticates with your API key and decrypts open-banking.io's zero-knowledge
|
|
69
|
+
data envelopes locally with your exported private key (ECDH P-256 -> HKDF-SHA256
|
|
70
|
+
-> AES-256-GCM). The service only ever returns ciphertext it cannot read.
|
|
71
|
+
executables: []
|
|
72
|
+
extensions: []
|
|
73
|
+
extra_rdoc_files: []
|
|
74
|
+
files:
|
|
75
|
+
- README.md
|
|
76
|
+
- lib/open_banking_io.rb
|
|
77
|
+
- lib/open_banking_io/client.rb
|
|
78
|
+
- lib/open_banking_io/envelope.rb
|
|
79
|
+
- lib/open_banking_io/models.rb
|
|
80
|
+
- lib/open_banking_io/version.rb
|
|
81
|
+
homepage: https://open-banking.io
|
|
82
|
+
licenses:
|
|
83
|
+
- MIT
|
|
84
|
+
metadata:
|
|
85
|
+
homepage_uri: https://open-banking.io
|
|
86
|
+
source_code_uri: https://github.com/open-banking-io/clients
|
|
87
|
+
rubygems_mfa_required: 'true'
|
|
88
|
+
rdoc_options: []
|
|
89
|
+
require_paths:
|
|
90
|
+
- lib
|
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.1'
|
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
|
+
requirements:
|
|
98
|
+
- - ">="
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '0'
|
|
101
|
+
requirements: []
|
|
102
|
+
rubygems_version: 3.6.9
|
|
103
|
+
specification_version: 4
|
|
104
|
+
summary: Server-to-server client for open-banking.io with local zero-knowledge envelope
|
|
105
|
+
decryption.
|
|
106
|
+
test_files: []
|