parse-stack-next 5.5.4 → 5.5.5

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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -6
  3. data/README.md +26 -13
  4. data/bin/parse-console +9 -1
  5. data/docs/TEST_SERVER.md +115 -238
  6. data/docs/mcp_guide.md +1 -1
  7. data/docs/mongodb_index_optimization_guide.md +3 -2
  8. data/docs/usage_guide.md +1 -1
  9. data/docs/yard-template/default/fulldoc/html/css/common.css +52 -9
  10. data/docs/yard-template/default/fulldoc/html/css/full_list.css +40 -13
  11. data/lib/parse/agent/constraint_translator.rb +18 -18
  12. data/lib/parse/agent/errors.rb +29 -7
  13. data/lib/parse/agent/metadata_dsl.rb +6 -6
  14. data/lib/parse/agent/tools.rb +250 -59
  15. data/lib/parse/agent.rb +42 -30
  16. data/lib/parse/api/aggregate.rb +3 -3
  17. data/lib/parse/api/cloud_functions.rb +19 -10
  18. data/lib/parse/api/objects.rb +8 -8
  19. data/lib/parse/api/users.rb +9 -9
  20. data/lib/parse/atlas_search/session.rb +34 -34
  21. data/lib/parse/atlas_search.rb +243 -110
  22. data/lib/parse/client/body_builder.rb +10 -10
  23. data/lib/parse/client/logging.rb +5 -2
  24. data/lib/parse/client/profiling.rb +5 -2
  25. data/lib/parse/client/protocol.rb +1 -1
  26. data/lib/parse/client/url_redaction.rb +94 -0
  27. data/lib/parse/client.rb +43 -28
  28. data/lib/parse/embeddings/image_fetch.rb +6 -1
  29. data/lib/parse/embeddings/voyage.rb +16 -17
  30. data/lib/parse/live_query/client.rb +7 -7
  31. data/lib/parse/live_query/subscription.rb +1 -1
  32. data/lib/parse/lock.rb +1 -1
  33. data/lib/parse/lock_backend.rb +118 -2
  34. data/lib/parse/model/acl.rb +24 -24
  35. data/lib/parse/model/classes/job_schedule.rb +8 -8
  36. data/lib/parse/model/classes/job_status.rb +9 -9
  37. data/lib/parse/model/classes/role.rb +49 -49
  38. data/lib/parse/model/classes/session.rb +2 -2
  39. data/lib/parse/model/classes/user.rb +66 -66
  40. data/lib/parse/model/core/builder.rb +7 -7
  41. data/lib/parse/model/core/create_lock.rb +1 -1
  42. data/lib/parse/model/core/properties.rb +4 -4
  43. data/lib/parse/model/file.rb +57 -16
  44. data/lib/parse/model/model.rb +19 -19
  45. data/lib/parse/model/object.rb +38 -38
  46. data/lib/parse/model/pointer.rb +4 -4
  47. data/lib/parse/model/push.rb +5 -5
  48. data/lib/parse/mongodb.rb +84 -26
  49. data/lib/parse/pipeline_security.rb +2 -2
  50. data/lib/parse/query/constraints.rb +38 -38
  51. data/lib/parse/query.rb +151 -75
  52. data/lib/parse/retrieval/reranker/cohere.rb +30 -0
  53. data/lib/parse/schema.rb +1 -1
  54. data/lib/parse/stack/version.rb +1 -1
  55. data/lib/parse/stack.rb +23 -10
  56. data/lib/parse/two_factor_auth/user_extension.rb +25 -25
  57. data/lib/parse/webhooks/payload.rb +35 -35
  58. data/lib/parse/webhooks/registration.rb +2 -2
  59. data/lib/parse/webhooks/replay_protection.rb +16 -16
  60. data/lib/parse/webhooks.rb +11 -11
  61. data/parse-stack-next.gemspec +19 -1
  62. metadata +2 -38
  63. data/.bundle/config +0 -5
  64. data/.env.sample +0 -138
  65. data/.env.test +0 -10
  66. data/.github/ISSUE_TEMPLATE/bug_report.yml +0 -105
  67. data/.github/ISSUE_TEMPLATE/feature_request.yml +0 -67
  68. data/.github/dependabot.yml +0 -13
  69. data/.github/workflows/codeql.yml +0 -44
  70. data/.github/workflows/docs.yml +0 -39
  71. data/.github/workflows/release.yml +0 -43
  72. data/.github/workflows/ruby.yml +0 -38
  73. data/.gitignore +0 -56
  74. data/.ruby-version +0 -1
  75. data/.solargraph.yml +0 -22
  76. data/.vscode/settings.json +0 -3
  77. data/.yardopts +0 -19
  78. data/Gemfile +0 -43
  79. data/Gemfile.lock +0 -198
  80. data/Makefile +0 -63
  81. data/Rakefile +0 -825
  82. data/config/parse-config.json +0 -12
  83. data/scripts/debug-ips.js +0 -35
  84. data/scripts/docker/Dockerfile.parse +0 -17
  85. data/scripts/docker/atlas-init.js +0 -284
  86. data/scripts/docker/docker-compose.atlas.yml +0 -80
  87. data/scripts/docker/docker-compose.test.yml +0 -159
  88. data/scripts/docker/docker-compose.verifyemail.yml +0 -4
  89. data/scripts/docker/mongo-init.js +0 -21
  90. data/scripts/docker/preflight.sh +0 -76
  91. data/scripts/eval_mcp_with_lm_studio.rb +0 -274
  92. data/scripts/start-parse.sh +0 -154
  93. data/scripts/start_mcp_server.rb +0 -78
  94. data/scripts/test_server_connection.rb +0 -82
  95. data/scripts/vector_prototype/create_vector_index.js +0 -105
  96. data/scripts/vector_prototype/fetch_embeddings.py +0 -241
  97. data/scripts/vector_prototype/fixture_manifest.json +0 -9
  98. data/scripts/vector_prototype/query_prototype.rb +0 -84
  99. data/scripts/vector_prototype/run.sh +0 -34
