parse-stack-next 5.3.0 → 5.4.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +461 -0
  4. data/Gemfile +7 -0
  5. data/Gemfile.lock +12 -4
  6. data/README.md +160 -3
  7. data/Rakefile +52 -3
  8. data/docs/atlas_vector_search_guide.md +86 -2
  9. data/docs/client_sdk_guide.md +5 -0
  10. data/docs/mcp_guide.md +59 -4
  11. data/docs/mongodb_direct_guide.md +93 -1
  12. data/docs/usage_guide.md +11 -1
  13. data/docs/webhooks_guide.md +418 -0
  14. data/examples/README.md +46 -0
  15. data/examples/basic_client.rb +93 -0
  16. data/examples/basic_server.rb +109 -0
  17. data/examples/live_query_listener.rb +98 -0
  18. data/examples/rag_chatbot.rb +221 -0
  19. data/examples/webhook_server.rb +111 -0
  20. data/lib/parse/agent/mcp_rack_app.rb +285 -62
  21. data/lib/parse/agent/tools.rb +45 -5
  22. data/lib/parse/api/aggregate.rb +7 -1
  23. data/lib/parse/api/cloud_functions.rb +12 -4
  24. data/lib/parse/api/hooks.rb +46 -9
  25. data/lib/parse/api/objects.rb +16 -2
  26. data/lib/parse/api/path_segment.rb +33 -0
  27. data/lib/parse/api/server.rb +94 -0
  28. data/lib/parse/api/users.rb +58 -2
  29. data/lib/parse/atlas_search.rb +7 -7
  30. data/lib/parse/client/body_builder.rb +5 -0
  31. data/lib/parse/client/protocol.rb +4 -0
  32. data/lib/parse/client.rb +55 -2
  33. data/lib/parse/embeddings/spend_cap.rb +255 -0
  34. data/lib/parse/embeddings.rb +1 -0
  35. data/lib/parse/live_query/client.rb +3 -1
  36. data/lib/parse/live_query/subscription.rb +32 -5
  37. data/lib/parse/model/acl.rb +4 -2
  38. data/lib/parse/model/classes/audience.rb +52 -4
  39. data/lib/parse/model/classes/user.rb +180 -3
  40. data/lib/parse/model/core/embed_managed.rb +113 -0
  41. data/lib/parse/model/core/querying.rb +3 -1
  42. data/lib/parse/model/core/vector_searchable.rb +161 -0
  43. data/lib/parse/model/object.rb +28 -5
  44. data/lib/parse/mongodb.rb +7 -1
  45. data/lib/parse/pipeline_security.rb +5 -3
  46. data/lib/parse/query/constraints.rb +29 -0
  47. data/lib/parse/query.rb +265 -27
  48. data/lib/parse/retrieval/agent_tool.rb +49 -0
  49. data/lib/parse/retrieval/reranker/cohere.rb +218 -0
  50. data/lib/parse/retrieval/reranker.rb +157 -0
  51. data/lib/parse/retrieval/retriever.rb +110 -23
  52. data/lib/parse/stack/version.rb +1 -1
  53. data/lib/parse/stack.rb +17 -0
  54. data/lib/parse/two_factor_auth/user_extension.rb +123 -31
  55. data/lib/parse/vector_search/hybrid.rb +578 -0
  56. data/lib/parse/webhooks/payload.rb +252 -7
  57. data/lib/parse/webhooks/trigger_audit.rb +502 -0
  58. data/lib/parse/webhooks.rb +215 -3
  59. data/scripts/docker/Dockerfile.parse +5 -1
  60. data/scripts/docker/docker-compose.test.yml +31 -0
  61. data/scripts/docker/docker-compose.verifyemail.yml +4 -0
  62. data/scripts/docker/preflight.sh +76 -0
  63. data/scripts/start-parse.sh +52 -4
  64. metadata +15 -1
@@ -155,7 +155,7 @@ module Parse
155
155
  },
