parse-stack-next 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. checksums.yaml +7 -0
  2. data/.bundle/config +2 -0
  3. data/.env.sample +112 -0
  4. data/.env.test +10 -0
  5. data/.github/workflows/ruby.yml +36 -0
  6. data/.gitignore +49 -0
  7. data/.ruby-version +1 -0
  8. data/.solargraph.yml +22 -0
  9. data/CHANGELOG.md +5816 -0
  10. data/Gemfile +30 -0
  11. data/Gemfile.lock +175 -0
  12. data/LICENSE.txt +23 -0
  13. data/Makefile +63 -0
  14. data/README.md +5655 -0
  15. data/Rakefile +573 -0
  16. data/bin/console +38 -0
  17. data/bin/parse-console +136 -0
  18. data/bin/server +17 -0
  19. data/bin/setup +7 -0
  20. data/config/parse-config.json +12 -0
  21. data/docs/TEST_SERVER.md +271 -0
  22. data/docs/_config.yml +1 -0
  23. data/docs/mcp_guide.md +3484 -0
  24. data/docs/mongodb_direct_guide.md +1348 -0
  25. data/docs/mongodb_index_optimization_guide.md +631 -0
  26. data/examples/transaction_example.rb +219 -0
  27. data/lib/parse/acl_scope.rb +728 -0
  28. data/lib/parse/agent/cancellation_token.rb +80 -0
  29. data/lib/parse/agent/constraint_translator.rb +480 -0
  30. data/lib/parse/agent/describe.rb +420 -0
  31. data/lib/parse/agent/errors.rb +133 -0
  32. data/lib/parse/agent/mcp_client.rb +557 -0
  33. data/lib/parse/agent/mcp_dispatcher.rb +1023 -0
  34. data/lib/parse/agent/mcp_rack_app.rb +1143 -0
  35. data/lib/parse/agent/mcp_server.rb +376 -0
  36. data/lib/parse/agent/metadata_audit.rb +259 -0
  37. data/lib/parse/agent/metadata_dsl.rb +733 -0
  38. data/lib/parse/agent/metadata_registry.rb +794 -0
  39. data/lib/parse/agent/pipeline_validator.rb +82 -0
  40. data/lib/parse/agent/prompts.rb +351 -0
  41. data/lib/parse/agent/rate_limiter.rb +158 -0
  42. data/lib/parse/agent/relation_graph.rb +162 -0
  43. data/lib/parse/agent/result_formatter.rb +453 -0
  44. data/lib/parse/agent/tools.rb +5489 -0
  45. data/lib/parse/agent.rb +3249 -0
  46. data/lib/parse/api/aggregate.rb +79 -0
  47. data/lib/parse/api/all.rb +26 -0
  48. data/lib/parse/api/analytics.rb +18 -0
  49. data/lib/parse/api/batch.rb +33 -0
  50. data/lib/parse/api/cloud_functions.rb +58 -0
  51. data/lib/parse/api/config.rb +125 -0
  52. data/lib/parse/api/files.rb +29 -0
  53. data/lib/parse/api/hooks.rb +117 -0
  54. data/lib/parse/api/objects.rb +146 -0
  55. data/lib/parse/api/path_segment.rb +75 -0
  56. data/lib/parse/api/push.rb +20 -0
  57. data/lib/parse/api/schema.rb +49 -0
  58. data/lib/parse/api/server.rb +50 -0
  59. data/lib/parse/api/sessions.rb +24 -0
  60. data/lib/parse/api/users.rb +250 -0
  61. data/lib/parse/atlas_search/index_manager.rb +353 -0
  62. data/lib/parse/atlas_search/result.rb +204 -0
  63. data/lib/parse/atlas_search/search_builder.rb +604 -0
  64. data/lib/parse/atlas_search/session.rb +253 -0
  65. data/lib/parse/atlas_search.rb +995 -0
  66. data/lib/parse/client/authentication.rb +97 -0
  67. data/lib/parse/client/batch.rb +234 -0
  68. data/lib/parse/client/body_builder.rb +240 -0
  69. data/lib/parse/client/caching.rb +203 -0
  70. data/lib/parse/client/logging.rb +293 -0
  71. data/lib/parse/client/profiling.rb +181 -0
  72. data/lib/parse/client/protocol.rb +91 -0
  73. data/lib/parse/client/request.rb +233 -0
  74. data/lib/parse/client/response.rb +208 -0
  75. data/lib/parse/client.rb +1104 -0
  76. data/lib/parse/clp_scope.rb +361 -0
  77. data/lib/parse/live_query/circuit_breaker.rb +256 -0
  78. data/lib/parse/live_query/client.rb +1001 -0
  79. data/lib/parse/live_query/configuration.rb +224 -0
  80. data/lib/parse/live_query/event.rb +115 -0
  81. data/lib/parse/live_query/event_queue.rb +272 -0
  82. data/lib/parse/live_query/health_monitor.rb +214 -0
  83. data/lib/parse/live_query/logging.rb +149 -0
  84. data/lib/parse/live_query/subscription.rb +294 -0
  85. data/lib/parse/live_query.rb +163 -0
  86. data/lib/parse/lookup_rewriter.rb +445 -0
  87. data/lib/parse/model/acl.rb +968 -0
  88. data/lib/parse/model/associations/belongs_to.rb +275 -0
  89. data/lib/parse/model/associations/collection_proxy.rb +435 -0
  90. data/lib/parse/model/associations/has_many.rb +597 -0
  91. data/lib/parse/model/associations/has_one.rb +158 -0
  92. data/lib/parse/model/associations/pointer_collection_proxy.rb +134 -0
  93. data/lib/parse/model/associations/relation_collection_proxy.rb +177 -0
  94. data/lib/parse/model/bytes.rb +62 -0
  95. data/lib/parse/model/classes/audience.rb +262 -0
  96. data/lib/parse/model/classes/installation.rb +363 -0
  97. data/lib/parse/model/classes/job_schedule.rb +153 -0
  98. data/lib/parse/model/classes/job_status.rb +264 -0
  99. data/lib/parse/model/classes/product.rb +75 -0
  100. data/lib/parse/model/classes/push_status.rb +263 -0
  101. data/lib/parse/model/classes/role.rb +751 -0
  102. data/lib/parse/model/classes/session.rb +201 -0
  103. data/lib/parse/model/classes/user.rb +943 -0
  104. data/lib/parse/model/clp.rb +544 -0
  105. data/lib/parse/model/core/actions.rb +1268 -0
  106. data/lib/parse/model/core/builder.rb +139 -0
  107. data/lib/parse/model/core/create_lock.rb +386 -0
  108. data/lib/parse/model/core/describe.rb +382 -0
  109. data/lib/parse/model/core/enhanced_change_tracking.rb +159 -0
  110. data/lib/parse/model/core/errors.rb +38 -0
  111. data/lib/parse/model/core/fetching.rb +566 -0
  112. data/lib/parse/model/core/field_guards.rb +220 -0
  113. data/lib/parse/model/core/indexing.rb +382 -0
  114. data/lib/parse/model/core/parse_reference.rb +407 -0
  115. data/lib/parse/model/core/properties.rb +809 -0
  116. data/lib/parse/model/core/querying.rb +491 -0
  117. data/lib/parse/model/core/schema.rb +202 -0
  118. data/lib/parse/model/core/search_indexing.rb +174 -0
  119. data/lib/parse/model/date.rb +88 -0
  120. data/lib/parse/model/email.rb +213 -0
  121. data/lib/parse/model/file.rb +527 -0
  122. data/lib/parse/model/geojson.rb +271 -0
  123. data/lib/parse/model/geopoint.rb +261 -0
  124. data/lib/parse/model/model.rb +260 -0
  125. data/lib/parse/model/object.rb +2068 -0
  126. data/lib/parse/model/phone.rb +520 -0
  127. data/lib/parse/model/pointer.rb +443 -0
  128. data/lib/parse/model/polygon.rb +406 -0
  129. data/lib/parse/model/push.rb +975 -0
  130. data/lib/parse/model/shortnames.rb +8 -0
  131. data/lib/parse/model/time_zone.rb +141 -0
  132. data/lib/parse/model/validations/uniqueness_validator.rb +97 -0
  133. data/lib/parse/model/validations.rb +96 -0
  134. data/lib/parse/mongodb.rb +2300 -0
  135. data/lib/parse/pipeline_security.rb +554 -0
  136. data/lib/parse/query/constraint.rb +198 -0
  137. data/lib/parse/query/constraints.rb +3279 -0
  138. data/lib/parse/query/cursor.rb +434 -0
  139. data/lib/parse/query/n_plus_one_detector.rb +445 -0
  140. data/lib/parse/query/operation.rb +104 -0
  141. data/lib/parse/query/ordering.rb +66 -0
  142. data/lib/parse/query.rb +7028 -0
  143. data/lib/parse/schema/index_migrator.rb +291 -0
  144. data/lib/parse/schema/search_index_migrator.rb +289 -0
  145. data/lib/parse/schema.rb +494 -0
  146. data/lib/parse/stack/generators/rails.rb +40 -0
  147. data/lib/parse/stack/generators/templates/model.erb +51 -0
  148. data/lib/parse/stack/generators/templates/model_installation.rb +4 -0
  149. data/lib/parse/stack/generators/templates/model_role.rb +4 -0
  150. data/lib/parse/stack/generators/templates/model_session.rb +4 -0
  151. data/lib/parse/stack/generators/templates/model_user.rb +11 -0
  152. data/lib/parse/stack/generators/templates/parse.rb +12 -0
  153. data/lib/parse/stack/generators/templates/webhooks.rb +10 -0
  154. data/lib/parse/stack/railtie.rb +18 -0
  155. data/lib/parse/stack/tasks.rb +563 -0
  156. data/lib/parse/stack/version.rb +11 -0
  157. data/lib/parse/stack.rb +455 -0
  158. data/lib/parse/two_factor_auth/user_extension.rb +449 -0
  159. data/lib/parse/two_factor_auth.rb +310 -0
  160. data/lib/parse/webhooks/payload.rb +360 -0
  161. data/lib/parse/webhooks/registration.rb +199 -0
  162. data/lib/parse/webhooks/replay_protection.rb +189 -0
  163. data/lib/parse/webhooks.rb +510 -0
  164. data/lib/parse-stack-next.rb +5 -0
  165. data/lib/parse-stack.rb +5 -0
  166. data/parse-stack-next.gemspec +82 -0
  167. data/parse-stack.png +0 -0
  168. data/scripts/debug-ips.js +35 -0
  169. data/scripts/docker/Dockerfile.parse +13 -0
  170. data/scripts/docker/atlas-init.js +284 -0
  171. data/scripts/docker/docker-compose.atlas.yml +76 -0
  172. data/scripts/docker/docker-compose.test.yml +106 -0
  173. data/scripts/docker/mongo-init.js +21 -0
  174. data/scripts/eval_mcp_with_lm_studio.rb +274 -0
  175. data/scripts/start-parse.sh +90 -0
  176. data/scripts/start_mcp_server.rb +78 -0
  177. data/scripts/test_server_connection.rb +82 -0
  178. metadata +377 -0