@@ -31,13 +31,13 @@ module Parse
31
31
 
32
32
  # Error code 205 (Parse::Response::ERROR_EMAIL_NOT_FOUND) raised by
33
33
  # {Parse::User.login!} and {Parse::User#verify_password} when Parse Server
34
- # returns code 205 because +preventLoginWithUnverifiedEmail+ is enabled and
34
+ # returns code 205 because `preventLoginWithUnverifiedEmail` is enabled and
35
35
  # the account's email address has not been verified.
36
36
  #
37
37
  # It is a SUBCLASS of {AuthenticationError} on purpose: before this typed
38
38
  # error existed, the unverified-email rejection raised a plain
39
- # +AuthenticationError+, so existing callers wrapping {Parse::User.login!}
40
- # in +rescue AuthenticationError+ must keep catching it (subclassing keeps
39
+ # `AuthenticationError`, so existing callers wrapping {Parse::User.login!}
40
+ # in `rescue AuthenticationError` must keep catching it (subclassing keeps
41
41
  # that contract — making it a sibling would be a silent breaking change).
42
42
  # Callers who want to special-case the unverified-email path just rescue
43
43
  # this narrower subclass FIRST.
@@ -62,12 +62,12 @@ module Parse
62
62
  # <tt>rescue Parse::Error::AuthenticationError</tt> handler covers both
63
63
  # wrong-credential failures and lockout situations. Callers that need to
64
64
  # distinguish the lockout case just rescue this narrower subclass first.
65
- # Because the previous implementation raised a plain +RuntimeError+, there
66
- # is no prior +AuthenticationError+ rescue contract to preserve — this is
65
+ # Because the previous implementation raised a plain `RuntimeError`, there
66
+ # is no prior `AuthenticationError` rescue contract to preserve — this is
67
67
  # a new typed entry in the login-failure taxonomy.
68
68
  #
69
- # Note that +Parse::Error < StandardError+, so a bare +rescue+ or
70
- # +rescue StandardError+ still catches this error.
69
+ # Note that `Parse::Error < StandardError`, so a bare `rescue` or
70
+ # `rescue StandardError` still catches this error.
71
71
  #
72
72
  # @example
73
73
  # begin
@@ -266,7 +266,7 @@ module Parse
266
266
  # when the user follows the verification link delivered by the email
267
267
  # adapter, and applied to the in-memory object by {#signup!} / signup-on-save
268
268
  # when the server includes it in the signup response (see
269
- # +SIGNUP_RESPONSE_APPLY_KEYS+).
269
+ # `SIGNUP_RESPONSE_APPLY_KEYS`).
270
270
  # @return [Boolean]
271
271
  property :email_verified, :boolean
272
272
 
@@ -322,16 +322,16 @@ module Parse
322
322
  # Thread-local key used by {.with_authdata_trust} to mark the
323
323
  # current hydration as a legitimate self-fetch (login/signup/MFA/
324
324
  # `/users/me`). Outside that scope, {#apply_attributes!} strips
325
- # +authData+ from incoming server JSON so a `_User` query/find that
325
+ # `authData` from incoming server JSON so a `_User` query/find that
326
326
  # crosses ACL boundaries cannot leak another user's federated-identity
327
327
  # tokens into the in-memory object.
328
328
  AUTHDATA_TRUST_KEY = :__parse_stack_user_authdata_trusted
329
329
 
330
330
  class << self
331
331
  # @!visibility private
332
- # Run +block+ in a scope where the next {Parse::User#apply_attributes!}
333
- # call is permitted to hydrate +authData+ from the response. Used by
334
- # +login+/+login!+/+session!+/+create+/+link_auth_data!+/MFA paths
332
+ # Run `block` in a scope where the next {Parse::User#apply_attributes!}
333
+ # call is permitted to hydrate `authData` from the response. Used by
334
+ # `login`/`login!`/`session!`/`create`/`link_auth_data!`/MFA paths
335
335
  # where the row being hydrated is provably the authenticating user.
336
336
  def with_authdata_trust
337
337
  prior = Thread.current[AUTHDATA_TRUST_KEY]
@@ -538,20 +538,20 @@ module Parse
538
538
  end
539
539
 
540
540
  # @!visibility private
541
- # Defense-in-depth strip of +authData+ on the way into the in-memory
542
- # User. Parse Server returns +authData+ on +GET /users/:id+ to any
543
- # caller with ACL read on the row, and the default +_User+ ACL is
541
+ # Defense-in-depth strip of `authData` on the way into the in-memory
542
+ # User. Parse Server returns `authData` on `GET /users/:id` to any
543
+ # caller with ACL read on the row, and the default `_User` ACL is
544
544
  # permissive in many deployments — without this filter, fetching a
545
- # different user (or iterating a +Parse::Query.new(User)+ result set)
546
- # would expose their OAuth +access_token+ / +id_token+ to anyone who
545
+ # different user (or iterating a `Parse::Query.new(User)` result set)
546
+ # would expose their OAuth `access_token` / `id_token` to anyone who
547
547
  # JSON-renders the result (Rails views, agent tool output, logging).