156
156
  }
157
157
 
158
- response = client.update_user(id, { authData: auth_data_payload }, opts: { session_token: session_token })
158
+ response = client.update_user(id, { authData: auth_data_payload }, session_token: session_token)
159
159
 
160
160
  if response.error?
161
161
  if response.result.to_s.include?("Invalid MFA")
@@ -208,7 +208,7 @@ module Parse
208
208
  },
209
209
  }
210
210
 
211
- response = client.update_user(id, { authData: auth_data_payload }, opts: { session_token: session_token })
211
+ response = client.update_user(id, { authData: auth_data_payload }, session_token: session_token)
212
212
 
213
213
  if response.error?
214
214
  raise Parse::Client::ResponseError, response
@@ -245,7 +245,7 @@ module Parse
245
245
  },
246
246
  }
247
247
 
248
- response = client.update_user(id, { authData: auth_data_payload }, opts: { session_token: session_token })
248
+ response = client.update_user(id, { authData: auth_data_payload }, session_token: session_token)
249
249
 
250
250
  if response.error?
251
251
  if response.result.to_s.include?("Invalid MFA token")
@@ -276,27 +276,60 @@ module Parse
276
276
  raise MFA::NotEnabledError, "MFA is not enabled for this user" unless mfa_enabled?
277
277
  raise ArgumentError, "Current token is required" if current_token.blank?
278
278
 
279
- # To disable, we need to update authData.mfa with the old token for validation
280
- # and then set it to null
281
- auth_data_payload = {
282
- mfa: {
283
- old: current_token,
284
- secret: nil, # Setting to nil disables TOTP
285
- },
286
- }
287
-
288
- response = client.update_user(id, { authData: auth_data_payload }, opts: { session_token: session_token })
289
-
290
- if response.error?
291
- if response.result.to_s.include?("Invalid MFA token")
292
- raise MFA::VerificationError, response.result.to_s
293
- end
294
- raise Parse::Client::ResponseError, response
279
+ # Parse Server's TOTP adapter exposes no first-class "disable via authData
280
+ # update" path its validateUpdate always re-runs setup, so a partial
281
+ # mfa payload is rejected outright. Disabling is therefore a two-step:
282
+ #
283
+ # 1. Prove possession of the current code by submitting it as
284
+ # `{ mfa: { old: <token> } }`. In the *update* context (unlike a
285
+ # fresh login) the adapter validates that code against the stored
286
+ # secret. A WRONG code fails at validateLogin ("Invalid MFA token");
287
+ # a CORRECT code passes validateLogin and is then blocked by the
288
+ # re-setup requirement ("Invalid MFA data") which is precisely the
289
+ # signal that the code was accepted. (This re-entry of the current
290
+ # code is the deliberate confirmation gate for turning MFA off.)
291
+ # 2. Disable MFA by unlinking the provider with `{ mfa: nil }`.
292
+ #
293
+ # This keeps self-disable gated on a valid current code even though the
294
+ # server offers no dedicated TOTP self-disable endpoint.
295
+ verify = client.update_user(id, { authData: { mfa: { old: current_token } } },
296
+ session_token: session_token)
297
+ # Classify the two-step response POSITIVELY instead of treating
298
+ # "anything that isn't success-or-one-magic-string" as a bad
299
+ # token. The current code is ACCEPTED iff the server either
300
+ # succeeds or rejects only the follow-on re-setup ("Invalid MFA
301
+ # data") — that block fires AFTER validateLogin has already
302
+ # accepted the code. A WRONG code fails earlier at validateLogin
303
+ # ("Invalid MFA token"). Any OTHER error (transport, session, 5xx)
304
+ # is a real fault surfaced as-is, not mislabeled a verification
305
+ # failure.
306
+ err = verify.error.to_s
307
+ code_rejected = err.match?(/Invalid MFA token/i)
308
+ code_accepted = verify.success? || err.match?(/Invalid MFA data/i)
309
+ if code_rejected
310
+ raise MFA::VerificationError, "Invalid MFA token"
311
+ elsif !code_accepted
312
+ raise Parse::Client::ResponseError, verify
295
313
  end
