tungsten-runtime 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.
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ require "openssl"
5
+ require "securerandom"
6
+ require "digest"
7
+
8
+ module Tungsten
9
+ module Runtime
10
+ # Helpers shared by the pipeline: field paths, redaction, secret
11
+ # scrubbing, digests.
12
+ module Util
13
+ # The longest string kept in received_value, in UTF-16 code units.
14
+ MAX_RECEIVED_CHARS = 200
15
+ # The deepest JSON nesting accepted in arguments.
16
+ MAX_ARG_DEPTH = 256
17
+ # The largest integer a JavaScript number holds exactly.
18
+ MAX_SAFE_INTEGER = 9_007_199_254_740_991
19
+
20
+ SENSITIVE_WORDS = %w[secret password passwd passphrase token apikey api-key api_key privatekey
21
+ private-key private_key credential authorization cookie session].freeze
22
+
23
+ module_function
24
+
25
+ def sha256_hex(data) = Digest::SHA256.hexdigest(data.b)
26
+
27
+ def hmac_sha256(key, message) = OpenSSL::HMAC.digest("SHA256", key.b, message.b)
28
+
29
+ def base64url(data) = Base64.urlsafe_encode64(data, padding: false)
30
+
31
+ def timing_safe_equal?(a, b)
32
+ return false unless a.bytesize == b.bytesize
33
+
34
+ OpenSSL.fixed_length_secure_compare(a, b)
35
+ end
36
+
37
+ # A random UUID version 4.
38
+ def uuid_v4 = SecureRandom.uuid
39
+
40
+ # Split a field path (a.b, items[0].id, items.0.id).
41
+ def split_path(path)
42
+ return [] if path.nil? || path == "" || path == "."
43
+
44
+ path.to_s.gsub(/\[(\d+)\]/) { ".#{Regexp.last_match(1)}" }.split(".").reject(&:empty?)
45
+ end
46
+
47
+ def index?(segment) = segment.is_a?(String) && segment.match?(/\A\d+\z/)
48
+
49
+ # The value at a path of segments, or UNSET.
50
+ def get_path(value, segments)
51
+ return UNSET if UNSET.equal?(value)
52
+
53
+ segments.each do |seg|
54
+ case value
55
+ when Array
56
+ key = seg.is_a?(Integer) ? seg : (index?(seg) ? seg.to_i : nil)
57
+ return UNSET if key.nil? || key >= value.length
58
+
59
+ value = value[key]
60
+ when Hash
61
+ key = seg.to_s
62
+ return UNSET unless value.key?(key)
63
+
64
+ value = value[key]
65
+ else
66
+ return UNSET
67
+ end
68
+ end
69
+ value
70
+ end
71
+
72
+ def get_path_str(value, path) = get_path(value, split_path(path))
73
+
74
+ # Whether a name very likely holds a secret.
75
+ def looks_sensitive?(name)
76
+ lower = name.to_s.downcase
77
+ SENSITIVE_WORDS.any? { |w| lower.include?(w) }
78
+ end
79
+
80
+ def redact_sensitive_keys(value, depth = 0)
81
+ return nil if depth > 32
82
+
83
+ case value
84
+ when Array then value.map { |v| redact_sensitive_keys(v, depth + 1) }
85
+ when Hash
86
+ value.each_with_object({}) do |(k, v), out|
87
+ out[k] = looks_sensitive?(k) ? REDACTED : redact_sensitive_keys(v, depth + 1)
88
+ end
89
+ else value
90
+ end
91
+ end
92
+
93
+ def redact_at(value, segments)
94
+ return REDACTED if segments.empty?
95
+
96
+ head = segments.first
97
+ rest = segments.drop(1)
98
+ case value
99
+ when Array then value.map { |v| redact_at(v, segments) }
100
+ when Hash
101
+ return value unless value.key?(head)
102
+
103
+ out = value.dup
104
+ out[head] = redact_at(value[head], rest)
105
+ out
106
+ else value
107
+ end
108
+ end
109
+
110
+ # A copy of value with every listed field path redacted; arrays on the
111
+ # way fan out.
112
+ def redact_paths(value, paths)
113
+ out = Json.copy(value)
114
+ paths.each do |p|
115
+ segs = split_path(p)
116
+ out = redact_at(out, segs) unless segs.empty?
117
+ end
118
+ out
119
+ end
120
+
121
+ def cut(text)
122
+ return text if Json.utf16_length(text) <= MAX_RECEIVED_CHARS
123
+
124
+ "#{Json.take_units(text, MAX_RECEIVED_CHARS - 1)}\u2026"
125
+ end
126
+
127
+ # A value made safe for received_value: redacted when sensitive,
128
+ # strings cut to 200 UTF-16 code units, containers with sensitive
129
+ # members redacted and cut as JSON text when long.
130
+ def envelope_value(value, sensitive)
131
+ return REDACTED if sensitive
132
+ return nil if UNSET.equal?(value)
133
+
134
+ case value
135
+ when String then cut(value)
136
+ when Binary then "<#{value.bytesize} bytes>"
137
+ when Array, Hash
138
+ bin = Json.binary_of(value)
139
+ return "<#{bin.bytesize} bytes>" unless UNSET.equal?(bin)
140
+
141
+ text = Json.text(redact_sensitive_keys(value))
142
+ return cut(text) if Json.utf16_length(text) > MAX_RECEIVED_CHARS
143
+
144
+ Json.try_parse(text).then { |v| UNSET.equal?(v) ? nil : v }
145
+ else value
146
+ end
147
+ end
148
+
149
+ # Secret values to scrub from texts, in insertion order.
150
+ class Secrets
151
+ attr_reader :items
152
+
153
+ def initialize(items = [])
154
+ @items = items.dup
155
+ end
156
+
157
+ def <<(secret)
158
+ return self if secret.nil?
159
+
160
+ text = secret.to_s
161
+ @items << text unless text.empty? || @items.include?(text)
162
+ self
163
+ end
164
+
165
+ def empty? = @items.empty?
166
+ def dup = Secrets.new(@items)
167
+ end
168
+
169
+ # Every occurrence of the secrets (four characters or more) replaced.
170
+ def scrub_text(text, secrets)
171
+ return text if text.nil? || secrets.nil?
172
+
173
+ secrets.items.each do |secret|
174
+ text = text.gsub(secret, REDACTED) if secret.length >= 4 && text.include?(secret)
175
+ end
176
+ text
177
+ end
178
+
179
+ def scrub_value(value, secrets)
180
+ case value
181
+ when String then scrub_text(value, secrets)
182
+ when Array, Hash
183
+ parsed = Json.try_parse(scrub_text(Json.text(value), secrets))
184
+ UNSET.equal?(parsed) ? REDACTED : parsed
185
+ else value
186
+ end
187
+ end
188
+
189
+ # The envelope with the secrets scrubbed from its text members: a last
190
+ # line of defence.
191
+ def scrub_diagnostic(d, secrets)
192
+ return d if secrets.nil? || secrets.empty?
193
+
194
+ d.with(
195
+ code: scrub_text(d.code, secrets), failed_parameter: scrub_text(d.failed_parameter, secrets),
196
+ received_value: scrub_value(d.received_value, secrets), expected: scrub_text(d.expected, secrets),
197
+ remediation: scrub_text(d.remediation, secrets), next_action: scrub_text(d.next_action, secrets),
198
+ request_id: scrub_text(d.request_id, secrets)
199
+ )
200
+ end
201
+
202
+ # The Accept of a stream call: the caller's media ranges in order
203
+ # without repeats, text/event-stream first when they do not name it.
204
+ def merge_accept(value)
205
+ ranges = []
206
+ value.to_s.split(",").each do |part|
207
+ r = part.strip
208
+ ranges << r unless r.empty? || ranges.include?(r)
209
+ end
210
+ names = ranges.any? { |r| r.split(";").first.to_s.strip.casecmp?("text/event-stream") }
211
+ ranges.unshift("text/event-stream") unless names
212
+ ranges.join(", ")
213
+ end
214
+
215
+ # Whether a value nests deeper than limit.
216
+ def nests_deeper?(value, limit)
217
+ case value
218
+ when Array then limit.zero? || value.any? { |v| nests_deeper?(v, limit - 1) }
219
+ when Hash then limit.zero? || value.each_value.any? { |v| nests_deeper?(v, limit - 1) }
220
+ else false
221
+ end
222
+ end
223
+
224
+ # A finite number within [min, max], else the fallback.
225
+ def bounded(value, fallback, min, max)
226
+ return fallback unless value.is_a?(Numeric)
227
+ return fallback if value.is_a?(Float) && (value.nan? || value.infinite?)
228
+
229
+ value.clamp(min, max)
230
+ end
231
+
232
+ # A number as JSON writes it, integral floats as integers.
233
+ def as_number(value)
234
+ value.is_a?(Float) && value == value.truncate && value.abs < MAX_SAFE_INTEGER ? value.to_i : value
235
+ end
236
+
237
+ # A string literal as JSON.stringify writes it.
238
+ def js_string(text) = Json.quote(text)
239
+
240
+ # Remediation JSON, cut to 80 characters.
241
+ def short_json(value)
242
+ text = Json.text(value)
243
+ text.length > 80 ? "#{text[0, 77]}..." : text
244
+ end
245
+
246
+ # " with name value, ..." for remediation text.
247
+ def with_arguments(args)
248
+ return "" unless args.is_a?(Hash) && !args.empty?
249
+
250
+ " with #{args.map { |k, v| "#{k} #{short_json(v)}" }.join(', ')}"
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ module Tungsten
5
+ module Runtime
6
+ # The runtime's version, sent as X-Tungsten-Runtime: tungsten-rb/<version>.
7
+ VERSION = "0.1.0"
8
+ end
9
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ # The runtime of Ruby SDKs generated by tungsten (the `tungsten-runtime`
5
+ # gem): standard library only (net/http, json, openssl, securerandom).
6
+ #
7
+ # A generated SDK is mostly data, its SDK descriptor document
8
+ # (`tungsten-descriptors.json`), plus thin typed methods; everything with
9
+ # behaviour lives here, so every operation behaves the same way, and the
10
+ # same way as the TypeScript, Python, Rust and Go runtimes.
11
+ #
12
+ # Tungsten::Runtime::ClientCore runs each call through one pipeline:
13
+ # pre-flight validation of the arguments, auth (bearer with a required
14
+ # prefix, API keys, basic, composite profiles, OAuth2 client credentials and
15
+ # authorization code), idempotency keys, the confirmation rules of
16
+ # destructive operations, sending with tier-aware retries, and the
17
+ # classification of the answer (or of a lost request) into Ok or Err
18
+ # carrying the diagnostic envelope. On top of it: previews with
19
+ # confirmation tokens, verification hooks, pagination, polling, macros and
20
+ # server-sent event streams. Calls are synchronous; nothing raises for an
21
+ # API or transport failure.
22
+ module Tungsten
23
+ # See Tungsten::Runtime::ClientCore.
24
+ module Runtime
25
+ end
26
+ end
27
+
28
+ require_relative "runtime/version"
29
+ require_relative "runtime/json"
30
+ require_relative "runtime/envelope"
31
+ require_relative "runtime/util"
32
+ require_relative "runtime/schema"
33
+ require_relative "runtime/descriptors"
34
+ require_relative "runtime/options"
35
+ require_relative "runtime/serialize"
36
+ require_relative "runtime/idempotency"
37
+ require_relative "runtime/transport"
38
+ require_relative "runtime/classify"
39
+ require_relative "runtime/helpers"
40
+ require_relative "runtime/expr"
41
+ require_relative "runtime/client_core"
42
+ require_relative "runtime/auth"
43
+ require_relative "runtime/oauth"
44
+ require_relative "runtime/pages"
45
+ require_relative "runtime/macros"
46
+ require_relative "runtime/sse"
47
+ require_relative "runtime/stream"
48
+ require_relative "runtime/typed"
49
+ require_relative "runtime/dispatcher"
metadata ADDED
@@ -0,0 +1,82 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tungsten-runtime
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.2
5
+ platform: ruby
6
+ authors:
7
+ - Prince Boachie
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.2'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.2'
26
+ description: Pre-flight validation, auth, idempotency, confirmation, retries, error
27
+ classification, previews, pagination, macros and server-sent event streams for SDKs
28
+ generated by tungsten.
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - LICENSE
34
+ - README.md
35
+ - lib/tungsten/runtime.rb
36
+ - lib/tungsten/runtime/auth.rb
37
+ - lib/tungsten/runtime/classify.rb
38
+ - lib/tungsten/runtime/client_core.rb
39
+ - lib/tungsten/runtime/descriptors.rb
40
+ - lib/tungsten/runtime/dispatcher.rb
41
+ - lib/tungsten/runtime/envelope.rb
42
+ - lib/tungsten/runtime/expr.rb
43
+ - lib/tungsten/runtime/helpers.rb
44
+ - lib/tungsten/runtime/idempotency.rb
45
+ - lib/tungsten/runtime/json.rb
46
+ - lib/tungsten/runtime/macros.rb
47
+ - lib/tungsten/runtime/oauth.rb
48
+ - lib/tungsten/runtime/options.rb
49
+ - lib/tungsten/runtime/pages.rb
50
+ - lib/tungsten/runtime/schema.rb
51
+ - lib/tungsten/runtime/serialize.rb
52
+ - lib/tungsten/runtime/sse.rb
53
+ - lib/tungsten/runtime/stream.rb
54
+ - lib/tungsten/runtime/transport.rb
55
+ - lib/tungsten/runtime/typed.rb
56
+ - lib/tungsten/runtime/util.rb
57
+ - lib/tungsten/runtime/version.rb
58
+ homepage: https://github.com/pboachie/tungsten
59
+ licenses:
60
+ - Apache-2.0
61
+ metadata:
62
+ allowed_push_host: https://rubygems.org
63
+ rubygems_mfa_required: 'true'
64
+ source_code_uri: https://github.com/pboachie/tungsten/tree/v0.1.2/runtimes/ruby
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '3.3'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.6.9
80
+ specification_version: 4
81
+ summary: The runtime of Ruby SDKs generated by tungsten
82
+ test_files: []