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,510 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "active_model"
5
+ require "active_support"
6
+ require "active_support/inflector"
7
+ require "active_support/core_ext/object"
8
+ require "active_support/core_ext"
9
+ require "active_support/security_utils"
10
+ require "active_model/serializers/json"
11
+ require "rack"
12
+ require "ostruct"
13
+ require_relative "client"
14
+ # Note: Do not require "stack" here - this file is loaded from stack.rb
15
+ # and adding that require would create a circular dependency.
16
+ require_relative "model/object"
17
+ require_relative "webhooks/payload"
18
+ require_relative "webhooks/registration"
19
+ require_relative "webhooks/replay_protection"
20
+
21
+ module Parse
22
+ class Object
23
+
24
+ # Register a webhook function for this subclass.
25
+ # @example
26
+ # class Post < Parse::Object
27
+ #
28
+ # webhook_function :helloWorld do
29
+ # # ... do something when this function is called ...
30
+ # end
31
+ # end
32
+ # @param functionName [String] the literal name of the function to be registered with the server.
33
+ # @yield (see Parse::Object.webhook)
34
+ # @param block (see Parse::Object.webhook)
35
+ # @return (see Parse::Object.webhook)
36
+ def self.webhook_function(functionName, &block)
37
+ if block_given?
38
+ Parse::Webhooks.route(:function, functionName, &block)
39
+ else
40
+ block = functionName.to_s.underscore.to_sym if block.blank?
41
+ block = method(block.to_sym) if block.is_a?(Symbol)
42
+ Parse::Webhooks.route(:function, functionName, block)
43
+ end
44
+ end
45
+
46
+ # Register a webhook trigger or function for this subclass.
47
+ # @example
48
+ # class Post < Parse::Object
49
+ #
50
+ # webhook :before_save do
51
+ # # ... do something ...
52
+ # parse_object
53
+ # end
54
+ #
55
+ # end
56
+ # @param type (see Parse::Webhooks.route)
57
+ # @yield the body of the function to be evaluated in the scope of a {Parse::Webhooks::Payload} instance.
58
+ # @param block [Symbol] the name of the method to call, if no block is passed.
59
+ # @return (see Parse::Webhooks.route)
60
+ def self.webhook(type, &block)
61
+ if type == :function
62
+ unless block.is_a?(String) || block.is_a?(Symbol)
63
+ raise ArgumentError, "Invalid Cloud Code function name: #{block}"
64
+ end
65
+ Parse::Webhooks.route(:function, block, &block)
66
+ # then block must be a symbol or a string
67
+ else
68
+ if block_given?
69
+ Parse::Webhooks.route(type, self, &block)
70
+ else
71
+ Parse::Webhooks.route(type, self, block)
72
+ end
73
+ end
74
+ #if block
75
+
76
+ end
77
+ end
78
+
79
+ # A Rack-based application middlware to handle incoming Parse cloud code webhook
80
+ # requests.
81
+ class Webhooks
82
+ # The error to be raised in registered trigger or function webhook blocks that
83
+ # will trigger the Parse::Webhooks application to return the proper error response.
84
+ class ResponseError < StandardError; end
85
+
86
+ include Client::Connectable
87
+ extend Parse::Webhooks::Registration
88
+ # The name of the incoming env containing the webhook key.
89
+ HTTP_PARSE_WEBHOOK = "HTTP_X_PARSE_WEBHOOK_KEY"
90
+ # The name of the incoming env containing the application id key.
91
+ HTTP_PARSE_APPLICATION_ID = "HTTP_X_PARSE_APPLICATION_ID"
92
+ # The content type that needs to be sent back to Parse server.
93
+ CONTENT_TYPE = "application/json"
94
+
95
+ # The Parse Webhook Key to be used for authenticating webhook requests.
96
+ # See {Parse::Webhooks.key} on setting this value.
97
+ # @return [String]
98
+ def key
99
+ self.class.key
100
+ end
101
+
102
+ class << self
103
+
104
+ # Allows support for web frameworks that support auto-reloading of source.
105
+ # @!visibility private
106
+ def reload!(args = {})
107
+ end
108
+
109
+ # @return [Boolean] whether to print additional logging information. You may also
110
+ # set this to `:debug` for additional verbosity.
111
+ attr_accessor :logging
112
+
113
+ # A hash-like structure composing of all the registered webhook
114
+ # triggers and functions. These are `:before_save`, `:after_save`,
115
+ # `:before_delete`, `:after_delete` or `:function`.
116
+ # @return [OpenStruct]
117
+ def routes
118
+ return @routes unless @routes.nil?
119
+ r = Parse::API::Hooks::TRIGGER_NAMES_LOCAL + [:function]
120
+ @routes = OpenStruct.new(r.reduce({}) { |h, t| h[t] = {}; h })
121
+ end
122
+
123
+ # Internally registers a route for a specific webhook trigger or function.
124
+ # @param type [Symbol] The type of cloud code webhook to register. This can be any
125
+ # of the supported routes. These are `:before_save`, `:after_save`,
126
+ # `:before_delete`, `:after_delete` or `:function`.
127
+ # @param className [String] if `type` is not `:function`, then this registers
128
+ # a trigger for the given className. Otherwise, className is treated to be the function
129
+ # name to register with Parse server.
130
+ # @yield the block that will handle of the webhook trigger or function.
131
+ # @return (see routes)
132
+ def route(type, className, &block)
133
+ type = type.to_s.underscore.to_sym #support camelcase
134
+ if type != :function && className.respond_to?(:parse_class)
135
+ className = className.parse_class
136
+ end
137
+ className = className.to_s
138
+ if routes[type].nil? || block.respond_to?(:call) == false
139
+ raise ArgumentError, "Invalid Webhook registration trigger #{type} #{className}"
140
+ end
141
+
142
+ # AfterSave/AfterDelete hooks support more than one
143
+ if type == :after_save || type == :after_delete
144
+ routes[type][className] ||= []
145
+ routes[type][className].push block
146
+ else
147
+ routes[type][className] = block
148
+ end
149
+ @routes
150
+ end
151
+
152
+ # Run a locally registered webhook function. This bypasses calling a
153
+ # function through Parse-Server if the method handler is registered locally.
154
+ # @return [Object] the result of the function.
155
+ def run_function(name, params)
156
+ payload = Payload.new
157
+ payload.function_name = name
158
+ payload.params = params
159
+ call_route(:function, name, payload)
160
+ end
161
+
162
+ # Calls the set of registered webhook trigger blocks or the specific function block.
163
+ # This method is usually called when an incoming request from Parse Server is received.
164
+ # @param type (see route)
165
+ # @param className (see route)
166
+ # @param payload [Parse::Webhooks::Payload] the payload object received from the server.
167
+ # @return [Object] the result of the trigger or function.
168
+ def call_route(type, className, payload = nil)
169
+ type = type.to_s.underscore.to_sym #support camelcase
170
+ className = className.parse_class if className.respond_to?(:parse_class)
171
+ className = className.to_s
172
+
173
+ return unless routes[type].present? && routes[type][className].present?
174
+ registry = routes[type][className]
175
+
176
+ # Track the header-derived ruby_initiated flag on the payload so
177
+ # user code can introspect it (`payload.ruby_initiated?`). For the
178
+ # framework's own callback-deduplication logic below we use the
179
+ # stricter `trusted_ruby_initiated`, which additionally requires the
180
+ # master key. The X-Parse-Request-Id header is client-controllable,
181
+ # so honoring `_RB_` alone would let any client send `_RB_attacker`
182
+ # and trick the framework into skipping server-side callbacks.
183
+ # Server-side Parse-Stack saves use the master key by default, so
184
+ # the AND is a safe condition for legitimate Ruby-initiated traffic.
185
+ if payload
186
+ request_id = payload&.raw&.dig(:headers, "x-parse-request-id") ||
187
+ payload&.raw&.dig("headers", "x-parse-request-id") ||
188
+ payload&.raw&.dig(:headers, "X-Parse-Request-Id") ||
189
+ payload&.raw&.dig("headers", "X-Parse-Request-Id")
190
+ ruby_initiated = request_id&.start_with?("_RB_") || false
191
+ payload.instance_variable_set(:@ruby_initiated, ruby_initiated)
192
+ trusted_ruby_initiated = ruby_initiated && (payload.master? == true)
193
+ else
194
+ ruby_initiated = false
195
+ trusted_ruby_initiated = false
196
+ end
197
+
198
+ # Pre-block: apply declarative write protection (guard :field, :mode)
199
+ # to the parse_object that the handler will receive. Running BEFORE
200
+ # the handler block means trusted server-side writes performed inside
201
+ # the block are preserved -- only client-supplied values for guarded
202
+ # fields are reverted.
203
+ #
204
+ # Notably we do NOT gate this on ruby_initiated. That flag derives
205
+ # from a client-controlled X-Parse-Request-Id header, so trusting it
206
+ # to bypass write protection would allow a one-header attack. Master
207
+ # key requests still bypass via the master:/payload.master? check.
208
+ if type == :before_save && payload && payload.object?
209
+ klass = (className.present? && className != "*") ? Parse::Object.find_class(className) : nil
210
+ if klass && klass.respond_to?(:field_guards) && klass.field_guards.any?
211
+ pre_obj = payload.parse_object # memoized; the handler sees this same instance
212
+ if pre_obj.respond_to?(:apply_field_guards!)
213
+ pre_obj.apply_field_guards!(
214
+ master: payload.master? || false,
215
+ is_new: payload.original.blank?
216
+ )
217
+ end
218
+ end
219
+ end
220
+
221
+ if registry.is_a?(Array)
222
+ result = registry.map { |hook| payload.instance_exec(payload, &hook) }.last
223
+ else
224
+ result = payload.instance_exec(payload, &registry)
225
+ end
226
+
227
+ if result.is_a?(Parse::Object)
228
+ # if it is a Parse::Object, we will call the registered ActiveModel callbacks
229
+ if type == :before_save
230
+ # returning false from the callback block only runs the before_* callback
231
+ # Skip prepare_save! when this request is trusted-Ruby-initiated
232
+ # (both `_RB_` header AND master key), since Parse-Stack already
233
+ # ran ActiveModel before_save callbacks locally. A client-spoofed
234
+ # `_RB_` without master falls through and runs them here.
235
+ unless trusted_ruby_initiated
236
+ prepare_result = result.prepare_save!
237
+ # If prepare_save! returns false (callback chain was halted), throw an error
238
+ if prepare_result == false
239
+ raise Parse::Webhooks::ResponseError, "Save halted by before_save callback"
240
+ end
241
+ end
242
+ # For before_save, return the changes payload (what Parse Server expects)
243
+ result = result.changes_payload
244
+ elsif type == :before_delete
245
+ result.run_callbacks(:destroy) { false }
246
+ result = true
247
+ end
248
+ elsif type == :before_save && result == false
249
+ # If webhook block returns false, halt the save by throwing an error
250
+ raise Parse::Webhooks::ResponseError, "Save halted by before_save webhook"
251
+ elsif type == :before_save && (result == true || result.nil?)
252
+ # Open Source Parse server does not accept true results on before_save hooks.
253
+ result = {}
254
+ end
255
+
256
+ # Guard-injection: when a handler returns a Hash (or true/nil normalized
257
+ # to {}) for a class with field_guards, Parse Server would otherwise
258
+ # merge the response with the client's original payload and persist
259
+ # the client-supplied values for guarded fields. Inject the pre-built
260
+ # parse_object's changes_payload entries for any guarded field so the
261
+ # response carries the appropriate revert (Delete op on create, prior
262
+ # value on update). The Parse::Object return path already runs through
263
+ # changes_payload on the same memoized instance and therefore needs no
264
+ # extra injection.
265
+ if type == :before_save && result.is_a?(Hash) && payload && payload.object?
266
+ guard_klass = (className.present? && className != "*") ? Parse::Object.find_class(className) : nil
267
+ if guard_klass && guard_klass.respond_to?(:field_guards) && guard_klass.field_guards.any?
268
+ pre_obj = payload.parse_object # same memoized instance the pre-block step mutated
269
+ if pre_obj.respond_to?(:changes_payload)
270
+ guard_payload = pre_obj.changes_payload
271
+ field_map = guard_klass.respond_to?(:field_map) ? guard_klass.field_map : {}
272
+ guard_klass.field_guards.each_key do |field|
273
+ remote = (field_map[field.to_sym] || field).to_s
274
+ result[remote] = guard_payload[remote] if guard_payload.key?(remote)
275
+ end
276
+ end
277
+ end
278
+ end
279
+
280
+ if type == :after_save && (result == true || result.nil?) && payload&.parse_object.present? && payload.parse_object.is_a?(Parse::Object)
281
+ # Handle after_save callbacks intelligently based on request origin.
282
+ # For trusted-Ruby-initiated saves (both `_RB_` header AND master
283
+ # key), Parse Stack's local `run_callbacks :save` will fire
284
+ # after_create and after_save callbacks after the REST response
285
+ # returns; firing them again here would double-fire any side
286
+ # effect (e.g. an `after_save :send_email` would send two emails
287
+ # per save). For everything else -- client-initiated saves, or a
288
+ # spoofed `_RB_` from a non-master client -- Parse Stack never had
289
+ # a chance to run callbacks, so we fire them here.
290
+ is_new = payload.original.nil?
291
+ unless trusted_ruby_initiated
292
+ payload.parse_object.run_after_create_callbacks if is_new
293
+ payload.parse_object.run_after_save_callbacks
294
+ end
295
+ result = true
296
+ end
297
+
298
+ result
299
+ end
300
+
301
+ # Generates a success response for Parse Server.
302
+ # @param data [Object] the data to send back with the success.
303
+ # @return [Hash] a success data payload
304
+ def success(data = true)
305
+ { success: data }.to_json
306
+ end
307
+
308
+ # Generates an error response for Parse Server.
309
+ # @param data [Object] the data to send back with the error.
310
+ # @return [Hash] a error data payload
311
+ def error(data = false)
312
+ { error: data }.to_json
313
+ end
314
+
315
+ # @!attribute key
316
+ # Returns the configured webhook key if available. By default it will use
317
+ # the value of ENV['PARSE_SERVER_WEBHOOK_KEY'] if not configured.
318
+ # @return [String]
319
+ def key=(value)
320
+ @key = value
321
+ # Reset the warn-once flag so a deployment that configures the key
322
+ # after startup gets a clean state if the key is later cleared.
323
+ @missing_key_warned = nil
324
+ end
325
+
326
+ def key
327
+ @key ||= ENV["PARSE_SERVER_WEBHOOK_KEY"] || ENV["PARSE_WEBHOOK_KEY"]
328
+ end
329
+
330
+ # When no webhook key is configured, the endpoint refuses requests by
331
+ # default. Set this to true (or set PARSE_WEBHOOK_ALLOW_UNAUTHENTICATED=true)
332
+ # to opt into the legacy permissive behavior for local development.
333
+ # @return [Boolean]
334
+ attr_writer :allow_unauthenticated
335
+
336
+ def allow_unauthenticated
337
+ return @allow_unauthenticated unless @allow_unauthenticated.nil?
338
+ ENV["PARSE_WEBHOOK_ALLOW_UNAUTHENTICATED"] == "true"
339
+ end
340
+
341
+ # When set, {Parse::Webhooks::Registration#assert_webhook_url_safe!}
342
+ # skips the DNS resolution and private/internal CIDR refusal. Other
343
+ # checks (scheme, userinfo, host presence) still apply. Intended for
344
+ # integration tests that register webhooks at Docker bridge hosts
345
+ # (e.g. +host.docker.internal+) which only resolve from inside the
346
+ # Parse Server container. May also be enabled via
347
+ # +PARSE_WEBHOOK_ALLOW_PRIVATE_URLS=true+. Do not enable in
348
+ # production: the resolution guard is what blocks attacker-driven
349
+ # webhook redirection to internal hosts.
350
+ # @return [Boolean]
351
+ attr_writer :allow_private_webhook_urls
352
+
353
+ def allow_private_webhook_urls
354
+ return @allow_private_webhook_urls unless @allow_private_webhook_urls.nil?
355
+ ENV["PARSE_WEBHOOK_ALLOW_PRIVATE_URLS"] == "true"
356
+ end
357
+
358
+ # Standard Rack call method. This method processes an incoming cloud code
359
+ # webhook request from Parse Server, validates it and executes any registered handlers for it.
360
+ # The result of the handler for the matching webhook request is sent back to
361
+ # Parse Server. If the handler raises a {Parse::Webhooks::ResponseError},
362
+ # it will return the proper error response.
363
+ # @raise Parse::Webhooks::ResponseError whenever {Parse::Object}, ActiveModel::ValidationError
364
+ # @param env [Hash] the environment hash in a Rack request.
365
+ # @return [Array] the value of calling `finish` on the {http://www.rubydoc.info/github/rack/rack/Rack/Response Rack::Response} object.
366
+ def call(env)
367
+ # Thraed safety
368
+ dup.call!(env)
369
+ end
370
+
371
+ # @!visibility private
372
+ def call!(env)
373
+ request = Rack::Request.new env
374
+ response = Rack::Response.new
375
+
376
+ if self.key.present?
377
+ provided_key = request.env[HTTP_PARSE_WEBHOOK].to_s
378
+ unless ActiveSupport::SecurityUtils.secure_compare(self.key, provided_key)
379
+ puts "[Parse::Webhooks] Invalid Parse-Webhook Key received"
380
+ response.write error("Invalid Parse Webhook Key")
381
+ return response.finish
382
+ end
383
+ elsif !self.allow_unauthenticated
384
+ # Fail closed: without a configured webhook key, any host on the
385
+ # network could fire authenticated cloud triggers. Set
386
+ # PARSE_SERVER_WEBHOOK_KEY (matching the Parse Server config) or
387
+ # opt in to permissive mode via PARSE_WEBHOOK_ALLOW_UNAUTHENTICATED=true.
388
+ # Log the warning only once; otherwise an attacker hammering the
389
+ # endpoint can fill disk with repeated warnings. The flag lives on
390
+ # the original Parse::Webhooks class (not the per-request dup created
391
+ # by `call`), so it persists across requests.
392
+ unless Parse::Webhooks.instance_variable_get(:@missing_key_warned)
393
+ Parse::Webhooks.instance_variable_set(:@missing_key_warned, true)
394
+ warn "[Parse::Webhooks] Refusing requests: no webhook key configured. " \
395
+ "Set PARSE_SERVER_WEBHOOK_KEY or Parse::Webhooks.allow_unauthenticated = true."
396
+ end
397
+ response.write error("Webhook key not configured.")
398
+ return response.finish
399
+ end
400
+
401
+ # Use Rack's media_type (strips parameters/whitespace and lowercases)
402
+ # so the comparison is exact. The previous substring check on the raw
403
+ # Content-Type header accepted look-alikes like "application/jsonp"
404
+ # or "text/application/json" that should be rejected.
405
+ unless request.media_type == CONTENT_TYPE
406
+ response.write error("Invalid content-type format. Should be application/json.")
407
+ return response.finish
408
+ end
409
+
410
+ request.body.rewind
411
+ body_str = request.body.read
412
+ if body_str.bytesize > 1_048_576
413
+ response.write error("Payload too large.")
414
+ return response.finish
415
+ end
416
+
417
+ # NEW-EXT-4: reject in-window replays and (when configured)
418
+ # require a fresh HMAC over the body. Done before JSON parsing so
419
+ # a malformed payload can't bypass dedup, and before any handler
420
+ # runs so side effects aren't repeated.
421
+ replay_error = ReplayProtection.verify!(
422
+ request.env,
423
+ body_str,
424
+ request.env["HTTP_X_PARSE_REQUEST_ID"]
425
+ )
426
+ if replay_error
427
+ response.write error(replay_error)
428
+ return response.finish
429
+ end
430
+
431
+ begin
432
+ payload = Parse::Webhooks::Payload.new body_str
433
+ rescue => e
434
+ warn "Invalid webhook payload format: #{e}"
435
+ response.write error("Invalid payload format. Should be valid JSON.")
436
+ return response.finish
437
+ end
438
+
439
+ if self.logging.present?
440
+ if payload.trigger?
441
+ puts "[Webhooks::Request] --> #{payload.trigger_name} #{payload.parse_class}:#{payload.parse_id}"
442
+ elsif payload.function?
443
+ puts "[ParseWebhooks Request] --> Function #{payload.function_name}"
444
+ end
445
+ if self.logging == :debug
446
+ puts "[Webhooks::Payload] ----------------------------"
447
+ puts Parse::Middleware::BodyBuilder.redact(payload.as_json.to_json)
448
+ puts "----------------------------------------------------\n"
449
+ end
450
+ end
451
+
452
+ begin
453
+ result = true
454
+ if payload.function? && payload.function_name.present?
455
+ result = Parse::Webhooks.call_route(:function, payload.function_name, payload)
456
+ elsif payload.trigger? && payload.parse_class.present? && payload.trigger_name.present?
457
+ # call hooks subscribed to the specific class
458
+ result = Parse::Webhooks.call_route(payload.trigger_name, payload.parse_class, payload)
459
+
460
+ # call hooks subscribed to any class route
461
+ generic_result = Parse::Webhooks.call_route(payload.trigger_name, "*", payload)
462
+ result = generic_result if generic_result.present? && result.nil?
463
+ else
464
+ if self.logging.present?
465
+ puts "[Webhooks] --> Could not find mapping route for " \
466
+ "#{Parse::Middleware::BodyBuilder.redact(payload.to_json)}"
467
+ end
468
+ end
469
+
470
+ result = true if result.nil?
471
+ if self.logging.present?
472
+ puts "[Webhooks::Response] ----------------------------"
473
+ puts success(result)
474
+ puts "----------------------------------------------------\n"
475
+ end
476
+ response.write success(result)
477
+ return response.finish
478
+ rescue Parse::Webhooks::ResponseError, ActiveModel::ValidationError => e
479
+ if payload.trigger?
480
+ puts "[Webhooks::ResponseError] >> #{payload.trigger_name} #{payload.parse_class}:#{payload.parse_id}: #{e}"
481
+ elsif payload.function?
482
+ puts "[Webhooks::ResponseError] >> #{payload.function_name}: #{e}"
483
+ end
484
+ response.write error(e.to_s)
485
+ return response.finish
486
+ end
487
+
488
+ #check if we can handle the type trigger/functionName
489
+ response.write(success)
490
+ response.finish
491
+ end # call
492
+ end #class << self
493
+ end # Webhooks
494
+ end # Parse
495
+
496
+ # Load-order fixup for {Parse::Core::FieldGuards}: classes that declared
497
+ # `guard` in their class body (e.g. {Parse::User}) ran before this file
498
+ # was required, so their `ensure_field_guards_webhook!` call short-circuited
499
+ # with a "Parse::Webhooks not yet defined" guard. Walk every Parse::Object
500
+ # subclass that ended up with a non-empty `field_guards` hash and register
501
+ # the stub route now that {Parse::Webhooks} exists. Application code that
502
+ # uses `guard` from its own model files (which are required after this
503
+ # file) hits the normal path and bypasses this fixup.
504
+ if defined?(Parse::Object) && Parse::Object.respond_to?(:descendants)
505
+ Parse::Object.descendants.each do |klass|
506
+ next unless klass.respond_to?(:field_guards) && klass.field_guards.any?
507
+ next unless klass.respond_to?(:ensure_field_guards_webhook!)
508
+ klass.ensure_field_guards_webhook!
509
+ end
510
+ end
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # Auto-required entry point for the `parse-stack-next` gem.
5
+ require_relative "./parse/stack.rb"
@@ -0,0 +1,5 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # Useful for some users that require 'parse-stack' manually
5
+ require_relative "./parse/stack.rb"
@@ -0,0 +1,82 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "parse/stack/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "parse-stack-next"
8
+ spec.version = Parse::Stack::VERSION
9
+ spec.authors = ["Anthony Persaud", "Henry Spindell", "Adrian Curtin"]
10
+ spec.email = ["adrian+parse-stack@neurosynq.net"]
11
+
12
+ spec.summary = %q{Parse Server Ruby Client SDK (parse-stack-next fork)}
13
+ spec.description = %q{Parse Server Ruby Client. Perform Object-relational mapping between Parse Server and Ruby classes, with authentication, cloud code webhooks, push notifications and more built in.}
14
+ spec.homepage = "https://github.com/neurosynq/parse-stack-next"
15
+ spec.license = "MIT"
16
+
17
+ spec.metadata = {
18
+ "homepage_uri" => "https://github.com/neurosynq/parse-stack-next",
19
+ "source_code_uri" => "https://github.com/neurosynq/parse-stack-next",
20
+ "changelog_uri" => "https://github.com/neurosynq/parse-stack-next/blob/main/CHANGELOG.md",
21
+ "bug_tracker_uri" => "https://github.com/neurosynq/parse-stack-next/issues",
22
+ "documentation_uri" => "https://neurosynq.github.io/parse-stack-next/",
23
+ "rubygems_mfa_required" => "true",
24
+ }
25
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
26
+ # delete this section to allow pushing this gem to any host.
27
+ # if spec.respond_to?(:metadata)
28
+ # spec.metadata['allowed_push_host'] = "http://www.modernistik.com"
29
+ # else
30
+ # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
31
+ # end
32
+
33
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
34
+ spec.bindir = "bin"
35
+ spec.executables = ["parse-console"] #spec.files.grep(%r{^bin/pstack/}) { |f| File.basename(f) }
36
+ spec.require_paths = ["lib"]
37
+ spec.required_ruby_version = ">= 3.2"
38
+
39
+ spec.add_runtime_dependency "activemodel", [">= 6.1", "< 9"]
40
+ spec.add_runtime_dependency "activesupport", [">= 6.1", "< 9"]
41
+ spec.add_runtime_dependency "parallel", [">= 1.6", "< 3"]
42
+ spec.add_runtime_dependency "faraday", "~> 2.0"
43
+ spec.add_runtime_dependency "faraday-net_http_persistent", "~> 2.0"
44
+ spec.add_runtime_dependency "moneta", "< 2"
45
+ spec.add_runtime_dependency "rack", ">= 2.0.6", "< 4"
46
+ spec.add_runtime_dependency "csv", "~> 3.3"
47
+ spec.add_runtime_dependency "ostruct", "~> 0.6"
48
+
49
+ # Optional dependencies for MFA (Multi-Factor Authentication) support
50
+ # Users must add these to their Gemfile to use MFA features:
51
+ # gem 'rotp' # For TOTP generation/verification
52
+ # gem 'rqrcode' # For QR code generation
53
+
54
+ # Optional dependency for enhanced phone number validation
55
+ # Users can add this to their Gemfile for comprehensive phone validation:
56
+ # gem 'phonelib' # For full ITU-T E.164 validation with libphonenumber data
57
+
58
+ # Optional dependency for direct MongoDB queries and Atlas Search
59
+ # Required for: Parse::MongoDB, Parse::AtlasSearch, mongo_direct query methods
60
+ # Users can add this to their Gemfile for direct MongoDB access:
61
+ # gem 'mongo', '~> 2.18'
62
+ # Note: The gem is loaded at runtime only when MongoDB features are used
63
+
64
+ # spec.post_install_message = <<UPGRADE
65
+ #
66
+ # ** BREAKING CHANGES **
67
+ # The default `has_many` association form has changed from :array to :query.
68
+ # To use arrays, you must now pass `through: :array` option to `has_many`.
69
+ #
70
+ # Visit: https://github.com/modernistik/parse-stack/wiki/Changes-to-has_many-in-1.5.0
71
+ #
72
+ # UPGRADE
73
+ end
74
+
75
+ ## Development
76
+ # After checking out the repo, run `bin/setup` to install dependencies. You can
77
+ # also run `bin/console` for an interactive prompt that will allow you to experiment.
78
+ #
79
+ # To install this gem onto your local machine, run `bundle exec rake install`.
80
+ # To release a new version, update the version number in `version.rb`, and then run
81
+ # `bundle exec rake release`, which will create a git tag for the version,
82
+ # push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
data/parse-stack.png ADDED
Binary file
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Debug script to check Parse Server IP configuration
4
+ console.log('=== Parse Server IP Debug ===');
5
+ console.log('Environment variables:');
6
+ console.log('PARSE_SERVER_MASTER_KEY_IPS:', process.env.PARSE_SERVER_MASTER_KEY_IPS);
7
+
8
+ // Try to load the config file
9
+ try {
10
+ const fs = require('fs');
11
+ const config = JSON.parse(fs.readFileSync('/parse-server/config/parse-config.json', 'utf8'));
12
+ console.log('\nConfig file masterKeyIps:', config.masterKeyIps);
13
+ } catch (e) {
14
+ console.log('\nConfig file error:', e.message);
15
+ }
16
+
17
+ // Check what Parse Server would use as default
18
+ const defaultIps = ['127.0.0.1', '::1'];
19
+ console.log('\nDefault masterKeyIps:', defaultIps);
20
+
21
+ // Test IP parsing
22
+ const envIps = process.env.PARSE_SERVER_MASTER_KEY_IPS;
23
+ if (envIps) {
24
+ const parsedIps = envIps.split(',');
25
+ console.log('\nParsed environment IPs:', parsedIps);
26
+
27
+ const net = require('net');
28
+ parsedIps.forEach(ip => {
29
+ const cleanIp = ip.includes('/') ? ip.split('/')[0] : ip;
30
+ console.log(`IP "${ip}" -> clean: "${cleanIp}" -> isIP: ${net.isIP(cleanIp)}`);
31
+ });
32
+ }
33
+
34
+ console.log('\nRequest IP that Parse Server sees: (this would be logged in Parse Server)');
35
+ console.log('Expected request IP: 172.18.0.1 (Docker container network)');
@@ -0,0 +1,13 @@
1
+ FROM parseplatform/parse-server:9
2
+
3
+ # Switch to root to copy and set permissions
4
+ USER root
5
+
6
+ # Copy our custom startup script with execute permissions
7
+ COPY --chmod=755 start-parse.sh /start-parse.sh
8
+
9
+ # Switch back to node user (if needed)
10
+ USER node
11
+
12
+ # Set the entrypoint to our script
13
+ ENTRYPOINT ["/bin/sh", "/start-parse.sh"]