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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +409 -0
- data/README.md +378 -6
- data/docs/caching.md +748 -0
- data/examples/basic_client.rb +3 -3
- data/examples/basic_server.rb +3 -3
- data/examples/live_query_listener.rb +12 -12
- data/examples/rag_chatbot.rb +14 -14
- data/examples/transaction_example.rb +44 -45
- data/examples/webhook_server.rb +3 -3
- data/lib/parse/access.rb +493 -0
- data/lib/parse/acl_scope.rb +125 -52
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/cancellation_token.rb +3 -3
- data/lib/parse/agent/constraint_translator.rb +4 -4
- data/lib/parse/agent/describe.rb +34 -34
- data/lib/parse/agent/errors.rb +9 -9
- data/lib/parse/agent/mcp_client.rb +61 -59
- data/lib/parse/agent/mcp_dispatcher.rb +89 -101
- data/lib/parse/agent/mcp_rack_app.rb +92 -93
- data/lib/parse/agent/mcp_server.rb +1 -1
- data/lib/parse/agent/mcp_subscriptions.rb +22 -22
- data/lib/parse/agent/metadata_audit.rb +1 -2
- data/lib/parse/agent/metadata_dsl.rb +7 -7
- data/lib/parse/agent/metadata_registry.rb +12 -12
- data/lib/parse/agent/prompt_hardening.rb +4 -4
- data/lib/parse/agent/prompts.rb +10 -10
- data/lib/parse/agent/result_formatter.rb +4 -4
- data/lib/parse/agent/tools.rb +493 -400
- data/lib/parse/agent.rb +157 -116
- data/lib/parse/api/hooks.rb +1 -1
- data/lib/parse/api/server.rb +2 -2
- data/lib/parse/api/users.rb +2 -2
- data/lib/parse/atlas_search/index_manager.rb +1 -1
- data/lib/parse/atlas_search/session.rb +40 -218
- data/lib/parse/atlas_search.rb +181 -60
- data/lib/parse/authorization.rb +466 -0
- data/lib/parse/cache/invalidation.rb +167 -0
- data/lib/parse/cache/keyspace.rb +306 -0
- data/lib/parse/cache/moneta_surface.rb +126 -0
- data/lib/parse/cache/pool.rb +48 -5
- data/lib/parse/cache/redis.rb +425 -10
- data/lib/parse/cache/scoped_view.rb +443 -0
- data/lib/parse/cache/sub_cache.rb +264 -0
- data/lib/parse/cache/upstream_roles.rb +230 -0
- data/lib/parse/client/authentication.rb +1 -1
- data/lib/parse/client/body_builder.rb +1 -2
- data/lib/parse/client/caching.rb +84 -5
- data/lib/parse/client.rb +327 -189
- data/lib/parse/clp_scope.rb +225 -28
- data/lib/parse/console.rb +3 -3
- data/lib/parse/embeddings/batch_embedder.rb +3 -3
- data/lib/parse/embeddings/cache.rb +17 -17
- data/lib/parse/embeddings/cohere.rb +21 -22
- data/lib/parse/embeddings/image_fetch.rb +22 -21
- data/lib/parse/embeddings/jina.rb +21 -21
- data/lib/parse/embeddings/local_http.rb +6 -7
- data/lib/parse/embeddings/media_file.rb +5 -4
- data/lib/parse/embeddings/openai.rb +5 -5
- data/lib/parse/embeddings/provider.rb +1 -1
- data/lib/parse/embeddings/qwen.rb +10 -10
- data/lib/parse/embeddings/spend_cap.rb +2 -3
- data/lib/parse/embeddings/streaming_body.rb +11 -11
- data/lib/parse/embeddings/video_source.rb +7 -6
- data/lib/parse/embeddings/voyage.rb +59 -61
- data/lib/parse/embeddings.rb +28 -27
- data/lib/parse/graphql/type_generator.rb +1 -1
- data/lib/parse/graphql.rb +5 -5
- data/lib/parse/live_query/client.rb +1 -1
- data/lib/parse/live_query.rb +1 -1
- data/lib/parse/lock.rb +15 -18
- data/lib/parse/lock_backend.rb +1 -1
- data/lib/parse/lookup_rewriter.rb +6 -6
- data/lib/parse/model/classes/role.rb +227 -28
- data/lib/parse/model/classes/user.rb +60 -1
- data/lib/parse/model/clp.rb +4 -4
- data/lib/parse/model/core/actions.rb +97 -15
- data/lib/parse/model/core/create_lock.rb +0 -2
- data/lib/parse/model/core/describe.rb +57 -57
- data/lib/parse/model/core/embed_managed.rb +21 -23
- data/lib/parse/model/core/fetching.rb +2 -0
- data/lib/parse/model/core/indexing.rb +14 -14
- data/lib/parse/model/core/parse_reference.rb +1 -1
- data/lib/parse/model/core/properties.rb +2 -2
- data/lib/parse/model/core/querying.rb +1 -1
- data/lib/parse/model/core/schema.rb +2 -2
- data/lib/parse/model/core/search_indexing.rb +2 -2
- data/lib/parse/model/core/vector_searchable.rb +4 -5
- data/lib/parse/model/file.rb +21 -15
- data/lib/parse/model/geojson.rb +2 -2
- data/lib/parse/model/geopoint.rb +1 -0
- data/lib/parse/model/object.rb +132 -66
- data/lib/parse/model/pointer.rb +2 -0
- data/lib/parse/model/polygon.rb +3 -6
- data/lib/parse/model/push.rb +2 -2
- data/lib/parse/model/vector.rb +3 -1
- data/lib/parse/mongodb.rb +512 -282
- data/lib/parse/pipeline_security.rb +3 -0
- data/lib/parse/query/constraints.rb +46 -46
- data/lib/parse/query.rb +146 -59
- data/lib/parse/retrieval/agent_tool.rb +34 -20
- data/lib/parse/retrieval/chunk.rb +1 -0
- data/lib/parse/retrieval/reranker/cohere.rb +11 -11
- data/lib/parse/retrieval/reranker.rb +3 -4
- data/lib/parse/retrieval/retriever.rb +4 -6
- data/lib/parse/schema/index_migrator.rb +26 -26
- data/lib/parse/schema/search_index_migrator.rb +19 -19
- data/lib/parse/stack/tasks.rb +6 -6
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +10 -10
- data/lib/parse/vector_search/hybrid.rb +66 -41
- data/lib/parse/vector_search.rb +14 -11
- data/lib/parse/webhooks/payload.rb +1 -0
- data/lib/parse/webhooks/registration.rb +4 -4
- data/lib/parse/webhooks/trigger_audit.rb +50 -49
- data/lib/parse/webhooks.rb +22 -4
- data/parse-stack-next.gemspec +6 -6
- metadata +10 -1
data/lib/parse/client.rb
CHANGED
|
@@ -32,6 +32,8 @@ require_relative "client/body_builder"
|
|
|
32
32
|
require_relative "client/authentication"
|
|
33
33
|
require_relative "client/caching"
|
|
34
34
|
require_relative "cache/redis"
|
|
35
|
+
require_relative "cache/invalidation"
|
|
36
|
+
require_relative "authorization"
|
|
35
37
|
require_relative "client/logging"
|
|
36
38
|
require_relative "client/profiling"
|
|
37
39
|
require_relative "api/all"
|
|
@@ -219,7 +221,22 @@ module Parse
|
|
|
219
221
|
# @return [Moneta::Transformer,Moneta::Expires] the cache instance
|
|
220
222
|
# @see Parse::Client#cache
|
|
221
223
|
def self.cache
|
|
222
|
-
|
|
224
|
+
# Deliberately NOT memoized. `@shared_cache ||=` pinned the first default
|
|
225
|
+
# client's store for the life of the process, so a later `Parse.setup`
|
|
226
|
+
# (a re-configuration, or a test suite swapping clients between cases)
|
|
227
|
+
# kept handing back the previous client's cache with no way to reset it.
|
|
228
|
+
Parse::Client.client(:default).cache
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# The SDK-owned store for the default client: the response cache, the
|
|
232
|
+
# identity and role planes, and what {Parse::Client#clear_cache!} clears.
|
|
233
|
+
#
|
|
234
|
+
# Prefer {.cache} for application keys. This exists so the SDK's own slice
|
|
235
|
+
# is reachable and inspectable, not as a place to put application data.
|
|
236
|
+
# @return [Object]
|
|
237
|
+
# @see Parse::Client#sdk_cache
|
|
238
|
+
def self.sdk_cache
|
|
239
|
+
Parse::Client.client(:default).sdk_cache
|
|
223
240
|
end
|
|
224
241
|
|
|
225
242
|
# This class is the core and low level API for the Parse SDK REST interface that
|
|
@@ -337,6 +354,38 @@ module Parse
|
|
|
337
354
|
attr_reader :session_token
|
|
338
355
|
alias_method :app_id, :application_id
|
|
339
356
|
|
|
357
|
+
# The store the SDK itself reads and writes: the response cache, the
|
|
358
|
+
# identity and role planes, and {#clear_cache!}.
|
|
359
|
+
#
|
|
360
|
+
# With `cache_keyspace: true` this is a {Parse::Cache::ScopedView} (or a
|
|
361
|
+
# {Parse::Cache::KeyspacedStore} for a store that cannot produce one)
|
|
362
|
+
# confined to this client's keyspace. Without it, this IS {#cache}, so
|
|
363
|
+
# nothing about an existing deployment changes.
|
|
364
|
+
#
|
|
365
|
+
# Kept separate from {#cache} because the two answer different questions.
|
|
366
|
+
# `cache` is the store the application configured and may still use for
|
|
367
|
+
# its own keys, under their original physical names. `sdk_cache` is the
|
|
368
|
+
# SDK's slice of it, and clearing that slice must not reach application
|
|
369
|
+
# data.
|
|
370
|
+
#
|
|
371
|
+
# @return [Object] the SDK-owned store.
|
|
372
|
+
def sdk_cache
|
|
373
|
+
@sdk_cache || self.cache
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# This client's authorization context: the identity and role caches, and
|
|
377
|
+
# the session-token resolver that feeds every mongo-direct ACL decision.
|
|
378
|
+
#
|
|
379
|
+
# One context per client, created lazily and never shared. That is the
|
|
380
|
+
# point of it living here rather than in module-level state: two clients
|
|
381
|
+
# pointed at two Parse applications must not resolve a token against each
|
|
382
|
+
# other's caches, nor validate it against each other's `/users/me`.
|
|
383
|
+
#
|
|
384
|
+
# @return [Parse::Authorization::Context]
|
|
385
|
+
def authorization
|
|
386
|
+
@authorization ||= Parse::Authorization::Context.new(client: self)
|
|
387
|
+
end
|
|
388
|
+
|
|
340
389
|
# Redacted inspection. The default Ruby `#inspect` would dump every ivar,
|
|
341
390
|
# exposing the master key and any bound session token in cleartext wherever
|
|
342
391
|
# a client is logged or surfaced in an error reporter. Show only the
|
|
@@ -411,6 +460,7 @@ module Parse
|
|
|
411
460
|
raise ArgumentError, "Parse::Client#with_session requires a client with a bound session_token" if @session_token.nil?
|
|
412
461
|
Parse.with_session(@session_token, &block)
|
|
413
462
|
end
|
|
463
|
+
|
|
414
464
|
# The client can support multiple sessions. The first session created, will be placed
|
|
415
465
|
# under the default session tag. The :default session will be the default client to be used
|
|
416
466
|
# by the other classes including Parse::Query and Parse::Objects
|
|
@@ -586,10 +636,10 @@ module Parse
|
|
|
586
636
|
# explicit-nil case, putting a "non-master" client back into master mode
|
|
587
637
|
# in any deployment that exports PARSE_SERVER_MASTER_KEY / PARSE_MASTER_KEY.
|
|
588
638
|
@master_key = if opts.key?(:master_key)
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
639
|
+
opts[:master_key]
|
|
640
|
+
else
|
|
641
|
+
ENV["PARSE_SERVER_MASTER_KEY"] || ENV["PARSE_MASTER_KEY"]
|
|
642
|
+
end
|
|
593
643
|
# Optional token bound to this client; applied per request as the
|
|
594
644
|
# lowest-priority auth fallback (see #request). Normalize blank/whitespace
|
|
595
645
|
# to nil so it never trips the "token present" branch at request time
|
|
@@ -754,13 +804,89 @@ module Parse
|
|
|
754
804
|
end
|
|
755
805
|
|
|
756
806
|
self.cache = opts[:cache]
|
|
757
|
-
|
|
807
|
+
|
|
808
|
+
# Build one keyspace from the effective namespace and install it on
|
|
809
|
+
# both the middleware and the store, so key generation and eviction
|
|
810
|
+
# can never disagree. Previously the middleware could hold a
|
|
811
|
+
# `cache_namespace:` the store knew nothing about, and
|
|
812
|
+
# `clear_cache!` scoped using only the store's own namespace, so a
|
|
813
|
+
# namespaced client cleared every SDK key on the database.
|
|
814
|
+
#
|
|
815
|
+
# Opt-in: without `cache_keyspace: true` the legacy key shape is
|
|
816
|
+
# kept, so an upgrade changes nothing until an operator asks for it.
|
|
817
|
+
#
|
|
818
|
+
# `scoped` derives an immutable, per-client view rather than
|
|
819
|
+
# mutating the store in place. This matters when one backend
|
|
820
|
+
# (e.g. a `Parse::Cache::Redis` connection pool) is shared across
|
|
821
|
+
# more than one `Parse::Client`: mutating a shared store's
|
|
822
|
+
# keyspace would rebind it out from under whichever client
|
|
823
|
+
# configured it first, so this client's own `sdk_cache` becomes
|
|
824
|
+
# the view while `cache` stays the store that was configured, and
|
|
825
|
+
# the shared backend itself is never touched.
|
|
826
|
+
cache_keyspace = nil
|
|
827
|
+
if opts[:cache_keyspace]
|
|
828
|
+
cache_keyspace = Parse::Cache::Keyspace.new(
|
|
829
|
+
app_id: @application_id,
|
|
830
|
+
server_url: @server_url,
|
|
831
|
+
namespace: opts[:cache_namespace],
|
|
832
|
+
)
|
|
833
|
+
# Derive an immutable per-client view rather than mutating the
|
|
834
|
+
# backend, so two clients can share a connection pool without
|
|
835
|
+
# sharing ownership of the keyspace. There is deliberately no
|
|
836
|
+
# `keyspace=` fallback here, since reintroducing a mutable
|
|
837
|
+
# binding at the call site would restore the cross-client
|
|
838
|
+
# rebinding this replaces.
|
|
839
|
+
#
|
|
840
|
+
# A store that cannot produce a view is wrapped rather than left
|
|
841
|
+
# alone. Leaving it bare still composed keys correctly, because
|
|
842
|
+
# the middleware below receives the keyspace directly, so the
|
|
843
|
+
# deployment looked keyspaced; but `clear_cache!` then called the
|
|
844
|
+
# store's own unrestricted `clear`, which on a plain
|
|
845
|
+
# `Moneta.new(:Redis)` is FLUSHDB. Opting into keyspacing and
|
|
846
|
+
# getting a database-wide flush is the exact inversion of the
|
|
847
|
+
# option: it deletes other applications' entries and any
|
|
848
|
+
# `parse-stack:foc:v1:*` create-locks sharing the database.
|
|
849
|
+
#
|
|
850
|
+
# The view is assigned to `sdk_cache`, NOT over `cache`. An
|
|
851
|
+
# earlier version replaced `cache` outright, which meant the
|
|
852
|
+
# store an application configured and passed in was no longer
|
|
853
|
+
# reachable through the accessor documented for reaching it,
|
|
854
|
+
# and application reads and writes silently moved inside the
|
|
855
|
+
# SDK's keyspace. A scoped view also cannot honestly stand in
|
|
856
|
+
# for a complete Moneta store: `clear`, `each_key`, and `close`
|
|
857
|
+
# either break scope isolation or quietly change meaning.
|
|
858
|
+
@sdk_cache = if self.cache.respond_to?(:scoped)
|
|
859
|
+
self.cache.scoped(cache_keyspace)
|
|
860
|
+
else
|
|
861
|
+
Parse::Cache::KeyspacedStore.new(store: self.cache, keyspace: cache_keyspace)
|
|
862
|
+
end
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
# Register the invalidation triggers once a keyspace exists, so
|
|
866
|
+
# role and identity staleness is bounded by webhook rather than by
|
|
867
|
+
# application discipline. Opt-in with the keyspace, since without
|
|
868
|
+
# planes there is nothing to invalidate.
|
|
869
|
+
if cache_keyspace && sdk_cache.respond_to?(:roles) &&
|
|
870
|
+
opts.fetch(:cache_invalidation_hooks, true)
|
|
871
|
+
begin
|
|
872
|
+
Parse::Cache::Invalidation.install!(sdk_cache)
|
|
873
|
+
rescue StandardError => e
|
|
874
|
+
warn "[Parse::Client] cache invalidation hooks not installed: #{e.class}"
|
|
875
|
+
end
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
conn.use Parse::Middleware::Caching, sdk_cache, {
|
|
758
879
|
expires: opts[:expires].to_i,
|
|
759
880
|
# Optional `cache_namespace:` prefixes every key so two Parse
|
|
760
881
|
# apps sharing one Redis don't collide on `mk:/classes/Song/abc`.
|
|
761
882
|
# Explicit only — we do NOT auto-derive from app_id to keep
|
|
762
883
|
# existing single-app deployments backward-compatible.
|
|
763
884
|
namespace: opts[:cache_namespace],
|
|
885
|
+
keyspace: cache_keyspace,
|
|
886
|
+
# During a rolling deploy, new workers write keyspaced keys while
|
|
887
|
+
# old workers still read the legacy shape, so invalidation has to
|
|
888
|
+
# hit both until every old worker is drained.
|
|
889
|
+
delete_legacy_variants: opts.fetch(:cache_delete_legacy_variants, true),
|
|
764
890
|
}
|
|
765
891
|
|
|
766
892
|
# Inform about opt-in cache behavior
|
|
@@ -843,6 +969,7 @@ module Parse
|
|
|
843
969
|
# env-proxy autodiscovery.
|
|
844
970
|
faraday_opts[:proxy] = nil unless @allow_faraday_proxy
|
|
845
971
|
end
|
|
972
|
+
|
|
846
973
|
private :validate_faraday_opts!
|
|
847
974
|
|
|
848
975
|
# Hosts considered "loopback" for the cleartext-ws:// guard in
|
|
@@ -927,10 +1054,10 @@ module Parse
|
|
|
927
1054
|
return if unknown.empty?
|
|
928
1055
|
|
|
929
1056
|
warn "[Parse::Client] Ignoring unknown live_query option(s): " \
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1057
|
+
"#{unknown.inspect}. Valid keys are Parse::LiveQuery::Configuration " \
|
|
1058
|
+
"setters (url, application_id, client_key, master_key, ping_interval, " \
|
|
1059
|
+
"pong_timeout, allow_insecure, ssl_min_version, ssl_max_version, " \
|
|
1060
|
+
"logging_enabled, log_level, ...). Check for typos."
|
|
934
1061
|
end
|
|
935
1062
|
|
|
936
1063
|
# If set, returns the current retry count for this instance. Otherwise,
|
|
@@ -946,9 +1073,20 @@ module Parse
|
|
|
946
1073
|
@conn.url_prefix
|
|
947
1074
|
end
|
|
948
1075
|
|
|
949
|
-
# Clear the
|
|
1076
|
+
# Clear the SDK's own cached entries.
|
|
1077
|
+
#
|
|
1078
|
+
# Operates on {#sdk_cache}, so with `cache_keyspace: true` this removes
|
|
1079
|
+
# only keys under this client's keyspace and leaves application keys in
|
|
1080
|
+
# the same store untouched. Without a keyspace `sdk_cache` IS `cache`,
|
|
1081
|
+
# and the call has its historical whole-store meaning.
|
|
1082
|
+
#
|
|
1083
|
+
# To clear the underlying store outright, call `client.cache.clear`
|
|
1084
|
+
# explicitly. On a Redis-backed store that is `FLUSHDB` and takes
|
|
1085
|
+
# everything on the database with it, including other applications and
|
|
1086
|
+
# any `parse-stack:foc:v1:*` create-locks.
|
|
950
1087
|
def clear_cache!
|
|
951
|
-
|
|
1088
|
+
target = sdk_cache
|
|
1089
|
+
target.clear if target.present?
|
|
952
1090
|
end
|
|
953
1091
|
|
|
954
1092
|
# No-credentials liveness probe. Hits the Parse Server health endpoint and
|
|
@@ -1049,10 +1187,10 @@ module Parse
|
|
|
1049
1187
|
# Pre-declare locals referenced inside rescue blocks so CodeQL's
|
|
1050
1188
|
# uninitialized-variable analysis is satisfied even if an exception
|
|
1051
1189
|
# raises before the natural assignment site.
|
|
1052
|
-
response
|
|
1190
|
+
response = nil
|
|
1053
1191
|
_retry_count = nil
|
|
1054
1192
|
_retry_delay = nil
|
|
1055
|
-
_request
|
|
1193
|
+
_request = nil
|
|
1056
1194
|
# Kwarg-absorption guard. The `**opts` splat in API helper methods
|
|
1057
1195
|
# (lib/parse/api/*.rb) absorbs a caller-passed `opts: { ... }`
|
|
1058
1196
|
# keyword as a key named `:opts` rather than as the request options
|
|
@@ -1092,193 +1230,193 @@ module Parse
|
|
|
1092
1230
|
_retry_max ||= _retry_count
|
|
1093
1231
|
|
|
1094
1232
|
begin
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
# http method
|
|
1110
|
-
method = method.downcase.to_sym
|
|
1111
|
-
# set the User-Agent
|
|
1112
|
-
headers[USER_AGENT_HEADER] = USER_AGENT_VERSION
|
|
1113
|
-
|
|
1114
|
-
if opts[:cache] == false
|
|
1115
|
-
headers[Parse::Middleware::Caching::CACHE_CONTROL] = "no-cache"
|
|
1116
|
-
elsif opts[:cache] == :write_only
|
|
1117
|
-
# Write-only mode: skip reading from cache, but still write to cache
|
|
1118
|
-
# Useful for fetch!/reload! which want fresh data but should update cache
|
|
1119
|
-
headers[Parse::Middleware::Caching::CACHE_WRITE_ONLY] = "true"
|
|
1120
|
-
elsif opts[:cache].is_a?(Numeric)
|
|
1121
|
-
# specify the cache duration of this request
|
|
1122
|
-
headers[Parse::Middleware::Caching::CACHE_EXPIRES_DURATION] = opts[:cache].to_s
|
|
1123
|
-
end
|
|
1233
|
+
headers ||= {}
|
|
1234
|
+
# if the first argument is a Parse::Request object, then construct it
|
|
1235
|
+
_request = nil
|
|
1236
|
+
if method.is_a?(Request)
|
|
1237
|
+
_request = method
|
|
1238
|
+
method = _request.method
|
|
1239
|
+
uri ||= _request.path
|
|
1240
|
+
query ||= _request.query
|
|
1241
|
+
body ||= _request.body
|
|
1242
|
+
headers.merge! _request.headers
|
|
1243
|
+
else
|
|
1244
|
+
_request = Parse::Request.new(method, uri, body: body, headers: headers, opts: opts)
|
|
1245
|
+
end
|
|
1124
1246
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1247
|
+
# http method
|
|
1248
|
+
method = method.downcase.to_sym
|
|
1249
|
+
# set the User-Agent
|
|
1250
|
+
headers[USER_AGENT_HEADER] = USER_AGENT_VERSION
|
|
1251
|
+
|
|
1252
|
+
if opts[:cache] == false
|
|
1253
|
+
headers[Parse::Middleware::Caching::CACHE_CONTROL] = "no-cache"
|
|
1254
|
+
elsif opts[:cache] == :write_only
|
|
1255
|
+
# Write-only mode: skip reading from cache, but still write to cache
|
|
1256
|
+
# Useful for fetch!/reload! which want fresh data but should update cache
|
|
1257
|
+
headers[Parse::Middleware::Caching::CACHE_WRITE_ONLY] = "true"
|
|
1258
|
+
elsif opts[:cache].is_a?(Numeric)
|
|
1259
|
+
# specify the cache duration of this request
|
|
1260
|
+
headers[Parse::Middleware::Caching::CACHE_EXPIRES_DURATION] = opts[:cache].to_s
|
|
1261
|
+
end
|
|
1138
1262
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
# When no explicit token was passed AND the caller didn't ask to send
|
|
1153
|
-
# the master key, fall through to (in order) the fiber-local ambient set
|
|
1154
|
-
# by `Parse.with_session`, then this client's own bound `@session_token`.
|
|
1155
|
-
# Explicit `use_master_key: true` is treated as a deliberate admin call
|
|
1156
|
-
# and skips both — otherwise an `admin.do_thing(use_master_key: true)`
|
|
1157
|
-
# nested inside a `with_session(user)` block (or on a token-bound client)
|
|
1158
|
-
# would silently downgrade. The ambient wins over the bound token so a
|
|
1159
|
-
# `with_session` override inside a user-scoped client still takes effect.
|
|
1160
|
-
if token.nil? && !explicit_blank_token && !(explicit_master && opts[:use_master_key] == true)
|
|
1161
|
-
ambient = Parse.current_session_token
|
|
1162
|
-
# A whitespace-only ambient must not count as present: otherwise it
|
|
1163
|
-
# blocks the bound-token fallback below and then fails the later
|
|
1164
|
-
# `token.present?` check, silently sending the master key instead.
|
|
1165
|
-
token = ambient if ambient.is_a?(String) && !ambient.strip.empty?
|
|
1166
|
-
token = @session_token if (token.nil? || token.to_s.strip.empty?) && @session_token
|
|
1167
|
-
end
|
|
1168
|
-
if explicit_blank_token
|
|
1169
|
-
# Fail closed: never send the master key for an unusable explicit token.
|
|
1170
|
-
headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
|
|
1171
|
-
elsif token.present?
|
|
1172
|
-
token = token.session_token if token.respond_to?(:session_token)
|
|
1173
|
-
headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
|
|
1174
|
-
headers[Parse::Protocol::SESSION_TOKEN] = token
|
|
1175
|
-
end
|
|
1263
|
+
# Resolve the auth context in three layers:
|
|
1264
|
+
# 1. explicit per-call `use_master_key:` and `session_token:`
|
|
1265
|
+
# 2. ambient session set by `Parse.with_session { ... }` (fiber-local)
|
|
1266
|
+
# 3. process-wide `Parse.client_mode` flag — when true, master key is
|
|
1267
|
+
# never sent unless the caller explicitly passed `use_master_key: true`
|
|
1268
|
+
explicit_master = opts.key?(:use_master_key)
|
|
1269
|
+
|
|
1270
|
+
if opts[:use_master_key] == false
|
|
1271
|
+
headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
|
|
1272
|
+
elsif Parse.client_mode && opts[:use_master_key] != true
|
|
1273
|
+
# client mode defaults master key OFF unless explicitly opted in
|
|
1274
|
+
headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
|
|
1275
|
+
end
|
|
1176
1276
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1277
|
+
raw_token = opts[:session_token]
|
|
1278
|
+
# SEC-02: an EXPLICITLY-supplied session_token that is a blank /
|
|
1279
|
+
# whitespace-only string is an unusable credential — NOT an invitation
|
|
1280
|
+
# to fall back to the master key. Treat it as "no credential"
|
|
1281
|
+
# (anonymous) and fail closed: suppress the master key and send no
|
|
1282
|
+
# session header, so Parse Server applies public ACL/CLP instead of
|
|
1283
|
+
# silently executing with master authority. The caller passed a token
|
|
1284
|
+
# explicitly, so we also do NOT fall through to the ambient / bound
|
|
1285
|
+
# token — that was their stated (empty) scope. `session_token: nil`
|
|
1286
|
+
# (value literally nil) is unchanged: it means "not set", and still
|
|
1287
|
+
# resolves via the ambient / bound fallback below.
|
|
1288
|
+
explicit_blank_token = raw_token.is_a?(String) && raw_token.strip.empty?
|
|
1289
|
+
token = explicit_blank_token ? nil : raw_token
|
|
1290
|
+
# When no explicit token was passed AND the caller didn't ask to send
|
|
1291
|
+
# the master key, fall through to (in order) the fiber-local ambient set
|
|
1292
|
+
# by `Parse.with_session`, then this client's own bound `@session_token`.
|
|
1293
|
+
# Explicit `use_master_key: true` is treated as a deliberate admin call
|
|
1294
|
+
# and skips both — otherwise an `admin.do_thing(use_master_key: true)`
|
|
1295
|
+
# nested inside a `with_session(user)` block (or on a token-bound client)
|
|
1296
|
+
# would silently downgrade. The ambient wins over the bound token so a
|
|
1297
|
+
# `with_session` override inside a user-scoped client still takes effect.
|
|
1298
|
+
if token.nil? && !explicit_blank_token && !(explicit_master && opts[:use_master_key] == true)
|
|
1299
|
+
ambient = Parse.current_session_token
|
|
1300
|
+
# A whitespace-only ambient must not count as present: otherwise it
|
|
1301
|
+
# blocks the bound-token fallback below and then fails the later
|
|
1302
|
+
# `token.present?` check, silently sending the master key instead.
|
|
1303
|
+
token = ambient if ambient.is_a?(String) && !ambient.strip.empty?
|
|
1304
|
+
token = @session_token if (token.nil? || token.to_s.strip.empty?) && @session_token
|
|
1193
1305
|
end
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
Parse::
|
|
1197
|
-
|
|
1306
|
+
if explicit_blank_token
|
|
1307
|
+
# Fail closed: never send the master key for an unusable explicit token.
|
|
1308
|
+
headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
|
|
1309
|
+
elsif token.present?
|
|
1310
|
+
token = token.session_token if token.respond_to?(:session_token)
|
|
1311
|
+
headers[Parse::Middleware::Authentication::DISABLE_MASTER_KEY] = "true"
|
|
1312
|
+
headers[Parse::Protocol::SESSION_TOKEN] = token
|
|
1198
1313
|
end
|
|
1199
|
-
when 405, 406
|
|
1200
|
-
Parse::Client._safe_warn("ProtocolError", response)
|
|
1201
|
-
raise Parse::Error::ProtocolError, response
|
|
1202
|
-
when 429 # Request over the throttle limit
|
|
1203
|
-
Parse::Client._safe_warn("RequestLimitExceededError", response)
|
|
1204
|
-
raise Parse::Error::RequestLimitExceededError, response
|
|
1205
|
-
when 500, 503
|
|
1206
|
-
Parse::Client._safe_warn("ServiceUnavailableError", response)
|
|
1207
|
-
raise Parse::Error::ServiceUnavailableError, response
|
|
1208
|
-
end
|
|
1209
1314
|
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1315
|
+
#if it is a :get request, then use query params, otherwise body.
|
|
1316
|
+
params = (method == :get ? query : body) || {}
|
|
1317
|
+
# if the path does not start with the '/1/' prefix, then add it to be nice.
|
|
1318
|
+
# actually send the request and return the body
|
|
1319
|
+
response_env = @conn.send(method, uri, params, headers)
|
|
1320
|
+
response = response_env.body
|
|
1321
|
+
response.request = _request
|
|
1322
|
+
|
|
1323
|
+
case response.http_status
|
|
1324
|
+
when 401, 403
|
|
1325
|
+
Parse::Client._safe_warn("AuthenticationError", response)
|
|
1326
|
+
raise Parse::Error::AuthenticationError, response
|
|
1327
|
+
when 400, 408
|
|
1328
|
+
if response.code == Parse::Response::ERROR_TIMEOUT || response.code == 143 #"net/http: timeout awaiting response headers"
|
|
1329
|
+
Parse::Client._safe_warn("TimeoutError", response)
|
|
1330
|
+
raise Parse::Error::TimeoutError, response
|
|
1331
|
+
end
|
|
1332
|
+
when 404
|
|
1333
|
+
unless response.object_not_found?
|
|
1334
|
+
Parse::Client._safe_warn("ConnectionError", response)
|
|
1335
|
+
raise Parse::Error::ConnectionError, response
|
|
1336
|
+
end
|
|
1337
|
+
when 405, 406
|
|
1338
|
+
Parse::Client._safe_warn("ProtocolError", response)
|
|
1339
|
+
raise Parse::Error::ProtocolError, response
|
|
1340
|
+
when 429 # Request over the throttle limit
|
|
1218
1341
|
Parse::Client._safe_warn("RequestLimitExceededError", response)
|
|
1219
1342
|
raise Parse::Error::RequestLimitExceededError, response
|
|
1220
|
-
|
|
1221
|
-
Parse::Client._safe_warn("
|
|
1222
|
-
raise Parse::Error::
|
|
1223
|
-
elsif response.code == Parse::Response::ERROR_DUPLICATE_REQUEST # 159
|
|
1224
|
-
# Request-id idempotency rejected a duplicate — the original write
|
|
1225
|
-
# already applied (NOT a second time). Surface a typed, catchable
|
|
1226
|
-
# signal rather than a generic error; this is what a transparently-
|
|
1227
|
-
# retried write that landed-but-lost-its-response sees on the replay.
|
|
1228
|
-
Parse::Client._safe_warn("DuplicateRequestError", response)
|
|
1229
|
-
raise Parse::Error::DuplicateRequestError, response
|
|
1343
|
+
when 500, 503
|
|
1344
|
+
Parse::Client._safe_warn("ServiceUnavailableError", response)
|
|
1345
|
+
raise Parse::Error::ServiceUnavailableError, response
|
|
1230
1346
|
end
|
|
1231
|
-
end
|
|
1232
1347
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1348
|
+
if response.error?
|
|
1349
|
+
if response.code <= Parse::Response::ERROR_SERVICE_UNAVAILABLE
|
|
1350
|
+
Parse::Client._safe_warn("ServiceUnavailableError", response)
|
|
1351
|
+
raise Parse::Error::ServiceUnavailableError, response
|
|
1352
|
+
elsif response.code <= 100
|
|
1353
|
+
Parse::Client._safe_warn("ServerError", response)
|
|
1354
|
+
raise Parse::Error::ServerError, response
|
|
1355
|
+
elsif response.code == Parse::Response::ERROR_EXCEEDED_BURST_LIMIT
|
|
1356
|
+
Parse::Client._safe_warn("RequestLimitExceededError", response)
|
|
1357
|
+
raise Parse::Error::RequestLimitExceededError, response
|
|
1358
|
+
elsif response.code == 209 # Error 209: invalid session token
|
|
1359
|
+
Parse::Client._safe_warn("InvalidSessionTokenError", response)
|
|
1360
|
+
raise Parse::Error::InvalidSessionTokenError, response
|
|
1361
|
+
elsif response.code == Parse::Response::ERROR_DUPLICATE_REQUEST # 159
|
|
1362
|
+
# Request-id idempotency rejected a duplicate — the original write
|
|
1363
|
+
# already applied (NOT a second time). Surface a typed, catchable
|
|
1364
|
+
# signal rather than a generic error; this is what a transparently-
|
|
1365
|
+
# retried write that landed-but-lost-its-response sees on the replay.
|
|
1366
|
+
Parse::Client._safe_warn("DuplicateRequestError", response)
|
|
1367
|
+
raise Parse::Error::DuplicateRequestError, response
|
|
1368
|
+
end
|
|
1369
|
+
end
|
|
1370
|
+
|
|
1371
|
+
response
|
|
1372
|
+
rescue Parse::Error::RequestLimitExceededError, Parse::Error::ServiceUnavailableError => e
|
|
1373
|
+
# 429 (RequestLimitExceeded): the server threw the request away, so
|
|
1374
|
+
# re-sending is safe for any method. 500/503 (ServiceUnavailable) is
|
|
1375
|
+
# ambiguous — a write may have applied before the error — so only
|
|
1376
|
+
# re-send when the request is idempotent (see #idempotent_retry?).
|
|
1377
|
+
retryable = e.is_a?(Parse::Error::RequestLimitExceededError) || idempotent_retry?(method, body, headers)
|
|
1378
|
+
if _retry_count > 0 && retryable
|
|
1379
|
+
warn "[Parse:Retry] Retries remaining #{_retry_count} : #{response.request}"
|
|
1380
|
+
_retry_count -= 1
|
|
1381
|
+
# Use Retry-After header if available, otherwise use linear backoff
|
|
1382
|
+
retry_after = response.retry_after if response.respond_to?(:retry_after)
|
|
1383
|
+
if retry_after && retry_after > 0
|
|
1384
|
+
_retry_delay = retry_after
|
|
1385
|
+
warn "[Parse:Retry] Using Retry-After header: #{_retry_delay}s"
|
|
1386
|
+
else
|
|
1387
|
+
# Linear backoff (RETRY_DELAY × attempt number) with +/-25% jitter.
|
|
1388
|
+
# Never zero —
|
|
1389
|
+
# zero-wait retries amplify DoS against upstream and stampede on 429.
|
|
1390
|
+
backoff_delay = RETRY_DELAY * (_retry_max - _retry_count)
|
|
1391
|
+
_retry_delay = backoff_delay * (0.75 + rand * 0.5)
|
|
1392
|
+
end
|
|
1393
|
+
sleep _retry_delay if _retry_delay > 0
|
|
1394
|
+
retry
|
|
1395
|
+
end
|
|
1396
|
+
raise
|
|
1397
|
+
rescue Faraday::ClientError, Faraday::TimeoutError, Net::OpenTimeout => e
|
|
1398
|
+
# Request timed out mid-flight: the outcome is unknown (the server may
|
|
1399
|
+
# have received and applied the write but never answered), so only
|
|
1400
|
+
# re-send idempotent requests to avoid double-applying.
|
|
1401
|
+
#
|
|
1402
|
+
# Faraday 2.x raises `Faraday::TimeoutError` for a read timeout
|
|
1403
|
+
# (`Timeout::Error` / `Errno::ETIMEDOUT`); it subclasses `Faraday::Error`,
|
|
1404
|
+
# not `ClientError`, so it must be listed explicitly to be caught. We
|
|
1405
|
+
# deliberately do NOT catch `Faraday::ConnectionFailed` (connection
|
|
1406
|
+
# refused/reset, plus the wrapped connect-timeout): refused is a
|
|
1407
|
+
# non-transient "server down / misconfigured" failure, and auto-retrying
|
|
1408
|
+
# it only adds backoff latency before the inevitable error. Broadening to
|
|
1409
|
+
# reset connections safely (retry reset, fail fast on refused) is tracked
|
|
1410
|
+
# as a follow-up.
|
|
1411
|
+
if _retry_count > 0 && idempotent_retry?(method, body, headers)
|
|
1412
|
+
warn "[Parse:Retry] Retries remaining #{_retry_count} : #{_request}"
|
|
1413
|
+
_retry_count -= 1
|
|
1252
1414
|
backoff_delay = RETRY_DELAY * (_retry_max - _retry_count)
|
|
1253
1415
|
_retry_delay = backoff_delay * (0.75 + rand * 0.5)
|
|
1416
|
+
sleep _retry_delay if _retry_delay > 0
|
|
1417
|
+
retry
|
|
1254
1418
|
end
|
|
1255
|
-
|
|
1256
|
-
retry
|
|
1257
|
-
end
|
|
1258
|
-
raise
|
|
1259
|
-
rescue Faraday::ClientError, Faraday::TimeoutError, Net::OpenTimeout => e
|
|
1260
|
-
# Request timed out mid-flight: the outcome is unknown (the server may
|
|
1261
|
-
# have received and applied the write but never answered), so only
|
|
1262
|
-
# re-send idempotent requests to avoid double-applying.
|
|
1263
|
-
#
|
|
1264
|
-
# Faraday 2.x raises `Faraday::TimeoutError` for a read timeout
|
|
1265
|
-
# (`Timeout::Error` / `Errno::ETIMEDOUT`); it subclasses `Faraday::Error`,
|
|
1266
|
-
# not `ClientError`, so it must be listed explicitly to be caught. We
|
|
1267
|
-
# deliberately do NOT catch `Faraday::ConnectionFailed` (connection
|
|
1268
|
-
# refused/reset, plus the wrapped connect-timeout): refused is a
|
|
1269
|
-
# non-transient "server down / misconfigured" failure, and auto-retrying
|
|
1270
|
-
# it only adds backoff latency before the inevitable error. Broadening to
|
|
1271
|
-
# reset connections safely (retry reset, fail fast on refused) is tracked
|
|
1272
|
-
# as a follow-up.
|
|
1273
|
-
if _retry_count > 0 && idempotent_retry?(method, body, headers)
|
|
1274
|
-
warn "[Parse:Retry] Retries remaining #{_retry_count} : #{_request}"
|
|
1275
|
-
_retry_count -= 1
|
|
1276
|
-
backoff_delay = RETRY_DELAY * (_retry_max - _retry_count)
|
|
1277
|
-
_retry_delay = backoff_delay * (0.75 + rand * 0.5)
|
|
1278
|
-
sleep _retry_delay if _retry_delay > 0
|
|
1279
|
-
retry
|
|
1280
|
-
end
|
|
1281
|
-
raise Parse::Error::ConnectionError, "#{_request} : #{e.class} - #{e.message}"
|
|
1419
|
+
raise Parse::Error::ConnectionError, "#{_request} : #{e.class} - #{e.message}"
|
|
1282
1420
|
end
|
|
1283
1421
|
end
|
|
1284
1422
|
|
|
@@ -1488,7 +1626,7 @@ module Parse
|
|
|
1488
1626
|
when Hash
|
|
1489
1627
|
type = value["__type"] || value[:__type]
|
|
1490
1628
|
class_name = value["className"] || value[:className]
|
|
1491
|
-
object_id
|
|
1629
|
+
object_id = value["objectId"] || value[:objectId]
|
|
1492
1630
|
if type == Parse::Model::TYPE_POINTER && class_name && object_id
|
|
1493
1631
|
# Pointers carry no attributes, so building one is lossless even for
|
|
1494
1632
|
# an unregistered class (yields a Parse::Pointer).
|