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,253 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "set"
5
+ require_relative "../clp_scope"
6
+
7
+ module Parse
8
+ module AtlasSearch
9
+ # Resolves session tokens to user identities and inherited role
10
+ # sets for ACL-scoped Atlas Search queries.
11
+ #
12
+ # Atlas Search runs aggregations directly against MongoDB and
13
+ # therefore bypasses Parse Server's per-request ACL enforcement.
14
+ # To compile a +_rperm+ +$match+ stage (see {Parse::ACL.read_predicate})
15
+ # the caller needs to know two things about the requesting
16
+ # session:
17
+ #
18
+ # 1. The +_User.objectId+ that owns the session.
19
+ # 2. The transitive upward closure of role names that user
20
+ # inherits permissions from (cf. {Parse::Role.all_for_user}).
21
+ #
22
+ # Both lookups can be expensive — token → user requires a
23
+ # +/users/me+ round-trip, and user → roles can require multiple
24
+ # +_Role+ queries to walk the inheritance graph. Both are cached
25
+ # separately so a single agent turn that runs several Atlas Search
26
+ # tools amortizes the cost.
27
+ #
28
+ # Two distinct caches:
29
+ #
30
+ # * +session_cache+: maps +session_token+ to +user_id+. Long
31
+ # TTL (1 hour default), invalidation profile is logout. Apps
32
+ # that need sub-TTL revocation must call {.invalidate}
33
+ # explicitly from their logout path.
34
+ #
35
+ # * +role_cache+: maps +user_id+ to a +Set+ of role names. Short
36
+ # TTL (2 minutes default), invalidation profile is role-graph
37
+ # mutation. Stale role data here yields incorrect ACL
38
+ # decisions, so the default is conservatively short.
39
+ #
40
+ # The default cache implementation is process-local
41
+ # ({MemoryCache}) and guarded by a +Mutex+. Apps that need shared
42
+ # cross-process caching (Redis, Memcached) may install a
43
+ # replacement via {AtlasSearch.session_cache=} /
44
+ # {AtlasSearch.role_cache=}; the replacement must respond to
45
+ # +get(key)+, +set(key, value, ttl:)+, and +invalidate(key)+.
46
+ module Session
47
+ # Raised when a +session_token+ cannot be resolved — invalid
48
+ # token, expired session, or +/users/me+ returned an error.
49
+ # Atlas Search callers should treat this as a 401-equivalent.
50
+ class InvalidSession < StandardError; end
51
+
52
+ # Default cache: in-memory hash with per-entry TTL, guarded by a
53
+ # +Mutex+. Suitable for single-process apps. Apps running
54
+ # multi-process (Puma workers, Sidekiq processes) get a per-
55
+ # process cache — install a shared cache through
56
+ # {AtlasSearch.session_cache=} for cross-process sharing.
57
+ class MemoryCache
58
+ def initialize
59
+ @data = {}
60
+ @mutex = Mutex.new
61
+ end
62
+
63
+ # @param key [String]
64
+ # @return [Object, nil] the cached value, or +nil+ when the key
65
+ # is missing or its TTL has elapsed. Expired entries are
66
+ # evicted lazily on read.
67
+ def get(key)
68
+ @mutex.synchronize do
69
+ entry = @data[key]
70
+ return nil if entry.nil?
71
+ if entry[:expires_at] < Time.now
72
+ @data.delete(key)
73
+ return nil
74
+ end
75
+ entry[:value]
76
+ end
77
+ end
78
+
79
+ # @param key [String]
80
+ # @param value [Object]
81
+ # @param ttl [Numeric] seconds until the entry expires.
82
+ def set(key, value, ttl:)
83
+ @mutex.synchronize do
84
+ @data[key] = { value: value, expires_at: Time.now + ttl }
85
+ end
86
+ end
87
+
88
+ # @param key [String] cache key to forget.
89
+ def invalidate(key)
90
+ @mutex.synchronize { @data.delete(key) }
91
+ end
92
+
93
+ # Drop every entry. Used by {Session.reset_caches!} and by
94
+ # tests that need a clean slate.
95
+ def clear
96
+ @mutex.synchronize { @data.clear }
97
+ end
98
+ end
99
+
100
+ # Value returned by {Session.resolve}. +user_id+ is the
101
+ # +_User.objectId+ owning the session, or +nil+ for an anonymous
102
+ # caller. +role_names+ is a +Set+ of role names (no +role:+
103
+ # prefix) the user inherits permissions from, computed via
104
+ # {Parse::Role.all_for_user}.
105
+ Resolved = Struct.new(:user_id, :role_names) do
106
+ # Build the canonical +_rperm+/+_wperm+ permission-string set
107
+ # for this session. Always includes +"*"+ (public). Includes
108
+ # +user_id+ when present. Includes +"role:#{name}"+ for each
109
+ # inherited role.
110
+ # @return [Array<String>]
111
+ def permission_strings
112
+ out = ["*"]
113
+ out << user_id if user_id && !user_id.empty?
114
+ role_names.each { |name| out << "role:#{name}" if name && !name.empty? }
115
+ out.uniq
116
+ end
117
+
118
+ # @return [Boolean] +true+ for the anonymous-session case.
119
+ def anonymous?
120
+ user_id.nil? || user_id.empty?
121
+ end
122
+ end
123
+
124
+ class << self
125
+ # Resolve a +session_token+ to the requesting user and the
126
+ # transitive set of role names whose +role:NAME+ permission
127
+ # strings should be checked against +_rperm+.
128
+ #
129
+ # +nil+ or empty +session_token+ → anonymous {Resolved} with
130
+ # +user_id: nil+ and an empty +role_names+ set. The caller
131
+ # decides whether to refuse the request (the
132
+ # +require_session_token+ toggle on {Parse::AtlasSearch}) or
133
+ # treat as public-only.
134
+ #
135
+ # Cache layering: token-to-user_id is checked first; on hit
136
+ # the slower +/users/me+ round-trip is skipped. User-to-roles
137
+ # is then checked independently (a single user shared across
138
+ # sessions amortizes the role lookup).
139
+ #
140
+ # @param session_token [String, nil] the +X-Parse-Session-Token+
141
+ # value from the requesting session.
142
+ # @return [Resolved]
143
+ # @raise [InvalidSession] when the token cannot be resolved by
144
+ # +/users/me+ (404 / 209 invalid session token / 401).
145
+ def resolve(session_token)
146
+ return Resolved.new(nil, Set.new) if session_token.nil? || session_token.to_s.empty?
147
+
148
+ user_id = lookup_user_id(session_token.to_s)
149
+ role_names = lookup_role_names(user_id)
150
+ Resolved.new(user_id, role_names)
151
+ end
152
+
153
+ # Forget a +session_token+ entry from the session-token cache.
154
+ # Apps that revoke sessions out-of-band (logout, password
155
+ # reset, admin revoke) should call this from the same path so
156
+ # subsequent Atlas Search requests don't act on the stale
157
+ # +user_id+ mapping. The +role_names+ cache is keyed on
158
+ # +user_id+ and is not affected — call {.invalidate_user_roles}
159
+ # to clear that separately.
160
+ # @param session_token [String]
161
+ def invalidate(session_token)
162
+ return if session_token.nil?
163
+ Parse::AtlasSearch.session_cache.invalidate(session_token.to_s)
164
+ end
165
+
166
+ # Forget cached role membership for a +user_id+. Call after any
167
+ # +_Role.users+ mutation that affects this user (role grant /
168
+ # revoke, role-graph reshape).
169
+ # @param user_id [String]
170
+ def invalidate_user_roles(user_id)
171
+ return if user_id.nil?
172
+ Parse::AtlasSearch.role_cache.invalidate(user_id.to_s)
173
+ end
174
+
175
+ # Drop every cached entry across both caches. Useful in tests
176
+ # and in startup hooks for processes that fork after warming
177
+ # the cache.
178
+ def reset_caches!
179
+ Parse::AtlasSearch.session_cache.clear if Parse::AtlasSearch.session_cache.respond_to?(:clear)
180
+ Parse::AtlasSearch.role_cache.clear if Parse::AtlasSearch.role_cache.respond_to?(:clear)
181
+ end
182
+
183
+ private
184
+
185
+ # @!visibility private
186
+ # Resolve session_token → user_id via cache, falling through
187
+ # to +/users/me+. Raises {InvalidSession} on lookup failure;
188
+ # the caller is responsible for refusing the request.
189
+ def lookup_user_id(session_token)
190
+ cache = Parse::AtlasSearch.session_cache
191
+ cached = cache.get(session_token)
192
+ return cached if cached
193
+
194
+ response = begin
195
+ Parse.client.current_user(session_token)
196
+ rescue => e
197
+ raise InvalidSession, "session token lookup failed: #{e.class}: #{e.message}"
198
+ end
199
+ raise InvalidSession, "session token invalid or expired" if response.nil? || response.error?
200
+
201
+ result = response.result
202
+ user_id = result.is_a?(Hash) ? (result["objectId"] || result[:objectId]) : nil
203
+ raise InvalidSession, "session token resolved no user objectId" if user_id.nil? || user_id.to_s.empty?
204
+
205
+ user_id = user_id.to_s
206
+ cache.set(session_token, user_id, ttl: Parse::AtlasSearch.session_cache_ttl)
207
+ user_id
208
+ end
209
+
210
+ # @!visibility private
211
+ # Resolve user_id → Set<role_name> via cache, falling through
212
+ # to {Parse::Role.all_for_user}. Failures degrade silently to
213
+ # an empty set rather than raising — a Parse Server hiccup
214
+ # during the role walk must not turn every search call into a
215
+ # 500, and the worst case is a query that misses some
216
+ # role-restricted documents.
217
+ #
218
+ # ATLAS-7: explicitly re-raise the exceptions that signal
219
+ # attacks or policy denials BEFORE the generic rescue. Without
220
+ # this, a denied-operator probe (DeniedOperator), a timeout
221
+ # exhaustion (ExecutionTimeout), or a CLP denial during role
222
+ # graph traversal would silently downgrade to an empty role
223
+ # set — the caller would then run with public-only perms,
224
+ # missing role-restricted rows but also masking the attack
225
+ # signal from the operator. These exception classes are SDK
226
+ # contracts the caller must surface upward.
227
+ def lookup_role_names(user_id)
228
+ return Set.new if user_id.nil? || user_id.empty?
229
+
230
+ cache = Parse::AtlasSearch.role_cache
231
+ cached = cache.get(user_id)
232
+ return cached if cached.is_a?(Set)
233
+
234
+ pointer = Parse::Pointer.new(Parse::Model::CLASS_USER, user_id)
235
+ names = begin
236
+ Parse::Role.all_for_user(pointer, max_depth: 10)
237
+ rescue Parse::MongoDB::DeniedOperator,
238
+ Parse::MongoDB::ExecutionTimeout,
239
+ Parse::CLPScope::Denied
240
+ # Re-raise: these are attack signals or explicit policy
241
+ # denials and must NOT be swallowed into a fail-open
242
+ # public-only ACL state.
243
+ raise
244
+ rescue
245
+ Set.new
246
+ end
247
+ cache.set(user_id, names, ttl: Parse::AtlasSearch.role_cache_ttl)
248
+ names
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end