548
548
  #
549
549
  # The strip runs unconditionally unless the caller is inside a
550
550
  # {.with_authdata_trust} scope, which is set by the self-fetch
551
551
  # paths in this file (login/login!/session!/create/link_auth_data!/
552
552
  # unlink_auth_data!) and by the MFA login extension. Trusted callers
553
- # pass through to +super+ with the hash untouched. The PROTECTED
554
- # mass-assignment filter that runs inside +super+ is unaffected.
553
+ # pass through to `super` with the hash untouched. The PROTECTED
554
+ # mass-assignment filter that runs inside `super` is unaffected.
555
555
  def apply_attributes!(hash, dirty_track: false, filter_protected: nil, protected_set: nil)
556
556
  if hash.is_a?(Hash) && !self.class.authdata_trusted?
557
557
  if hash.key?(:authData) || hash.key?("authData") ||
@@ -576,12 +576,12 @@ module Parse
576
576
  end
577
577
 
578
578
  # @!visibility private
579
- # Reduce a server-returned +authData+ hash to a leak-safe MFA status.
580
- # Parse Server returns +authData.mfa+ as +{ "secret" => ..., "recovery" =>
579
+ # Reduce a server-returned `authData` hash to a leak-safe MFA status.
580
+ # Parse Server returns `authData.mfa` as +{ "secret" => ..., "recovery" =>
581
581
  # [...] }+ (the raw TOTP secret and one-time recovery codes) even on a
582
582
  # user's own session-token read, so the value itself must never be retained.
583
- # This keeps only +{ "mfa" => { "status" => "enabled" } }+ when MFA is
584
- # configured, and returns +nil+ otherwise (preserving the prior
583
+ # This keeps only `{ "mfa" => { "status" => "enabled" } }` when MFA is
584
+ # configured, and returns `nil` otherwise (preserving the prior
585
585
  # strip-to-nil behavior for OAuth-only / non-MFA authData).
586
586
  # @return [Hash, nil]
587
587
  def sanitized_mfa_authdata(raw)
@@ -607,7 +607,7 @@ module Parse
607
607
  end
608
608
 
609
609
  # @return [Boolean] true if this user is anonymous (i.e. created
610
- # via the +authData.anonymous+ provider rather than via signup
610
+ # via the `authData.anonymous` provider rather than via signup
611
611
  # with a username/password or a real OAuth provider).
612
612
  def anonymous?
613
613
  !anonymous_id.nil?
@@ -640,15 +640,15 @@ module Parse
640
640
  self.class.with_authdata_trust { apply_attributes!(response.result) }
641
641
  end
642
642
 
643
- # Upgrade an anonymous user (one created via the +authData.anonymous+
643
+ # Upgrade an anonymous user (one created via the `authData.anonymous`
644
644
  # provider) into a full username/password account. This is the
645
645
  # SDK-side counterpart of the Parse JS SDK's
646
- # +_linkWith('username', ...)+ flow — it sends a single
647
- # +PUT /users/:id+ with the new credentials and an explicit
648
- # +authData: { anonymous: nil }+ unlink in the same body, then
646
+ # `_linkWith('username', ...)` flow — it sends a single
647
+ # `PUT /users/:id` with the new credentials and an explicit
648
+ # `authData: { anonymous: nil }` unlink in the same body, then
649
649
  # narrowly applies the server's response to the in-memory user.
650
650
  #
651
- # The +authData.anonymous+ unlink is essential: leaving the anonymous
651
+ # The `authData.anonymous` unlink is essential: leaving the anonymous
652
652
  # provider attached after assigning a username would let anyone else
653
653
  # who somehow learned the (random) anonymous id silently log in as
654
654
  # the freshly-named account, a documented Parse foot-gun.
@@ -658,9 +658,9 @@ module Parse
658
658
  # @param email [String, nil] optional email address. Must be unique
659
659
  # if provided.
660
660
  # @raise [Parse::Error::AuthenticationError] when this instance has
661
- # no attached +@session_token+, no objectId, or is not anonymous.
662
- # @raise [Parse::Error::UsernameMissingError] when +username+ is blank.
663
- # @raise [Parse::Error::PasswordMissingError] when +password+ is blank.
661
+ # no attached `@session_token`, no objectId, or is not anonymous.
662
+ # @raise [Parse::Error::UsernameMissingError] when `username` is blank.
663
+ # @raise [Parse::Error::PasswordMissingError] when `password` is blank.
664
664
  # @raise [Parse::Error::UsernameTakenError] when Parse Server reports
665
665
  # the username already exists.
666
666
  # @raise [Parse::Error::EmailTakenError] when Parse Server reports
@@ -929,9 +929,9 @@ module Parse
929
929
  def session
930
930
  if @session.blank? && @session_token.present?
931
931
  response = client.fetch_session(@session_token)
932
- # Trusted hydration: +response.result+ is the server-side
933
- # _Session row, which legitimately includes +sessionToken+,
934
- # +createdAt+, +updatedAt+, and other protected keys. Route
932
+ # Trusted hydration: `response.result` is the server-side
933
+ # _Session row, which legitimately includes `sessionToken`,
934
+ # `createdAt`, `updatedAt`, and other protected keys. Route
935
935
  # through {Parse::Object.build} which handles the trusted-init
936
936
  # signalling.
937
937
  @session ||= Parse::Object.build(response.result, Parse::Model::CLASS_SESSION)
@@ -940,9 +940,9 @@ module Parse
940
940
  end
941
941
 
942
942
  # A non-master {Parse::Client} bound to this user's session token, for