@@ -0,0 +1,75 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "uri"
5
+
6
+ module Parse
7
+ module API
8
+ # Helpers for safely interpolating user-controlled segments into REST
9
+ # paths. Every site that builds a request URL via raw string
10
+ # interpolation (`"functions/#{name}"`, `"schemas/#{className}"`, etc.)
11
+ # should route the name through one of these helpers first so a caller
12
+ # passing `"../classes/_User?where=%7B%7D"` cannot traverse to a
13
+ # different endpoint and read it with whatever credentials the outer
14
+ # request was authorized to send.
15
+ module PathSegment
16
+ module_function
17
+
18
+ # Parse identifier pattern: starts with a letter or underscore (Parse
19
+ # uses leading underscore for system classes like `_User`,
20
+ # `_Session`, `_Role`), then alphanumerics and underscores. Matches
21
+ # the documented Parse class/field/function/job naming rules.
22
+ IDENTIFIER_PATTERN = /\A[A-Za-z_][A-Za-z0-9_]*\z/.freeze
23
+
24
+ # Validate a Parse identifier (class name, function name, job name,
25
+ # field name) and return it unchanged. Identifiers are already
26
+ # path-safe under the strict pattern, so no percent-encoding is
27
+ # needed; we just refuse anything that violates the shape.
28
+ #
29
+ # @param value the identifier to validate (anything responding to
30
+ # `to_s`).
31
+ # @param kind [String] human-readable name for error messages.
32
+ # @return [String] the validated identifier.
33
+ # @raise [ArgumentError] if blank, contains a slash, contains a dot,
34
+ # or otherwise fails the pattern.
35
+ def identifier!(value, kind: "name")
36
+ s = value.to_s
37
+ if s.empty?
38
+ raise ArgumentError, "#{kind} must not be empty"
39
+ end
40
+ unless IDENTIFIER_PATTERN.match?(s)
41
+ raise ArgumentError,
42
+ "#{kind} #{s.inspect} contains characters that are not allowed in " \
43
+ "a Parse identifier. Names must match /\\A[A-Za-z_][A-Za-z0-9_]*\\z/."
44
+ end
45
+ s
46
+ end
47
+
48
+ # Validate and percent-encode a less-restrictive path segment, used
49
+ # for file names which can contain hyphens, periods, and other
50
+ # filename-safe characters but must never contain a literal `/`,
51
+ # `..`, or NUL/control characters.
52
+ #
53
+ # @param value the segment to validate.
54
+ # @param kind [String] human-readable name for error messages.
55
+ # @return [String] percent-encoded segment safe for path interpolation.
56
+ # @raise [ArgumentError] if blank, contains a slash, is a path-
57
+ # traversal token, or contains control characters.
58
+ def file!(value, kind: "filename")
59
+ s = value.to_s
60
+ if s.empty?
61
+ raise ArgumentError, "#{kind} must not be empty"
62
+ end
63
+ if s.include?("/") || s == ".." || s == "."
64
+ raise ArgumentError,
65
+ "#{kind} #{s.inspect} contains path-traversal characters " \
66
+ "(`/`, `.`, or `..`). Names must be a single path segment."
67
+ end
68
+ if s.match?(/[\x00-\x1F\x7F]/)
69
+ raise ArgumentError, "#{kind} #{s.inspect} contains control characters"
70
+ end
71
+ URI.encode_www_form_component(s)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,20 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module Parse
5
+ module API
6
+ # Defines the Parse Push notification service interface for the Parse REST API
7
+ module Push
8
+ # @!visibility private
9
+ PUSH_PATH = "push"
10
+
11
+ # Update the schema for a collection.
12
+ # @param payload [Hash] the paylod for the Push notification.
13
+ # @return [Parse::Response]
14
+ # @see http://docs.parseplatform.org/rest/guide/#sending-pushes Sending Pushes
15
+ def push(payload = {})
16
+ request :post, PUSH_PATH, body: payload.as_json
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,49 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module Parse
5
+ module API
6
+ # Defines the Schema interface for the Parse REST API
7
+ module Schema
8
+ # @!visibility private
9
+ SCHEMAS_PATH = "schemas"
10
+
11
+ # Get all the schemas for the application.
12
+ # @param opts [Hash] additional options for the request.
13
+ # @return [Parse::Response]
14
+ def schemas(opts = {})
15
+ request_opts = { cache: false }.merge(opts)
16
+ request :get, SCHEMAS_PATH, opts: request_opts
17
+ end
18
+
19
+ # Get the schema for a collection.
20
+ # @param className [String] the name of the remote Parse collection.
21
+ # @return [Parse::Response]
22
+ def schema(className)
23
+ safe = Parse::API::PathSegment.identifier!(className, kind: "class name")
24
+ opts = { cache: false }
25
+ request :get, "#{SCHEMAS_PATH}/#{safe}", opts: opts
26
+ end
27
+
28
+ # Create a new collection with the specific schema.
29
+ # @param className [String] the name of the remote Parse collection.
30
+ # @param schema [Hash] the schema hash. This is a specific format specified by
31
+ # Parse.
32
+ # @return [Parse::Response]
33
+ def create_schema(className, schema)
34
+ safe = Parse::API::PathSegment.identifier!(className, kind: "class name")
35
+ request :post, "#{SCHEMAS_PATH}/#{safe}", body: schema
36
+ end
37
+
38
+ # Update the schema for a collection.
39
+ # @param className [String] the name of the remote Parse collection.
40
+ # @param schema [Hash] the schema hash. This is a specific format specified by
41
+ # Parse.
42
+ # @return [Parse::Response]
43
+ def update_schema(className, schema)
44
+ safe = Parse::API::PathSegment.identifier!(className, kind: "class name")
45
+ request :put, "#{SCHEMAS_PATH}/#{safe}", body: schema
46
+ end
47
+ end #Schema
48
+ end #API
49
+ end
@@ -0,0 +1,50 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module Parse
5
+ module API
6
+ # APIs related to the open source Parse Server.
7
+ module Server
8
+
9
+ # @!attribute server_info
10
+ # @return [Hash] the information about the server.
11
+ attr_writer :server_info
12
+
13
+ # @!visibility private
14
+ SERVER_INFO_PATH = "serverInfo"
15
+ # @!visibility private
16
+ SERVER_HEALTH_PATH = "health"
17
+ # Fetch and cache information about the Parse server configuration. This
18
+ # hash contains information specifically to the configuration of the running
19
+ # parse server.
20
+ # @return (see #server_info!)
21
+ def server_info
22
+ return @server_info if @server_info.present?
23
+ response = request :get, SERVER_INFO_PATH
24
+ @server_info = response.error? ? nil :
25
+ response.result.with_indifferent_access
26
+ end
27
+
28
+ # Fetches the status of the server based on the health check.
29
+ # @return [Boolean] whether the server is 'OK'.
30
+ def server_health
31
+ opts = { cache: false }
32
+ response = request :get, SERVER_HEALTH_PATH, opts: opts
33
+ response.success?
34
+ end
35
+
36
+ # Force fetches the server information.
37
+ # @return [Hash] a hash containing server configuration if available.
38
+ def server_info!
39
+ @server_info = nil
40
+ server_info
41
+ end
42
+
43
+ # Returns the version of the Parse server the client is connected to.
44
+ # @return [String] a version string (ex. '2.2.25') if available.
45
+ def server_version
46
+ server_info.present? ? @server_info[:parseServerVersion] : nil
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,24 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module Parse
5
+ module API
6
+ # Defines the Session class interface for the Parse REST API
7
+ module Sessions
8
+ # @!visibility private
9
+ SESSION_PATH_PREFIX = "sessions"
10
+
11
+ # Fetch a session record for a given session token.
12
+ # @param session_token [String] an active session token.
13
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
14
+ # @return [Parse::Response]
15
+ def fetch_session(session_token, **opts)
16
+ opts.merge!({ use_master_key: false, cache: false })
17
+ headers = { Parse::Protocol::SESSION_TOKEN => session_token }
18
+ response = request :get, "#{SESSION_PATH_PREFIX}/me", headers: headers, opts: opts
19
+ response.parse_class = Parse::Model::CLASS_SESSION
20
+ response
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,250 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require "open-uri"
5
+
6
+ module Parse
7
+ module API
8
+ # Defines the User class interface for the Parse REST API
9
+ module Users
10
+ # @!visibility private
11
+ USER_PATH_PREFIX = "users"
12
+ # @!visibility private
13
+ LOGOUT_PATH = "logout"
14
+ # @!visibility private
15
+ LOGIN_PATH = "login"
16
+ # @!visibility private
17
+ REQUEST_PASSWORD_RESET = "requestPasswordReset"
18
+
19
+ # Fetch a {Parse::User} for a given objectId.
20
+ # @param id [String] the user objectid
21
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
22
+ # @param headers [Hash] additional HTTP headers to send with the request.
23
+ # @return [Parse::Response]
24
+ def fetch_user(id, headers: {}, **opts)
25
+ request :get, "#{USER_PATH_PREFIX}/#{id}", headers: headers, opts: opts
26
+ end
27
+
28
+ # Find users matching a set of constraints.
29
+ # @param query [Hash] query parameters.
30
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
31
+ # @param headers [Hash] additional HTTP headers to send with the request.
32
+ # @return [Parse::Response]
33
+ def find_users(query = {}, headers: {}, **opts)
34
+ response = request :get, USER_PATH_PREFIX, query: query, headers: headers, opts: opts
35
+ response.parse_class = Parse::Model::CLASS_USER
36
+ response
37
+ end
38
+
39
+ # Find user matching this active session token.
40
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
41
+ # @param headers [Hash] additional HTTP headers to send with the request.
42
+ # @return [Parse::Response]
43
+ def current_user(session_token, headers: {}, **opts)
44
+ headers.merge!({ Parse::Protocol::SESSION_TOKEN => session_token })
45
+ response = request :get, "#{USER_PATH_PREFIX}/me", headers: headers, opts: opts
46
+ response.parse_class = Parse::Model::CLASS_USER
47
+ response
48
+ end
49
+
50
+ # Create a new user.
51
+ # @param body [Hash] a hash of values related to your _User schema.
52
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
53
+ # @param headers [Hash] additional HTTP headers to send with the request.
54
+ # @return [Parse::Response]
55
+ def create_user(body, headers: {}, **opts)
56
+ headers.merge!({ Parse::Protocol::REVOCABLE_SESSION => "1" })
57
+ if opts[:session_token].present?
58
+ headers.merge!({ Parse::Protocol::SESSION_TOKEN => opts[:session_token] })
59
+ end
60
+ response = request :post, USER_PATH_PREFIX, body: body, headers: headers, opts: opts
61
+ response.parse_class = Parse::Model::CLASS_USER
62
+ response
63
+ end
64
+
65
+ # Update a {Parse::User} record given an objectId.
66
+ # @param id [String] the Parse user objectId.
67
+ # @param body [Hash] the body of the API request.
68
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
69
+ # @param headers [Hash] additional HTTP headers to send with the request.
70
+ # @return [Parse::Response]
71
+ def update_user(id, body = {}, headers: {}, **opts)
72
+ response = request :put, "#{USER_PATH_PREFIX}/#{id}", body: body, opts: opts
73
+ response.parse_class = Parse::Model::CLASS_USER
74
+ response
75
+ end
76
+
77
+ # Set the authentication service OAUth data for a user. Deleting or unlinking
78
+ # is done by setting the authData of the service name to nil.
79
+ # @param id [String] the Parse user objectId.
80
+ # @param service_name [Symbol] the name of the OAuth service.
81
+ # @param auth_data [Hash] the hash data related to the third-party service.
82
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
83
+ # @param headers [Hash] additional HTTP headers to send with the request.
84
+ # @return [Parse::Response]
85
+ def set_service_auth_data(id, service_name, auth_data, headers: {}, **opts)
86
+ body = { authData: { service_name => auth_data } }
87
+ update_user(id, body, headers: headers, **opts)
88
+ end
89
+
90
+ # Delete a {Parse::User} record given an objectId.
91
+ # @param id [String] the Parse user objectId.
92
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
93
+ # @param headers [Hash] additional HTTP headers to send with the request.
94
+ # @return [Parse::Response]
95
+ def delete_user(id, headers: {}, **opts)
96
+ request :delete, "#{USER_PATH_PREFIX}/#{id}", headers: headers, opts: opts
97
+ end
98
+
99
+ # Request a password reset for a registered email.
100
+ # @param email [String] the Parse user email.
101
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
102
+ # @param headers [Hash] additional HTTP headers to send with the request.
103
+ # @return [Parse::Response]
104
+ def request_password_reset(email, headers: {}, **opts)
105
+ body = { email: email }
106
+ request :post, REQUEST_PASSWORD_RESET, body: body, opts: opts, headers: headers
107
+ end
108
+
109
+ # Login a user. Implements client-side rate limiting with exponential
110
+ # backoff after repeated failures to mitigate brute force attacks.
111
+ # @param username [String] the Parse user username.
112
+ # @param password [String] the Parse user's associated password.
113
+ # @param headers [Hash] additional HTTP headers to send with the request.
114
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
115
+ # @return [Parse::Response]
116
+ def login(username, password, headers: {}, **opts)
117
+ check_login_rate_limit!(username)
118
+ body = { username: username, password: password }
119
+ headers.merge!({ Parse::Protocol::REVOCABLE_SESSION => "1" })
120
+ response = request :post, LOGIN_PATH, body: body, headers: headers, opts: opts
121
+ response.parse_class = Parse::Model::CLASS_USER
122
+ track_login_attempt(username, response.success?)
123
+ response
124
+ end
125
+
126
+ # Login a user with MFA (Multi-Factor Authentication).
127
+ #
128
+ # This method handles Parse Server's MFA adapter which requires both
129
+ # standard credentials AND an MFA token when MFA is enabled for the user.
130
+ #
131
+ # @param username [String] the Parse user username.
132
+ # @param password [String] the Parse user's associated password.
133
+ # @param mfa_token [String] the TOTP code from authenticator app or recovery code.
134
+ # @param headers [Hash] additional HTTP headers to send with the request.
135
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
136
+ # @return [Parse::Response]
137
+ #
138
+ # @example
139
+ # response = client.login_with_mfa("john", "password123", "123456")
140
+ def login_with_mfa(username, password, mfa_token, headers: {}, **opts)
141
+ check_login_rate_limit!(username)
142
+ # Parse Server expects authData to be sent with POST for MFA login
143
+ body = {
144
+ username: username,
145
+ password: password,
146
+ authData: {
147
+ mfa: {
148
+ token: mfa_token,
149
+ },
150
+ },
151
+ }
152
+ headers.merge!({ Parse::Protocol::REVOCABLE_SESSION => "1" })
153
+ response = request :post, LOGIN_PATH, body: body, headers: headers, opts: opts
154
+ response.parse_class = Parse::Model::CLASS_USER
155
+ track_login_attempt(username, response.success?)
156
+ response
157
+ end
158
+
159
+ # Logout a user by deleting the associated session.
160
+ # @param session_token [String] the Parse user session token to delete.
161
+ # @param headers [Hash] additional HTTP headers to send with the request.
162
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
163
+ # @return [Parse::Response]
164
+ def logout(session_token, headers: {}, **opts)
165
+ headers.merge!({ Parse::Protocol::SESSION_TOKEN => session_token })
166
+ opts.merge!({ use_master_key: false, session_token: session_token })
167
+ request :post, LOGOUT_PATH, headers: headers, opts: opts
168
+ end
169
+
170
+ # Signup a user given a username, password and, optionally, their email.
171
+ # @param username [String] the Parse user username.
172
+ # @param password [String] the Parse user's associated password.
173
+ # @param email [String] the desired Parse user's email.
174
+ # @param body [Hash] additional property values to pass when creating the user record.
175
+ # @param opts [Hash] additional options to pass to the {Parse::Client} request.
176
+ # @return [Parse::Response]
177
+ def signup(username, password, email = nil, body: {}, **opts)
178
+ body = body.merge({ username: username, password: password })
179
+ body[:email] = email || body[:email]
180
+ create_user(body, **opts)
181
+ end
182
+ private
183
+
184
+ # @!visibility private
185
+ # Thread-safe tracker for login rate limiting. Keys are usernames, values are
186
+ # { failures: Integer, locked_until: Time }.
187
+ def login_rate_limits
188
+ @login_rate_limit_mutex ||= Mutex.new
189
+ @login_rate_limits ||= {}
190
+ end
191
+
192
+ # Maximum consecutive failures before lockout.
193
+ LOGIN_MAX_FAILURES = 5
194
+ # Base delay in seconds for exponential backoff.
195
+ LOGIN_BASE_DELAY = 2
196
+ # Maximum number of tracked usernames before cleanup.
197
+ LOGIN_RATE_LIMIT_MAX_ENTRIES = 10_000
198
+ # Entries older than this (seconds) are eligible for cleanup.
199
+ LOGIN_RATE_LIMIT_TTL = 600
200
+
201
+ # Checks if a login attempt is allowed for the given username.
202
+ # @raise [RuntimeError] if the account is temporarily locked out.
203
+ def check_login_rate_limit!(username)
204
+ @login_rate_limit_mutex ||= Mutex.new
205
+ @login_rate_limit_mutex.synchronize do
206
+ entry = login_rate_limits[username]
207
+ return unless entry
208
+ if entry[:locked_until] && Time.now < entry[:locked_until]
209
+ wait = (entry[:locked_until] - Time.now).ceil
210
+ raise "Login rate limited for '#{username}'. Try again in #{wait} seconds."
211
+ end
212
+ end
213
+ end
214
+
215
+ # Records a login attempt result and applies exponential backoff on failure.
216
+ def track_login_attempt(username, success)
217
+ @login_rate_limit_mutex ||= Mutex.new
218
+ @login_rate_limit_mutex.synchronize do
219
+ if success
220
+ login_rate_limits.delete(username)
221
+ else
222
+ entry = login_rate_limits[username] || { failures: 0, locked_until: nil }
223
+ entry[:failures] += 1
224
+ if entry[:failures] >= LOGIN_MAX_FAILURES
225
+ delay = LOGIN_BASE_DELAY**(entry[:failures] - LOGIN_MAX_FAILURES + 1)
226
+ delay = [delay, 300].min # cap at 5 minutes
227
+ entry[:locked_until] = Time.now + delay
228
+ end
229
+ login_rate_limits[username] = entry
230
+ end
231
+ # Periodic cleanup of expired entries to prevent memory leak
232
+ cleanup_login_rate_limits if login_rate_limits.size > LOGIN_RATE_LIMIT_MAX_ENTRIES
233
+ end
234
+ end
235
+
236
+ # Removes expired entries from the rate limit tracker.
237
+ # Only deletes entries whose lockout has actually expired past the TTL —
238
+ # never deletes pre-lockout failure counters (which would defeat rate limiting
239
+ # by letting an attacker flood random usernames to trigger cleanup and reset
240
+ # a target's in-progress counter).
241
+ def cleanup_login_rate_limits
242
+ now = Time.now
243
+ login_rate_limits.delete_if do |_username, entry|
244
+ entry[:locked_until] && (now - entry[:locked_until]) > LOGIN_RATE_LIMIT_TTL
245
+ end
246
+ end
247
+
248
+ end # Users
249
+ end #API
250
+ end #Parse