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.
- checksums.yaml +7 -0
- data/LICENSE +55 -0
- data/README.md +23 -0
- data/lib/tungsten/runtime/auth.rb +258 -0
- data/lib/tungsten/runtime/classify.rb +248 -0
- data/lib/tungsten/runtime/client_core.rb +1147 -0
- data/lib/tungsten/runtime/descriptors.rb +173 -0
- data/lib/tungsten/runtime/dispatcher.rb +83 -0
- data/lib/tungsten/runtime/envelope.rb +230 -0
- data/lib/tungsten/runtime/expr.rb +186 -0
- data/lib/tungsten/runtime/helpers.rb +185 -0
- data/lib/tungsten/runtime/idempotency.rb +110 -0
- data/lib/tungsten/runtime/json.rb +346 -0
- data/lib/tungsten/runtime/macros.rb +343 -0
- data/lib/tungsten/runtime/oauth.rb +329 -0
- data/lib/tungsten/runtime/options.rb +142 -0
- data/lib/tungsten/runtime/pages.rb +173 -0
- data/lib/tungsten/runtime/schema.rb +320 -0
- data/lib/tungsten/runtime/serialize.rb +353 -0
- data/lib/tungsten/runtime/sse.rb +148 -0
- data/lib/tungsten/runtime/stream.rb +500 -0
- data/lib/tungsten/runtime/transport.rb +386 -0
- data/lib/tungsten/runtime/typed.rb +107 -0
- data/lib/tungsten/runtime/util.rb +254 -0
- data/lib/tungsten/runtime/version.rb +9 -0
- data/lib/tungsten/runtime.rb +49 -0
- metadata +82 -0
|
@@ -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,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: []
|