943
- # acting on the server *as this user* with full ACL / CLP / +protectedFields+
943
+ # acting on the server *as this user* with full ACL / CLP / `protectedFields`
944
944
  # enforcement and no master-key fallback. It mirrors the connection settings
945
- # of +base+ (the configured client by default) but carries no master key and
945
+ # of `base` (the configured client by default) but carries no master key and
946
946
  # binds {#session_token}, so even raw REST calls through it are authorized as
947
947
  # the user with no per-call ceremony. The web-counterpart of
948
948
  # {Parse::Webhooks::Payload#user_client}; the typical client-side entry point
@@ -952,7 +952,7 @@ module Parse
952
952
  # Parse::Query.new("Post", client: client).results # scoped to the user
953
953
  #
954
954
  # @param base [Parse::Client] the client whose connection settings to mirror.
955
- # @return [Parse::Client, nil] +nil+ when the user has no session token
955
+ # @return [Parse::Client, nil] `nil` when the user has no session token
956
956
  # (e.g. fetched/saved under the master key rather than logged in).
957
957
  def session_client(base = self.client)
958
958
  return nil if @session_token.nil? || @session_token.to_s.strip.empty?
@@ -960,23 +960,23 @@ module Parse
960
960
  end
961
961
 
962
962
  # @!visibility private
963
- # Keys that must never flow through +Parse::User.create+ from a
964
- # mass-assigned hash. +authData+ on the user-signup endpoint causes
963
+ # Keys that must never flow through `Parse::User.create` from a
964
+ # mass-assigned hash. `authData` on the user-signup endpoint causes
965
965
  # Parse Server to silently log into the existing account that matches
966
966
  # that auth_data and return ITS sessionToken — full account takeover
967
967
  # if the caller blindly forwards client-supplied parameters.
968
- # +objectId+ allows the caller to pick the user's identifier on
968
+ # `objectId` allows the caller to pick the user's identifier on
969
969
  # creation, sometimes targetable depending on Parse Server config.
970
970
  UNSAFE_CREATE_KEYS = %i[authData auth_data objectId id].freeze
971
971
 
972
972
  # @!visibility private
973
973
  # Fields that are server-controlled and must be stripped from any body
974
- # that the SDK sends to the signup endpoint or +Parse::User.create+,
974
+ # that the SDK sends to the signup endpoint or `Parse::User.create`,
975
975
  # regardless of who supplied them. Unlike {UNSAFE_CREATE_KEYS}, passing
976
976
  # one of these is not refused (no exception is raised); the field is
977
977
  # silently dropped before wire transit.
978
978
  #
979
- # +emailVerified+ is the canonical case: Parse Server's default `_User`
979
+ # `emailVerified` is the canonical case: Parse Server's default `_User`
980
980
  # CLP restricts writes to the master key, so a caller-supplied value
981
981
  # would normally be rejected anyway — but the SDK strips it as
982
982
  # defense-in-depth so signup with mass-assigned attributes cannot
@@ -1008,7 +1008,7 @@ module Parse
1008
1008
 
1009
1009
  # Creates a new Parse::User given a hash that maps to the fields defined in your Parse::User collection.
1010
1010
  #
1011
- # Mass-assignment of +authData+/+auth_data+/+objectId+ is refused. If you
1011
+ # Mass-assignment of `authData`/`auth_data`/`objectId` is refused. If you
1012
1012
  # intend to create-or-login a user via federated identity, use
1013
1013
  # {.autologin_service} or {.link_or_create_with_auth_data}. Passing
1014
1014
  # those keys directly bypasses the SDK's federated-identity wrapper
@@ -1017,7 +1017,7 @@ module Parse
1017
1017
  #
1018
1018
  # @param body [Hash] The hash containing the Parse::User fields. The field `username` and `password` are required.
1019
1019
  # @option opts [Boolean] :master_key Whether the master key should be used for this request.
1020
- # @raise [ArgumentError] If +body+ contains +authData+/+auth_data+/+objectId+ — use {.autologin_service} for federated flows.
1020
+ # @raise [ArgumentError] If `body` contains `authData`/`auth_data`/`objectId` — use {.autologin_service} for federated flows.
1021
1021
  # @raise [Parse::Error::UsernameMissingError] If username is missing.
1022
1022
  # @raise [Parse::Error::PasswordMissingError] If password is missing.
1023
1023
  # @raise [Parse::Error::UsernameTakenError] If the username has already been taken.
@@ -1057,11 +1057,11 @@ module Parse
1057
1057
  end
1058
1058
 
1059
1059
  # @!visibility private
1060
- # Silently strips {SERVER_CONTROLLED_KEYS} from +body+ in place. Used
1060
+ # Silently strips {SERVER_CONTROLLED_KEYS} from `body` in place. Used
1061
1061
  # by {.create}, {#signup!}, and {#signup_create} as defense-in-depth so
1062
1062
  # caller-supplied values for fields that Parse Server is meant to
1063
- # control (currently just +emailVerified+) never reach the wire.
1064
- # @return [Hash, Object] the same +body+ object, mutated.
1063
+ # control (currently just `emailVerified`) never reach the wire.
1064
+ # @return [Hash, Object] the same `body` object, mutated.
1065
1065
  def self.strip_server_controlled_keys!(body)
1066
1066
  return body unless body.is_a?(Hash)
1067
1067
  SERVER_CONTROLLED_KEYS.each do |k|
@@ -1072,10 +1072,10 @@ module Parse
1072
1072
  end
1073
1073
 
1074
1074
  # @!visibility private