296
314
 
297
- # Refresh auth_data
298
- fetch
315
+ response = client.update_user(id, { authData: { mfa: nil } }, session_token: session_token)
316
+ raise Parse::Client::ResponseError, response if response.error?
317
+
318
+ # CONFIRM the disable took effect from the SERVER's own view — a
319
+ # positive post-condition rather than trusting the unlink response
320
+ # alone. We must read the server directly here, NOT lean on the
321
+ # in-memory #mfa_enabled? projection: Parse Server omits +authData+
322
+ # entirely for a user with no providers, so once MFA is unlinked an
323
+ # ordinary fetch carries no +authData+ key at all and therefore can
324
+ # never clear the +{ mfa: { status: "enabled" } }+ value pinned at
325
+ # enrollment. An enabled account's own (session-token) read returns
326
+ # +authData.mfa+; a disabled one omits it — so an absent/mfa-less
327
+ # authData on this trusted self-read is the authoritative signal.
328
+ if mfa_enabled_on_server?
329
+ raise MFA::VerificationError, "MFA disable did not take effect (still enabled after unlink)"
330
+ end
299
331
 
332
+ clear_local_mfa_projection!
300
333
  true
301
334
  end
302
335
 
@@ -315,20 +348,26 @@ module Parse
315
348
  #
316
349
  # @param authorized_by [Parse::User, Parse::Pointer] the operator
317
350
  # performing the override. Required.
318
- # @param admin_role [Parse::Role, String, nil] optional role (or role
319
- # name) that +authorized_by+ must belong to.
351
+ # @param admin_role [Parse::Role, String, nil] role (or role name)
352
+ # that +authorized_by+ must belong to. Library-enforced. Either
353
+ # this or +allow_unverified: true+ is REQUIRED (fail-closed).
354
+ # @param allow_unverified [Boolean] explicitly accept caller-side
355
+ # authorization without a library role check. Defaults to +false+;
356
+ # must be set deliberately to bypass MFA without an +admin_role+.
320
357
  # @return [Boolean] True if disabled successfully.
321
358
  # @raise [ArgumentError] when +authorized_by:+ is missing or not a User.
322
- # @raise [Parse::MFA::ForbiddenError] when +admin_role+ is supplied
359
+ # @raise [Parse::MFA::ForbiddenError] when neither +admin_role+ nor
360
+ # +allow_unverified:+ is supplied, or when +admin_role+ is supplied
323
361
  # and the operator is not a member.
324
362
  #
325
- # @example Caller-verified authorization
326
- # user.disable_mfa_master_key!(authorized_by: current_admin)
327
- #
328
- # @example Library-enforced role check
363
+ # @example Library-enforced role check (preferred)
329
364
  # user.disable_mfa_master_key!(authorized_by: current_admin,
330
365
  # admin_role: "Admin")
331
- def disable_mfa_master_key!(authorized_by:, admin_role: nil)
366
+ #
367
+ # @example Caller-verified authorization (explicit opt-out)
368
+ # user.disable_mfa_master_key!(authorized_by: current_admin,
369
+ # allow_unverified: true)
370
+ def disable_mfa_master_key!(authorized_by:, admin_role: nil, allow_unverified: false)
332
371
  operator = authorized_by
333
372
  unless operator.is_a?(Parse::User) ||
334
373
  (operator.is_a?(Parse::Pointer) && operator.parse_class == Parse::User.parse_class)
@@ -340,6 +379,18 @@ module Parse
340
379
  raise ArgumentError, "authorized_by: User must be persisted (have an objectId)"
341
380
  end
342
381
 
