parse-stack-next 5.6.0 → 5.7.1

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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +489 -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 +219 -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/associations/belongs_to.rb +4 -0
  75. data/lib/parse/model/associations/collection_proxy.rb +3 -0
  76. data/lib/parse/model/associations/has_many.rb +4 -0
  77. data/lib/parse/model/classes/role.rb +227 -28
  78. data/lib/parse/model/classes/user.rb +60 -1
  79. data/lib/parse/model/clp.rb +4 -4
  80. data/lib/parse/model/core/actions.rb +366 -113
  81. data/lib/parse/model/core/create_lock.rb +0 -2
  82. data/lib/parse/model/core/describe.rb +57 -57
  83. data/lib/parse/model/core/embed_managed.rb +21 -23
  84. data/lib/parse/model/core/fetching.rb +6 -0
  85. data/lib/parse/model/core/indexing.rb +14 -14
  86. data/lib/parse/model/core/parse_reference.rb +1 -1
  87. data/lib/parse/model/core/properties.rb +6 -2
  88. data/lib/parse/model/core/querying.rb +1 -1
  89. data/lib/parse/model/core/schema.rb +2 -2
  90. data/lib/parse/model/core/search_indexing.rb +2 -2
  91. data/lib/parse/model/core/vector_searchable.rb +4 -5
  92. data/lib/parse/model/file.rb +21 -15
  93. data/lib/parse/model/geojson.rb +2 -2
  94. data/lib/parse/model/geopoint.rb +1 -0
  95. data/lib/parse/model/object.rb +136 -66
  96. data/lib/parse/model/pointer.rb +2 -0
  97. data/lib/parse/model/polygon.rb +3 -6
  98. data/lib/parse/model/push.rb +2 -2
  99. data/lib/parse/model/vector.rb +3 -1
  100. data/lib/parse/mongodb.rb +512 -282
  101. data/lib/parse/pipeline_security.rb +3 -0
  102. data/lib/parse/query/constraints.rb +46 -46
  103. data/lib/parse/query.rb +146 -59
  104. data/lib/parse/retrieval/agent_tool.rb +34 -20
  105. data/lib/parse/retrieval/chunk.rb +1 -0
  106. data/lib/parse/retrieval/reranker/cohere.rb +11 -11
  107. data/lib/parse/retrieval/reranker.rb +3 -4
  108. data/lib/parse/retrieval/retriever.rb +4 -6
  109. data/lib/parse/schema/index_migrator.rb +26 -26
  110. data/lib/parse/schema/search_index_migrator.rb +19 -19
  111. data/lib/parse/stack/tasks.rb +6 -6
  112. data/lib/parse/stack/version.rb +1 -1
  113. data/lib/parse/stack.rb +10 -10
  114. data/lib/parse/vector_search/hybrid.rb +66 -41
  115. data/lib/parse/vector_search.rb +14 -11
  116. data/lib/parse/webhooks/payload.rb +1 -0
  117. data/lib/parse/webhooks/registration.rb +4 -4
  118. data/lib/parse/webhooks/trigger_audit.rb +50 -49
  119. data/lib/parse/webhooks.rb +120 -5
  120. data/parse-stack-next.gemspec +6 -6
  121. metadata +10 -1
@@ -93,39 +93,85 @@ module Parse
93
93
  # @return [Boolean]
94
94
  attr_accessor :require_session_token
95
95
 
