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,455 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ require_relative "stack/version"
8
+ require_relative "client"
9
+ require_relative "query"
10
+ require_relative "model/object"
11
+ require_relative "webhooks"
12
+ require_relative "agent"
13
+ require_relative "two_factor_auth"
14
+ require_relative "two_factor_auth/user_extension"
15
+ require_relative "schema"
16
+ require_relative "schema/index_migrator"
17
+ require_relative "schema/search_index_migrator"
18
+ require_relative "lookup_rewriter"
19
+
20
+ module Parse
21
+ class Error < StandardError; end
22
+
23
+ module Stack
24
+ end
25
+
26
+ # Fiber-local key consulted by the authentication middleware. A truthy
27
+ # entry suppresses the master-key header for the duration of the block
28
+ # set by {Parse.without_master_key}; a +:enabled+ entry forces the
29
+ # master-key header back on inside a nested {Parse.with_master_key}
30
+ # block.
31
+ MASTER_KEY_STATE_KEY = :__parse_master_key_state__
32
+
33
+ # Run +block+ with the master key suppressed for every Parse request
34
+ # originating in the current fiber. Equivalent to setting the
35
+ # +X-Disable-Parse-Master-Key+ header on each request, but block-scoped
36
+ # so callers can wrap a unit of work — e.g. running an action "as if
37
+ # the configured master key were not available" — without threading
38
+ # the header through every intermediate call.
39
+ #
40
+ # Survives Faraday retries (the per-request header would be stripped on
41
+ # the first attempt and gone by the retry; the fiber-local state lives
42
+ # for the lifetime of the block).
43
+ #
44
+ # @yield runs the block with master-key disabled
45
+ # @return [Object] the block's return value
46
+ # @example
47
+ # Parse.without_master_key do
48
+ # song = Song.find(id) # session-token / API-key auth only
49
+ # song.title = "Renamed"
50
+ # song.save # subject to ACL/CLP
51
+ # end
52
+ def self.without_master_key
53
+ previous = Fiber[MASTER_KEY_STATE_KEY]
54
+ Fiber[MASTER_KEY_STATE_KEY] = :disabled
55
+ yield
56
+ ensure
57
+ Fiber[MASTER_KEY_STATE_KEY] = previous
58
+ end
59
+
60
+ # Inverse of {.without_master_key}: forces the master key back on for
61
+ # the duration of the block, even if a containing {.without_master_key}
62
+ # had suppressed it. Useful for re-entering an admin-only operation
63
+ # inside a session-scoped block. If no master key is configured on the
64
+ # client, this is a no-op — the helper does not synthesise one.
65
+ #
66
+ # @yield runs the block with master-key enabled (if configured)
67
+ # @return [Object] the block's return value
68
+ def self.with_master_key
69
+ previous = Fiber[MASTER_KEY_STATE_KEY]
70
+ Fiber[MASTER_KEY_STATE_KEY] = :enabled
71
+ yield
72
+ ensure
73
+ Fiber[MASTER_KEY_STATE_KEY] = previous
74
+ end
75
+
76
+ # @return [Boolean] true if the current fiber is inside a
77
+ # {.without_master_key} block. Consulted by the authentication
78
+ # middleware in addition to the per-request disable header.
79
+ def self.master_key_disabled?
80
+ Fiber[MASTER_KEY_STATE_KEY] == :disabled
81
+ end
82
+
83
+ # Configuration for query validation warnings
84
+ # Set to false to disable warnings about unnecessary includes
85
+ # @example Disable query warnings
86
+ # Parse.warn_on_query_issues = false
87
+ @warn_on_query_issues = true
88
+
89
+ # Configuration for debugging autofetch behavior.
90
+ # When set to true, autofetch will raise Parse::AutofetchTriggeredError instead of
91
+ # automatically fetching data. This helps identify where additional keys are needed
92
+ # in queries to avoid unnecessary network requests.
93
+ # @example Enable autofetch debugging
94
+ # Parse.autofetch_raise_on_missing_keys = true
95
+ # # Now accessing an unfetched field will raise an error:
96
+ # # Parse::AutofetchTriggeredError: Autofetch triggered on Post#abc123 - field :content was not fetched
97
+ @autofetch_raise_on_missing_keys = false
98
+
99
+ # Configuration for serialization of partially fetched objects.
100
+ # When set to true (default), calling as_json or to_json on a partially fetched
101
+ # object will only serialize the fields that were fetched, preventing autofetch
102
+ # from being triggered during serialization. This is particularly useful for
103
+ # webhook responses where you intentionally want to return partial data.
104
+ # @example Disable (serialize all fields, triggering autofetch)
105
+ # Parse.serialize_only_fetched_fields = false
106
+ # @example Override per-call
107
+ # user.as_json(only_fetched: false) # Force full serialization
108
+ @serialize_only_fetched_fields = true
109
+
110
+ # Configuration for validating keys in partial fetch operations.
111
+ # When set to true (default), fetch!(keys: [...]) will warn about keys that
112
+ # don't match any defined property on the model. This helps catch typos and
113
+ # undefined field references early.
114
+ # Set to false if you use dynamic schemas or want to suppress warnings.
115
+ # @example Disable key validation warnings
116
+ # Parse.validate_query_keys = false
117
+ # @example With validation enabled (default)
118
+ # song.fetch!(keys: [:title, :nonexistent])
119
+ # # => [Parse::Fetch] Warning: unknown keys [:nonexistent] for Song
120
+ @validate_query_keys = true
121
+
122
+ # Configuration for experimental LiveQuery feature.
123
+ # LiveQuery provides real-time WebSocket subscriptions for reactive applications.
124
+ # This feature is experimental and not fully implemented. Enable at your own risk.
125
+ # @example Enable LiveQuery (experimental)
126
+ # Parse.live_query_enabled = true
127
+ # require 'parse/live_query'
128
+ # @note WebSocket client implementation is incomplete
129
+ @live_query_enabled = false
130
+
131
+ # Configuration for cache write-through on fetch operations.
132
+ # When set to true (default), fetch!/reload!/find operations will:
133
+ # - Skip reading from cache (always get fresh data from server)
134
+ # - Write the fresh data back to cache for future cached reads
135
+ # This is the "write-only" cache mode - ensures data freshness while keeping cache updated.
136
+ # Set to false to completely bypass cache (no read or write) on fetch operations.
137
+ # @example Disable cache write-on-fetch
138
+ # Parse.cache_write_on_fetch = false
139
+ # # Now fetch!/reload!/find will completely bypass cache
140
+ # @example Default behavior (write-only mode)
141
+ # song.fetch! # Gets fresh data, updates cache
142
+ # song.fetch!(cache: true) # Uses cached data if available
143
+ @cache_write_on_fetch = true
144
+
145
+ # Configuration for default query caching behavior.
146
+ # When set to false (default), queries do NOT use cache unless explicitly enabled.
147
+ # When set to true, queries use cache by default (opt-out behavior).
148
+ # This only affects queries - individual queries can always override with cache: true/false.
149
+ # @example Enable cache by default (opt-out behavior)
150
+ # Parse.default_query_cache = true
151
+ # Song.first # Uses cache
152
+ # Song.query(cache: false).first # Explicitly bypasses cache
153
+ # @example Default behavior (opt-in, cache disabled by default)
154
+ # Song.first # Does NOT use cache
155
+ # Song.query(cache: true).first # Explicitly uses cache
156
+ @default_query_cache = false
157
+
158
+ # Configuration for experimental Agent MCP server feature.
159
+ # The MCP (Model Context Protocol) server allows AI agents to interact with Parse data.
160
+ # This feature requires TWO steps to enable for safety:
161
+ # 1. Set environment variable: PARSE_MCP_ENABLED=true
162
+ # 2. Set in code: Parse.mcp_server_enabled = true
163
+ # @example Enable MCP server (experimental)
164
+ # # In environment or .env file:
165
+ # # PARSE_MCP_ENABLED=true
166
+ #
167
+ # # In code:
168
+ # Parse.mcp_server_enabled = true
169
+ # Parse::Agent.enable_mcp!(port: 3001)
170
+ # @note MCP server implementation is experimental
171
+ @mcp_server_enabled = false
172
+
173
+ # Configuration for MCP server port.
174
+ # @example Set custom port
175
+ # Parse.mcp_server_port = 3002
176
+ @mcp_server_port = 3001
177
+
178
+ # Configuration for MCP remote API.
179
+ # When set, the MCP server can forward requests to a remote AI API (e.g., OpenAI, Claude).
180
+ # @example Configure remote API
181
+ # Parse.mcp_remote_api = {
182
+ # provider: :openai, # :openai, :claude, or :custom
183
+ # api_key: ENV['OPENAI_API_KEY'],
184
+ # model: 'gpt-4',
185
+ # base_url: nil # Optional custom base URL
186
+ # }
187
+ @mcp_remote_api = nil
188
+
189
+ # Auto-rewrite LLM-style `$lookup` stages in aggregation pipelines passed
190
+ # to `Parse::Query#aggregate` and `Parse::MongoDB.aggregate`. When true
191
+ # (the default), pipelines using pretty/logical field names (e.g.
192
+ # `localField: "author", foreignField: "_id"`) are translated to the
193
+ # Parse-on-Mongo column-name form (`_p_author`/`parseReference`) when
194
+ # the foreign class declares `parse_reference`. Pipelines already in
195
+ # `_p_*`/`parseReference` form pass through unchanged (idempotent), and
196
+ # when the foreign class lacks `parse_reference` the stage is left
197
+ # alone (no `$split` fallback in the auto path — it's an optimization,
198
+ # not a correction).
199
+ # @example Disable auto-rewrite
200
+ # Parse.rewrite_lookups = false
201
+ @rewrite_lookups = true
202
+
203
+ # Configuration for strict property redefinition checks.
204
+ # When set to true (default), redeclaring a property with a different data type
205
+ # than the existing definition raises ArgumentError instead of warning and
206
+ # silently dropping the new declaration. Identical redeclarations (same data
207
+ # type and remote field name) are always silent. A type mismatch on a core
208
+ # Parse field (e.g. Installation#badge defined as :integer but redeclared as
209
+ # :string) is almost always a bug, so it is a hard failure by default. Set to
210
+ # false to fall back to the legacy warn-and-ignore behavior.
211
+ # @example Opt out of strict redefinition
212
+ # Parse.strict_property_redefinition = false
213
+ @strict_property_redefinition = true
214
+
215
+ # Configuration for globally enabling the synchronize-create lock on
216
+ # `Parse::Object.first_or_create!` and `create_or_update!`. When true, every
217
+ # call to those methods acquires a Moneta-backed mutex (typically Redis) to
218
+ # prevent duplicate creation under concurrency. Per-call `synchronize: false`
219
+ # still opts out. See {Parse::CreateLock}.
220
+ # @example Enable globally
221
+ # Parse.synchronize_create_default = true
222
+ # @example ENV fallback
223
+ # PARSE_STACK_SYNCHRONIZE_CREATE=true
224
+ @synchronize_create_default = ENV["PARSE_STACK_SYNCHRONIZE_CREATE"] == "true"
225
+
226
+ # Configuration for raising on impossible pointer-shape constraints
227
+ # (e.g. bare objectId strings inside an `$in` array against a pointer
228
+ # column whose target class cannot be resolved). When true, the SDK
229
+ # raises {Parse::Query::PointerShapeError} instead of silently
230
+ # returning a value that won't match — preventing the silent-zero
231
+ # failure mode where the LLM/operator reads "0 results" as a real
232
+ # answer. When false (default), the SDK logs a one-shot warning via
233
+ # `Parse.logger` and leaves the value unchanged for backwards
234
+ # compatibility.
235
+ # @example Enable globally
236
+ # Parse.strict_pointer_shapes = true
237
+ # @example ENV fallback (recommended for test/CI)
238
+ # PARSE_STRICT_POINTER_SHAPES=true
239
+ @strict_pointer_shapes = ENV["PARSE_STRICT_POINTER_SHAPES"] == "true"
240
+
241
+ # Tuning bundle for the synchronize-create lock. Per-call kwargs override.
242
+ # Keys: :ttl (seconds, default 3, max 30), :wait (seconds, default 2.0,
243
+ # max 30), :on_degraded (:warn, :warn_throttled, :raise, :proceed).
244
+ # @example
245
+ # Parse.synchronize_create_options = { ttl: 5, wait: 1.0, on_degraded: :warn_throttled }
246
+ @synchronize_create_options = {}
247
+
248
+ # HMAC secret for synchronize-create lock-key derivation. When set, lock
249
+ # keys are HMAC-SHA256 of the canonical payload (hides query_attrs content
250
+ # from Redis MONITOR / snapshot snoopers). When unset and the cache store
251
+ # is Redis-backed, a one-time warning is emitted and plain SHA256 is used
252
+ # so cross-process locking still works. When unset and the store is the
253
+ # in-memory adapter, an auto-derived per-process secret is used.
254
+ # @example
255
+ # Parse.synchronize_create_secret = ENV["PARSE_STACK_LOCK_SECRET"]
256
+ @synchronize_create_secret = nil
257
+
258
+ # Optional dedicated Moneta store for the synchronize-create lock. When
259
+ # nil, falls back to {Parse.cache}.
260
+ # @example
261
+ # Parse.synchronize_create_store = Moneta.new(:Redis, url: "redis://locks:6379/1")
262
+ @synchronize_create_store = nil
263
+
264
+ # Optional allowlist of {Parse::Object} subclasses that may use the
265
+ # synchronize-create lock. When set, calls from any other class raise
266
+ # {Parse::CreateLockUnavailableError}. When nil (default) with the global
267
+ # default enabled, a one-time +[Parse::Stack:SECURITY]+ warning is emitted
268
+ # noting the unbounded surface; the lock still applies to every class.
269
+ #
270
+ # **Inheritance behavior:** The allowlist check in
271
+ # {Parse::Core::Actions::ClassMethods#_assert_synchronize_class_allowed!}
272
+ # uses `self <= entry`, so any subclass of an allowlisted Class entry is
273
+ # itself allowlisted. Allowlisting `User` transitively allowlists every
274
+ # `class GuestUser < User` / `class AdminUser < User` etc. — declared now
275
+ # OR ever defined later in the process. If you need strict per-class
276
+ # gating, pass entries as String names (`"User"`) — those are matched
277
+ # against `self.name` / `parse_class` only, with no inheritance walk.
278
+ # @example Restrict to specific classes (subclasses inherit)
279
+ # Parse.synchronize_classes = [User, Device, Subscription]
280
+ # @example Strict equality, no inheritance
281
+ # Parse.synchronize_classes = ["User", "Device", "Subscription"]
282
+ @synchronize_classes = nil
283
+
284
+ class << self
285
+ attr_accessor :warn_on_query_issues, :autofetch_raise_on_missing_keys, :serialize_only_fetched_fields, :validate_query_keys,
286
+ :live_query_enabled, :cache_write_on_fetch, :default_query_cache, :mcp_server_enabled, :mcp_server_port, :mcp_remote_api,
287
+ :rewrite_lookups, :strict_property_redefinition,
288
+ :synchronize_create_default, :synchronize_create_options, :synchronize_create_secret,
289
+ :synchronize_create_store, :synchronize_classes,
290
+ :strict_pointer_shapes
291
+
292
+ # Check if LiveQuery feature is enabled
293
+ # @return [Boolean]
294
+ def live_query_enabled?
295
+ @live_query_enabled == true
296
+ end
297
+
298
+ # Check if strict pointer-shape validation is enabled. When true,
299
+ # impossible shapes (e.g. bare string `$in` element against a
300
+ # pointer column whose target class is unknown) raise
301
+ # {Parse::Query::PointerShapeError} instead of silently returning
302
+ # zero rows. See {Parse.strict_pointer_shapes=}.
303
+ # @return [Boolean]
304
+ def strict_pointer_shapes?
305
+ @strict_pointer_shapes == true
306
+ end
307
+
308
+ # Check if MCP server feature is enabled
309
+ # Requires PARSE_MCP_ENABLED=true in environment AND Parse.mcp_server_enabled = true
310
+ # @return [Boolean]
311
+ def mcp_server_enabled?
312
+ return false unless ENV["PARSE_MCP_ENABLED"] == "true"
313
+ @mcp_server_enabled == true
314
+ end
315
+
316
+ # Configure MCP remote API connection
317
+ # @param provider [Symbol] the API provider (:openai, :claude, :custom)
318
+ # @param api_key [String] the API key
319
+ # @param model [String] the model to use (e.g., 'gpt-4', 'claude-3-opus')
320
+ # @param base_url [String, nil] optional custom base URL
321
+ # @return [Hash] the configuration hash
322
+ def configure_mcp_remote_api(provider:, api_key:, model: nil, base_url: nil)
323
+ @mcp_remote_api = {
324
+ provider: provider.to_sym,
325
+ api_key: api_key,
326
+ model: model,
327
+ base_url: base_url,
328
+ }
329
+ end
330
+
331
+ # Check if MCP remote API is configured
332
+ # @return [Boolean]
333
+ def mcp_remote_api_configured?
334
+ @mcp_remote_api.is_a?(Hash) && @mcp_remote_api[:api_key].present?
335
+ end
336
+ end
337
+
338
+ # Error raised when {Parse::CreateLock#synchronize} cannot acquire the
339
+ # mutex within the configured wait budget. Callers typically rescue and either
340
+ # retry or treat as a temporary unavailability.
341
+ class CreateLockTimeoutError < Parse::Error; end
342
+
343
+ # Error raised when query_attrs passed to a synchronized `first_or_create!`
344
+ # contain values that cannot be canonicalized into a stable lock key (Procs,
345
+ # Regexps, query operators, unsaved pointers, nested Hashes, oversized
346
+ # payloads).
347
+ class CreateLockInvalidKey < Parse::Error; end
348
+
349
+ # Error raised when a synchronized call is made but the lock store is
350
+ # unavailable (typically `on_degraded: :raise` was configured and the store
351
+ # is process-local).
352
+ class CreateLockUnavailableError < Parse::Error; end
353
+
354
+ # Error raised when autofetch would be triggered but Parse.autofetch_raise_on_missing_keys is true.
355
+ # This helps developers identify where they need to add additional keys to their queries.
356
+ class AutofetchTriggeredError < StandardError
357
+ attr_reader :klass, :parse_object_id, :field, :is_pointer
358
+
359
+ def initialize(klass, object_id, field, is_pointer:)
360
+ @klass = klass
361
+ @parse_object_id = object_id
362
+ @field = field
363
+ @is_pointer = is_pointer
364
+
365
+ if is_pointer
366
+ super("Autofetch triggered on #{klass}##{object_id} - pointer accessed field :#{field}. Add this field to your includes or fetch the object first.")
367
+ else
368
+ super("Autofetch triggered on #{klass}##{object_id} - field :#{field} was not included in partial fetch. Add :#{field} to your query keys.")
369
+ end
370
+ end
371
+ end
372
+
373
+ # Special class to support Modernistik Hyperdrive server.
374
+ class Hyperdrive
375
+ # Applies a remote JSON hash containing the ENV keys and values from a remote
376
+ # URL. Values from the JSON hash are only applied to the current ENV hash ONLY if
377
+ # it does not already have a value. Therefore local ENV values will take precedence
378
+ # over remote ones. By default, it uses the url in environment value in 'CONFIG_URL' or 'HYPERDRIVE_URL'.
379
+ # @param url [String] the remote url that responds with the JSON body.
380
+ # @return [Boolean] true if the JSON hash was found and applied successfully.
381
+ def self.config!(url = nil)
382
+ url ||= ENV["HYPERDRIVE_URL"] || ENV["CONFIG_URL"]
383
+ return false if url.blank?
384
+
385
+ begin
386
+ uri = URI.parse(url)
387
+
388
+ # Security: Only allow HTTPS or localhost HTTP for development
389
+ unless uri.is_a?(URI::HTTPS) || (uri.is_a?(URI::HTTP) && %w[localhost 127.0.0.1].include?(uri.host))
390
+ warn "[Parse::Stack] Security: Config URL must be HTTPS (got: #{url})"
391
+ return false
392
+ end
393
+
394
+ # Use Net::HTTP instead of open-uri to avoid command injection via pipe characters
395
+ http = Net::HTTP.new(uri.host, uri.port)
396
+ http.use_ssl = uri.scheme == "https"
397
+ http.open_timeout = 10
398
+ http.read_timeout = 10
399
+
400
+ request = Net::HTTP::Get.new(uri.request_uri)
401
+ response = http.request(request)
402
+
403
+ unless response.is_a?(Net::HTTPSuccess)
404
+ warn "[Parse::Stack] Config fetch failed: #{url} (HTTP #{response.code})"
405
+ return false
406
+ end
407
+
408
+ # Parse JSON safely
409
+ remote_config = JSON.parse(response.body)
410
+
411
+ unless remote_config.is_a?(Hash)
412
+ warn "[Parse::Stack] Config must be a JSON object: #{url}"
413
+ return false
414
+ end
415
+
416
+ remote_config.each do |key, value|
417
+ k = key.to_s.upcase
418
+ # Validate key format to prevent injection
419
+ next unless k.match?(/\A[A-Z][A-Z0-9_]*\z/)
420
+ next unless ENV[k].nil?
421
+ ENV[k] = value.to_s
422
+ end
423
+ true
424
+ rescue URI::InvalidURIError => e
425
+ warn "[Parse::Stack] Invalid config URL: #{url} (#{e.message})"
426
+ false
427
+ rescue JSON::ParserError => e
428
+ warn "[Parse::Stack] Invalid JSON in config: #{url} (#{e.message})"
429
+ false
430
+ rescue StandardError => e
431
+ warn "[Parse::Stack] Error loading config: #{url} (#{e.class}: #{e.message})"
432
+ false
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ # Startup warning: If ENV is set but programmatic flag isn't, warn the user
439
+ if ENV["PARSE_MCP_ENABLED"] == "true" && !Parse.instance_variable_get(:@mcp_server_enabled)
440
+ warn "[Parse::Stack] PARSE_MCP_ENABLED is set in environment but Parse.mcp_server_enabled is false. " \
441
+ "Call Parse.mcp_server_enabled = true to enable the MCP agent feature."
442
+ end
443
+
444
+ # Startup warning: synchronize-create global-default mode without a class
445
+ # allowlist exposes the whole first_or_create!/create_or_update! surface to
446
+ # attacker-controlled lock contention. Operators should either restrict via
447
+ # Parse.synchronize_classes or audit each call site that takes untrusted input.
448
+ if Parse.synchronize_create_default && Parse.synchronize_classes.nil?
449
+ warn "[Parse::Stack:SECURITY] Parse.synchronize_create_default is true with no Parse.synchronize_classes allowlist. " \
450
+ "Every first_or_create!/create_or_update! caller is now subject to Redis-backed lock contention; an attacker " \
451
+ "controlling query_attrs on a public path can hold lock keys × TTL. Set Parse.synchronize_classes = [User, …] " \
452
+ "to restrict the surface, or audit each call site."
453
+ end
454
+
455
+ require_relative "stack/railtie" if defined?(::Rails)