parse-stack-next 5.6.0 → 5.7.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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +409 -0
  3. data/README.md +378 -6
  4. data/docs/caching.md +748 -0
  5. data/examples/basic_client.rb +3 -3
  6. data/examples/basic_server.rb +3 -3
  7. data/examples/live_query_listener.rb +12 -12
  8. data/examples/rag_chatbot.rb +14 -14
  9. data/examples/transaction_example.rb +44 -45
  10. data/examples/webhook_server.rb +3 -3
  11. data/lib/parse/access.rb +493 -0
  12. data/lib/parse/acl_scope.rb +125 -52
  13. data/lib/parse/agent/approval_gate.rb +0 -0
  14. data/lib/parse/agent/cancellation_token.rb +3 -3
  15. data/lib/parse/agent/constraint_translator.rb +4 -4
  16. data/lib/parse/agent/describe.rb +34 -34
  17. data/lib/parse/agent/errors.rb +9 -9
  18. data/lib/parse/agent/mcp_client.rb +61 -59
  19. data/lib/parse/agent/mcp_dispatcher.rb +89 -101
  20. data/lib/parse/agent/mcp_rack_app.rb +92 -93
  21. data/lib/parse/agent/mcp_server.rb +1 -1
  22. data/lib/parse/agent/mcp_subscriptions.rb +22 -22
  23. data/lib/parse/agent/metadata_audit.rb +1 -2
  24. data/lib/parse/agent/metadata_dsl.rb +7 -7
  25. data/lib/parse/agent/metadata_registry.rb +12 -12
  26. data/lib/parse/agent/prompt_hardening.rb +4 -4
  27. data/lib/parse/agent/prompts.rb +10 -10
  28. data/lib/parse/agent/result_formatter.rb +4 -4
  29. data/lib/parse/agent/tools.rb +493 -400
  30. data/lib/parse/agent.rb +157 -116
  31. data/lib/parse/api/hooks.rb +1 -1
  32. data/lib/parse/api/server.rb +2 -2
  33. data/lib/parse/api/users.rb +2 -2
  34. data/lib/parse/atlas_search/index_manager.rb +1 -1
  35. data/lib/parse/atlas_search/session.rb +40 -218
  36. data/lib/parse/atlas_search.rb +181 -60
  37. data/lib/parse/authorization.rb +466 -0
  38. data/lib/parse/cache/invalidation.rb +167 -0
  39. data/lib/parse/cache/keyspace.rb +306 -0
  40. data/lib/parse/cache/moneta_surface.rb +126 -0
  41. data/lib/parse/cache/pool.rb +48 -5
  42. data/lib/parse/cache/redis.rb +425 -10
  43. data/lib/parse/cache/scoped_view.rb +443 -0
  44. data/lib/parse/cache/sub_cache.rb +264 -0
  45. data/lib/parse/cache/upstream_roles.rb +230 -0
  46. data/lib/parse/client/authentication.rb +1 -1
  47. data/lib/parse/client/body_builder.rb +1 -2
  48. data/lib/parse/client/caching.rb +84 -5
  49. data/lib/parse/client.rb +327 -189
  50. data/lib/parse/clp_scope.rb +225 -28
  51. data/lib/parse/console.rb +3 -3
  52. data/lib/parse/embeddings/batch_embedder.rb +3 -3
  53. data/lib/parse/embeddings/cache.rb +17 -17
  54. data/lib/parse/embeddings/cohere.rb +21 -22
  55. data/lib/parse/embeddings/image_fetch.rb +22 -21
  56. data/lib/parse/embeddings/jina.rb +21 -21
  57. data/lib/parse/embeddings/local_http.rb +6 -7
  58. data/lib/parse/embeddings/media_file.rb +5 -4
  59. data/lib/parse/embeddings/openai.rb +5 -5
  60. data/lib/parse/embeddings/provider.rb +1 -1
  61. data/lib/parse/embeddings/qwen.rb +10 -10
  62. data/lib/parse/embeddings/spend_cap.rb +2 -3
  63. data/lib/parse/embeddings/streaming_body.rb +11 -11
  64. data/lib/parse/embeddings/video_source.rb +7 -6
  65. data/lib/parse/embeddings/voyage.rb +59 -61
  66. data/lib/parse/embeddings.rb +28 -27
  67. data/lib/parse/graphql/type_generator.rb +1 -1
  68. data/lib/parse/graphql.rb +5 -5
  69. data/lib/parse/live_query/client.rb +1 -1
  70. data/lib/parse/live_query.rb +1 -1
  71. data/lib/parse/lock.rb +15 -18
  72. data/lib/parse/lock_backend.rb +1 -1
  73. data/lib/parse/lookup_rewriter.rb +6 -6
  74. data/lib/parse/model/classes/role.rb +227 -28
  75. data/lib/parse/model/classes/user.rb +60 -1
  76. data/lib/parse/model/clp.rb +4 -4
  77. data/lib/parse/model/core/actions.rb +97 -15
  78. data/lib/parse/model/core/create_lock.rb +0 -2
  79. data/lib/parse/model/core/describe.rb +57 -57
  80. data/lib/parse/model/core/embed_managed.rb +21 -23
  81. data/lib/parse/model/core/fetching.rb +2 -0
  82. data/lib/parse/model/core/indexing.rb +14 -14
  83. data/lib/parse/model/core/parse_reference.rb +1 -1
  84. data/lib/parse/model/core/properties.rb +2 -2
  85. data/lib/parse/model/core/querying.rb +1 -1
  86. data/lib/parse/model/core/schema.rb +2 -2
  87. data/lib/parse/model/core/search_indexing.rb +2 -2
  88. data/lib/parse/model/core/vector_searchable.rb +4 -5
  89. data/lib/parse/model/file.rb +21 -15
  90. data/lib/parse/model/geojson.rb +2 -2
  91. data/lib/parse/model/geopoint.rb +1 -0
  92. data/lib/parse/model/object.rb +132 -66
  93. data/lib/parse/model/pointer.rb +2 -0
  94. data/lib/parse/model/polygon.rb +3 -6
  95. data/lib/parse/model/push.rb +2 -2
  96. data/lib/parse/model/vector.rb +3 -1
  97. data/lib/parse/mongodb.rb +512 -282
  98. data/lib/parse/pipeline_security.rb +3 -0
  99. data/lib/parse/query/constraints.rb +46 -46
  100. data/lib/parse/query.rb +146 -59
  101. data/lib/parse/retrieval/agent_tool.rb +34 -20
  102. data/lib/parse/retrieval/chunk.rb +1 -0
  103. data/lib/parse/retrieval/reranker/cohere.rb +11 -11
  104. data/lib/parse/retrieval/reranker.rb +3 -4
  105. data/lib/parse/retrieval/retriever.rb +4 -6
  106. data/lib/parse/schema/index_migrator.rb +26 -26
  107. data/lib/parse/schema/search_index_migrator.rb +19 -19
  108. data/lib/parse/stack/tasks.rb +6 -6
  109. data/lib/parse/stack/version.rb +1 -1
  110. data/lib/parse/stack.rb +10 -10
  111. data/lib/parse/vector_search/hybrid.rb +66 -41
  112. data/lib/parse/vector_search.rb +14 -11
  113. data/lib/parse/webhooks/payload.rb +1 -0
  114. data/lib/parse/webhooks/registration.rb +4 -4
  115. data/lib/parse/webhooks/trigger_audit.rb +50 -49
  116. data/lib/parse/webhooks.rb +22 -4
  117. data/parse-stack-next.gemspec +6 -6
  118. metadata +10 -1