96
- # @!attribute [rw] session_cache_ttl
97
- # TTL (seconds) for {Session}'s session-token → user-id cache.
98
- # Default: 3600 (1 hour). Longer values reduce `/users/me`
99
- # round-trips but extend the window during which a revoked
100
- # session can still authenticate Atlas Search calls; apps
101
- # with sub-TTL revocation requirements should call
102
- # {Session.invalidate} from their logout path.
103
- # @return [Integer]
104
- attr_accessor :session_cache_ttl
105
-
106
- # @!attribute [rw] role_cache_ttl
107
- # TTL (seconds) for {Session}'s user-id role-name cache.
108
- # Default: 30. Short on purpose: stale role data yields
109
- # incorrect ACL decisions, so the cache is sized to amortize
110
- # within a single request/turn but expire well inside the
111
- # response time the operator notices a role grant or revoke.
112
- # @return [Integer]
113
- attr_accessor :role_cache_ttl
114
-
115
- # @!attribute [rw] session_cache
116
- # Pluggable cache for {Session}'s session-token lookups.
117
- # Replace with a Redis/Memcached adapter for cross-process
118
- # sharing; the object must respond to `get(key)`,
119
- # `set(key, value, ttl:)`, and `invalidate(key)`. Defaults
120
- # to a process-local {Session::MemoryCache}.
121
- # @return [#get, #set, #invalidate]
122
- attr_accessor :session_cache
123
-
124
- # @!attribute [rw] role_cache
125
- # Pluggable cache for {Session}'s role-name lookups. See
126
- # {.session_cache} for the interface contract.
127
- # @return [#get, #set, #invalidate]
128
- attr_accessor :role_cache
96
+ # --- authorization state: delegated, no longer stored here ----------
97
+ #
98
+ # These five used to be module-level ivars on Atlas Search, which made
99
+ # Atlas Search the owner of identity and role resolution for the whole
100
+ # SDK: `Parse::ACLScope` reached through this namespace, and
101
+ # `Parse::MongoDB.aggregate` reaches through `Parse::ACLScope`, so a
102
+ # plain mongo-direct query with no `$search` in it depended on them.
103
+ # They now read and write the DEFAULT client's
104
+ # {Parse::Authorization::Context}. See {Parse::Authorization}.
105
+ #
106
+ # Being module-level, they can only ever address `Parse.client`. Code
107
+ # running against a secondary application must use
108
+ # `other_client.authorization` directly, which is the whole reason the
109
+ # state moved onto the client.
110
+ #
111
+ # Slated for removal in 6.0.
112
+
113
+ # @deprecated Use `client.authorization.identity_cache_ttl`.
114
+ # Renamed because it never stored sessions: it stores one user id per
115
+ # token, and the old name led readers to reason about `_Session`
116
+ # semantics that were never involved.
117
+ def session_cache_ttl = authorization&.identity_cache_ttl || Parse::Authorization::Context::DEFAULT_IDENTITY_TTL
118
+
119
+ def session_cache_ttl=(value)
120
+ authorization&.identity_cache_ttl = value
121
+ end
122
+
123
+ # @deprecated Use `client.authorization.role_cache_ttl`.
124
+ def role_cache_ttl = authorization&.role_cache_ttl || Parse::Authorization::Context::DEFAULT_ROLE_TTL
125
+
126
+ def role_cache_ttl=(value)
127
+ authorization&.role_cache_ttl = value
128
+ end
129
+
130
+ # @deprecated Use `client.authorization.identity_cache`.
131
+ def session_cache = authorization&.identity_cache
132
+
133
+ def session_cache=(value)
134
+ authorization&.identity_cache = value
135
+ end
136
+
137
+ # @deprecated Use `client.authorization.role_cache`.
138
+ def role_cache = authorization&.role_cache
139
+
140
+ def role_cache=(value)
141
+ authorization&.role_cache = value
142
+ end
143
+
144
+ # @deprecated Use `client.authorization.upstream_role_reader`.
145
+ def upstream_role_reader = authorization&.upstream_role_reader
146
+
147
+ def upstream_role_reader=(value)
148
+ authorization&.upstream_role_reader = value
149
+ end
150
+
151
+ # @deprecated Use `client.authorization.compare_upstream_roles`.
152
+ def compare_upstream_roles = authorization&.compare_upstream_roles || false
153
+
154
+ def compare_upstream_roles=(value)
155
+ authorization&.compare_upstream_roles = value
156
+ end
157
+
158
+ # The default client's authorization context, or `nil` when no client
159
+ # has been configured yet.
160
+ #
161
+ # Nil-tolerant on purpose. These delegators exist only for backward
162
+ # compatibility, and the module-level API they replace worked in a
163
+ # process that had never called `Parse.setup` (it kept its own
164
+ # process-local caches). Raising `ConnectionError` from what used to be
165
+ # a plain attribute reader would break configuration code that touches
166
+ # these before setting up a client, and `reset!` is a test helper that
167
+ # must not require a live client to run.
168
+ #
169
+ # @return [Parse::Authorization::Context, nil]
170
+ def authorization
171
+ Parse.client&.authorization
172
+ rescue Parse::Error::ConnectionError
173
+ nil
174
+ end
129
175
 