1075
- # Raises +ArgumentError+ if +body+ carries keys that would let an
1076
- # attacker turn +Parse::User.create+ into an account-takeover sink.
1075
+ # Raises `ArgumentError` if `body` carries keys that would let an
1076
+ # attacker turn `Parse::User.create` into an account-takeover sink.
1077
1077
  # Skipped when called through the SDK's federated-identity wrapper
1078
- # ({.autologin_service}), which deliberately supplies +authData+ and
1078
+ # ({.autologin_service}), which deliberately supplies `authData` and
1079
1079
  # is responsible for its provenance.
1080
1080
  def self.assert_create_body_safe!(body)
1081
1081
  return unless body.is_a?(Hash)
@@ -1102,7 +1102,7 @@ module Parse
1102
1102
  # @see User.create
1103
1103
  def self.autologin_service(service_name, auth_data, body: {})
1104
1104
  # Trust-mark this call so {.assert_create_body_safe!} permits the
1105
- # +authData+ that we are explicitly responsible for here. The
1105
+ # `authData` that we are explicitly responsible for here. The
1106
1106
  # marker is consumed inside {.create} before forwarding to the
1107
1107
  # server.
1108
1108
  body = body.merge({
@@ -1113,15 +1113,15 @@ module Parse
1113
1113
  end
1114
1114
 
1115
1115
  # Create and log in a new anonymous user via the
1116
- # +authData.anonymous+ provider. The returned user instance has a
1117
- # +session_token+ and an objectId, and {#anonymous?} returns true.
1116
+ # `authData.anonymous` provider. The returned user instance has a
1117
+ # `session_token` and an objectId, and {#anonymous?} returns true.
1118
1118
  # Later, after the user has chosen a username and password, upgrade
1119
1119
  # the account in-place with {#upgrade_anonymous!}.
1120
1120
  #
1121
1121
  # Parse Server requires the anonymous-provider payload to include a
1122
- # client-generated +id+; this helper produces one via
1123
- # +SecureRandom.uuid+ so callers don't have to hand-roll the
1124
- # +authData+ shape.
1122
+ # client-generated `id`; this helper produces one via
1123
+ # `SecureRandom.uuid` so callers don't have to hand-roll the
1124
+ # `authData` shape.
1125
1125
  #
1126
1126
  # @return [User] a freshly-created, logged-in anonymous user.
1127
1127
  # @see #upgrade_anonymous!
@@ -1176,7 +1176,7 @@ module Parse
1176
1176
  case response.code
1177
1177
  when Parse::Response::ERROR_EMAIL_NOT_FOUND
1178
1178
  # Parse Server throws code 205 (EMAIL_NOT_FOUND) when
1179
- # +preventLoginWithUnverifiedEmail+ is set and the account's email
1179
+ # `preventLoginWithUnverifiedEmail` is set and the account's email
1180
1180
  # address has not yet been verified. Raise the typed error so callers
1181
1181
  # can direct the user to verify their inbox without catching every
1182
1182
  # AuthenticationError.
@@ -1398,24 +1398,24 @@ module Parse
1398
1398
 
1399
1399
  # Verify this user's password without minting a session token.
1400
1400
  #
1401
- # Delegates to the +GET /parse/verifyPassword+ endpoint (Parse Server
1402
- # 7.1.0+) using this user's +username+ and the supplied +password+. The
1401
+ # Delegates to the `GET /parse/verifyPassword` endpoint (Parse Server
1402
+ # 7.1.0+) using this user's `username` and the supplied `password`. The
1403
1403
  # check is purely credential validation — no session is created on
1404
1404
  # success, and the user's existing sessions are unaffected.
1405
1405
  #
1406
1406
  # Use this as a step-up authentication gate: before allowing a sensitive
1407
1407
  # action (e.g. changing an email address or deleting an account), call
1408
- # +verify_password+ to confirm the caller still knows the password.
1408
+ # `verify_password` to confirm the caller still knows the password.
1409
1409
  #
1410
1410
  # @param password [String] the password to verify.
1411
- # @return [Boolean] +true+ if the credentials are valid.
1411
+ # @return [Boolean] `true` if the credentials are valid.
1412
1412
  # @raise [Parse::Error::EmailNotVerifiedError] when the account exists but
1413
- # +preventLoginWithUnverifiedEmail+ is enabled and the email has not been
1413
+ # `preventLoginWithUnverifiedEmail` is enabled and the email has not been
1414
1414
  # verified (Parse Server error code 205). The caller may want to prompt
1415
1415
  # the user to check their inbox rather than treating this as a wrong-
1416
1416
  # password failure.
1417
1417
  # @raise [Parse::Error::AuthenticationError] when the username does not
1418
- # exist or the password is wrong (code 101, +OBJECT_NOT_FOUND+).
1418
+ # exist or the password is wrong (code 101, `OBJECT_NOT_FOUND`).
1419
1419
  # @return [Boolean]
1420
1420
  # @example
1421
1421
  # # Step-up check before a destructive action
@@ -1472,7 +1472,7 @@ module Parse
1472
1472
  # use `master: true` for the previous behavior.
1473
1473
  #
1474
1474
  # @param max_depth [Integer] maximum BFS depth (default: 10).
1475
- # @param master [Boolean] when +true+, bypass `_Role` CLP and run
1475
+ # @param master [Boolean] when `true`, bypass `_Role` CLP and run
1476
1476
  # the role-graph lookup under master mode. Use for ACL-building
1477
1477
  # code paths inside the SDK or in admin tooling.
1478
1478
  # @param as [Parse::User, Parse::Pointer, nil] caller-scope. When
@@ -1480,7 +1480,7 @@ module Parse
1480
1480
  # case). Pass a different user to ask "what would this caller
1481
1481
  # see when introspecting this user's roles?"; the scope's
1482
1482
  # permission set is checked against `_Role` CLP.
1483
- # @return [Set<String>] role names (no +role:+ prefix). Empty set
1483
+ # @return [Set<String>] role names (no `role:` prefix). Empty set
1484
1484
  # when the user has no objectId yet or holds no roles.
1485
1485
  # @raise [Parse::CLPScope::Denied] when the scope cannot `find`
1486
1486
  # on `_Role` under the current CLP.
@@ -19,8 +19,8 @@ module Parse
19
19
  end
20
20
  end
21
21
 
22
- # Namespace where +Parse.auto_generate_models!+ installs dynamically
23
- # generated +Parse::Object+ subclasses derived from server-side schema.
22
+ # Namespace where `Parse.auto_generate_models!` installs dynamically
23
+ # generated `Parse::Object` subclasses derived from server-side schema.
24
24
  # Isolating them here prevents server-returned className strings from
25
25
  # rebinding top-level constants like ::File, ::Logger, ::Process.
26
26
  module Generated
@@ -39,9 +39,9 @@ module Parse
39
39
  # Parse Server system classes that ship with the SDK as hand-written
40
40
  # subclasses (Parse::User, Parse::Role, etc.). Schema-driven builds
41
41
  # must NOT install additional fields or associations on these — a
42
- # compromised Parse Server could otherwise inject an +is_admin+
43
- # property onto the real +Parse::User+ class, or a +password_history+
44
- # accessor onto +_Session+, by returning a poisoned schema.
42
+ # compromised Parse Server could otherwise inject an `is_admin`
43
+ # property onto the real `Parse::User` class, or a `password_history`
44
+ # accessor onto `_Session`, by returning a poisoned schema.
45
45
  PROTECTED_SYSTEM_CLASSES = %w[
46
46
  _User _Role _Session _Installation _Product _Audience _PushStatus
47
47
  _JobStatus _JobSchedule _Hooks _GlobalConfig _SCHEMA _GraphQLConfig
@@ -123,8 +123,8 @@ module Parse
123
123
  end
124
124
 
125
125
  # @!visibility private
126
- # Validates a server-returned +targetClass+ string before forwarding
127
- # it to +belongs_to+/+has_many+. Returns +nil+ for missing or
126
+ # Validates a server-returned `targetClass` string before forwarding
127
+ # it to `belongs_to`/`has_many`. Returns `nil` for missing or
128
128
  # invalid values so the association DSL falls back to its inferred
129
129
  # default rather than installing an attacker-controlled class name
130
130
  # (which could pivot a later type-confusion bypass).
@@ -78,7 +78,7 @@ module Parse
78
78
  source: "Parse::CreateLock",
79
79
  unavailable_error: Parse::CreateLockUnavailableError,
80
80
  )
81
- return LockBackend.process_mutex(key).synchronize(&block)
81
+ return LockBackend.synchronize_process_mutex(key, &block)
82
82
  end
83
83
 
84
84
  owner = SecureRandom.uuid
@@ -708,13 +708,13 @@ module Parse
708
708
  # writers when the caller is trusted.
709
709
  # @param filter_protected [Boolean, nil] whether to filter out
710
710
  # {Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS}. Defaults to
711
- # +dirty_track+ for backwards-compat (the historical coupling). Callers
712
- # can pass +true+ explicitly to filter even on the trusted hydration
711
+ # `dirty_track` for backwards-compat (the historical coupling). Callers
712
+ # can pass `true` explicitly to filter even on the trusted hydration
713
713
  # path (used by {Parse::Object#initialize} when constructed with
714
- # +trusted: false+ but an +objectId+ is in the hash). +false+ explicitly
714
+ # `trusted: false` but an `objectId` is in the hash). `false` explicitly
715
715
  # preserves the legacy "server response" semantics.
716
716
  # @param protected_set [Array<String>, nil] override which key list to
717
- # filter when +filter_protected+ is true. Defaults to the wider
717
+ # filter when `filter_protected` is true. Defaults to the wider
718
718
  # {Parse::Properties::PROTECTED_MASS_ASSIGNMENT_KEYS}.
719
719
  # {Parse::Object#initialize} passes
720
720
  # {Parse::Properties::PROTECTED_INITIALIZE_KEYS} here to allow
@@ -69,7 +69,7 @@ module Parse
69
69
 
70
70
  # @!visibility private
71
71
  # Default cap on remote-fetched file size (50 MiB). Override via
72
- # +Parse::File.max_remote_size+.
72
+ # `Parse::File.max_remote_size`.
73
73
  DEFAULT_MAX_REMOTE_SIZE = 50 * 1024 * 1024
74
74
  # @!visibility private
75
75
  # Default read/open timeout for remote fetches in seconds.
@@ -90,7 +90,7 @@ module Parse
90
90
  ].map { |c| IPAddr.new(c) }.freeze
91
91
  # Restrictive port allowlist for Parse::File URL fetches. By default
92
92
  # only the standard HTTP/HTTPS ports are permitted. Operators may
93
- # extend +Parse::File.allowed_remote_ports+ for legitimate non-standard
93
+ # extend `Parse::File.allowed_remote_ports` for legitimate non-standard
94
94
  # CDN ports.
95
95
  DEFAULT_ALLOWED_REMOTE_PORTS = [80, 443, 8080, 8443].freeze
96
96
  # @return [String] the name of the file including extension (if any)
@@ -164,7 +164,7 @@ module Parse
164
164
  end
165
165
 
166
166
  # @return [Integer] Maximum byte size for a remote URL fetch via
167
- # +Parse::File.create+ / +Parse::File.new(url)+.
167
+ # `Parse::File.create` / `Parse::File.new(url)`.
168
168
  attr_writer :max_remote_size
169
169
  def max_remote_size
170
170
  @max_remote_size ||= DEFAULT_MAX_REMOTE_SIZE
@@ -526,16 +526,22 @@ module Parse
526
526
  # the caller can read from.
527
527
  #
528
528
  # DNS rebinding mitigation: the host is resolved twice — once before
529
- # the fetch and once via +URI.open+'s underlying resolver. The
530
- # second-pass addresses are re-validated against +BLOCKED_CIDRS+;
531
- # any new private/internal IP causes an +ArgumentError+ at progress
529
+ # the fetch and once via `URI.open`'s underlying resolver. The
530
+ # second-pass addresses are re-validated against `BLOCKED_CIDRS`;
531
+ # any new private/internal IP causes an `ArgumentError` at progress
532
532
  # time so the body cannot be streamed back. (Caveat: this is a
533
- # best-effort defense — the TCP +connect()+ uses a third resolution
533
+ # best-effort defense — the TCP `connect()` uses a third resolution
534
534
  # that we cannot intercept without a custom socket factory. Operators
535
535
  # who need strict guarantees should also enforce egress allowlists
536
536
  # at the network layer.)
537
537
  # @raise [ArgumentError] on any disallowed input or unsafe target.
538
- def safe_open_url(url_string)
538
+ def safe_open_url(url_string, max_bytes: nil)
539
+ # Validate the per-call streaming cap up front. A non-positive value is
540
+ # a caller error, not a "cap everything" instruction: 0 would reject any
541
+ # non-empty body, and a negative cap makes `max_bytes < size_cap` always
542
+ # true, driving size_cap negative so every non-empty response raises
543
+ # "exceeds". Fail fast with a clear message before any DNS/host work.
544
+ max_bytes = coerce_positive_max_bytes(max_bytes)
539
545
  uri = begin
540
546
  URI.parse(url_string)
541
547
  rescue URI::InvalidURIError => e
@@ -561,14 +567,22 @@ module Parse
561
567
  end
562
568
  resolved = assert_host_allowed!(host)
563
569
 
570
+ # The global cap always bounds the stream; a caller may pass a
571
+ # tighter `max_bytes:` to abort even earlier (e.g. image fetch with
572
+ # a per-request ceiling below Parse::File.max_remote_size). Only a
573
+ # smaller value takes effect — a caller cannot loosen the global cap.
574
+ # `max_bytes` is already a validated positive Integer (or nil) here.
575
+ # Only a value below the global cap takes effect — a caller cannot
576
+ # loosen the global ceiling.
564
577
  size_cap = max_remote_size
578
+ size_cap = max_bytes if max_bytes && max_bytes < size_cap
565
579
  timeout = remote_timeout
566
580
  uri.open(read_timeout: timeout,
567
581
  open_timeout: timeout,
568
582
  redirect: false,
569
583
  content_length_proc: ->(len) {
570
584
  if len && len > size_cap
571
- raise ArgumentError, "Remote file exceeds Parse::File.max_remote_size (#{size_cap} bytes)"
585
+ raise ArgumentError, "Remote file exceeds the size cap (#{size_cap} bytes)"
572
586
  end
573
587
  # DNS-rebinding re-check: by the time content_length_proc
574
588
  # fires, the connection has been established. Re-resolve
@@ -577,14 +591,33 @@ module Parse
577
591
  },
578
592
  progress_proc: ->(transferred) {
579
593
  if transferred > size_cap
580
- raise ArgumentError, "Remote file exceeds Parse::File.max_remote_size (#{size_cap} bytes)"
594
+ raise ArgumentError, "Remote file exceeds the size cap (#{size_cap} bytes)"
581
595
  end
582
596
  })
583
597
  end
584
598
 
585
599
  # @!visibility private
586
- # Validates that +host+ resolves only to public, non-blocked addresses.
587
- # When +Parse::File.allowed_remote_hosts+ is non-empty, host must also
600
+ # Coerce a caller-supplied `max_bytes:` to a positive Integer, or nil
601
+ # when unset. A non-numeric, zero, or negative value raises ArgumentError
602
+ # — the streaming cap is a positive byte ceiling, never a sentinel.
603
+ # @return [Integer, nil]
604
+ def coerce_positive_max_bytes(max_bytes)
605
+ return nil if max_bytes.nil?
606
+ requested =
607
+ begin
608
+ Integer(max_bytes)
609
+ rescue ArgumentError, TypeError
610
+ raise ArgumentError, "max_bytes must be a positive integer (got #{max_bytes.inspect})"
611
+ end
612
+ unless requested.positive?
613
+ raise ArgumentError, "max_bytes must be a positive integer (got #{requested})"
614
+ end
615
+ requested
616
+ end
617
+
618
+ # @!visibility private
619
+ # Validates that `host` resolves only to public, non-blocked addresses.
620
+ # When `Parse::File.allowed_remote_hosts` is non-empty, host must also
588
621
  # match an allowlist entry.
589
622
  # @return [Array<IPAddr>] the addresses that passed validation.
590
623
  def assert_host_allowed!(host)
@@ -614,10 +647,18 @@ module Parse
614
647
  end
615
648
 
616
649
  # @!visibility private
617
- # DNS rebinding re-check. Resolves +host+ again and refuses if any
618
- # currently-resolved address is private or differs from the first
619
- # resolution. Best-effort: kernel resolver caches and a third
620
- # resolution at connect-time are out of scope.
650
+ # DNS rebinding re-check, best-effort. After the connection is
651
+ # established, re-resolves `host` and refuses if any currently-resolved
652
+ # address is private/internal (in BLOCKED_CIDRS) the SSRF shape where a
653
+ # host that first resolved to a public address later rebinds to an
654
+ # internal one. It deliberately does NOT require the re-resolved set to
655
+ # equal the first resolution (`prior_addrs`): legitimate round-robin / CDN
656
+ # hosts return different public IPs per lookup, and the HTTP client
657
+ # resolves the actual socket itself, so pinning to the first IPs here
658
+ # would add false positives without binding the real connection.
659
+ # `prior_addrs` only gates the check to the case where the initial
660
+ # resolution succeeded. Kernel resolver caches and a resolution at
661
+ # connect-time proper are out of scope.
621
662
  def assert_host_not_rebound!(host, prior_addrs)
622
663
  return if prior_addrs.nil? || prior_addrs.empty?
623
664
  current = resolve_addresses(host)
@@ -81,15 +81,15 @@ module Parse
81
81
  # leading-underscore storage form. Consulted by {String#to_parse_class}
82
82
  # ONLY as a fallback when {find_class} cannot resolve the name — which
83
83
  # happens at class-declaration time for a built-in whose Ruby class is not
84
- # yet registered (e.g. +Parse::Installation+ declares +belongs_to :user+
85
- # before +Parse::User+ is loaded). Without this fallback the conversion
86
- # froze the wrong literal (+"User"+) into the association +references+ map
87
- # and pushed it to the server schema as the pointer +targetClass+, which
88
- # Parse Server then rejected (+Pointer<User>+ vs +Pointer<_User>+). A
84
+ # yet registered (e.g. `Parse::Installation` declares `belongs_to :user`
85
+ # before `Parse::User` is loaded). Without this fallback the conversion
86
+ # froze the wrong literal (`"User"`) into the association `references` map
87
+ # and pushed it to the server schema as the pointer `targetClass`, which
88
+ # Parse Server then rejected (`Pointer<User>` vs `Pointer<_User>`). A
89
89
  # genuinely-registered class still wins via {find_class}, so a custom
90
- # +parse_class+ table mapping is never overridden by this map.
90
+ # `parse_class` table mapping is never overridden by this map.
91
91
  # Keys are the camelized bare names (the form {String#to_parse_class}
92
- # computes before lookup); only +User+ is actually targeted by a built-in
92
+ # computes before lookup); only `User` is actually targeted by a built-in
93
93
  # association before its class registers, the rest are hygiene so an app
94
94
  # that declares a pointer/relation to any built-in resolves correctly
95
95
  # regardless of load order.
@@ -235,27 +235,27 @@ module Parse
235
235
  # Whether two Parse class-name strings denote the same class. This is the
236
236
  # canonical equality used to suppress spurious className-mismatch warnings.
237
237
  # Two names are the same class when they are string-equal, when one is the
238
- # leading-underscore system form of the other (+User+ <-> +_User+, +Role+
239
- # <-> +_Role+, +Installation+ <-> +_Installation+, +Session+ <->
240
- # +_Session+), or when both resolve to the same registered +Parse::Object+
241
- # subclass (covers custom +parse_class+ table mappings).
238
+ # leading-underscore system form of the other (`User` <-> `_User`, `Role`
239
+ # <-> `_Role`, `Installation` <-> `_Installation`, `Session` <->
240
+ # `_Session`), or when both resolve to the same registered `Parse::Object`
241
+ # subclass (covers custom `parse_class` table mappings).
242
242
  #
243
243
  # The underscore rule is what makes the comparison correct independent of
244
244
  # autoload order — at declaration time {find_class} may not yet have
245
- # +Parse::User+ registered (so a +belongs_to :user+ captures +"User"+
246
- # rather than +"_User"+), but the server always emits the +_User+ storage
245
+ # `Parse::User` registered (so a `belongs_to :user` captures `"User"`
246
+ # rather than `"_User"`), but the server always emits the `_User` storage
247
247
  # form, and both denote the same class. The rule matches exactly one
248
- # system prefix underscore, so a malformed +"__User"+ is not conflated
249
- # with +"_User"+.
248
+ # system prefix underscore, so a malformed `"__User"` is not conflated
249
+ # with `"_User"`.
250
250
  #
251
251
  # These warnings are an ADVISORY type-confusion signal, not the
252
252
  # enforcement mechanism: every call site that consults this method builds
253
253
  # the resulting object from the declared/trusted class regardless of the
254
- # incoming className (see +Parse::Object.build+, which always uses the
255
- # +table+ argument the caller passes). A false-positive here can therefore
254
+ # incoming className (see `Parse::Object.build`, which always uses the
255
+ # `table` argument the caller passes). A false-positive here can therefore
256
256
  # only suppress a log line — it can never route a pointer of the wrong
257
- # class into a typed slot. Distinct classes still compare unequal (+User+
258
- # vs +_Session+, +User+ vs +_Role+, and +nil+), so a genuinely mismatched
257
+ # class into a typed slot. Distinct classes still compare unequal (`User`
258
+ # vs `_Session`, `User` vs `_Role`, and `nil`), so a genuinely mismatched
259
259
  # pointer still surfaces in logs.
260
260
  #
261
261
  # @param a [String, Symbol, nil] a Parse class name.