@@ -44,7 +44,7 @@ module Parse
44
44
  def _verify_trigger(triggerName)
45
45
  camel = triggerName.to_s.camelize(:lower).to_sym
46
46
  if %i[beforeCreate afterCreate].include?(camel)
47
- save = camel == :beforeCreate ? "beforeSave" : "afterSave"
47
+ save = camel == :beforeCreate ? "beforeSave" : "afterSave"
48
48
  callback = camel == :beforeCreate ? "before_create" : "after_create"
49
49
  raise ArgumentError,
50
50
  "Parse Server has no #{camel} webhook trigger. Register a " \
@@ -192,10 +192,10 @@ module Parse
192
192
  # unparseable input so a wire-format surprise never raises.
193
193
  def server_version_below?(actual, floor)
194
194
  actual_parts = actual.scan(/\d+/).first(2).map(&:to_i)
195
- floor_parts = floor.scan(/\d+/).first(2).map(&:to_i)
195
+ floor_parts = floor.scan(/\d+/).first(2).map(&:to_i)
196
196
  return false if actual_parts.empty? || floor_parts.empty?
197
197
  actual_parts << 0 while actual_parts.length < 2
198
- floor_parts << 0 while floor_parts.length < 2
198
+ floor_parts << 0 while floor_parts.length < 2
199
199
  (actual_parts <=> floor_parts) < 0
200
200
  end
201
201
  end
@@ -271,6 +271,7 @@ module Parse
271
271
  body[:email] = email || body[:email]
272
272
  create_user(body, **opts)
273
273
  end
274
+
274
275
  private
275
276
 
276
277
  # @!visibility private
@@ -315,7 +316,7 @@ module Parse
315
316
  entry = login_rate_limits[username] || { failures: 0, locked_until: nil }
316
317
  entry[:failures] += 1
317
318
  if entry[:failures] >= LOGIN_MAX_FAILURES