130
176
  # Configure Atlas Search (uses Parse::MongoDB connection)
131
177
  # @param enabled [Boolean] whether to enable Atlas Search (default: true)
@@ -142,6 +188,12 @@ module Parse
142
188
  # (seconds). Default: 3600.
143
189
  # @param role_cache_ttl [Integer] role-name cache TTL (seconds).
144
190
  # Default: 30.
191
+ # @param upstream_role_reader [#roles_for, nil] see
192
+ # {#upstream_role_reader}. Default: unchanged (`nil` unless set
193
+ # separately).
194
+ # @param compare_upstream_roles [Boolean] see
195
+ # {#compare_upstream_roles}. Default: unchanged (`false` unless
196
+ # set separately).
145
197
  # @example
146
198
  # Parse::AtlasSearch.configure(enabled: true, default_index: "default")
147
199
  def configure(enabled: true,
@@ -149,14 +201,22 @@ module Parse
149
201
  allow_raw: nil,
150
202
  require_session_token: nil,
151
203
  session_cache_ttl: nil,
152
- role_cache_ttl: nil)
204
+ role_cache_ttl: nil,
205
+ upstream_role_reader: nil,
206
+ compare_upstream_roles: nil)
153
207
  Parse::MongoDB.require_gem!
154
208
  @enabled = enabled
155
209
  @default_index = default_index
156
210
  @allow_raw = allow_raw.nil? ? default_allow_raw : allow_raw
157
211
  @require_session_token = require_session_token unless require_session_token.nil?
158
- @session_cache_ttl = session_cache_ttl unless session_cache_ttl.nil?
159
- @role_cache_ttl = role_cache_ttl unless role_cache_ttl.nil?
212
+ # Authorization settings belong to the client, not to this module.
213
+ # Forwarded rather than stored so there is exactly one copy.
214
+ authorization&.configure(
215
+ identity_cache_ttl: session_cache_ttl,
216
+ role_cache_ttl: role_cache_ttl,
217
+ upstream_role_reader: upstream_role_reader,
218
+ compare_upstream_roles: compare_upstream_roles,
219
+ )
160
220
  IndexManager.clear_cache
161
221
  end
162
222
 
@@ -194,10 +254,15 @@ module Parse
194
254
  @default_index = "default"
195
255
  @allow_raw = default_allow_raw
196
256
  @require_session_token = false
197
- @session_cache_ttl = 3600
198
- @role_cache_ttl = 30
199
- @session_cache = Session::MemoryCache.new
200
- @role_cache = Session::MemoryCache.new
257
+ authorization&.configure(
258
+ identity_cache_ttl: Parse::Authorization::Context::DEFAULT_IDENTITY_TTL,
259
+ role_cache_ttl: Parse::Authorization::Context::DEFAULT_ROLE_TTL,
260
+ identity_cache: Parse::Authorization::MemoryCache.new,
261
+ role_cache: Parse::Authorization::MemoryCache.new,
262
+ upstream_role_reader: false,
263
+ compare_upstream_roles: false,
264
+ )
265
+ authorization&.upstream_role_reader = nil
201
266
  @master_warned = false
202
267
  IndexManager.clear_cache
203
268
  end
@@ -497,6 +562,7 @@ module Parse
497
562
  raw_results = run_atlas_pipeline!(
498
563
  collection_name, pipeline, options[:max_time_ms],
499
564
  read_preference: read_preference,
565
+ authorizing_client: Parse::ACLScope.client_of(resolution),
500
566
  )
501
567
 
502
568
  unless resolution.master?
@@ -635,6 +701,7 @@ module Parse
635
701
  facet_results_raw = run_atlas_pipeline!(
636
702
  collection_name, facet_pipeline, options[:max_time_ms],
637
703
  read_preference: read_preference,
704
+ authorizing_client: Parse::ACLScope.client_of(resolution),
638
705
  )
639
706
 
