parse-stack-next 4.5.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.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.env.sample +112 -0
  4. data/.env.test +10 -0
  5. data/.github/workflows/ruby.yml +36 -0
  6. data/.gitignore +49 -0
  7. data/.ruby-version +1 -0
  8. data/.solargraph.yml +22 -0
  9. data/CHANGELOG.md +5816 -0
  10. data/Gemfile +30 -0
  11. data/Gemfile.lock +175 -0
  12. data/LICENSE.txt +23 -0
  13. data/Makefile +63 -0
  14. data/README.md +5655 -0
  15. data/Rakefile +573 -0
  16. data/bin/console +38 -0
  17. data/bin/parse-console +136 -0
  18. data/bin/server +17 -0
  19. data/bin/setup +7 -0
  20. data/config/parse-config.json +12 -0
  21. data/docs/TEST_SERVER.md +271 -0
  22. data/docs/_config.yml +1 -0
  23. data/docs/mcp_guide.md +3484 -0
  24. data/docs/mongodb_direct_guide.md +1348 -0
  25. data/docs/mongodb_index_optimization_guide.md +631 -0
  26. data/examples/transaction_example.rb +219 -0
  27. data/lib/parse/acl_scope.rb +728 -0
  28. data/lib/parse/agent/cancellation_token.rb +80 -0
  29. data/lib/parse/agent/constraint_translator.rb +480 -0
  30. data/lib/parse/agent/describe.rb +420 -0
  31. data/lib/parse/agent/errors.rb +133 -0
  32. data/lib/parse/agent/mcp_client.rb +557 -0
  33. data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
  34. data/lib/parse/agent/mcp_rack_app.rb +1143 -0
  35. data/lib/parse/agent/mcp_server.rb +376 -0
  36. data/lib/parse/agent/metadata_audit.rb +259 -0
  37. data/lib/parse/agent/metadata_dsl.rb +733 -0
  38. data/lib/parse/agent/metadata_registry.rb +794 -0
  39. data/lib/parse/agent/pipeline_validator.rb +82 -0
  40. data/lib/parse/agent/prompts.rb +351 -0
  41. data/lib/parse/agent/rate_limiter.rb +158 -0
  42. data/lib/parse/agent/relation_graph.rb +162 -0
  43. data/lib/parse/agent/result_formatter.rb +453 -0
  44. data/lib/parse/agent/tools.rb +5489 -0
  45. data/lib/parse/agent.rb +3249 -0
  46. data/lib/parse/api/aggregate.rb +79 -0
  47. data/lib/parse/api/all.rb +26 -0
  48. data/lib/parse/api/analytics.rb +18 -0
  49. data/lib/parse/api/batch.rb +33 -0
  50. data/lib/parse/api/cloud_functions.rb +58 -0
  51. data/lib/parse/api/config.rb +125 -0
  52. data/lib/parse/api/files.rb +29 -0
  53. data/lib/parse/api/hooks.rb +117 -0
  54. data/lib/parse/api/objects.rb +146 -0
  55. data/lib/parse/api/path_segment.rb +75 -0
  56. data/lib/parse/api/push.rb +20 -0
  57. data/lib/parse/api/schema.rb +49 -0
  58. data/lib/parse/api/server.rb +50 -0
  59. data/lib/parse/api/sessions.rb +24 -0
  60. data/lib/parse/api/users.rb +250 -0
  61. data/lib/parse/atlas_search/index_manager.rb +353 -0
  62. data/lib/parse/atlas_search/result.rb +204 -0
  63. data/lib/parse/atlas_search/search_builder.rb +604 -0
  64. data/lib/parse/atlas_search/session.rb +253 -0
  65. data/lib/parse/atlas_search.rb +995 -0
  66. data/lib/parse/client/authentication.rb +97 -0
  67. data/lib/parse/client/batch.rb +234 -0
  68. data/lib/parse/client/body_builder.rb +240 -0
  69. data/lib/parse/client/caching.rb +203 -0
  70. data/lib/parse/client/logging.rb +293 -0
  71. data/lib/parse/client/profiling.rb +181 -0
  72. data/lib/parse/client/protocol.rb +91 -0
  73. data/lib/parse/client/request.rb +233 -0
  74. data/lib/parse/client/response.rb +208 -0
  75. data/lib/parse/client.rb +1104 -0
  76. data/lib/parse/clp_scope.rb +361 -0
  77. data/lib/parse/live_query/circuit_breaker.rb +256 -0
  78. data/lib/parse/live_query/client.rb +1001 -0
  79. data/lib/parse/live_query/configuration.rb +224 -0
  80. data/lib/parse/live_query/event.rb +115 -0
  81. data/lib/parse/live_query/event_queue.rb +272 -0
  82. data/lib/parse/live_query/health_monitor.rb +214 -0
  83. data/lib/parse/live_query/logging.rb +149 -0
  84. data/lib/parse/live_query/subscription.rb +294 -0
  85. data/lib/parse/live_query.rb +163 -0
  86. data/lib/parse/lookup_rewriter.rb +445 -0
  87. data/lib/parse/model/acl.rb +968 -0
  88. data/lib/parse/model/associations/belongs_to.rb +275 -0
  89. data/lib/parse/model/associations/collection_proxy.rb +435 -0
  90. data/lib/parse/model/associations/has_many.rb +597 -0
  91. data/lib/parse/model/associations/has_one.rb +158 -0
  92. data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
  93. data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
  94. data/lib/parse/model/bytes.rb +62 -0
  95. data/lib/parse/model/classes/audience.rb +262 -0
  96. data/lib/parse/model/classes/installation.rb +363 -0
  97. data/lib/parse/model/classes/job_schedule.rb +153 -0
  98. data/lib/parse/model/classes/job_status.rb +264 -0
  99. data/lib/parse/model/classes/product.rb +75 -0
  100. data/lib/parse/model/classes/push_status.rb +263 -0
  101. data/lib/parse/model/classes/role.rb +751 -0
  102. data/lib/parse/model/classes/session.rb +201 -0
  103. data/lib/parse/model/classes/user.rb +943 -0
  104. data/lib/parse/model/clp.rb +544 -0
  105. data/lib/parse/model/core/actions.rb +1268 -0
  106. data/lib/parse/model/core/builder.rb +139 -0
  107. data/lib/parse/model/core/create_lock.rb +386 -0
  108. data/lib/parse/model/core/describe.rb +382 -0
  109. data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
  110. data/lib/parse/model/core/errors.rb +38 -0
  111. data/lib/parse/model/core/fetching.rb +566 -0
  112. data/lib/parse/model/core/field_guards.rb +220 -0
  113. data/lib/parse/model/core/indexing.rb +382 -0
  114. data/lib/parse/model/core/parse_reference.rb +407 -0
  115. data/lib/parse/model/core/properties.rb +809 -0
  116. data/lib/parse/model/core/querying.rb +491 -0
  117. data/lib/parse/model/core/schema.rb +202 -0
  118. data/lib/parse/model/core/search_indexing.rb +174 -0
  119. data/lib/parse/model/date.rb +88 -0
  120. data/lib/parse/model/email.rb +213 -0
  121. data/lib/parse/model/file.rb +527 -0
  122. data/lib/parse/model/geojson.rb +271 -0
  123. data/lib/parse/model/geopoint.rb +261 -0
  124. data/lib/parse/model/model.rb +260 -0
  125. data/lib/parse/model/object.rb +2068 -0
  126. data/lib/parse/model/phone.rb +520 -0
  127. data/lib/parse/model/pointer.rb +443 -0
  128. data/lib/parse/model/polygon.rb +406 -0
  129. data/lib/parse/model/push.rb +975 -0
  130. data/lib/parse/model/shortnames.rb +8 -0
  131. data/lib/parse/model/time_zone.rb +141 -0
  132. data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
  133. data/lib/parse/model/validations.rb +96 -0
  134. data/lib/parse/mongodb.rb +2300 -0
  135. data/lib/parse/pipeline_security.rb +554 -0
  136. data/lib/parse/query/constraint.rb +198 -0
  137. data/lib/parse/query/constraints.rb +3279 -0
  138. data/lib/parse/query/cursor.rb +434 -0
  139. data/lib/parse/query/n_plus_one_detector.rb +445 -0
  140. data/lib/parse/query/operation.rb +104 -0
  141. data/lib/parse/query/ordering.rb +66 -0
  142. data/lib/parse/query.rb +7028 -0
  143. data/lib/parse/schema/index_migrator.rb +291 -0
  144. data/lib/parse/schema/search_index_migrator.rb +289 -0
  145. data/lib/parse/schema.rb +494 -0
  146. data/lib/parse/stack/generators/rails.rb +40 -0
  147. data/lib/parse/stack/generators/templates/model.erb +51 -0
  148. data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
  149. data/lib/parse/stack/generators/templates/model_role.rb +4 -0
  150. data/lib/parse/stack/generators/templates/model_session.rb +4 -0
  151. data/lib/parse/stack/generators/templates/model_user.rb +11 -0
  152. data/lib/parse/stack/generators/templates/parse.rb +12 -0
  153. data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
  154. data/lib/parse/stack/railtie.rb +18 -0
  155. data/lib/parse/stack/tasks.rb +563 -0
  156. data/lib/parse/stack/version.rb +11 -0
  157. data/lib/parse/stack.rb +455 -0
  158. data/lib/parse/two_factor_auth/user_extension.rb +449 -0
  159. data/lib/parse/two_factor_auth.rb +310 -0
  160. data/lib/parse/webhooks/payload.rb +360 -0
  161. data/lib/parse/webhooks/registration.rb +199 -0
  162. data/lib/parse/webhooks/replay_protection.rb +189 -0
  163. data/lib/parse/webhooks.rb +510 -0
  164. data/lib/parse-stack-next.rb +5 -0
  165. data/lib/parse-stack.rb +5 -0
  166. data/parse-stack-next.gemspec +82 -0
  167. data/parse-stack.png +0 -0
  168. data/scripts/debug-ips.js +35 -0
  169. data/scripts/docker/Dockerfile.parse +13 -0
  170. data/scripts/docker/atlas-init.js +284 -0
  171. data/scripts/docker/docker-compose.atlas.yml +76 -0
  172. data/scripts/docker/docker-compose.test.yml +106 -0
  173. data/scripts/docker/mongo-init.js +21 -0
  174. data/scripts/eval_mcp_with_lm_studio.rb +274 -0
  175. data/scripts/start-parse.sh +90 -0
  176. data/scripts/start_mcp_server.rb +78 -0
  177. data/scripts/test_server_connection.rb +82 -0
  178. metadata +377 -0
