weft-sdk 0.24.0 → 0.32.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 +4 -4
- data/docs/FetchApi.md +3 -3
- data/docs/FetchErrorResponse.md +1 -1
- data/docs/FetchRequest.md +6 -2
- data/docs/FetchResponse.md +4 -4
- data/docs/GetPurchaseIdParameter.md +48 -0
- data/docs/PaymentsApi.md +2 -0
- data/docs/Purchase.md +6 -0
- data/docs/PurchasesApi.md +2 -2
- data/docs/SearchApi.md +1 -68
- data/docs/SearchCompactAccess.md +23 -0
- data/docs/SearchCompactAccessPrice.md +23 -0
- data/docs/SearchCompactNextCall.md +23 -0
- data/docs/SearchCompactResult.md +47 -0
- data/docs/SearchCompactResultAttribution.md +21 -0
- data/docs/SearchCompactResultBlockedScopesInner.md +19 -0
- data/docs/SearchCompactResultOperation.md +21 -0
- data/docs/SearchCompactResultProvider.md +19 -0
- data/docs/SearchEndpointCall.md +1 -1
- data/docs/SearchEndpointCallInputSchema.md +14 -0
- data/docs/SearchEndpointHit.md +5 -1
- data/docs/SearchHelperDocs.md +23 -0
- data/docs/SearchRequest.md +3 -1
- data/docs/SearchResponse.md +9 -3
- data/docs/SearchWeftFetchCompatibility.md +3 -1
- data/docs/WebhooksApi.md +414 -0
- data/lib/weft/client.rb +271 -0
- data/lib/weft/error.rb +41 -0
- data/lib/weft/facilitator/client.rb +179 -42
- data/lib/weft/facilitator/echo.rb +99 -0
- data/lib/weft/facilitator/extensions.rb +183 -0
- data/lib/weft/facilitator/fee.rb +26 -11
- data/lib/weft/facilitator/handshake.rb +92 -0
- data/lib/weft/facilitator/json_wire.rb +74 -0
- data/lib/weft/facilitator/middleware.rb +562 -137
- data/lib/weft/facilitator/path_match.rb +179 -0
- data/lib/weft/facilitator/product.rb +380 -0
- data/lib/weft/facilitator/replay.rb +46 -0
- data/lib/weft/facilitator/settlement.rb +117 -0
- data/lib/weft/facilitator/x402.rb +355 -0
- data/lib/weft/generated/api/account_api.rb +2 -2
- data/lib/weft/generated/api/agent_bootstrap_api.rb +2 -2
- data/lib/weft/generated/api/api_keys_api.rb +2 -2
- data/lib/weft/generated/api/auth_api.rb +2 -2
- data/lib/weft/generated/api/balance_api.rb +2 -2
- data/lib/weft/generated/api/documentation_api.rb +2 -2
- data/lib/weft/generated/api/fetch_api.rb +6 -6
- data/lib/weft/generated/api/payments_api.rb +4 -2
- data/lib/weft/generated/api/purchases_api.rb +4 -4
- data/lib/weft/generated/api/resources_api.rb +2 -2
- data/lib/weft/generated/api/search_api.rb +4 -83
- data/lib/weft/generated/api/webhooks_api.rb +388 -0
- data/lib/weft/generated/api_client.rb +2 -2
- data/lib/weft/generated/api_error.rb +2 -2
- data/lib/weft/generated/api_model_base.rb +2 -2
- data/lib/weft/generated/configuration.rb +2 -2
- data/lib/weft/generated/models/account_bootstrap_approval.rb +2 -2
- data/lib/weft/generated/models/account_bootstrap_created.rb +2 -2
- data/lib/weft/generated/models/account_bootstrap_created_approval.rb +2 -2
- data/lib/weft/generated/models/account_bootstrap_created_response.rb +2 -2
- data/lib/weft/generated/models/account_bootstrap_request.rb +2 -2
- data/lib/weft/generated/models/account_bootstrap_status.rb +2 -2
- data/lib/weft/generated/models/account_bootstrap_status_response.rb +2 -2
- data/lib/weft/generated/models/account_details.rb +2 -2
- data/lib/weft/generated/models/api_key.rb +2 -2
- data/lib/weft/generated/models/api_key_created.rb +2 -2
- data/lib/weft/generated/models/api_key_created_response.rb +2 -2
- data/lib/weft/generated/models/api_key_list_response.rb +2 -2
- data/lib/weft/generated/models/auth_response.rb +2 -2
- data/lib/weft/generated/models/auth_response_data.rb +2 -2
- data/lib/weft/generated/models/balance_response.rb +2 -2
- data/lib/weft/generated/models/confirm_request.rb +2 -2
- data/lib/weft/generated/models/create_api_key_request.rb +2 -2
- data/lib/weft/generated/models/enrolled_resource.rb +2 -2
- data/lib/weft/generated/models/error.rb +2 -2
- data/lib/weft/generated/models/error_response.rb +2 -2
- data/lib/weft/generated/models/fetch403_response.rb +2 -2
- data/lib/weft/generated/models/fetch_balance_snapshot.rb +2 -2
- data/lib/weft/generated/models/fetch_error_response.rb +5 -5
- data/lib/weft/generated/models/fetch_request.rb +46 -5
- data/lib/weft/generated/models/fetch_request_body.rb +2 -2
- data/lib/weft/generated/models/fetch_response.rb +30 -85
- data/lib/weft/generated/models/get_purchase_id_parameter.rb +104 -0
- data/lib/weft/generated/models/insufficient_scope_response.rb +2 -2
- data/lib/weft/generated/models/me_api_key.rb +2 -2
- data/lib/weft/generated/models/me_api_key_creator.rb +2 -2
- data/lib/weft/generated/models/me_response.rb +2 -2
- data/lib/weft/generated/models/me_response_data.rb +2 -2
- data/lib/weft/generated/models/message_response.rb +2 -2
- data/lib/weft/generated/models/message_response_data.rb +2 -2
- data/lib/weft/generated/models/pagination.rb +2 -2
- data/lib/weft/generated/models/password_reset_request.rb +2 -2
- data/lib/weft/generated/models/password_update_request.rb +2 -2
- data/lib/weft/generated/models/payment.rb +2 -2
- data/lib/weft/generated/models/payment_list_response.rb +2 -2
- data/lib/weft/generated/models/payment_response.rb +2 -2
- data/lib/weft/generated/models/principal_wallet.rb +2 -2
- data/lib/weft/generated/models/promo_balance.rb +2 -2
- data/lib/weft/generated/models/purchase.rb +75 -3
- data/lib/weft/generated/models/purchase_artifact.rb +2 -2
- data/lib/weft/generated/models/purchase_list_response.rb +2 -2
- data/lib/weft/generated/models/purchase_response.rb +2 -2
- data/lib/weft/generated/models/rate_limit_response.rb +2 -2
- data/lib/weft/generated/models/resend_confirmation_request.rb +2 -2
- data/lib/weft/generated/models/resource_enrollment_request.rb +2 -2
- data/lib/weft/generated/models/resource_enrollment_response.rb +2 -2
- data/lib/weft/generated/models/resource_stats.rb +2 -2
- data/lib/weft/generated/models/search_access_method.rb +2 -2
- data/lib/weft/generated/models/search_capability_ref.rb +2 -2
- data/lib/weft/generated/models/search_compact_access.rb +179 -0
- data/lib/weft/generated/models/search_compact_access_price.rb +176 -0
- data/lib/weft/generated/models/search_compact_next_call.rb +275 -0
- data/lib/weft/generated/models/search_compact_result.rb +389 -0
- data/lib/weft/generated/models/search_compact_result_attribution.rb +166 -0
- data/lib/weft/generated/models/search_compact_result_blocked_scopes_inner.rb +156 -0
- data/lib/weft/generated/models/search_compact_result_operation.rb +183 -0
- data/lib/weft/generated/models/search_compact_result_provider.rb +156 -0
- data/lib/weft/generated/models/search_curated_callability.rb +2 -2
- data/lib/weft/generated/models/search_curated_execution.rb +2 -2
- data/lib/weft/generated/models/search_curated_operation.rb +2 -2
- data/lib/weft/generated/models/search_curated_service.rb +2 -2
- data/lib/weft/generated/models/search_curated_source.rb +2 -2
- data/lib/weft/generated/models/search_endpoint_call.rb +4 -4
- data/lib/weft/generated/models/search_endpoint_call_input_schema.rb +103 -0
- data/lib/weft/generated/models/search_endpoint_hit.rb +25 -6
- data/lib/weft/generated/models/search_endpoint_price.rb +2 -2
- data/lib/weft/generated/models/search_error_response.rb +4 -4
- data/lib/weft/generated/models/search_execution_mode_filter.rb +2 -2
- data/lib/weft/generated/models/search_filter_spec.rb +2 -2
- data/lib/weft/generated/models/search_helper_docs.rb +175 -0
- data/lib/weft/generated/models/search_method_filter.rb +2 -2
- data/lib/weft/generated/models/search_payment_offer.rb +2 -2
- data/lib/weft/generated/models/search_price_atomic_filter.rb +2 -2
- data/lib/weft/generated/models/search_price_usd_filter.rb +2 -2
- data/lib/weft/generated/models/search_protocol_filter.rb +2 -2
- data/lib/weft/generated/models/search_provider_ref.rb +2 -2
- data/lib/weft/generated/models/search_request.rb +50 -6
- data/lib/weft/generated/models/search_resource_type_filter.rb +2 -2
- data/lib/weft/generated/models/search_response.rb +71 -8
- data/lib/weft/generated/models/search_response_warnings_inner.rb +2 -2
- data/lib/weft/generated/models/search_result.rb +2 -2
- data/lib/weft/generated/models/search_string_set_filter.rb +2 -2
- data/lib/weft/generated/models/search_weft_fetch_compatibility.rb +28 -6
- data/lib/weft/generated/models/sign_in_request.rb +2 -2
- data/lib/weft/generated/models/sign_up_request.rb +2 -2
- data/lib/weft/generated/models/spending_policy.rb +2 -2
- data/lib/weft/generated/models/user.rb +2 -2
- data/lib/weft/generated/models/user_principal.rb +2 -2
- data/lib/weft/generated/models/wallet.rb +2 -2
- data/lib/weft/generated/version.rb +3 -3
- data/lib/weft/sdk.rb +3 -1
- metadata +39 -8
- data/docs/CuratedMarketplaceContract.md +0 -31
- data/docs/Merchant.md +0 -23
- data/lib/weft/generated/models/curated_marketplace_contract.rb +0 -384
- data/lib/weft/generated/models/merchant.rb +0 -264
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'json_wire'
|
|
4
|
+
require_relative 'product'
|
|
5
|
+
|
|
6
|
+
module Weft
|
|
7
|
+
module Facilitator
|
|
8
|
+
WEFT_REQUEST_EXTENSION_KEY = 'weft.request'
|
|
9
|
+
MAX_EXTENSION_BYTES = 16 * 1024
|
|
10
|
+
WEFT_REQUEST_INFO_SCHEMA = {
|
|
11
|
+
'$schema' => 'https://json-schema.org/draft/2020-12/schema',
|
|
12
|
+
'title' => 'Weft per-request context',
|
|
13
|
+
'description' => 'Seller-authored context for one paid request, for display only. ' \
|
|
14
|
+
'Unauthenticated buyer input by the time it is read; key nothing on it.',
|
|
15
|
+
'type' => 'object',
|
|
16
|
+
'additionalProperties' => true
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
module Extensions
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def dynamic_extension(key, sink = nil)
|
|
23
|
+
sink ||= Product.create_warn
|
|
24
|
+
Hook.new(key, sink)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def register_dynamic_extensions(server, routes)
|
|
28
|
+
keys = dynamic_keys(routes)
|
|
29
|
+
return if keys.empty?
|
|
30
|
+
|
|
31
|
+
sink = Product.create_warn
|
|
32
|
+
unless server.respond_to?(:register_extension)
|
|
33
|
+
sink.call(
|
|
34
|
+
'this @x402/core build has no registerExtension, so per-request route ' \
|
|
35
|
+
"extensions (#{keys.join(', ')}) cannot be resolved and will not ship"
|
|
36
|
+
)
|
|
37
|
+
return
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
keys.each { |key| server.register_extension(dynamic_extension(key, sink)) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def dynamic_keys(routes)
|
|
44
|
+
keys = []
|
|
45
|
+
route_configs(routes).each do |config|
|
|
46
|
+
extensions = Product.read(config, 'extensions')
|
|
47
|
+
next unless extensions.is_a?(Hash)
|
|
48
|
+
|
|
49
|
+
extensions.each do |key, value|
|
|
50
|
+
keys << key.to_s if value.respond_to?(:call)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
keys.uniq
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def route_configs(routes)
|
|
57
|
+
return [] unless routes.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
configs = Product.single_route?(routes) ? [routes] : routes.values
|
|
60
|
+
configs.select { |config| config.is_a?(Hash) }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class Hook
|
|
65
|
+
attr_reader :key
|
|
66
|
+
|
|
67
|
+
def initialize(key, sink)
|
|
68
|
+
@key = key
|
|
69
|
+
@sink = sink
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def enrich_payment_required_response(declaration, context)
|
|
73
|
+
return nil unless declaration.respond_to?(:call)
|
|
74
|
+
|
|
75
|
+
request = request_of(context)
|
|
76
|
+
unless request
|
|
77
|
+
@sink.call(
|
|
78
|
+
"extensions[#{@key}] is a callback but no HTTP request context " \
|
|
79
|
+
'reached it; dropping the key from the challenge',
|
|
80
|
+
"#{@key}:no-request"
|
|
81
|
+
)
|
|
82
|
+
return drop_key(context)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
begin
|
|
86
|
+
resolved = declaration.call(request)
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
@sink.call(
|
|
89
|
+
"extensions[#{@key}] callback failed; dropping the key from the " \
|
|
90
|
+
"challenge: #{e.message}",
|
|
91
|
+
"#{@key}:threw"
|
|
92
|
+
)
|
|
93
|
+
return drop_key(context)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
return drop_key(context) if resolved.nil?
|
|
97
|
+
|
|
98
|
+
json = JsonWire.generate(resolved)
|
|
99
|
+
if json.nil?
|
|
100
|
+
@sink.call(
|
|
101
|
+
"extensions[#{@key}] callback returned a value JSON cannot carry (a " \
|
|
102
|
+
'function, a circular reference, a BigInt or similar); dropping ' \
|
|
103
|
+
'the key from the challenge',
|
|
104
|
+
"#{@key}:unserializable"
|
|
105
|
+
)
|
|
106
|
+
return drop_key(context)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
wire = JSON.parse(json)
|
|
110
|
+
unless wire.is_a?(Hash)
|
|
111
|
+
@sink.call(
|
|
112
|
+
"extensions[#{@key}] callback returned #{wire.is_a?(Array) ? 'an array' : "a #{js_type(wire)}"}; " \
|
|
113
|
+
'the x402 extensions channel carries objects, so the key is ' \
|
|
114
|
+
'dropped from the challenge',
|
|
115
|
+
"#{@key}:not-an-object"
|
|
116
|
+
)
|
|
117
|
+
return drop_key(context)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
value = if @key == WEFT_REQUEST_EXTENSION_KEY
|
|
121
|
+
{ 'info' => wire, 'schema' => WEFT_REQUEST_INFO_SCHEMA }
|
|
122
|
+
else
|
|
123
|
+
wire
|
|
124
|
+
end
|
|
125
|
+
bytes = projected_bytes(context, value) || json.bytesize
|
|
126
|
+
if bytes > MAX_EXTENSION_BYTES
|
|
127
|
+
@sink.call(
|
|
128
|
+
"extensions[#{@key}] takes the challenge's extensions to #{bytes} " \
|
|
129
|
+
"bytes, over the #{MAX_EXTENSION_BYTES}-byte facilitator relay " \
|
|
130
|
+
'cap; dropping the key so the rest of the declaration still ' \
|
|
131
|
+
'reaches settlement',
|
|
132
|
+
"#{@key}:over-cap"
|
|
133
|
+
)
|
|
134
|
+
return drop_key(context)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
value
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def request_of(context)
|
|
143
|
+
transport = read_context(context, 'transportContext')
|
|
144
|
+
return nil unless transport.is_a?(Hash)
|
|
145
|
+
|
|
146
|
+
Product.read(transport, 'request')
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def drop_key(context)
|
|
150
|
+
response = read_context(context, 'paymentRequiredResponse')
|
|
151
|
+
extensions = response.is_a?(Hash) ? Product.read(response, 'extensions') : nil
|
|
152
|
+
if extensions.is_a?(Hash)
|
|
153
|
+
extensions.delete(@key)
|
|
154
|
+
extensions.delete(@key.to_sym)
|
|
155
|
+
end
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def projected_bytes(context, value)
|
|
160
|
+
response = read_context(context, 'paymentRequiredResponse')
|
|
161
|
+
extensions = response.is_a?(Hash) ? Product.read(response, 'extensions') : nil
|
|
162
|
+
projected = Product.stringify_hash(extensions || {})
|
|
163
|
+
projected[@key] = value
|
|
164
|
+
json = JsonWire.generate(projected)
|
|
165
|
+
json&.bytesize
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def read_context(context, key)
|
|
169
|
+
Product.read(context, key)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def js_type(value)
|
|
173
|
+
case value
|
|
174
|
+
when String then 'string'
|
|
175
|
+
when Integer, Float then 'number'
|
|
176
|
+
when TrueClass, FalseClass then 'boolean'
|
|
177
|
+
when nil then 'object'
|
|
178
|
+
else 'object'
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
data/lib/weft/facilitator/fee.rb
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
require_relative 'client'
|
|
4
|
+
|
|
1
5
|
module Weft
|
|
2
6
|
module Facilitator
|
|
3
7
|
module Fee
|
|
4
|
-
|
|
8
|
+
DEFAULT_TTL_MS = 5 * 60 * 1000
|
|
5
9
|
|
|
6
10
|
@cache = nil
|
|
7
11
|
|
|
8
12
|
class << self
|
|
9
|
-
def get_fee_info(client: Client.new, ttl:
|
|
13
|
+
def get_fee_info(client: Client.new, ttl: DEFAULT_TTL_MS)
|
|
10
14
|
return @cache[:fee] if cache_valid?
|
|
11
15
|
|
|
12
16
|
supported = client.supported
|
|
13
|
-
fee = supported['fee']
|
|
17
|
+
fee = supported['fee'] || supported[:fee]
|
|
14
18
|
raise 'Fee information not found in /supported response' unless fee
|
|
15
19
|
|
|
16
20
|
validate_fee!(fee)
|
|
21
|
+
normalized = {
|
|
22
|
+
'amount' => fee['amount'] || fee[:amount],
|
|
23
|
+
'asset' => fee['asset'] || fee[:asset],
|
|
24
|
+
'network' => fee['network'] || fee[:network]
|
|
25
|
+
}
|
|
17
26
|
|
|
18
27
|
@cache = {
|
|
19
|
-
fee:
|
|
20
|
-
fetched_at:
|
|
28
|
+
fee: normalized,
|
|
29
|
+
fetched_at: now_ms,
|
|
21
30
|
ttl: ttl
|
|
22
31
|
}
|
|
23
|
-
|
|
24
|
-
fee
|
|
32
|
+
normalized
|
|
25
33
|
end
|
|
26
34
|
|
|
27
35
|
def invalidate_fee_cache
|
|
@@ -33,13 +41,20 @@ module Weft
|
|
|
33
41
|
def cache_valid?
|
|
34
42
|
return false unless @cache
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
now_ms - @cache[:fetched_at] < @cache[:ttl]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def now_ms
|
|
48
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
37
49
|
end
|
|
38
50
|
|
|
39
51
|
def validate_fee!(fee)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
52
|
+
amount = fee['amount'] || fee[:amount]
|
|
53
|
+
asset = fee['asset'] || fee[:asset]
|
|
54
|
+
network = fee['network'] || fee[:network]
|
|
55
|
+
raise 'Invalid fee structure: amount must be a string' unless amount.is_a?(String)
|
|
56
|
+
raise 'Invalid fee structure: asset must be a string' unless asset.is_a?(String)
|
|
57
|
+
raise 'Invalid fee structure: network must be a string' unless network.is_a?(String)
|
|
43
58
|
end
|
|
44
59
|
end
|
|
45
60
|
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../generated/version'
|
|
4
|
+
require_relative 'json_wire'
|
|
5
|
+
require_relative 'product'
|
|
6
|
+
|
|
7
|
+
module Weft
|
|
8
|
+
module Facilitator
|
|
9
|
+
WEFT_DECLARED_HEADER = 'X-Weft-Declared'
|
|
10
|
+
WEFT_API_KEY_HEADER = 'X-API-Key'
|
|
11
|
+
HEADER_SAFE = /\A[\x21-\x7e]+\z/
|
|
12
|
+
ADAPTER_NAME = 'rack'
|
|
13
|
+
|
|
14
|
+
module Handshake
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def build_auth_headers(adapter, api_key, declaration)
|
|
18
|
+
declaration = stringify_declaration(declaration)
|
|
19
|
+
key = resolve_api_key(api_key)
|
|
20
|
+
supported = {
|
|
21
|
+
'User-Agent' => "weft-sdk-#{adapter}/#{sdk_version}"
|
|
22
|
+
}
|
|
23
|
+
supported['Authorization'] = "Bearer #{key}" unless key.nil?
|
|
24
|
+
|
|
25
|
+
declared = declared_identity_value(declaration)
|
|
26
|
+
supported[WEFT_DECLARED_HEADER] = declared unless declared.nil?
|
|
27
|
+
|
|
28
|
+
headers = { 'supported' => supported }
|
|
29
|
+
unless key.nil?
|
|
30
|
+
headers['settle'] = { WEFT_API_KEY_HEADER => key }
|
|
31
|
+
headers['verify'] = { WEFT_API_KEY_HEADER => key }
|
|
32
|
+
end
|
|
33
|
+
headers
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def declared_identity_value(declaration)
|
|
37
|
+
declared = Product.sanitize_product_identity(declaration)
|
|
38
|
+
dimensions = Product.resolve_dimensions(declaration['dimensions'], Product.create_warn)
|
|
39
|
+
payload = {}
|
|
40
|
+
payload['name'] = declared['name'] if declared.key?('name')
|
|
41
|
+
payload['type'] = declared['type'] if declared.key?('type')
|
|
42
|
+
payload['tags'] = declared['tags'] if declared.key?('tags')
|
|
43
|
+
payload['icon_url'] = declared['iconUrl'] if declared.key?('iconUrl')
|
|
44
|
+
payload['dimensions'] = dimensions unless dimensions.nil?
|
|
45
|
+
return nil if payload.empty?
|
|
46
|
+
|
|
47
|
+
JsonWire.base64url_encode(JsonWire.generate(payload))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def resolve_api_key(api_key)
|
|
51
|
+
return nil if api_key.nil?
|
|
52
|
+
unless api_key.is_a?(String)
|
|
53
|
+
warn "[weft] ignoring apiKey: expected a string, got #{js_type(api_key)}"
|
|
54
|
+
return nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
trimmed = api_key.strip
|
|
58
|
+
if trimmed.empty?
|
|
59
|
+
warn '[weft] ignoring empty apiKey'
|
|
60
|
+
return nil
|
|
61
|
+
end
|
|
62
|
+
unless HEADER_SAFE.match?(trimmed)
|
|
63
|
+
warn '[weft] ignoring apiKey: it contains whitespace or non-printable ' \
|
|
64
|
+
'characters that cannot travel in an HTTP header'
|
|
65
|
+
return nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
trimmed
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def stringify_declaration(declaration)
|
|
72
|
+
return {} unless declaration.is_a?(Hash)
|
|
73
|
+
|
|
74
|
+
declaration.each_with_object({}) do |(key, value), out|
|
|
75
|
+
out[key.to_s] = value
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def sdk_version
|
|
80
|
+
defined?(SDK) ? SDK::VERSION : VERSION
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def js_type(value)
|
|
84
|
+
case value
|
|
85
|
+
when Integer, Float then 'number'
|
|
86
|
+
when TrueClass, FalseClass then 'boolean'
|
|
87
|
+
else 'object'
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'base64'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Weft
|
|
7
|
+
module Facilitator
|
|
8
|
+
# JSON.stringify and the two base64 forms @x402/core puts on the wire.
|
|
9
|
+
module JsonWire
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def generate(value)
|
|
13
|
+
return nil if circular?(value)
|
|
14
|
+
|
|
15
|
+
JSON.generate(sanitize(value))
|
|
16
|
+
rescue JSON::GeneratorError, JSON::NestingError
|
|
17
|
+
nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def circular?(value, seen = {})
|
|
21
|
+
case value
|
|
22
|
+
when Hash, Array
|
|
23
|
+
return true if seen[value.object_id]
|
|
24
|
+
|
|
25
|
+
seen[value.object_id] = true
|
|
26
|
+
children = value.is_a?(Hash) ? value.values : value
|
|
27
|
+
children.any? { |item| circular?(item, seen) }
|
|
28
|
+
else
|
|
29
|
+
false
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def sanitize(value)
|
|
34
|
+
case value
|
|
35
|
+
when Hash
|
|
36
|
+
value.each_with_object({}) do |(key, item), out|
|
|
37
|
+
out[key.is_a?(Symbol) ? key.to_s : key] = sanitize(item)
|
|
38
|
+
end
|
|
39
|
+
when Array
|
|
40
|
+
value.map { |item| sanitize(item) }
|
|
41
|
+
when Float
|
|
42
|
+
value.finite? ? value : nil
|
|
43
|
+
else
|
|
44
|
+
value
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def base64_encode(text)
|
|
49
|
+
Base64.strict_encode64(text.b)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def base64_decode(text)
|
|
53
|
+
Base64.strict_decode64(text).force_encoding(Encoding::UTF_8)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def base64url_encode(text)
|
|
57
|
+
Base64.urlsafe_encode64(text.b, padding: false)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def base64url_decode(text)
|
|
61
|
+
padded = text.tr('-_', '+/')
|
|
62
|
+
padded += '=' * ((4 - (padded.length % 4)) % 4)
|
|
63
|
+
Base64.decode64(padded).force_encoding(Encoding::UTF_8)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def encode_json(value)
|
|
67
|
+
json = generate(value)
|
|
68
|
+
raise ArgumentError, 'value is not JSON' if json.nil?
|
|
69
|
+
|
|
70
|
+
base64_encode(json)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|