382
+ # FAIL CLOSED: this method bypasses MFA verification entirely via
383
+ # the master key, so it refuses to run without SOME authorization
384
+ # signal. Either supply an `admin_role:` for the library to verify,
385
+ # or pass `allow_unverified: true` to deliberately assert that the
386
+ # caller has already authorized the operator out-of-band.
387
+ if admin_role.nil? && !allow_unverified
388
+ raise MFA::ForbiddenError,
389
+ "disable_mfa_master_key! refuses to bypass MFA without an authorization " \
390
+ "check: pass admin_role: to enforce role membership, or " \
391
+ "allow_unverified: true to explicitly accept caller-side authorization."
392
+ end
393
+
343
394
  if admin_role
344
395
  role = admin_role.is_a?(Parse::Role) ? admin_role : Parse::Role.find_by_name(admin_role.to_s)
345
396
  if role.nil?
@@ -357,14 +408,19 @@ module Parse
357
408
  end
358
409
 
359
410
  auth_data_payload = { mfa: nil }
360
- response = client.update_user(id, { authData: auth_data_payload }, opts: { use_master_key: true })
411
+ response = client.update_user(id, { authData: auth_data_payload }, use_master_key: true)
361
412
 
362
413
  if response.error?
363
414
  raise Parse::Client::ResponseError, response
364
415
  end
365
416
 
366
- # Refresh auth_data
417
+ # Refresh auth_data, then drop the in-memory MFA projection. As in
418
+ # #disable_mfa!, a disabled user's read omits +authData+, so the
419
+ # +{ mfa: { status: "enabled" } }+ value pinned at enrollment won't
420
+ # self-clear on fetch — clear it explicitly so #mfa_enabled? reports
421
+ # the truth after a master-key disable.
367
422
  fetch
423
+ clear_local_mfa_projection!
368
424
 
369
425
  true
370
426
  end
@@ -435,6 +491,42 @@ module Parse
435
491
  account_name = email.presence || username.presence || id
436
492
  MFA.qr_code(secret, account_name, issuer: issuer, format: format)
437
493
  end
494
+
495
+ private
496
+
497
+ # @!visibility private
498
+ # Authoritative server-side MFA check via a trusted self-read.
499
+ # Reads +authData.mfa+ straight from a fresh session-token fetch
500
+ # rather than the (possibly stale) in-memory projection. An enabled
501
+ # account returns +authData.mfa+ with a +status+/+secret+; a disabled
502
+ # one omits +authData+ — so absence (or an mfa-less authData) means
503
+ # disabled.
504
+ # @return [Boolean]
505
+ def mfa_enabled_on_server?
506
+ result = client.fetch_object(self.class.parse_class, id,
507
+ session_token: session_token).result
508
+ mfa = result.is_a?(Hash) ? result["authData"] : nil
509
+ mfa = mfa["mfa"] if mfa.is_a?(Hash)
510
+ mfa.is_a?(Hash) && (mfa["status"] == "enabled" || mfa["secret"].present?)
511
+ end
512
+
513
+ # @!visibility private
514
+ # Drop the in-memory MFA projection after a disable. A disabled user's
515
+ # server read omits +authData+ entirely, so an ordinary fetch can
516
+ # never clear the +{ mfa: { status: "enabled" } }+ value pinned at
517
+ # enrollment; do it explicitly here. Only the +mfa+ subkey is removed
518
+ # (any anonymous/OAuth authData is preserved), and the assignment runs
519
+ # through the non-dirtying hydration path inside a +with_authdata_trust+
520
+ # scope so it is neither stripped nor marked dirty — a later #save will
521
+ # not resend +authData+.
522
+ def clear_local_mfa_projection!
523
+ cleared = auth_data.is_a?(Hash) ? auth_data.dup : {}
524
+ cleared.delete("mfa")
525
+ cleared.delete(:mfa)
526
+ self.class.with_authdata_trust do
527
+ apply_attributes!({ "authData" => cleared }, dirty_track: false)
528
+ end
529
+ end
438
530
  end
439
531
 
440
532
  # Not enabled error