318
- delay = LOGIN_BASE_DELAY**(entry[:failures] - LOGIN_MAX_FAILURES + 1)
319
+ delay = LOGIN_BASE_DELAY ** (entry[:failures] - LOGIN_MAX_FAILURES + 1)
319
320
  delay = [delay, 300].min # cap at 5 minutes
320
321
  entry[:locked_until] = Time.now + delay
321
322
  end
@@ -337,7 +338,6 @@ module Parse
337
338
  entry[:locked_until] && (now - entry[:locked_until]) > LOGIN_RATE_LIMIT_TTL
338
339
  end
339
340
  end
340
-
341
341
  end # Users
342
342
  end #API
343
343
  end #Parse
@@ -220,7 +220,7 @@ module Parse
220
220
  def drop_index(collection_name, index_name, confirm:, allow_system_classes: false)
221
221
  result = Parse::MongoDB.drop_search_index(
222
222
  collection_name, index_name, confirm: confirm,
223
- allow_system_classes: allow_system_classes,
223
+ allow_system_classes: allow_system_classes,
224
224
  )
225
225
  clear_cache(collection_name)
226
226
  result
@@ -3,249 +3,71 @@
3
3
 
4
4
  require "set"
5
5
  require_relative "../clp_scope"
6
+ require_relative "../authorization"
6
7
 
7
8
  module Parse
8
9
  module AtlasSearch
9
- # Resolves session tokens to user identities and inherited role
10
- # sets for ACL-scoped Atlas Search queries.
10
+ # Compatibility shim over {Parse::Authorization}.
11
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:
12
+ # This module used to own session-token resolution, role-closure
13
+ # expansion, and both caches. It no longer does. Deciding who a caller is
14
+ # and what they may read is authorization infrastructure that Atlas
15
+ # Search consumes, not infrastructure Atlas Search owns, and the old
16
+ # arrangement meant `Parse::Query#results_direct` on a query with no
17
+ # `$search` in it went through `Parse::ACLScope` into this namespace to
18
+ # find out who was asking. See {Parse::Authorization} for the reasoning
19
+ # and for the real implementation.
17
20
  #
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
+ # Everything here now delegates to the default client's authorization
22
+ # context. That is the one behavior difference worth knowing: these
23
+ # methods take no `client:`, so they can only ever address
24
+ # `Parse.client`. Code running against a secondary application should
25
+ # call `other_client.authorization` directly.
21
26
  #
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)`.
27
+ # Slated for removal in 6.0.
46
28
  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
29
+ # @deprecated Use {Parse::Authorization::InvalidSession}. Kept as the
30
+ # same object rather than a subclass so existing `rescue
31
+ # Parse::AtlasSearch::Session::InvalidSession` clauses still catch
32
+ # what the resolver actually raises.
33
+ InvalidSession = Parse::Authorization::InvalidSession
99
34
 
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
35
+ # @deprecated Use {Parse::Authorization::MemoryCache}.
36
+ MemoryCache = Parse::Authorization::MemoryCache
117
37
 
118
- # @return [Boolean] `true` for the anonymous-session case.
119
- def anonymous?
120
- user_id.nil? || user_id.empty?
121
- end
122
- end
38
+ # @deprecated Use {Parse::Authorization::Resolved}.
39
+ Resolved = Parse::Authorization::Resolved
123
40
 
124
41
  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).
42
+ # @deprecated Use `client.authorization.resolve(token)`, or
43
+ # {Parse::Authorization.resolve} with an explicit `client:`.
44
+ # @return [Parse::Authorization::Resolved]
145
45
  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)
46
+ context.resolve(session_token)
151
47
  end
152
48
 
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]
49
+ # @deprecated Use `client.authorization.invalidate(token)`. Note that
50
+ # `Parse::Cache::Invalidation` now does this automatically from the
51
+ # `_Session` `after_logout` trigger, so an application no longer has
52
+ # to remember to call it from its own logout path.
161
53
  def invalidate(session_token)
162
- return if session_token.nil?
163
- Parse::AtlasSearch.session_cache.invalidate(session_token.to_s)
54
+ context.invalidate(session_token)
164
55
  end
165
56
 
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]
57
+ # @deprecated Use `client.authorization.invalidate_user_roles(id)`.
170
58
  def invalidate_user_roles(user_id)
171
- return if user_id.nil?
172
- Parse::AtlasSearch.role_cache.invalidate(user_id.to_s)
59
+ context.invalidate_user_roles(user_id)
173
60
  end
174
61
 
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.
62
+ # @deprecated Use `client.authorization.reset_caches!`.
178
63
  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)
64
+ context.reset_caches!
181
65
  end
182
66
 
183
67
  private
184
68
 
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
69
+ def context
70
+ Parse.client.authorization
249
71
  end
250
72
  end
251
73
  end