640
707
  # Extract facet results
@@ -668,6 +735,11 @@ module Parse
668
735
  search_opts = options.merge(limit: limit, skip: skip_val)
669
736
  search_opts[:master] = true if acl[:master]
670
737
  search_opts[:read_preference] = read_preference if read_preference
738
+ # Re-thread the client for the same reason as the auth kwargs
739
+ # above: the outer faceted_search popped it in resolve_scope!, so
740
+ # this inner search would otherwise resolve and read as the
741
+ # default application.
742
+ search_opts[:client] = Parse::ACLScope.client_of(resolution) if Parse::ACLScope.client_of(resolution)
671
743
  search(collection_name, query, **search_opts).results
672
744
  else
673
745
  []
@@ -686,10 +758,10 @@ module Parse
686
758
  # `find` / pointerFields / protectedFields / highlight-field
687
759
  # checks.
688
760
  def search_pipeline!(collection_name, search_stage, resolution:,
689
- protected_fields:, pointer_fields:,
690
- highlight_field: nil, filter: nil, sort: nil,
691
- skip: 0, limit: 100, max_time_ms: nil,
692
- read_preference: nil, class_name: nil, raw: false)
761
+ protected_fields:, pointer_fields:,
762
+ highlight_field: nil, filter: nil, sort: nil,
763
+ skip: 0, limit: 100, max_time_ms: nil,
764
+ read_preference: nil, class_name: nil, raw: false)
693
765
  # Backstop the stage-safety check at the shared execution chokepoint
694
766
  # so ANY path that runs a $search stage (not just search_with_stage)
695
767
  # rejects a non-$search stage / returnStoredSource. The .search and
@@ -735,6 +807,7 @@ module Parse
735
807
  # enforcement chain inline below.
736
808
  raw_results = run_atlas_pipeline!(
737
809
  collection_name, pipeline, max_time_ms, read_preference: read_preference,
810
+ authorizing_client: Parse::ACLScope.client_of(resolution),
738
811
  )
739
812
 
740
813
  # Post-fetch enforcement: walk the result rows the same way
@@ -797,6 +870,10 @@ module Parse
797
870
  master = options.delete(:master)
798
871
  acl_user = options.delete(:acl_user)
799
872
  acl_role = options.delete(:acl_role)
873
+ # Consumed like the other auth kwargs so it never reaches the driver,
874
+ # and so `agent.acl_scope_kwargs` can carry the agent's own client
875
+ # into a `$search` the same way it carries the identity.
876
+ scope_client = options.delete(:client)
800
877
 
801
878
  # 4-way mutex. Mirrors Parse::ACLScope.resolve!'s
802
879
  # `provided.length > 1` check so an `acl_user:` + `acl_role:`
@@ -814,28 +891,57 @@ module Parse
814
891
  "session_token:, master: true, acl_user:, or acl_role:. Pick one."
815
892
  end
816
893
 
894
+ # Resolve against the caller's own client when one was supplied.
895
+ # Atlas Search used to go only through the `Session` shim, which can
896
+ # address nothing but `Parse.client`, so a `$search` issued by a
897
+ # secondary client resolved its token against the wrong application.
898
+ #
899
+ # With no explicit client the RESOLUTION still goes through
900
+ # `Session.resolve` against the default, preserving the historical
901
+ # path, but the Resolution records the default client rather than
902
+ # nil.
903
+ #
904
+ # Recording nil was wrong. This is a public entry point, and the rule
905
+ # is that a public entry point resolves omission to the default once
906
+ # while internal sinks treat nil as unidentified. Leaving it nil here
907
+ # pushed an omission all the way down, so after a second application
908
+ # had been observed every ordinary default-client `$search` was
909
+ # rejected as unidentified.
910
+ auth_client = scope_client || default_authorizing_client
911
+
817
912
  if session_token
