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,943 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ # Note: Do not require "../object" here - this file is loaded from object.rb
5
+ # and adding that require would create a circular dependency.
6
+
7
+ module Parse
8
+ class Error
9
+ # 200 Error code indicating that the username is missing or empty.
10
+ class UsernameMissingError < Error; end
11
+
12
+ # 201 Error code indicating that the password is missing or empty.
13
+ class PasswordMissingError < Error; end
14
+
15
+ # Error code 202: indicating that the username has already been taken.
16
+ class UsernameTakenError < Error; end
17
+
18
+ # 203 Error code indicating that the email has already been taken.
19
+ class EmailTakenError < Error; end
20
+
21
+ # 204 Error code indicating that the email is missing, but must be specified.
22
+ class EmailMissing < Error; end
23
+
24
+ # 205 Error code indicating that a user with the specified email was not found.
25
+ class EmailNotFound < Error; end
26
+
27
+ # 125 Error code indicating that the email address was invalid.
28
+ class InvalidEmailAddress < Error; end
29
+ end
30
+
31
+ # The main class representing the _User table in Parse. A user can either be signed up or anonymous.
32
+ # All users need to have a username and a password, with email being optional but globally unique if set.
33
+ # You may add additional properties by redeclaring the class to match your specific schema.
34
+ #
35
+ # The default schema for the {User} class is as follows:
36
+ #
37
+ # class Parse::User < Parse::Object
38
+ # # See Parse::Object for inherited properties...
39
+ #
40
+ # property :auth_data, :object
41
+ # property :username
42
+ # property :password
43
+ # property :email
44
+ #
45
+ # has_many :active_sessions, as: :session
46
+ # end
47
+ #
48
+ # *Signup*
49
+ #
50
+ # You can signup new users in two ways. You can either use a class method
51
+ # {Parse::User.signup} to create a new user with the minimum fields of username,
52
+ # password and email, or create a {Parse::User} object can call the {#signup!}
53
+ # method. If signup fails, it will raise the corresponding exception.
54
+ #
55
+ # user = Parse::User.signup(username, password, email)
56
+ #
57
+ # #or
58
+ # user = Parse::User.new username: "user", password: "s3cret"
59
+ # user.signup!
60
+ #
61
+ # *Login/Logout*
62
+ #
63
+ # With the {Parse::User} class, you can also perform login and logout
64
+ # functionality. The class special accessors for {#session_token} and {#session}
65
+ # to manage its authentication state. This will allow you to authenticate
66
+ # users as well as perform Parse queries as a specific user using their session
67
+ # token. To login a user, use the {Parse::User.login} method by supplying the
68
+ # corresponding username and password, or if you already have a user record,
69
+ # use {#login!} with the proper password.
70
+ #
71
+ # user = Parse::User.login(username,password)
72
+ # user.session_token # session token from a Parse::Session
73
+ # user.session # Parse::Session tied to the token
74
+ #
75
+ # # You can login user records
76
+ # user = Parse::User.first
77
+ # user.session_token # nil
78
+ #
79
+ # passwd = 'p_n7!-e8' # corresponding password
80
+ # user.login!(passwd) # true
81
+ #
82
+ # user.session_token # 'r:pnktnjyb996sj4p156gjtp4im'
83
+ #
84
+ # # logout to delete the session
85
+ # user.logout
86
+ #
87
+ # If you happen to already have a valid session token, you can use it to
88
+ # retrieve the corresponding Parse::User.
89
+ #
90
+ # # finds user with session token
91
+ # user = Parse::User.session(session_token)
92
+ #
93
+ # user.logout # deletes the corresponding session
94
+ #
95
+ # *OAuth-Login*
96
+ #
97
+ # You can signup users using third-party services like Facebook and Twitter as
98
+ # described in {http://docs.parseplatform.org/rest/guide/#signing-up
99
+ # Signing Up and Logging In}. To do this with Parse-Stack, you can call the
100
+ # {Parse::User.autologin_service} method by passing the service name and the
101
+ # corresponding authentication hash data. For a listing of supported third-party
102
+ # authentication services, see {http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication OAuth}.
103
+ #
104
+ # fb_auth = {}
105
+ # fb_auth[:id] = "123456789"
106
+ # fb_auth[:access_token] = "SaMpLeAAiZBLR995wxBvSGNoTrEaL"
107
+ # fb_auth[:expiration_date] = "2025-02-21T23:49:36.353Z"
108
+ #
109
+ # # signup or login a user with this auth data.
110
+ # user = Parse::User.autologin_service(:facebook, fb_auth)
111
+ #
112
+ # You may also combine both approaches of signing up a new user with a
113
+ # third-party service and set additional custom fields. For this, use the
114
+ # method {Parse::User.create}.
115
+ #
116
+ # # or to signup a user with additional data, but linked to Facebook
117
+ # data = {
118
+ # username: "johnsmith",
119
+ # name: "John",
120
+ # email: "user@example.com",
121
+ # authData: { facebook: fb_auth }
122
+ # }
123
+ # user = Parse::User.create data
124
+ #
125
+ # *Linking/Unlinking*
126
+ #
127
+ # You can link or unlink user accounts with third-party services like
128
+ # Facebook and Twitter as described in:
129
+ # {http://docs.parseplatform.org/rest/guide/#linking-users Linking and Unlinking Users}.
130
+ # To do this, you must first get the corresponding authentication data for the
131
+ # specific service, and then apply it to the user using the linking and
132
+ # unlinking methods. Each method returns true or false if the action was
133
+ # successful. For a listing of supported third-party authentication services,
134
+ # see {http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication OAuth}.
135
+ #
136
+ # user = Parse::User.first
137
+ #
138
+ # fb_auth = { ... } # Facebook auth data
139
+ #
140
+ # # Link this user's Facebook account with Parse
141
+ # user.link_auth_data! :facebook, fb_auth
142
+ #
143
+ # # Unlinks this user's Facebook account from Parse
144
+ # user.unlink_auth_data! :facebook
145
+ #
146
+ # @see Parse::Object
147
+ class User < Parse::Object
148
+ parse_class Parse::Model::CLASS_USER
149
+
150
+ # When true (default), saving a new {Parse::User} that has a `password`
151
+ # value routes through Parse Server's signup endpoint (`POST /parse/users`)
152
+ # with the `X-Parse-Revocable-Session` header set, so the signup response
153
+ # returns a session token that is applied to the in-memory user object
154
+ # via the standard `sessionToken_set_attribute!` hydration path. Without
155
+ # this flag, `Parse::User.new(...).save!` left `session_token` `nil`
156
+ # because the underlying create path did not request a revocable session.
157
+ #
158
+ # Set to `false` to always create users without requesting a revocable
159
+ # session token - for example, when a master-key server-side script is
160
+ # provisioning user rows that will receive credentials later. New users
161
+ # created with no password always fall through to the standard create
162
+ # path regardless of this flag.
163
+ #
164
+ # `auth_data` (federated identity / OAuth) signup is deliberately NOT
165
+ # triggered by this flag. `POST /parse/users` treats `auth_data` as a
166
+ # claim against an existing account, so allowing mass-assigned `auth_data`
167
+ # to trigger a revocable-session signup would let attacker-controlled
168
+ # params plant another user's session token onto the in-memory object.
169
+ # Use {.autologin_service} or {.signup} (the explicit class methods) for
170
+ # OAuth-driven signup; both bypass the mass-assignment filter because the
171
+ # caller is explicitly choosing the federated-identity flow.
172
+ #
173
+ # Inherited through subclasses via {ActiveSupport::Concern}'s
174
+ # `class_attribute`, so an application-specific subclass may override
175
+ # the default without affecting `Parse::User` itself.
176
+ #
177
+ # @return [Boolean]
178
+ class_attribute :signup_on_save, instance_writer: false
179
+ self.signup_on_save = true
180
+
181
+ # @return [String] The session token if this user is logged in.
182
+ attr_reader :session_token
183
+
184
+ # @!attribute auth_data
185
+ # The auth data for this Parse::User. Depending on how this user is authenticated or
186
+ # logged in, the contents may be different, especially if you are using another third-party
187
+ # authentication mechanism like Facebook/Twitter.
188
+ # @return [Hash] Auth data hash object.
189
+ property :auth_data, :object
190
+
191
+ # @!attribute email
192
+ # Emails are optional in Parse, but if set, they must be unique.
193
+ # @return [String] The email field.
194
+ property :email
195
+
196
+ # @overload password=(value)
197
+ # You may set a password for this user when you are creating them. Parse never returns a
198
+ # Parse::User's password when a record is fetched. Therefore, normally this getter is nil.
199
+ # While this API exists, it is recommended you use either the #login! or #signup! methods.
200
+ # (see #login!)
201
+ # @return [String] The password you set.
202
+ property :password
203
+
204
+ # @!attribute username
205
+ # All Parse users have a username and must be globally unique.
206
+ # @return [String] The user's username.
207
+ property :username
208
+
209
+ # @!attribute email_verified
210
+ # Whether this user's email address has been verified. Set by Parse Server
211
+ # when the user follows the verification link delivered by the email
212
+ # adapter, and applied to the in-memory object by {#signup!} / signup-on-save
213
+ # when the server includes it in the signup response (see
214
+ # +SIGNUP_RESPONSE_APPLY_KEYS+).
215
+ # @return [Boolean]
216
+ property :email_verified, :boolean
217
+
218
+ # @!attribute active_sessions
219
+ # A has_many relationship to all {Parse::Session} instances for this user. This
220
+ # will query the _Session collection for all sessions which have this user in it's `user`
221
+ # column.
222
+ # @version 1.7.1
223
+ # @return [Array<Parse::Session>] A list of active Parse::Session objects.
224
+ has_many :active_sessions, as: :session
225
+
226
+ # CHANGE -- ACLs can be managed
227
+ # before_save do
228
+ # # You cannot specify user ACLs.
229
+ # self.clear_attribute_change!([:acl])
230
+ # end
231
+
232
+ # `emailVerified` is server-controlled: Parse Server flips it when the
233
+ # user follows the verification link, and only master-key callers (e.g.
234
+ # a `beforeSignUp` cloud function approving an internal email domain)
235
+ # are meant to set it explicitly. Client writes from any platform —
236
+ # this Ruby SDK, iOS, JS, etc. — are silently reverted at the
237
+ # `_User.beforeSave` webhook boundary.
238
+ #
239
+ # This complements the SDK-side {SERVER_CONTROLLED_KEYS} strip
240
+ # ({strip_server_controlled_keys!}), which removes the field from
241
+ # outbound signup/create bodies before the request leaves the SDK.
242
+ # The guard is the cross-client backstop and only runs when the
243
+ # deployment has the Parse Server webhook callback wired to a Ruby app
244
+ # running the `Parse::Webhooks` middleware. Reads are unaffected — a
245
+ # logged-in user can still see their own `email_verified` flag.
246
+ guard :email_verified, :master_only
247
+
248
+ # @return [Boolean] true if this user is anonymous (i.e. created
249
+ # via the +authData.anonymous+ provider rather than via signup
250
+ # with a username/password or a real OAuth provider).
251
+ def anonymous?
252
+ !anonymous_id.nil?
253
+ end
254
+
255
+ # Returns the anonymous identifier only if this user is anonymous.
256
+ # @see #anonymous?
257
+ # @return [String] The anonymous identifier for this anonymous user.
258
+ def anonymous_id
259
+ auth_data["anonymous"]["id"] if auth_data.present? && auth_data["anonymous"].is_a?(Hash)
260
+ end
261
+
262
+ # Adds the third-party authentication data to for a given service.
263
+ # @param service_name [Symbol] The name of the service (ex. :facebook)
264
+ # @param data [Hash] The body of the OAuth data. Dependent on each service.
265
+ # @raise [Parse::Client::ResponseError] If user was not successfully linked
266
+ def link_auth_data!(service_name, **data)
267
+ response = client.set_service_auth_data(id, service_name, data)
268
+ raise Parse::Client::ResponseError, response if response.error?
269
+ apply_attributes!(response.result)
270
+ end
271
+
272
+ # Removes third-party authentication data for this user
273
+ # @param service_name [Symbol] The name of the third-party service (ex. :facebook)
274
+ # @raise [Parse::Client::ResponseError] If user was not successfully unlinked
275
+ # @return [Boolean] True/false if successful.
276
+ def unlink_auth_data!(service_name)
277
+ response = client.set_service_auth_data(id, service_name, nil)
278
+ raise Parse::Client::ResponseError, response if response.error?
279
+ apply_attributes!(response.result)
280
+ end
281
+
282
+ # @!visibility private
283
+ # So that apply_attributes! works with session_token for login
284
+ def session_token_set_attribute!(token, track = false)
285
+ @session_token = token.to_s
286
+ end
287
+
288
+ alias_method :sessionToken_set_attribute!, :session_token_set_attribute!
289
+
290
+ # @return [Boolean] true if this user has a session token.
291
+ def logged_in?
292
+ self.session_token.present?
293
+ end
294
+
295
+ # Request a password reset for this user
296
+ # @return [Boolean] true if it was successful requested. false otherwise.
297
+ # @see Parse::User.request_password_reset
298
+ def request_password_reset
299
+ return false if email.nil?
300
+ Parse::User.request_password_reset(email)
301
+ end
302
+
303
+ # You may set a password for this user when you are creating them. Parse never returns a
304
+ # @param passwd The user's password to be used for signing up.
305
+ # @raise [Parse::Error::UsernameMissingError] If username is missing.
306
+ # @raise [Parse::Error::PasswordMissingError] If password is missing.
307
+ # @raise [Parse::Error::UsernameTakenError] If the username has already been taken.
308
+ # @raise [Parse::Error::EmailTakenError] If the email has already been taken (or exists in the system).
309
+ # @raise [Parse::Error::InvalidEmailAddress] If the email is invalid.
310
+ # @raise [Parse::Client::ResponseError] An unknown error occurred.
311
+ # @return [Boolean] True if signup it was successful. If it fails an exception is thrown.
312
+ def signup!(passwd = nil)
313
+ self.password = passwd || password
314
+ if username.blank?
315
+ raise Parse::Error::UsernameMissingError, "Signup requires a username."
316
+ end
317
+
318
+ if password.blank?
319
+ raise Parse::Error::PasswordMissingError, "Signup requires a password."
320
+ end
321
+
322
+ signup_attrs = attribute_updates
323
+ # See {#signup_create} for the rationale on the safe-pattern check.
324
+ if self.class.signup_body_self_only_acl_safe?(signup_attrs)
325
+ signup_attrs.except!(:createdAt, :updatedAt, "createdAt", "updatedAt")
326
+ else
327
+ signup_attrs.except!(*Parse::Properties::BASE_FIELD_MAP.flatten)
328
+ end
329
+ self.class.strip_server_controlled_keys!(signup_attrs)
330
+
331
+ # first signup the user, then save any additional attributes
332
+ response = client.create_user signup_attrs
333
+
334
+ if response.success?
335
+ # Restrict what the server can plant into the in-memory user via
336
+ # the signup response, matching the defense in {#signup_create}.
337
+ # `POST /parse/users` legitimately returns objectId, createdAt,
338
+ # updatedAt (extracted into @-vars directly below), sessionToken,
339
+ # and emailVerified. Any other key in the response body --
340
+ # `authData`, `_rperm`, `_wperm`, `roles`, etc. -- is dropped, so
341
+ # a compromised or MITM'd Parse Server cannot use this code path
342
+ # to plant credentials/permissions onto the user we just signed
343
+ # up. The previous `apply_attributes! response.result` accepted
344
+ # every key the server returned through the typed property
345
+ # writers (`authData_set_attribute!` exists because we declare
346
+ # `property :auth_data, :object`), which was a footgun the
347
+ # save-as-signup path had already addressed.
348
+ result = response.result
349
+ @id = result[Parse::Model::OBJECT_ID] || @id
350
+ @created_at = result["createdAt"] || @created_at
351
+ @updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
352
+ set_attributes!(result.slice(*SIGNUP_RESPONSE_APPLY_KEYS))
353
+ # Drop the plaintext password from memory now that the server
354
+ # has it hashed and we no longer need it. Matches the Parse JS
355
+ # SDK behavior of clearing the password attribute after a
356
+ # successful save/signup. Uses direct ivar assignment so the
357
+ # dirty tracker doesn't record this clear as a pending change
358
+ # that would be re-sent on the next save.
359
+ @password = nil
360
+ # Mirror Parse::Object#save: a successful round-trip means the
361
+ # locally-set credential fields are now in sync with the server
362
+ # and must NOT be re-sent on the next save. Without this, a
363
+ # subsequent user.save! re-transmits `password`, which Parse
364
+ # Server treats as a password change under
365
+ # revokeSessionOnPasswordReset and revokes the session just
366
+ # minted by this signup.
367
+ changes_applied!
368
+ clear_partial_fetch_state!
369
+ return true
370
+ end
371
+
372
+ case response.code
373
+ when Parse::Response::ERROR_USERNAME_MISSING
374
+ raise Parse::Error::UsernameMissingError, response
375
+ when Parse::Response::ERROR_PASSWORD_MISSING
376
+ raise Parse::Error::PasswordMissingError, response
377
+ when Parse::Response::ERROR_USERNAME_TAKEN
378
+ raise Parse::Error::UsernameTakenError, response
379
+ when Parse::Response::ERROR_EMAIL_TAKEN
380
+ raise Parse::Error::EmailTakenError, response
381
+ when Parse::Response::ERROR_EMAIL_INVALID
382
+ raise Parse::Error::InvalidEmailAddress, response
383
+ end
384
+ raise Parse::Client::ResponseError, response
385
+ end
386
+
387
+ # Override of {Parse::Core::Actions::InstanceMethods#create} so that
388
+ # saving a new user that has a `password` goes through Parse Server's
389
+ # signup endpoint and the returned session token is applied to the
390
+ # in-memory object. Falls through to the inherited raw `_User` insert
391
+ # when the new user has no password or when {.signup_on_save} has been
392
+ # disabled. Like the inherited `:create` path, the `before_create` /
393
+ # `after_create` callback chain still fires and the method returns the
394
+ # response's success flag (errors propagate to {Parse::Object#save} as
395
+ # a `false` return, which the caller may turn into a
396
+ # {Parse::RecordNotSaved} via `save!` / `autoraise: true`).
397
+ #
398
+ # `auth_data`-only signups (federated-identity / OAuth flows where no
399
+ # password is set) are deliberately NOT routed through this path,
400
+ # because `POST /parse/users` treats `auth_data` as an identity claim
401
+ # against an existing user — accepting it from a mass-assigned hash
402
+ # would expose a session-token planting vector. OAuth signup is the
403
+ # responsibility of the explicit {#signup!} method (or
404
+ # {Parse::User.autologin_service}), whose call sites necessarily make
405
+ # the federated-identity decision themselves.
406
+ # @!visibility private
407
+ def create
408
+ if self.class.signup_on_save && self.password.present?
409
+ signup_create
410
+ else
411
+ super
412
+ end
413
+ end
414
+
415
+ # Login and get a session token for this user.
416
+ # @param passwd [String] The password for this user.
417
+ # @return [Boolean] True/false if we received a valid session token.
418
+ def login!(passwd = nil)
419
+ self.password = passwd || self.password
420
+ response = client.login(username.to_s, password.to_s)
421
+ if response.success?
422
+ # Unlike signup, login's response is the canonical state of an
423
+ # existing user, including any linked authData. Applying the
424
+ # full response body here is intentional -- the server is
425
+ # telling us what the account currently looks like. (Compare
426
+ # signup, where we narrow to an allow-list because a brand-new
427
+ # account has no legitimate authData to report.)
428
+ apply_attributes! response.result
429
+ # Drop the plaintext password from memory now that the login
430
+ # has succeeded. Direct ivar assignment so the dirty tracker
431
+ # doesn't record this clear as a pending change.
432
+ @password = nil
433
+ # Clear dirty state so a subsequent user.save! does not re-send
434
+ # `password` (which Parse Server would treat as a password
435
+ # change and use to revoke the session this login just issued).
436
+ # See the matching note in #signup!.
437
+ changes_applied!
438
+ clear_partial_fetch_state!
439
+ end
440
+ self.session_token.present?
441
+ end
442
+
443
+ # Invalid the current session token for this logged in user.
444
+ # @return [Boolean] True/false if successful
445
+ def logout
446
+ return true if self.session_token.blank?
447
+ client.logout session_token
448
+ self.session_token = nil
449
+ true
450
+ rescue
451
+ false
452
+ end
453
+
454
+ # @!visibility private
455
+ def session_token=(token)
456
+ @session = nil
457
+ @session_token = token
458
+ end
459
+
460
+ # @return [Session] the session corresponding to the user's session token.
461
+ def session
462
+ if @session.blank? && @session_token.present?
463
+ response = client.fetch_session(@session_token)
464
+ # Trusted hydration: +response.result+ is the server-side
465
+ # _Session row, which legitimately includes +sessionToken+,
466
+ # +createdAt+, +updatedAt+, and other protected keys. Route
467
+ # through {Parse::Object.build} which handles the trusted-init
468
+ # signalling.
469
+ @session ||= Parse::Object.build(response.result, Parse::Model::CLASS_SESSION)
470
+ end
471
+ @session
472
+ end
473
+
474
+ # @!visibility private
475
+ # Keys that must never flow through +Parse::User.create+ from a
476
+ # mass-assigned hash. +authData+ on the user-signup endpoint causes
477
+ # Parse Server to silently log into the existing account that matches
478
+ # that auth_data and return ITS sessionToken — full account takeover
479
+ # if the caller blindly forwards client-supplied parameters.
480
+ # +objectId+ allows the caller to pick the user's identifier on
481
+ # creation, sometimes targetable depending on Parse Server config.
482
+ UNSAFE_CREATE_KEYS = %i[authData auth_data objectId id].freeze
483
+
484
+ # @!visibility private
485
+ # Fields that are server-controlled and must be stripped from any body
486
+ # that the SDK sends to the signup endpoint or +Parse::User.create+,
487
+ # regardless of who supplied them. Unlike {UNSAFE_CREATE_KEYS}, passing
488
+ # one of these is not refused (no exception is raised); the field is
489
+ # silently dropped before wire transit.
490
+ #
491
+ # +emailVerified+ is the canonical case: Parse Server's default `_User`
492
+ # CLP restricts writes to the master key, so a caller-supplied value
493
+ # would normally be rejected anyway — but the SDK strips it as
494
+ # defense-in-depth so signup with mass-assigned attributes cannot
495
+ # smuggle a verified=true onto a brand-new account if the deployment
496
+ # has loosened the default CLP. (Update-path coverage is handled by
497
+ # the {Parse::Core::FieldGuards} declaration
498
+ # {guard :email_verified, :master_only} below, which silently reverts
499
+ # client writes at the `_User.beforeSave` webhook boundary.)
500
+ #
501
+ # The underscore-prefixed entries are internal Parse Server `_User`
502
+ # bookkeeping columns (verify tokens, perishable tokens, the bcrypt
503
+ # password hash, lockout state, etc.). Parse Server rejects writes to
504
+ # them from non-master callers anyway, but the SDK strips them as a
505
+ # belt-and-suspenders measure so a mass-assigned hash from request
506
+ # parameters cannot reach the wire with these keys at all.
507
+ #
508
+ # The trusted signup-response apply path ({SIGNUP_RESPONSE_APPLY_KEYS})
509
+ # is unaffected by this strip because it uses {#set_attributes!}, not
510
+ # the dirty-tracked setter that {#attribute_updates} reads from.
511
+ SERVER_CONTROLLED_KEYS = %i[
512
+ emailVerified email_verified
513
+ _hashed_password
514
+ _email_verify_token _email_verify_token_expires_at
515
+ _perishable_token _perishable_token_expires_at
516
+ _password_history
517
+ _failed_login_count
518
+ _account_lockout_expires_at
519
+ ].freeze
520
+
521
+ # Creates a new Parse::User given a hash that maps to the fields defined in your Parse::User collection.
522
+ #
523
+ # Mass-assignment of +authData+/+auth_data+/+objectId+ is refused. If you
524
+ # intend to create-or-login a user via federated identity, use
525
+ # {.autologin_service} or {.link_or_create_with_auth_data}. Passing
526
+ # those keys directly bypasses the SDK's federated-identity wrapper
527
+ # and risks returning a victim's sessionToken to whoever submitted
528
+ # the request.
529
+ #
530
+ # @param body [Hash] The hash containing the Parse::User fields. The field `username` and `password` are required.
531
+ # @option opts [Boolean] :master_key Whether the master key should be used for this request.
532
+ # @raise [ArgumentError] If +body+ contains +authData+/+auth_data+/+objectId+ — use {.autologin_service} for federated flows.
533
+ # @raise [Parse::Error::UsernameMissingError] If username is missing.
534
+ # @raise [Parse::Error::PasswordMissingError] If password is missing.
535
+ # @raise [Parse::Error::UsernameTakenError] If the username has already been taken.
536
+ # @raise [Parse::Error::EmailTakenError] If the email has already been taken (or exists in the system).
537
+ # @raise [Parse::Error::InvalidEmailAddress] If the email is invalid.
538
+ # @raise [Parse::Client::ResponseError] An unknown error occurred.
539
+ # @return [User] Returns a successfully created Parse::User.
540
+ def self.create(body, **opts)
541
+ # Consume and clear the SDK-internal trust marker before validation
542
+ # or wire transit. This prevents trusted-authdata flag smuggling
543
+ # through callers that copy hashes from a request parameter.
544
+ trusted = body.is_a?(Hash) ? (body.delete(:__parse_stack_trusted_authdata) ||
545
+ body.delete("__parse_stack_trusted_authdata")) : false
546
+ assert_create_body_safe!(body) unless trusted
547
+ strip_server_controlled_keys!(body)
548
+ response = client.create_user(body, opts: opts)
549
+ if response.success?
550
+ body.delete :password # clear password before merging
551
+ return Parse::User.build body.merge(response.result)
552
+ end
553
+
554
+ case response.code
555
+ when Parse::Response::ERROR_USERNAME_MISSING
556
+ raise Parse::Error::UsernameMissingError, response
557
+ when Parse::Response::ERROR_PASSWORD_MISSING
558
+ raise Parse::Error::PasswordMissingError, response
559
+ when Parse::Response::ERROR_USERNAME_TAKEN
560
+ raise Parse::Error::UsernameTakenError, response
561
+ when Parse::Response::ERROR_EMAIL_TAKEN
562
+ raise Parse::Error::EmailTakenError, response
563
+ end
564
+ raise Parse::Client::ResponseError, response
565
+ end
566
+
567
+ # @!visibility private
568
+ # Silently strips {SERVER_CONTROLLED_KEYS} from +body+ in place. Used
569
+ # by {.create}, {#signup!}, and {#signup_create} as defense-in-depth so
570
+ # caller-supplied values for fields that Parse Server is meant to
571
+ # control (currently just +emailVerified+) never reach the wire.
572
+ # @return [Hash, Object] the same +body+ object, mutated.
573
+ def self.strip_server_controlled_keys!(body)
574
+ return body unless body.is_a?(Hash)
575
+ SERVER_CONTROLLED_KEYS.each do |k|
576
+ body.delete(k)
577
+ body.delete(k.to_s)
578
+ end
579
+ body
580
+ end
581
+
582
+ # @!visibility private
583
+ # Raises +ArgumentError+ if +body+ carries keys that would let an
584
+ # attacker turn +Parse::User.create+ into an account-takeover sink.
585
+ # Skipped when called through the SDK's federated-identity wrapper
586
+ # ({.autologin_service}), which deliberately supplies +authData+ and
587
+ # is responsible for its provenance.
588
+ def self.assert_create_body_safe!(body)
589
+ return unless body.is_a?(Hash)
590
+ unsafe = body.each_key.select do |k|
591
+ ks = k.is_a?(String) ? k.to_sym : k
592
+ UNSAFE_CREATE_KEYS.include?(ks)
593
+ end
594
+ unless unsafe.empty?
595
+ raise ArgumentError,
596
+ "Refusing Parse::User.create with #{unsafe.inspect}. " \
597
+ "These keys can be used for account takeover via federated-id " \
598
+ "linking. Use Parse::User.autologin_service for federated " \
599
+ "flows, or pass authData via that wrapper."
600
+ end
601
+ end
602
+
603
+ # Automatically and implicitly signup a user if it did not already exists and
604
+ # authenticates them (login) using third-party authentication data. May raise exceptions
605
+ # similar to `create` depending on what you provide the _body_ parameter.
606
+ # @param service_name [Symbol] the name of the service key (ex. :facebook)
607
+ # @param auth_data [Hash] the specific service data to place in the user's auth-data for this service.
608
+ # @param body [Hash] any additional User related fields or properties when signing up this User record.
609
+ # @return [User] a logged in user, or nil.
610
+ # @see User.create
611
+ def self.autologin_service(service_name, auth_data, body: {})
612
+ # Trust-mark this call so {.assert_create_body_safe!} permits the
613
+ # +authData+ that we are explicitly responsible for here. The
614
+ # marker is consumed inside {.create} before forwarding to the
615
+ # server.
616
+ body = body.merge({
617
+ authData: { service_name => auth_data },
618
+ __parse_stack_trusted_authdata: true,
619
+ })
620
+ self.create(body)
621
+ end
622
+
623
+ # This method will signup a new user using the parameters below. The required fields
624
+ # to create a user in Parse is the _username_ and _password_ fields. The _email_ field is optional.
625
+ # Both _username_ and _email_ (if provided), must be unique. At a minimum, it is recommended you perform
626
+ # a query using the supplied _username_ first to verify do not already have an account with that username.
627
+ # This method will raise all the exceptions from the similar `create` method.
628
+ # @see User.create
629
+ def self.signup(username, password, email = nil, body: {})
630
+ body = body.merge({ username: username, password: password })
631
+ body[:email] = email if email.present?
632
+ self.create(body)
633
+ end
634
+
635
+ # Login and return a Parse::User with this username/password combination.
636
+ # @param username [String] the user's username
637
+ # @param password [String] the user's password
638
+ # @return [User] a logged in user for the provided username. Returns nil otherwise.
639
+ def self.login(username, password)
640
+ response = client.login(username.to_s, password.to_s)
641
+ response.success? ? Parse::User.build(response.result) : nil
642
+ end
643
+
644
+ # Request a password reset for a registered email.
645
+ # @example
646
+ # user = Parse::User.first
647
+ #
648
+ # # pass a user object
649
+ # Parse::User.request_password_reset user
650
+ # # or email
651
+ # Parse::User.request_password_reset("user@example.com")
652
+ # @param email [String] The user's email address.
653
+ # @return [Boolean] True/false if successful.
654
+ def self.request_password_reset(email)
655
+ email = email.email if email.is_a?(Parse::User)
656
+ return false if email.blank?
657
+ response = client.request_password_reset(email)
658
+ response.success?
659
+ end
660
+
661
+ # Same as `session!` but returns nil if a user was not found or sesion token was invalid.
662
+ # @return [User] the user matching this active token, otherwise nil.
663
+ # @see #session!
664
+ def self.session(token, opts = {})
665
+ self.session! token, opts
666
+ rescue Parse::Error::InvalidSessionTokenError
667
+ nil
668
+ end
669
+
670
+ # Return a Parse::User for this active session token.
671
+ # @raise [InvalidSessionTokenError] Invalid session token.
672
+ # @return [User] the user matching this active token
673
+ # @see #session
674
+ def self.session!(token, opts = {})
675
+ # support Parse::Session objects
676
+ token = token.session_token if token.respond_to?(:session_token)
677
+ response = client.current_user(token, **opts)
678
+ response.success? ? Parse::User.build(response.result) : nil
679
+ end
680
+
681
+ # If the current session token for this instance is nil, this method finds
682
+ # the most recent active Parse::Session token for this user and applies it to the instance.
683
+ # The user instance will now be authenticated and logged in with the selected session token.
684
+ # Useful if you need to call save or destroy methods on behalf of a logged in user.
685
+ # @return [String] The session token or nil if no session was found for this user.
686
+ def any_session!
687
+ unless @session_token.present?
688
+ _active_session = active_sessions(restricted: false, order: :updated_at.desc).first
689
+ self.session_token = _active_session.session_token if _active_session.present?
690
+ end
691
+ @session_token
692
+ end
693
+
694
+ # =========================================================================
695
+ # Session Management Methods
696
+ # =========================================================================
697
+
698
+ # Logout from all sessions, effectively signing out on all devices.
699
+ # Optionally keep the current session active.
700
+ # @param keep_current [Boolean] if true, keeps the current session active (default: false)
701
+ # @return [Integer] the number of sessions revoked
702
+ # @example
703
+ # # Logout from all devices
704
+ # user.logout_all!
705
+ #
706
+ # # Logout from all devices except current
707
+ # user.logout_all!(keep_current: true)
708
+ def logout_all!(keep_current: false)
709
+ return 0 unless id.present?
710
+ except_token = keep_current ? @session_token : nil
711
+ count = Parse::Session.revoke_all_for_user(self, except: except_token)
712
+ @session_token = nil unless keep_current
713
+ @session = nil unless keep_current
714
+ count
715
+ end
716
+
717
+ # Get the count of active (non-expired) sessions for this user.
718
+ # @return [Integer] the number of active sessions
719
+ # @example
720
+ # count = user.active_session_count
721
+ # puts "User is logged in on #{count} devices"
722
+ def active_session_count
723
+ return 0 unless id.present?
724
+ Parse::Session.active_count_for_user(self)
725
+ end
726
+
727
+ # Get all active sessions for this user.
728
+ # @return [Array<Parse::Session>] array of active session objects
729
+ # @example
730
+ # user.sessions.each do |session|
731
+ # puts "Session created: #{session.created_at}"
732
+ # end
733
+ def sessions
734
+ return [] unless id.present?
735
+ Parse::Session.for_user(self).all
736
+ end
737
+
738
+ # Check if this user has multiple active sessions (logged in on multiple devices).
739
+ # @return [Boolean] true if user has more than one active session
740
+ # @example
741
+ # if user.multi_session?
742
+ # puts "User is logged in on multiple devices"
743
+ # end
744
+ def multi_session?
745
+ active_session_count > 1
746
+ end
747
+
748
+ # Return the transitive upward closure of role names this user
749
+ # inherits permissions from.
750
+ #
751
+ # ## Authorization
752
+ #
753
+ # The role graph is privileged data: Parse Server's `_Role` class
754
+ # ships with `acl_policy :private` precisely so anonymous clients
755
+ # cannot enumerate role memberships. This method therefore routes
756
+ # through the mongo-direct fast path under an EXPLICIT
757
+ # authorization scope.
758
+ #
759
+ # By default, `as:` is set to `self` — the user instance itself,
760
+ # meaning "I (this user) am asking about my own roles". The scope
761
+ # is resolved via {Parse::ACLScope} and CLP is enforced against
762
+ # `_Role`: the call succeeds iff the user's permission set
763
+ # (`["*", user.id, "role:..."]`) is permitted to `find` on
764
+ # `_Role`. Under Parse Server's default `_Role` CLP (master-only,
765
+ # which {Parse::Role}'s `acl_policy :private` does not change),
766
+ # the user's scope is NOT permitted, so this call raises
767
+ # {Parse::CLPScope::Denied}. Apps that have explicitly opened
768
+ # `_Role` CLP for authenticated users (e.g. `find:
769
+ # { requiresAuthentication: true }`) will have the call succeed.
770
+ #
771
+ # Callers performing privileged work (computing ACL permission
772
+ # sets, e.g. server-side filters) should pass `master: true` to
773
+ # bypass the CLP check.
774
+ #
775
+ # **Breaking change:** Previously this method bypassed the
776
+ # authorization check entirely (callers could construct a
777
+ # `Parse::User` with any objectId via
778
+ # `Parse::User.new.tap { |u| u.id = victim_id }` and enumerate
779
+ # the victim's roles). The new contract is explicit-auth-required;
780
+ # use `master: true` for the previous behavior.
781
+ #
782
+ # @param max_depth [Integer] maximum BFS depth (default: 10).
783
+ # @param master [Boolean] when +true+, bypass `_Role` CLP and run
784
+ # the role-graph lookup under master mode. Use for ACL-building
785
+ # code paths inside the SDK or in admin tooling.
786
+ # @param as [Parse::User, Parse::Pointer, nil] caller-scope. When
787
+ # `nil`, defaults to `self` (the user-asking-about-their-own-roles
788
+ # case). Pass a different user to ask "what would this caller
789
+ # see when introspecting this user's roles?"; the scope's
790
+ # permission set is checked against `_Role` CLP.
791
+ # @return [Set<String>] role names (no +role:+ prefix). Empty set
792
+ # when the user has no objectId yet or holds no roles.
793
+ # @raise [Parse::CLPScope::Denied] when the scope cannot `find`
794
+ # on `_Role` under the current CLP.
795
+ # @example
796
+ # # User reading their own roles (subject to _Role CLP):
797
+ # permission_set = (["*", user.id] + user.acl_roles.map { |n| "role:#{n}" }).uniq
798
+ # # Admin/SDK-internal code building ACL filters:
799
+ # permission_set = (["*", user.id] + user.acl_roles(master: true).map { |n| "role:#{n}" }).uniq
800
+ def acl_roles(max_depth: 10, master: false, as: nil)
801
+ return Set.new unless id.is_a?(String) && !id.empty?
802
+ # Default `as:` to self so the common "user reading their own
803
+ # roles" case works without ceremony when _Role CLP permits the
804
+ # user. The CLP check + scope resolution happens inside
805
+ # Parse::Role.all_for_user → Parse::MongoDB.role_names_for_user.
806
+ effective_as = as.nil? && master != true ? self : as
807
+ Parse::Role.all_for_user(
808
+ self, max_depth: max_depth, master: master, as: effective_as,
809
+ )
810
+ end
811
+
812
+ private
813
+
814
+ # Keys that {#signup_create} will accept from a `POST /parse/users`
815
+ # response body and feed through {#set_attributes!}. `sessionToken`
816
+ # is the operative output of the signup endpoint; `emailVerified` is
817
+ # the only other field Parse Server commonly emits and is harmless to
818
+ # apply. All other keys are dropped, even if the server response
819
+ # contains them — this blocks a compromised or MITM'd Parse Server
820
+ # from planting `authData`, `_rperm`, `_wperm`, `roles`, or other
821
+ # security-sensitive fields into the in-memory user object via the
822
+ # save-as-signup path. `objectId`, `createdAt`, and `updatedAt` are
823
+ # extracted directly into the corresponding `@`-vars below and so do
824
+ # not need to appear in this list.
825
+ SIGNUP_RESPONSE_APPLY_KEYS = %w[sessionToken emailVerified].freeze
826
+
827
+ # Strict matcher for a client-supplied `objectId` that the SDK could
828
+ # plausibly have generated via Parse::Core::ParseReference. Used by
829
+ # {.signup_body_self_only_acl_safe?} to gate the narrow whitelist of
830
+ # client-supplied ACL+objectId pairs allowed through the signup body.
831
+ PARSE_OBJECT_ID_FORMAT = /\A[A-Za-z0-9]{10}\z/.freeze
832
+
833
+ # True when the signup-body `objectId` and `ACL` together describe the
834
+ # safe self-only ownership pattern that {acl_policy} produces under
835
+ # `owner: :self`: the body has a client-assigned `objectId` matching
836
+ # the Parse-id format, and the ACL has exactly one entry granting
837
+ # read+write to that same objectId. Any deviation — multiple keys, a
838
+ # non-self key, a `*` (public) entry, a `role:` entry, missing or
839
+ # extra permissions — fails the check and the strip-everything fallback
840
+ # in {#signup_create} / {#signup!} runs as before.
841
+ # @param body [Hash] signup request body, with symbol or string keys.
842
+ # @return [Boolean]
843
+ # @api private
844
+ def self.signup_body_self_only_acl_safe?(body)
845
+ return false unless body.is_a?(Hash)
846
+ oid = body[:objectId] || body["objectId"]
847
+ acl = body[:ACL] || body["ACL"]
848
+ return false unless oid.is_a?(String) && oid.match?(PARSE_OBJECT_ID_FORMAT)
849
+ return false unless acl.is_a?(Hash) && acl.size == 1
850
+ perms = acl[oid] || acl[oid.to_s]
851
+ return false unless perms.is_a?(Hash)
852
+ normalized = perms.transform_keys(&:to_s)
853
+ normalized == { "read" => true, "write" => true }
854
+ end
855
+
856
+ # Body of {#create} when signup-on-save applies. Mirrors the inherited
857
+ # Parse::Object create path but uses `create_user` (signup endpoint)
858
+ # instead of `create_object`, and so picks up the `sessionToken` that
859
+ # Parse Server only emits on the signup endpoint. Errors are not
860
+ # promoted to typed exceptions here (see {#signup!} for that variant);
861
+ # the response's success flag is returned so the caller's `save` /
862
+ # `save!` handles the failure via the standard `RecordNotSaved` path.
863
+ def signup_create
864
+ run_callbacks :create do
865
+ body = attribute_updates
866
+ # Strip server-managed and special fields from the request body.
867
+ # createdAt/updatedAt are always stripped (purely server-managed).
868
+ # objectId/ACL are normally stripped too (to prevent a caller
869
+ # planting a permissive ACL or a colliding objectId), but the
870
+ # narrow self-only ownership pattern produced by
871
+ # `acl_policy ..., owner: :self` is allowed through so the user
872
+ # can be created with self-R/W-only ACL in a single roundtrip.
873
+ if self.class.signup_body_self_only_acl_safe?(body)
874
+ body.except!(:createdAt, :updatedAt, "createdAt", "updatedAt")
875
+ else
876
+ body.except!(*Parse::Properties::BASE_FIELD_MAP.flatten)
877
+ end
878
+ self.class.strip_server_controlled_keys!(body)
879
+ # Anonymous signup: do NOT forward the caller's session token to
880
+ # POST /parse/users. The caller may be authenticated for an
881
+ # unrelated reason (e.g., an admin app session running a signup
882
+ # flow on behalf of someone else), but the user being created is
883
+ # by definition someone new. Forwarding `_session_token` makes
884
+ # Cloud Code `beforeSave(_User)` see `request.user = caller`,
885
+ # which an integrator can mistake for "the new user". The signup
886
+ # endpoint authenticates by the signup itself, not by a prior
887
+ # session — pass `nil` explicitly. Master key continues to flow
888
+ # via the normal authentication middleware when configured.
889
+ res = client.create_user(body, session_token: nil)
890
+ unless res.error?
891
+ result = res.result
892
+ @id = result[Parse::Model::OBJECT_ID] || @id
893
+ @created_at = result["createdAt"] || @created_at
894
+ @updated_at = result["updatedAt"] || result["createdAt"] || @updated_at
895
+ # Plaintext password is no longer needed locally; the server
896
+ # has it hashed. Direct ivar assignment avoids re-dirtying the
897
+ # field.
898
+ @password = nil
899
+ set_attributes!(result.slice(*SIGNUP_RESPONSE_APPLY_KEYS))
900
+ # Promote the freshly-applied session token into `@_session_token`
901
+ # so any in-flight after_create callback that calls back through
902
+ # the SDK authenticates as the just-signed-up user. Without this,
903
+ # the after_create `_assign_<field>!` callback installed by
904
+ # `parse_reference` (and any other after_create hook that issues
905
+ # an `update!`) reads `_session_token` (actions.rb:732) and finds
906
+ # nil — `client.update_object(..., session_token: nil)` then
907
+ # silently falls back to the master key under any configuration
908
+ # that supplies one (client.rb:682-687 only attaches the session
909
+ # token when `present?`; `DISABLE_MASTER_KEY` is not set on the
910
+ # nil branch). The result was a user-scoped PUT silently
911
+ # escalated to master-key authority, bypassing CLP and
912
+ # `request.user` checks in `beforeSave` cloud code. Promoting
913
+ # the new user's own session token here scopes the follow-up
914
+ # update to the just-created user — the appropriate authority
915
+ # for writes to their own row. The outer `save` zeroes
916
+ # `@_session_token` again at actions.rb:830, so the promotion
917
+ # is bounded by this in-flight save. The trust boundary here
918
+ # is identical to the existing `SIGNUP_RESPONSE_APPLY_KEYS`
919
+ # contract: the SDK already trusts `sessionToken` from a signup
920
+ # response (it has to, to honor the signup contract); this fix
921
+ # routes that same token to the in-flight auth context.
922
+ @_session_token = @session_token if @session_token.present?
923
+ # Clear dirty state BEFORE the `after_create` callback chain
924
+ # fires. If a subclass declares `parse_reference` (default
925
+ # field name with `precompute: false`), the after_create
926
+ # `_assign_<field>!` callback issues an `update!` from inside
927
+ # this `run_callbacks :create` block — and `attribute_updates`
928
+ # would otherwise still carry `password` as dirty with a nil
929
+ # current value, serializing as `password: { __op: "Delete" }`.
930
+ # Parse Server's `_User` write path feeds that hash to
931
+ # `@node-rs/bcrypt`, which raises
932
+ # `Value is non of these types TypedArray<u8>, String`. Same
933
+ # cleanup as `signup!`, just timed so the after_create
934
+ # callbacks see a clean dirty set.
935
+ changes_applied!
936
+ clear_partial_fetch_state!
937
+ end
938
+ puts "Error creating #{self.parse_class}: #{res.error}" if res.error?
939
+ res.success?
940
+ end
941
+ end
942
+ end
943
+ end