@@ -0,0 +1,97 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "faraday"
5
+ require "active_support"
6
+ require "active_support/core_ext"
7
+
8
+ require_relative "protocol"
9
+
10
+ module Parse
11
+ module Middleware
12
+ # This middleware handles sending the proper authentication headers to the
13
+ # Parse REST API endpoint.
14
+ class Authentication < Faraday::Middleware
15
+ include Parse::Protocol
16
+ # @!visibility private
17
+ DISABLE_MASTER_KEY = "X-Disable-Parse-Master-Key".freeze
18
+ # @return [String] the application id for this Parse endpoint.
19
+ attr_accessor :application_id
20
+ # @return [String] the REST API Key for this Parse endpoint.
21
+ attr_accessor :api_key
22
+ # The Master key API Key for this Parse endpoint. This is optional. If
23
+ # provided, it will be sent in every request.
24
+ # @return [String]
25
+ attr_accessor :master_key
26
+
27
+ #
28
+ # @param adapter [Faraday::Adapter] An instance of the Faraday adapter
29
+ # used for the connection. Defaults Faraday::Adapter::NetHttp.
30
+ # @param options [Hash] the options containing Parse authentication data.
31
+ # @option options [String] :application_id the application id.
32
+ # @option options [String] :api_key the REST API key.
33
+ # @option options [String] :master_key the Master Key for this application.
34
+ # If it is set, it will be sent on every request unless this middleware sees
35
+ # {DISABLE_MASTER_KEY} as an entry in the headers section.
36
+ # @option options [String] :content_type the content type format header. Defaults to
37
+ # {Parse::Protocol::CONTENT_TYPE_FORMAT}.
38
+ def initialize(adapter, options = {})
39
+ super(adapter)
40
+ @application_id = options[:application_id]
41
+ @api_key = options[:api_key]
42
+ @master_key = options[:master_key]
43
+ @content_type = options[:content_type] || CONTENT_TYPE_FORMAT
44
+ end
45
+
46
+ # Thread-safety
47
+ # @!visibility private
48
+ def call(env)
49
+ dup.call!(env)
50
+ end
51
+
52
+ # @!visibility private
53
+ def call!(env)
54
+ # We add the main Parse protocol headers
55
+ headers = {}
56
+ raise ArgumentError, "No Parse Application Id specified for authentication." unless @application_id.present?
57
+ headers[APP_ID] = @application_id
58
+ headers[API_KEY] = @api_key unless @api_key.blank?
59
+
60
+ # Three sources can suppress the master key for this request:
61
+ # 1. The per-request `X-Disable-Parse-Master-Key` header (one-off
62
+ # opt-out, kept for backwards compatibility).
63
+ # 2. Fiber-local state set by {Parse.without_master_key} (block-
64
+ # scoped opt-out, also visible on Faraday retries because the
65
+ # fiber persists across the retry loop — the header at (1)
66
+ # gets stripped on first call and is gone by the retry).
67
+ # 3. A session-token-authenticated request (the existing check
68
+ # below; session token wins over master key).
69
+ header_disable = env[:request_headers][DISABLE_MASTER_KEY].present?
70
+ fiber_disable = Parse.master_key_disabled?
71
+ unless @master_key.blank? || header_disable || fiber_disable
72
+ headers[MASTER_KEY] = @master_key
73
+ end
74
+
75
+ env[:request_headers].delete(DISABLE_MASTER_KEY)
76
+
77
+ # delete the use of master key if we are using session token.
78
+ if env[:request_headers].key?(Parse::Protocol::SESSION_TOKEN)
79
+ headers.delete(MASTER_KEY)
80
+ end
81
+ # merge the headers with the current provided headers
82
+ env[:request_headers].merge! headers
83
+ # set the content type of the request if it was not provided already.
84
+ env[:request_headers][CONTENT_TYPE] ||= @content_type
85
+ # only modify header
86
+
87
+ @app.call(env).on_complete do |response_env|
88
+ # check for return code raise an error when authentication was a failure
89
+ # if response_env[:status] == 401
90
+ # warn "Unauthorized Parse API Credentials for Application Id: #{@application_id}"
91
+ # end
92
+
93
+ end
94
+ end
95
+ end # Authenticator
96
+ end
97
+ end
@@ -0,0 +1,234 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "request"
5
+ require_relative "response"
6
+
7
+ module Parse
8
+ # Create a new batch operation.
9
+ # @param reqs [Array<Parse::Request>] a set of requests to batch.
10
+ # @return [BatchOperation] a new {BatchOperation} with the given change requests.
11
+ def self.batch(reqs = nil)
12
+ BatchOperation.new(reqs)
13
+ end
14
+
15
+ # This class provides a standard way to submit, manage and process batch operations
16
+ # for Parse::Objects and associations.
17
+ #
18
+ # Batch requests are supported implicitly and intelligently through an
19
+ # extension of array. When an array of Parse::Object subclasses is saved,
20
+ # Parse-Stack will batch all possible save operations for the objects in the
21
+ # array that have changed. It will also batch save 50 at a time until all items
22
+ # in the array are saved. Note: Parse does not allow batch saving Parse::User objects.
23
+ #
24
+ # songs = Songs.first 1000 #first 1000 songs
25
+ # songs.each do |song|
26
+ # # ... modify song ...
27
+ # end
28
+ #
29
+ # # will batch save 50 items at a time until all are saved.
30
+ # songs.save
31
+ #
32
+ # The objects do not have to be of the same collection in order to be supported in the
33
+ # batch request.
34
+ # @see Array.save
35
+ # @see Array.destroy
36
+ class BatchOperation
37
+ include Enumerable
38
+
39
+ # Default number of threads used to dispatch batch segments concurrently.
40
+ # Raise via `Parse::BatchOperation.parallelism = N` (or pass `parallelism:`
41
+ # to `#submit`) for higher throughput on bulk writes; 2 is intentionally
42
+ # conservative to avoid overwhelming smaller Parse Server deployments.
43
+ DEFAULT_PARALLELISM = 2
44
+
45
+ class << self
46
+ attr_writer :parallelism
47
+
48
+ def parallelism
49
+ @parallelism || DEFAULT_PARALLELISM
50
+ end
51
+ end
52
+
53
+ # @!attribute requests
54
+ # @return [Array] the set of requests in this batch.
55
+
56
+ # @!attribute responses
57
+ # @return [Array] the set of responses from this batch.
58
+
59
+ # @!attribute transaction
60
+ # @return [Boolean] whether this batch should be executed as a transaction.
61
+ attr_accessor :requests, :responses, :transaction
62
+
63
+ # @return [Parse::Client] the client to be used for the request.
64
+ def client
65
+ @client ||= Parse::Client.client
66
+ end
67
+
68
+ # @param reqs [Array<Parse::Request>] an array of requests.
69
+ # @param transaction [Boolean] whether to execute as a transaction.
70
+ def initialize(reqs = nil, transaction: false)
71
+ @requests = []
72
+ @responses = []
73
+ @transaction = transaction
74
+ reqs = [reqs] unless reqs.is_a?(Enumerable)
75
+ reqs.each { |r| add(r) } if reqs.is_a?(Enumerable)
76
+ end
77
+
78
+ # Add an additional request to this batch.
79
+ # @overload add(req)
80
+ # @param req [Parse::Request] the request to append.
81
+ # @return [Array<Parse::Request>] the set of requests.
82
+ # @overload add(batch)
83
+ # @param req [Parse::BatchOperation] add all the requests from this batch operation.
84
+ # @return [Array<Parse::Request>] the set of requests.
85
+ def add(req)
86
+ if req.respond_to?(:change_requests)
87
+ requests = req.change_requests.select { |r| r.is_a?(Parse::Request) }
88
+ @requests += requests
89
+ elsif req.is_a?(Array)
90
+ requests = req.select { |r| r.is_a?(Parse::Request) }
91
+ @requests += requests
92
+ elsif req.is_a?(BatchOperation)
93
+ @requests += req.requests if req.is_a?(BatchOperation)
94
+ else
95
+ @requests.push(req) if req.is_a?(Parse::Request)
96
+ end
97
+ @requests
98
+ end
99
+
100
+ # This method is for interoperability with Parse::Object instances.
101
+ # @see Parse::Object#change_requests
102
+ def change_requests
103
+ @requests
104
+ end
105
+
106
+ # @return [Array]
107
+ def each(&block)
108
+ return enum_for(:each) unless block_given?
109
+ @requests.each(&block)
110
+ end
111
+
112
+ # @return [Hash] a formatted payload for the batch request.
113
+ def as_json(*args)
114
+ payload = { requests: requests }
115
+ payload[:transaction] = true if @transaction
116
+ payload.as_json
117
+ end
118
+
119
+ # @return [Integer] the number of requests in the batch.
120
+ def count
121
+ @requests.count
122
+ end
123
+
124
+ # Remove all requests in this batch.
125
+ # @return [Array]
126
+ def clear!
127
+ @requests.clear
128
+ end
129
+
130
+ # @return [Boolean] true if the request was successful.
131
+ def success?
132
+ return false if @responses.empty?
133
+ @responses.compact.all?(&:success?)
134
+ end
135
+
136
+ # @return [Boolean] true if the request had an error.
137
+ def error?
138
+ return false if @responses.empty?
139
+ !success?
140
+ end
141
+
142
+ # Submit the batch operation in chunks until they are all complete. In general,
143
+ # Parse limits requests in each batch to 50 and it is possible that a {BatchOperation}
144
+ # instance contains more than 50 requests. This method will slice up the array of
145
+ # request and send them based on the `segment` amount until they have all been submitted.
146
+ # @param segment [Integer] the number of requests to send in each batch. Default 50.
147
+ # @param parallelism [Integer] the number of segments dispatched in
148
+ # parallel. Defaults to `Parse::BatchOperation.parallelism` (2).
149
+ # @return [Array<Parse::Response>] the corresponding set of responses for
150
+ # each request in the batch.
151
+ def submit(segment = 50, parallelism: self.class.parallelism, &block)
152
+ @responses = []
153
+ @requests.uniq!(&:signature)
154
+ parallelism = 1 if parallelism.nil? || parallelism < 1
155
+ @responses = @requests.each_slice(segment).to_a.threaded_map(parallelism) do |slice|
156
+ client.batch_request(BatchOperation.new(slice))
157
+ end
158
+ @responses.flatten!
159
+ @requests.zip(@responses).each(&block) if block_given?
160
+ @responses
161
+ end
162
+
163
+ alias_method :save, :submit
164
+ end
165
+ end
166
+
167
+ class Array
168
+
169
+ # Submit a batch request for deleting a set of Parse::Objects.
170
+ # @example
171
+ # # assume Post and Author are Parse models
172
+ # author = Author.first
173
+ # posts = Post.all author: author
174
+ # posts.destroy # batch destroy request
175
+ # @return [Parse::BatchOperation] the batch operation performed.
176
+ # @see Parse::BatchOperation
177
+ def destroy
178
+ batch = Parse::BatchOperation.new
179
+ each do |o|
180
+ next unless o.respond_to?(:destroy_request)
181
+ r = o.destroy_request
182
+ batch.add(r) unless r.nil?
183
+ end
184
+ batch.submit
185
+ batch
186
+ end
187
+
188
+ # Do not alias method as :delete is already part of array.
189
+ # alias_method :delete, :destroy
190
+
191
+ # Submit a batch request for deleting a set of Parse::Objects.
192
+ # Batch requests are supported implicitly and intelligently through an
193
+ # extension of array. When an array of Parse::Object subclasses is saved,
194
+ # Parse-Stack will batch all possible save operations for the objects in the
195
+ # array that have changed. It will also batch save 50 at a time until all items
196
+ # in the array are saved. Note: Parse does not allow batch saving Parse::User objects.
197
+ # @note The objects of the array to be saved do not all have to be of the same collection.
198
+ # @param merge [Boolean] whether to merge the updated changes to the series of
199
+ # objects back to the original ones submitted. If you don't need the original objects
200
+ # to be updated with the changes, set this to false for improved performance.
201
+ # @param force [Boolean] Do not skip objects that do not have pending changes (dirty tracking).
202
+ # @example
203
+ # # assume Post and Author are Parse models
204
+ # author = Author.first
205
+ # posts = Post.first 100
206
+ # posts.each { |post| post.author = author }
207
+ # posts.save # batch save
208
+ # @return [Parse::BatchOperation] the batch operation performed.
209
+ # @see Parse::BatchOperation
210
+ def save(merge: true, force: false)
211
+ batch = Parse::BatchOperation.new
212
+ objects = {}
213
+ each do |o|
214
+ next unless o.is_a?(Parse::Object)
215
+ objects[o.object_id] = o
216
+ batch.add o.change_requests(force)
217
+ end
218
+ if merge == false
219
+ batch.submit
220
+ return batch
221
+ end
222
+ #rebind updates
223
+ batch.submit do |request, response|
224
+ next unless request.tag.present? && response.present? && response.success?
225
+ o = objects[request.tag]
226
+ next unless o.is_a?(Parse::Object)
227
+ result = response.result
228
+ o.id = result["objectId"] if o.id.blank?
229
+ o.set_attributes!(result)
230
+ o.clear_changes!
231
+ end
232
+ batch
233
+ end #save!
234
+ end
@@ -0,0 +1,240 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "faraday"
5
+ require_relative "response"
6
+ require_relative "protocol"
7
+ require "active_support"
8
+ require "active_support/core_ext"
9
+ require "active_model/serializers/json"
10
+ require "json"
11
+ require "set"
12
+
13
+ module Parse
14
+
15
+ # @!attribute self.logging
16
+ # Sets {Parse::Middleware::BodyBuilder} logging.
17
+ # You may specify `:debug` for additional verbosity.
18
+ # @return (see Parse::Middleware::BodyBuilder.logging)
19
+ def self.logging
20
+ Parse::Middleware::BodyBuilder.logging
21
+ end
22
+ # @!visibility private
23
+ def self.logging=(value)
24
+ Parse::Middleware::BodyBuilder.logging = value
25
+ end
26
+
27
+ # Namespace for Parse-Stack related middleware.
28
+ module Middleware
29
+ # This middleware takes an incoming Parse response, after an outgoing request,
30
+ # and creates a Parse::Response object.
31
+ class BodyBuilder < Faraday::Middleware
32
+ include Parse::Protocol
33
+ # Header sent when a GET requests exceeds the limit.
34
+ HTTP_METHOD_OVERRIDE = "X-Http-Method-Override"
35
+ # Maximum url length for most server requests before HTTP Method Override is used.
36
+ MAX_URL_LENGTH = 2_000.freeze
37
+ # Fields that should be redacted from log output.
38
+ SENSITIVE_FIELDS = %w[
39
+ password token sessionToken session_token access_token authData
40
+ masterKey master_key apiKey api_key clientKey client_key
41
+ javascriptKey javascript_key refreshToken refresh_token
42
+ ].freeze
43
+ SENSITIVE_PATTERN = /(#{SENSITIVE_FIELDS.join("|")})(["']?\s*[=:>]\s*["']?)([^"&\s,}\]]+)/i
44
+ # Lookup set of sensitive field names for structural (JSON) redaction
45
+ # — case-insensitive match on the key, not the value. Walks the parsed
46
+ # structure so nested objects like {"password":{"nested":"value"}}
47
+ # and escaped-quote payloads (which the regex misses) are scrubbed.
48
+ SENSITIVE_FIELDS_SET = SENSITIVE_FIELDS.map(&:downcase).to_set.freeze
49
+ # Placeholder used in place of redacted values.
50
+ REDACTED_PLACEHOLDER = "[FILTERED]"
51
+ # Request headers that must never be printed verbatim in debug logs.
52
+ # Matched case-insensitively against Faraday header keys.
53
+ REDACTED_HEADERS = [
54
+ Parse::Protocol::MASTER_KEY,
55
+ Parse::Protocol::API_KEY,
56
+ Parse::Protocol::SESSION_TOKEN,
57
+ "X-Parse-JavaScript-Key",
58
+ "Authorization",
59
+ "Cookie",
60
+ ].map(&:downcase).freeze
61
+
62
+ class << self
63
+ # Allows logging. Set to `true` to enable logging, `false` to disable.
64
+ # You may specify `:debug` for additional verbosity.
65
+ # @return [Boolean]
66
+ attr_accessor :logging
67
+ end
68
+
69
+ # Redacts sensitive fields from a string for safe logging.
70
+ #
71
+ # Two passes run in sequence so that no payload shape leaks secrets:
72
+ #
73
+ # 1. **Structural pass.** If the body (after whitespace trim) parses as
74
+ # JSON, the parsed structure is walked recursively. Any value whose
75
+ # key matches +SENSITIVE_FIELDS_SET+ (case-insensitive) is replaced.
76
+ # String values that themselves look like JSON are recursively
77
+ # parsed and scrubbed — catches +{"body":"{\"password\":\"x\"}"}+
78
+ # payloads.
79
+ #
80
+ # 2. **Regex pass.** The result of the structural pass (or the original
81
+ # string if parsing failed) is always also run through the
82
+ # +SENSITIVE_PATTERN+ regex as defense-in-depth. This catches form-
83
+ # encoded bodies, partial JSON, escaped-quote payloads, and string
84
+ # array elements like +["password=hunter2"]+ that the structural
85
+ # walker can't redact in-place.
86
+ # @param str [String] the string to redact.
87
+ # @return [String] the redacted string.
88
+ def self.redact(str)
89
+ s = str.to_s
90
+ return s if s.empty?
91
+ after_structural = s
92
+ if (parsed = try_parse_json(s))
93
+ scrubbed = scrub_sensitive!(parsed)
94
+ begin
95
+ after_structural = scrubbed.to_json
96
+ rescue StandardError
97
+ after_structural = s
98
+ end
99
+ end
100
+ after_structural.gsub(SENSITIVE_PATTERN) do
101
+ key_part = $1
102
+ sep_part = $2
103
+ val_part = $3
104
+ # Skip values that the structural pass already redacted —
105
+ # otherwise the regex value-class +[^"&\s,}\]]+ stops at the
106
+ # bracket and we end up with +[FILTERED]]+ from the trailing
107
+ # close-bracket left over from +"[FILTERED]"+.
108
+ if val_part == "[FILTERED" || val_part == REDACTED_PLACEHOLDER
109
+ "#{key_part}#{sep_part}#{val_part}"
110
+ else
111
+ "#{key_part}#{sep_part}#{REDACTED_PLACEHOLDER}"
112
+ end
113
+ end
114
+ end
115
+
116
+ # @!visibility private
117
+ def self.try_parse_json(str)
118
+ # Find first non-whitespace byte; allow leading whitespace and BOM.
119
+ trimmed = str.byteslice(0, 16).to_s.dup
120
+ trimmed.force_encoding("BINARY")
121
+ trimmed.sub!(/\A\xEF\xBB\xBF/n, "")
122
+ first = trimmed.lstrip[0]
123
+ return nil unless first == "{" || first == "["
124
+ JSON.parse(str, max_nesting: 32)
125
+ rescue JSON::ParserError, JSON::NestingError
126
+ nil
127
+ end
128
+
129
+ # @!visibility private
130
+ # Recursively walks a parsed JSON structure replacing values under any
131
+ # sensitive key with the redaction placeholder. Returns the same node
132
+ # for chaining; mutates Hashes/Arrays in place.
133
+ #
134
+ # When a value is itself a String that looks like JSON, attempt to
135
+ # parse-scrub-re-encode it so embedded-JSON payloads are also covered
136
+ # (e.g. +{"body":"{\"password\":\"x\"}"}+).
137
+ def self.scrub_sensitive!(node)
138
+ case node
139
+ when Hash
140
+ node.each do |key, value|
141
+ if key.is_a?(String) && SENSITIVE_FIELDS_SET.include?(key.downcase)
142
+ node[key] = REDACTED_PLACEHOLDER
143
+ elsif value.is_a?(Hash) || value.is_a?(Array)
144
+ scrub_sensitive!(value)
145
+ elsif value.is_a?(String)
146
+ redacted_string = maybe_scrub_embedded_json(value)
147
+ node[key] = redacted_string unless redacted_string.equal?(value)
148
+ end
149
+ end
150
+ when Array
151
+ node.each_with_index do |item, i|
152
+ if item.is_a?(Hash) || item.is_a?(Array)
153
+ scrub_sensitive!(item)
154
+ elsif item.is_a?(String)
155
+ redacted_string = maybe_scrub_embedded_json(item)
156
+ node[i] = redacted_string unless redacted_string.equal?(item)
157
+ end
158
+ end
159
+ end
160
+ node
161
+ end
162
+
163
+ # @!visibility private
164
+ # If +str+ parses as JSON (object or array), scrub structurally and
165
+ # re-encode. Otherwise return the original string unchanged.
166
+ def self.maybe_scrub_embedded_json(str)
167
+ return str unless (inner = try_parse_json(str))
168
+ scrub_sensitive!(inner)
169
+ begin
170
+ inner.to_json
171
+ rescue StandardError
172
+ str
173
+ end
174
+ end
175
+
176
+ # Thread-safety
177
+ # @!visibility private
178
+ def call(env)
179
+ dup.call!(env)
180
+ end
181
+
182
+ # @!visibility private
183
+ def call!(env)
184
+ # the maximum url size is ~2KB, so if we request a Parse API url greater than this
185
+ # (which is most likely a very complicated query), we need to override the request method
186
+ # to be POST instead of GET and send the query parameters in the body of the POST request.
187
+ # The standard maximum POST request (which is a server setting), is usually set to 20MBs
188
+ if env[:method] == :get && env[:url].to_s.length >= MAX_URL_LENGTH
189
+ env[:request_headers][HTTP_METHOD_OVERRIDE] = "GET"
190
+ env[:request_headers][CONTENT_TYPE] = "application/x-www-form-urlencoded"
191
+ # parse-sever looks for method overrides in the body under the `_method` param.
192
+ # so we will add it to the query string, which will now go into the body.
193
+ env[:body] = "_method=GET&" + env[:url].query
194
+ env[:url].query = nil
195
+ #override
196
+ env[:method] = :post
197
+ # else if not a get, always make sure the request is JSON encoded if the content type matches
198
+ elsif env[:request_headers][CONTENT_TYPE] == CONTENT_TYPE_FORMAT &&
199
+ (env[:body].is_a?(Hash) || env[:body].is_a?(Array))
200
+ env[:body] = env[:body].to_json
201
+ end
202
+
203
+ if self.class.logging
204
+ puts "[Request #{env.method.upcase}] #{self.class.redact(env[:url].to_s)}"
205
+ env[:request_headers].each do |k, v|
206
+ if REDACTED_HEADERS.include?(k.to_s.downcase)
207
+ puts "[Header] #{k} : [FILTERED]"
208
+ else
209
+ puts "[Header] #{k} : #{v}"
210
+ end
211
+ end
212
+
213
+ puts "[Request Body] #{self.class.redact(env[:body].to_s)}"
214
+ end
215
+ @app.call(env).on_complete do |response_env|
216
+ # on a response, create a new Parse::Response and replace the :body
217
+ # of the env
218
+ # @todo CHECK FOR HTTP STATUS CODES
219
+ if self.class.logging
220
+ puts "[[Response #{response_env[:status]}]] ----------------------------------"
221
+ puts self.class.redact(response_env.body.to_s)
222
+ puts "[[Response]] --------------------------------------\n"
223
+ end
224
+
225
+ begin
226
+ r = Parse::Response.new(response_env.body)
227
+ rescue => e
228
+ r = Parse::Response.new
229
+ r.code = response_env.status
230
+ r.error = "Invalid response for #{env[:method]} #{env[:url]}: #{e}"
231
+ end
232
+ r.http_status = response_env[:status]
233
+ r.headers = response_env[:response_headers]
234
+ r.code ||= response_env[:status] if r.error.present?
235
+ response_env[:body] = r
236
+ end
237
+ end
238
+ end
239
+ end #Middleware
240
+ end