818
- resolved = Session.resolve(session_token)
913
+ # Branch on the EXPLICIT client, not the resolved one. With none
914
+ # supplied this must stay on `Session.resolve` (the historical
915
+ # default-client path) while still recording the default on the
916
+ # Resolution below. Branching on `auth_client` routed the ordinary
917
+ # case through a different resolver.
918
+ resolved = if scope_client
919
+ scope_client.authorization.resolve(session_token)
920
+ else
921
+ Session.resolve(session_token)
922
+ end
819
923
  return Parse::ACLScope::Resolution.new(
820
- mode: :session,
821
- permission_strings: resolved.permission_strings,
822
- user_id: resolved.user_id,
823
- session: resolved,
824
- )
924
+ mode: :session,
925
+ permission_strings: resolved.permission_strings,
926
+ user_id: resolved.user_id,
927
+ session: resolved,
928
+ client: auth_client,
929
+ )
825
930
  end
826
931
 
827
932
  if acl_user
828
- return Parse::ACLScope.resolve_for_user(acl_user)
933
+ return Parse::ACLScope.resolve_for_user(acl_user, client: auth_client)
829
934
  end
830
935
 
831
936
  if acl_role
832
- return Parse::ACLScope.resolve_for_role(acl_role)
937
+ return Parse::ACLScope.resolve_for_role(acl_role, client: auth_client)
833
938
  end
834
939
 
835
940
  if master == true
836
941
  return Parse::ACLScope::Resolution.new(
837
- mode: :master, permission_strings: nil, user_id: nil, session: nil,
838
- )
942
+ mode: :master, permission_strings: nil, user_id: nil, session: nil,
943
+ client: auth_client,
944
+ )
839
945
  end
840
946
 
841
947
  if @require_session_token == true
@@ -854,9 +960,20 @@ module Parse
854
960
  permission_strings: anonymous.permission_strings,
855
961
  user_id: nil,
856
962
  session: anonymous,
963
+ client: auth_client,
857
964
  )
858
965
  end
859
966
 
967
+ # @!visibility private
968
+ # The default Parse client, or nil when none is configured. Never
969
+ # raises and never constructs one: `Parse.client` does both.
970
+ def default_authorizing_client
971
+ return nil unless Parse::Client.client?
972
+ Parse.client
973
+ rescue StandardError
974
+ nil
975
+ end
976
+
860
977
  # CLP `find` boundary check. Master-mode skips; for every other
861
978
  # scope, refuse the call when the resolved claim set can't
862
979
  # `find` on the collection. Mirrors what Parse::MongoDB.aggregate
@@ -940,10 +1057,13 @@ module Parse
940
1057
  # helper {Parse::MongoDB.aggregate} uses so the kwarg semantics
941
1058
  # are identical on both paths (invalid values warn and route to
942
1059
  # primary; nil = no override).
943
- def run_atlas_pipeline!(collection_name, pipeline, max_time_ms = nil, read_preference: nil)
1060
+ def run_atlas_pipeline!(collection_name, pipeline, max_time_ms = nil, read_preference: nil,
1061
+ authorizing_client: nil)
944
1062
  agg_opts = {}
945
1063
  agg_opts[:max_time_ms] = max_time_ms if max_time_ms
946
- coll = Parse::MongoDB.collection(collection_name)
1064
+ # Atlas Search does not go through Parse::MongoDB.aggregate, so this
1065
+ # is the only place its reads meet the binding guard.
1066
+ coll = Parse::MongoDB.collection(collection_name, authorizing_client: authorizing_client)
947
1067
  if (mode = Parse::MongoDB.send(:normalize_read_preference, read_preference))
948
1068
  coll = coll.with(read: { mode: mode })
949
1069
  end
@@ -1147,10 +1267,11 @@ module Parse
1147
1267
  @default_index = "default"
1148
1268
  @allow_raw = nil
1149
1269
  @require_session_token = false
1150
- @session_cache_ttl = 3600
1151
- @role_cache_ttl = 30
1152
- @session_cache = Session::MemoryCache.new
1153
- @role_cache = Session::MemoryCache.new
1270
+ # No authorization ivars here anymore. The identity and role caches, their
1271
+ # TTLs, and the upstream reader live on each client's
1272
+ # Parse::Authorization::Context and are initialized there, so this module
1273
+ # cannot hold a second, divergent copy. Initializing them at load time
1274
+ # would also require a configured client before one exists.
1154
1275
  @master_warned = false
1155
1276
  end
1156
1277
  end