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
@@ -17,6 +17,7 @@ require_relative "model/object"
17
17
  require_relative "webhooks/payload"
18
18
  require_relative "webhooks/registration"
19
19
  require_relative "webhooks/replay_protection"
20
+ require_relative "webhooks/trigger_audit"
20
21
 
21
22
  module Parse
22
23
  class Object
@@ -83,6 +84,36 @@ module Parse
83
84
  # will trigger the Parse::Webhooks application to return the proper error response.
84
85
  class ResponseError < StandardError; end
85
86
 
87
+ # The authentication-side triggers (local underscore form). These carry a
88
+ # +_User+ / +_Session+ as the payload object but are NOT object save/delete
89
+ # triggers: the router runs no ActiveModel save/create/destroy callbacks for
90
+ # them, and Parse Server ignores their response body.
91
+ AUTH_TRIGGERS = %i[
92
+ before_login after_login after_logout before_password_reset_request
93
+ ].freeze
94
+
95
+ # The LiveQuery triggers (local underscore form). Connection-global or
96
+ # event-scoped; Parse Server ignores their response body. Delivered over an
97
+ # HTTP webhook only in a co-located single-process LiveQuery setup.
98
+ LIVE_QUERY_TRIGGERS = %i[before_connect before_subscribe after_event].freeze
99
+
100
+ # Every trigger whose payload is not an object save/delete/find shape.
101
+ # Parse Server's webhook response handler resolves +{}+ for all of these
102
+ # (the body is ignored), so the router normalizes their handler result to a
103
+ # success no-op rather than serializing a returned object into the response.
104
+ NON_OBJECT_TRIGGERS = (AUTH_TRIGGERS + LIVE_QUERY_TRIGGERS).freeze
105
+
106
+ # The +before*+ subset of {NON_OBJECT_TRIGGERS} for which a handler can DENY
107
+ # the operation. Parse Server only treats an +{error}+ response as a
108
+ # rejection -- a +{success:false}+ body resolves and lets the login /
109
+ # connect / subscribe / reset proceed. So, mirroring the +before_save+
110
+ # convention, the router converts a +false+ return from one of these into a
111
+ # {ResponseError} (which serializes to +{error}+). +error!+ works for any
112
+ # trigger; the +after*+ variants fire after the fact and cannot undo it.
113
+ REJECTABLE_NON_OBJECT_TRIGGERS = %i[
114
+ before_login before_password_reset_request before_connect before_subscribe
115
+ ].freeze
116
+
86
117
  include Client::Connectable
87
118
  extend Parse::Webhooks::Registration
88
119
  # The name of the incoming env containing the webhook key.
@@ -135,6 +166,18 @@ module Parse
135
166
  className = className.parse_class
136
167
  end
137
168
  className = className.to_s
169
+ # Parse Server has no beforeCreate/afterCreate webhook trigger; the
170
+ # create variants are ActiveModel callbacks that run inside the
171
+ # beforeSave/afterSave handler for new objects. Point callers there
172
+ # rather than registering a route that can never fire.
173
+ if type == :before_create || type == :after_create
174
+ save = type == :before_create ? :before_save : :after_save
175
+ raise ArgumentError,
176
+ "There is no #{type} webhook. Register `webhook :#{save}` instead — " \
177
+ "your #{type} ActiveModel callbacks run inside the #{save} handler " \
178
+ "for new objects (registering #{save} enables BOTH the #{save} and " \
179
+ "#{type} callbacks)."
180
+ end
138
181
  if routes[type].nil? || block.respond_to?(:call) == false
139
182
  raise ArgumentError, "Invalid Webhook registration trigger #{type} #{className}"
140
183
  end
@@ -159,6 +202,104 @@ module Parse
159
202
  call_route(:function, name, payload)
160
203
  end
161
204
 
205
+ # Evaluate a single registered handler block in the scope of the payload.
206
+ #
207
+ # The block runs with `self` bound to the {Parse::Webhooks::Payload}, so a
208
+ # handler can call `parse_object`, `params`, `error!`, etc. directly --
209
+ # exactly as it could under the historical `payload.instance_exec(payload,
210
+ # &block)` invocation. The difference is the return semantics:
211
+ #
212
+ # - `return value` returns `value` as the handler result (instead of the
213
+ # `LocalJumpError: unexpected return` that bare `instance_exec` raised
214
+ # when the block was defined inside a method).
215
+ # - The legacy idioms still work unchanged: the last expression's value,
216
+ # `next value`, and `break value` all return `value`, and `raise`
217
+ # propagates untouched (so `error!` / before_save rejections behave the
218
+ # same).
219
+ #
220
+ # This is achieved by attaching the block as a singleton method on the
221
+ # per-request payload (so `return` gets method semantics) and removing it
222
+ # afterward. The payload is a per-request instance, so this neither leaks
223
+ # nor mutates shared state across threads.
224
+ #
225
+ # Arity is matched to the old `instance_exec(payload, ...)` contract: a
226
+ # zero-arity block (`do ... end` / `proc { }`) is called with no args; a
227
+ # block that declares a parameter (`do |payload| ... end`) or a splat
228
+ # receives the payload.
229
+ #
230
+ # @param payload [Parse::Webhooks::Payload] the request payload (becomes `self`).
231
+ # @param block [Proc] the registered handler block.
232
+ # @return [Object] the handler's result value.
233
+ def invoke_handler(payload, block)
234
+ name = :"__parse_webhook_handler_#{block.object_id}__"
235
+ payload.define_singleton_method(name, &block)
236
+ handler = payload.method(name)
237
+ begin
238
+ # Match the old `payload.instance_exec(payload, &block)` arity
239
+ # leniency: a zero-arity block is called bare; otherwise it receives
240
+ # the payload, plus a nil for each additional REQUIRED positional so a
241
+ # block declaring `|payload, extra|` (or more) does not raise — under
242
+ # instance_exec those surplus params were silently nil. `arity` is
243
+ # negative for optional/splat params (e.g. -1 for `|*a|`, -2 for
244
+ # `|a, *b|`); `~arity` gives the required count in that case.
245
+ if handler.arity == 0
246
+ handler.call
247
+ else
248
+ required = handler.arity.negative? ? ~handler.arity : handler.arity
249
+ handler.call(payload, *Array.new([required - 1, 0].max))
250
+ end
251
+ ensure
252
+ singleton = payload.singleton_class
253
+ if singleton.method_defined?(name) || singleton.private_method_defined?(name)
254
+ singleton.send(:remove_method, name)
255
+ end
256
+ end
257
+ end
258
+
259
+ # Run any {Parse::Webhooks::Payload#after_response} callbacks a handler
260
+ # registered, AFTER the response has been produced. Prefers the server's
261
+ # `rack.after_reply` hook (Puma / Unicorn), which fires once the response
262
+ # is flushed to the socket on the same worker thread; falls back to a
263
+ # detached thread when the server does not provide it (e.g. WEBrick). Each
264
+ # callback is isolated so one raising neither aborts the others nor reaches
265
+ # the client. No-op when nothing was deferred.
266
+ #
267
+ # @param env [Hash] the Rack environment (for `rack.after_reply`).
268
+ # @param payload [Parse::Webhooks::Payload, nil] the request payload.
269
+ # @return [void]
270
+ def dispatch_deferred(env, payload)
271
+ return if payload.nil? || !payload.respond_to?(:deferred_callbacks)
272
+ callbacks = payload.deferred_callbacks
273
+ return if callbacks.blank?
274
+
275
+ runner = proc do
276
+ callbacks.each do |cb|
277
+ begin
278
+ cb.call
279
+ rescue => e
280
+ warn "[Webhooks::after_response] deferred callback raised: #{e.class}: #{e.message}"
281
+ end
282
+ end
283
+ end
284
+
285
+ # Enqueueing must never break an otherwise-successful response: this runs
286
+ # just before `response.finish`, so a raise here (a frozen after_reply
287
+ # array, thread exhaustion) would discard the buffered reply and surface
288
+ # as a 500. Failing to schedule deferred work degrades to "not run",
289
+ # never to a failed response.
290
+ begin
291
+ after_reply = env.is_a?(Hash) ? env["rack.after_reply"] : nil
292
+ if after_reply.respond_to?(:<<)
293
+ after_reply << runner
294
+ else
295
+ Thread.new(&runner)
296
+ end
297
+ rescue => e
298
+ warn "[Webhooks::after_response] could not schedule deferred work: #{e.class}: #{e.message}"
299
+ end
300
+ nil
301
+ end
302
+
162
303
  # Calls the set of registered webhook trigger blocks or the specific function block.
163
304
  # This method is usually called when an incoming request from Parse Server is received.
164
305
  # @param type (see route)
@@ -219,9 +360,9 @@ module Parse
219
360
  end
220
361
 
221
362
  if registry.is_a?(Array)
222
- result = registry.map { |hook| payload.instance_exec(payload, &hook) }.last
363
+ result = registry.map { |hook| invoke_handler(payload, hook) }.last
223
364
  else
224
- result = payload.instance_exec(payload, &registry)
365
+ result = invoke_handler(payload, registry)
225
366
  end
226
367
 
227
368
  if result.is_a?(Parse::Object)
@@ -265,6 +406,30 @@ module Parse
265
406
  result = {}
266
407
  end
267
408
 
409
+ # Auth- and LiveQuery-trigger dispatch (beforeLogin/afterLogin/
410
+ # afterLogout/beforePasswordResetRequest, beforeConnect/beforeSubscribe/
411
+ # afterEvent). Parse Server IGNORES the response body for all of these --
412
+ # its webhook response handler resolves {} regardless -- so the ONLY way
413
+ # a handler can affect the operation is the error path, and only for the
414
+ # "before" variants (a login/connect/subscribe/reset can be denied; an
415
+ # after_* fires after the fact and cannot be undone).
416
+ #
417
+ # Crucially, Parse Server treats only an {error} response as a rejection:
418
+ # a {success:false} body RESOLVES and lets the operation proceed. So a
419
+ # handler that returns `false` to "deny login" would silently allow it.
420
+ # We mirror the before_save convention and convert that false into a
421
+ # ResponseError (=> {error} => Parse Server denies). `error!` works for
422
+ # any of them (the call! rescue converts it). Every other return value --
423
+ # including a Parse::Object a handler happened to return (e.g. the _User
424
+ # from beforeLogin) -- is normalized to a success no-op so we never
425
+ # serialize an object into the response or the redacted request log.
426
+ if NON_OBJECT_TRIGGERS.include?(type)
427
+ if result == false && REJECTABLE_NON_OBJECT_TRIGGERS.include?(type)
428
+ raise Parse::Webhooks::ResponseError, "#{type} rejected by webhook handler"
429
+ end
430
+ result = true
431
+ end
432
+
268
433
  # Guard-injection: when a handler returns a Hash (or true/nil normalized
269
434
  # to {}) for a class with field_guards, Parse Server would otherwise
270
435
  # merge the response with the client's original payload and persist
@@ -380,6 +545,40 @@ module Parse
380
545
  dup.call!(env)
381
546
  end
382
547
 
548
+ # Extract the Parse class name from a webhook request path. Parse Server
549
+ # registers each trigger at `<endpoint>/<triggerName>/<className>`
550
+ # (functions at `<endpoint>/<functionName>`), so for a trigger the class
551
+ # is the last segment and the second-to-last is a known trigger name.
552
+ # Returns nil for a function path, a path with no recognizable trigger
553
+ # segment, or a className that fails the conservative charset check
554
+ # (Parse class names are `[A-Za-z0-9_]`, built-ins prefixed with `_`).
555
+ # The charset gate keeps an attacker-supplied path (reachable when
556
+ # `allow_unauthenticated` is set) from injecting an arbitrary routing /
557
+ # scrub key.
558
+ #
559
+ # @param path [String] the request PATH_INFO.
560
+ # @return [String, nil] the sanitized class name, or nil.
561
+ def trigger_class_from_path(path)
562
+ segments = path.to_s.split("/").reject(&:empty?)
563
+ return nil if segments.size < 2
564
+ trigger, klass = segments[-2], segments[-1]
565
+ # register_triggers! builds the URL with the LOCAL snake_case trigger
566
+ # name (`after_find`), while Parse Server sends the camelCase form in the
567
+ # body — accept both so the path segment is recognized either way.
568
+ known = (Parse::API::Hooks::TRIGGER_NAMES + Parse::API::Hooks::TRIGGER_NAMES_LOCAL).map(&:to_s)
569
+ return nil unless known.include?(trigger)
570
+ # Allow a leading `@` for the Parse pseudo-classes (`@Connect` for the
571
+ # connection-global LiveQuery trigger, `@File` for file triggers): the
572
+ # SDK encodes the className in the per-trigger URL, so beforeConnect
573
+ # would not route without it. Mirrors the trigger-className validator
574
+ # (Parse::API::PathSegment.trigger_class_name!). Still anchored and
575
+ # charset-limited -- this gate keeps an attacker-supplied path (reachable
576
+ # only under allow_unauthenticated) from injecting an arbitrary routing
577
+ # / scrub key.
578
+ return nil unless /\A@?_?[A-Za-z][A-Za-z0-9_]*\z/.match?(klass)
579
+ klass
580
+ end
581
+
383
582
  # @!visibility private
384
583
  def call!(env)
385
584
  request = Rack::Request.new env
@@ -440,8 +639,17 @@ module Parse
440
639
  return response.finish
441
640
  end
442
641
 
642
+ # Parse Server registers each trigger at
643
+ # `<endpoint>/<triggerName>/<className>`. For beforeFind/afterFind the
644
+ # payload body carries NO className anywhere, so the request PATH is the
645
+ # only authoritative source of the class — without it, find triggers
646
+ # don't route (parse_class is nil) and afterFind `objects` can't have
647
+ # their :vector columns stripped. Thread it into the payload here, before
648
+ # construction, so it is available for both routing and the scrub. Nil
649
+ # for function requests and for malformed paths.
650
+ webhook_class = Parse::Webhooks.trigger_class_from_path(request.path)
443
651
  begin
444
- payload = Parse::Webhooks::Payload.new body_str
652
+ payload = Parse::Webhooks::Payload.new(body_str, webhook_class)
445
653
  rescue => e
446
654
  warn "Invalid webhook payload format: #{e}"
447
655
  response.write error("Invalid payload format. Should be valid JSON.")
@@ -486,6 +694,10 @@ module Parse
486
694
  puts "----------------------------------------------------\n"
487
695
  end
488
696
  response.write success(result)
697
+ # Schedule any after_response work to run once this reply is flushed,
698
+ # off the client's critical path. Registered on the success path so the
699
+ # deferred work overlaps a response Parse Server will act on.
700
+ dispatch_deferred(env, payload)
489
701
  return response.finish
490
702
  rescue Parse::Webhooks::ResponseError, ActiveModel::ValidationError => e
491
703
  if payload.trigger?
@@ -1,4 +1,8 @@
1
- FROM parseplatform/parse-server:9
1
+ # Pinned to a specific patch release (was the floating `:9` tag, which had
2
+ # cached to a pre-patch 8.4.0 build). 9.9.0 includes the MFA / authData security
3
+ # fixes — GHSA-pfj7-wv7c-22pr (auth-provider validation bypass on login) and
4
+ # GHSA-37mj-c2wf-cx96 / CVE-2026-33627 (TOTP secret leak via /users/me).
5
+ FROM parseplatform/parse-server:9.9.0
2
6
 
3
7
  # Switch to root to copy and set permissions
4
8
  USER root
@@ -5,9 +5,37 @@ version: '3.8'
5
5
  name: ${PSNEXT_PREFIX:-psnext-it}
6
6
 
7
7
  services:
8
+ # Security preflight — TEST STACK ONLY. Runs to completion before any
9
+ # real service starts (each gates on it via
10
+ # `service_completed_successfully`). Fails the stack closed when a
11
+ # `*_BIND` override would expose the stack on the LAN while privileged
12
+ # credentials are still the committed defaults. Invisible to the normal
13
+ # loopback run. See scripts/docker/preflight.sh for the rationale and
14
+ # escape hatches (ALLOW_INSECURE_BIND=1, or set real credentials).
15
+ preflight:
16
+ image: busybox:1.36
17
+ container_name: ${PSNEXT_PREFIX:-psnext-it}-preflight
18
+ environment:
19
+ # Resolved binds (compose applies the 127.0.0.1 default here).
20
+ PARSE_BIND: ${PARSE_BIND:-127.0.0.1}
21
+ MONGO_BIND: ${MONGO_BIND:-127.0.0.1}
22
+ REDIS_BIND: ${REDIS_BIND:-127.0.0.1}
23
+ DASHBOARD_BIND: ${DASHBOARD_BIND:-127.0.0.1}
24
+ # "1" only when the operator supplied a non-empty override; empty
25
+ # means the committed default credential is still in force.
26
+ PARSE_MASTER_KEY_SET: ${PARSE_MASTER_KEY:+1}
27
+ MONGO_ROOT_PASSWORD_SET: ${MONGO_ROOT_PASSWORD:+1}
28
+ ALLOW_INSECURE_BIND: ${ALLOW_INSECURE_BIND:-}
29
+ volumes:
30
+ - ./preflight.sh:/preflight.sh:ro
31
+ command: ["sh", "/preflight.sh"]
32
+
8
33
  mongo:
9
34
  image: mongo:8
10
35
  container_name: ${PSNEXT_PREFIX:-psnext-it}-mongo
36
+ depends_on:
37
+ preflight:
38
+ condition: service_completed_successfully
11
39
  # Bind to loopback so the test database isn't reachable from the LAN
12
40
  # when a developer runs `docker-compose up`. Override with
13
41
  # `MONGO_BIND=0.0.0.0` if you really want it exposed.
@@ -83,6 +111,9 @@ services:
83
111
  redis:
84
112
  image: redis:7-alpine
85
113
  container_name: ${PSNEXT_PREFIX:-psnext-it}-redis
114
+ depends_on:
115
+ preflight:
116
+ condition: service_completed_successfully
86
117
  # Loopback-only by default. Used by the cache integration test
87
118
  # (cache_redis_integration_test.rb) and the synchronize-create lock
88
119
  # tests. Override with `REDIS_BIND=0.0.0.0` if you need to point a
@@ -0,0 +1,4 @@
1
+ services:
2
+ parse:
3
+ environment:
4
+ PARSE_SERVER_VERIFY_USER_EMAILS: "true"
@@ -0,0 +1,76 @@
1
+ #!/bin/sh
2
+ # Preflight guard for the integration stack — TEST STACK ONLY.
3
+ #
4
+ # The Compose defaults bind every service to loopback (127.0.0.1) and fall
5
+ # back to KNOWN, COMMITTED test credentials (master key psnextItMasterKey,
6
+ # Mongo root admin:password, Dashboard admin:admin). That combination is
7
+ # safe on loopback — nothing on the LAN can reach it.
8
+ #
9
+ # It is NOT safe the moment a developer overrides a `*_BIND` to a
10
+ # non-loopback address (e.g. MONGO_BIND=0.0.0.0 to attach a remote client):
11
+ # the stack is then reachable from the LAN while protected only by
12
+ # credentials that are published in this very repository. An admin-
13
+ # credentialed Mongo / Parse master key exposed to a shared network is a
14
+ # real footgun, and the failure is silent — `docker compose up` just works
15
+ # and the developer never sees it.
16
+ #
17
+ # This guard runs first (every other service gates on it via
18
+ # `service_completed_successfully`) and FAILS THE STACK CLOSED whenever a
19
+ # non-loopback bind is combined with still-default privileged credentials.
20
+ # It is invisible to the normal loopback run.
21
+ #
22
+ # Escape hatches (pick one):
23
+ # 1. Keep it loopback — unset the *_BIND override (the default).
24
+ # 2. Use real secrets — set PARSE_MASTER_KEY and MONGO_ROOT_PASSWORD
25
+ # (inject them with `op run` / `doppler run` —
26
+ # see the README "Secret injection" recipe).
27
+ # 3. Acknowledge the risk — ALLOW_INSECURE_BIND=1 on a trusted/isolated
28
+ # network where exposure is intentional.
29
+
30
+ set -eu
31
+
32
+ # Treat an empty value as loopback: the compose interpolation passes the
33
+ # resolved bind, and an unset override resolves to the 127.0.0.1 default.
34
+ is_loopback() {
35
+ case "$1" in
36
+ "" | 127.0.0.1 | ::1 | localhost) return 0 ;;
37
+ *) return 1 ;;
38
+ esac
39
+ }
40
+
41
+ exposed=""
42
+ for pair in "PARSE_BIND=${PARSE_BIND:-}" "MONGO_BIND=${MONGO_BIND:-}" \
43
+ "REDIS_BIND=${REDIS_BIND:-}" "DASHBOARD_BIND=${DASHBOARD_BIND:-}"; do
44
+ val=${pair#*=}
45
+ is_loopback "$val" || exposed="$exposed ${pair%%=*}=$val"
46
+ done
47
+
48
+ # Privileged credentials are "default" unless BOTH have been overridden.
49
+ # *_SET is "1" only when Compose interpolated a non-empty override
50
+ # (`${VAR:+1}`); empty means the committed default is in force.
51
+ if [ -n "${PARSE_MASTER_KEY_SET:-}" ] && [ -n "${MONGO_ROOT_PASSWORD_SET:-}" ]; then
52
+ default_creds=0
53
+ else
54
+ default_creds=1
55
+ fi
56
+
57
+ if [ -n "$exposed" ] && [ "$default_creds" = "1" ] && [ "${ALLOW_INSECURE_BIND:-}" != "1" ]; then
58
+ echo "[preflight] REFUSING TO START — non-loopback bind with default credentials." >&2
59
+ echo "[preflight]" >&2
60
+ echo "[preflight] Exposed (non-loopback) bind(s):$exposed" >&2
61
+ echo "[preflight] ...while still using the committed test credentials" >&2
62
+ echo "[preflight] (master key psnextItMasterKey / Mongo admin:password)." >&2
63
+ echo "[preflight] This publishes an admin-credentialed stack onto your LAN." >&2
64
+ echo "[preflight]" >&2
65
+ echo "[preflight] Resolve ONE of:" >&2
66
+ echo "[preflight] 1. Keep it loopback — unset the *_BIND override." >&2
67
+ echo "[preflight] 2. Set real secrets — PARSE_MASTER_KEY=... MONGO_ROOT_PASSWORD=..." >&2
68
+ echo "[preflight] 3. Acknowledge intent — ALLOW_INSECURE_BIND=1 (trusted network only)." >&2
69
+ exit 1
70
+ fi
71
+
72
+ if [ -n "$exposed" ]; then
73
+ echo "[preflight] OK — non-loopback bind(s)$exposed permitted (credentials overridden or ALLOW_INSECURE_BIND=1)."
74
+ else
75
+ echo "[preflight] OK — all services bound to loopback."
76
+ fi
@@ -64,6 +64,52 @@ export PARSE_SERVER_ALLOW_CUSTOM_OBJECT_ID="${PARSE_SERVER_ALLOW_CUSTOM_OBJECT_I
64
64
  export PARSE_SERVER_LIVE_QUERY="${PARSE_SERVER_LIVE_QUERY:-{\"classNames\":[\"Song\",\"Album\",\"User\",\"_User\",\"TestLiveQuery\"]}}"
65
65
  export PARSE_SERVER_START_LIVE_QUERY_SERVER="${PARSE_SERVER_START_LIVE_QUERY_SERVER:-true}"
66
66
 
67
+ # Push configuration — test-stack only. Points at a no-op adapter bind-mounted
68
+ # from test/cloud (see test/cloud/dummy-push-adapter.js). It does NOT deliver to
69
+ # any real device gateway; it lets Parse Server accept `POST /parse/push` and
70
+ # create/complete a real `_PushStatus` so the push send+status lifecycle is
71
+ # integration-testable offline. Without this, `POST /push` returns code 115
72
+ # "Missing push configuration". DO NOT use a no-op adapter in a deployed
73
+ # environment — it silently drops every notification.
74
+ export PARSE_SERVER_PUSH="${PARSE_SERVER_PUSH:-{\"adapter\":\"/parse-server/cloud/dummy-push-adapter.js\"}}"
75
+
76
+ # MFA / 2FA configuration — test-stack only. Enables Parse Server's built-in
77
+ # TOTP MFA adapter so the Parse::MFA / two_factor_auth integration tests can
78
+ # enroll a user (authData.mfa.{secret,token}) and log in with a time-based code.
79
+ # Params match rotp's defaults (SHA1 / 6 digits / 30s period) so codes generated
80
+ # client-side validate server-side.
81
+ #
82
+ # NOTE: the `auth` option is the one Parse Server option that CANNOT be passed
83
+ # as a JSON env var — its Definitions entry has no objectParser, so
84
+ # PARSE_SERVER_AUTH_PROVIDERS is taken as a raw string and never JSON-parsed
85
+ # (the MFA adapter then receives `undefined` options and 500s). It must come
86
+ # from a config file, which parse-server JSON-parses natively. We write a
87
+ # minimal config file holding only the `auth` block and pass it to parse-server
88
+ # below; env vars still provide — and take precedence for — everything else
89
+ # (parse-server applies env first, then fills gaps from the file).
90
+ PARSE_AUTH_CONFIG_FILE="${PARSE_AUTH_CONFIG_FILE:-/tmp/psnext-parse-auth-config.json}"
91
+ cat > "$PARSE_AUTH_CONFIG_FILE" <<'AUTHCFG'
92
+ { "auth": { "mfa": { "options": ["TOTP"], "digits": 6, "period": 30, "algorithm": "SHA1" } } }
93
+ AUTHCFG
94
+
95
+ # Email — test-stack only. Captures outgoing mail into an `EmailCapture` class
96
+ # (see test/cloud/capturing-email-adapter.js) instead of sending it, so the
97
+ # client-side password-reset / verification integration tests can assert
98
+ # delivery and read back the reset link. `PARSE_PUBLIC_SERVER_URL` is required
99
+ # for Parse Server to build those links. Email verification is NOT enabled, so
100
+ # ordinary signups still work without a verification round-trip. DO NOT use a
101
+ # capturing adapter in a deployed environment — it drops every email.
102
+ export PARSE_SERVER_EMAIL_ADAPTER="${PARSE_SERVER_EMAIL_ADAPTER:-/parse-server/cloud/capturing-email-adapter.js}"
103
+ export PARSE_PUBLIC_SERVER_URL="${PARSE_PUBLIC_SERVER_URL:-http://localhost:${PARSE_HOST_PORT:-29337}/parse}"
104
+ export PARSE_SERVER_APP_NAME="${PARSE_SERVER_APP_NAME:-parse-stack-next-it}"
105
+ # Keep email verification OFF. Configuring an email adapter otherwise flips
106
+ # Parse Server into requiring verification, which makes signup return a user
107
+ # with NO session token until the address is verified — breaking the
108
+ # signup-on-save suite. Password reset does not need verification, only the
109
+ # adapter + public URL above.
110
+ export PARSE_SERVER_VERIFY_USER_EMAILS="${PARSE_SERVER_VERIFY_USER_EMAILS:-false}"
111
+ export PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL="${PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL:-false}"
112
+
67
113
  # File upload — test-stack only. Authenticated session-token uploads are
68
114
  # permitted; public/anonymous uploads are NOT (mirrors a typical hardened
69
115
  # Parse Server config). The client_rest_files integration tests assert
@@ -92,15 +138,17 @@ echo "Looking for parse-server..."
92
138
  which node
93
139
  ls -la /parse-server/
94
140
 
95
- # Try different ways to start parse-server
141
+ # Try different ways to start parse-server. The config file argument supplies
142
+ # the `auth` (MFA) block; every other option still comes from the environment.
143
+ echo " Auth config file: $PARSE_AUTH_CONFIG_FILE"
96
144
  if [ -f "/parse-server/bin/parse-server" ]; then
97
145
  echo "Using /parse-server/bin/parse-server"
98
- exec /parse-server/bin/parse-server
146
+ exec /parse-server/bin/parse-server "$PARSE_AUTH_CONFIG_FILE"
99
147
  elif [ -f "/usr/src/app/bin/parse-server" ]; then
100
148
  echo "Using /usr/src/app/bin/parse-server"
101
- exec /usr/src/app/bin/parse-server
149
+ exec /usr/src/app/bin/parse-server "$PARSE_AUTH_CONFIG_FILE"
102
150
  else
103
151
  echo "Trying with node and index.js"
104
152
  cd /parse-server
105
- exec node ./bin/parse-server
153
+ exec node ./bin/parse-server "$PARSE_AUTH_CONFIG_FILE"
106
154
  fi
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parse-stack-next
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.3.0
4
+ version: 5.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Curtin
@@ -250,9 +250,16 @@ files:
250
250
  - docs/mongodb_direct_guide.md
251
251
  - docs/mongodb_index_optimization_guide.md
252
252
  - docs/usage_guide.md
253
+ - docs/webhooks_guide.md
253
254
  - docs/yard-template/default/fulldoc/html/css/common.css
254
255
  - docs/yard-template/default/fulldoc/html/css/full_list.css
256
+ - examples/README.md
257
+ - examples/basic_client.rb
258
+ - examples/basic_server.rb
259
+ - examples/live_query_listener.rb
260
+ - examples/rag_chatbot.rb
255
261
  - examples/transaction_example.rb
262
+ - examples/webhook_server.rb
256
263
  - lib/parse-stack-next.rb
257
264
  - lib/parse-stack.rb
258
265
  - lib/parse/acl_scope.rb
@@ -319,6 +326,7 @@ files:
319
326
  - lib/parse/embeddings/openai.rb
320
327
  - lib/parse/embeddings/provider.rb
321
328
  - lib/parse/embeddings/qwen.rb
329
+ - lib/parse/embeddings/spend_cap.rb
322
330
  - lib/parse/embeddings/voyage.rb
323
331
  - lib/parse/graphql.rb
324
332
  - lib/parse/graphql/scalars.rb
@@ -399,6 +407,8 @@ files:
399
407
  - lib/parse/retrieval/agent_tool.rb
400
408
  - lib/parse/retrieval/chunk.rb
401
409
  - lib/parse/retrieval/chunker.rb
410
+ - lib/parse/retrieval/reranker.rb
411
+ - lib/parse/retrieval/reranker/cohere.rb
402
412
  - lib/parse/retrieval/retriever.rb
403
413
  - lib/parse/schema.rb
404
414
  - lib/parse/schema/index_migrator.rb
@@ -418,17 +428,21 @@ files:
418
428
  - lib/parse/two_factor_auth.rb
419
429
  - lib/parse/two_factor_auth/user_extension.rb
420
430
  - lib/parse/vector_search.rb
431
+ - lib/parse/vector_search/hybrid.rb
421
432
  - lib/parse/webhooks.rb
422
433
  - lib/parse/webhooks/payload.rb
423
434
  - lib/parse/webhooks/registration.rb
424
435
  - lib/parse/webhooks/replay_protection.rb
436
+ - lib/parse/webhooks/trigger_audit.rb
425
437
  - parse-stack-next.gemspec
426
438
  - scripts/debug-ips.js
427
439
  - scripts/docker/Dockerfile.parse
428
440
  - scripts/docker/atlas-init.js
429
441
  - scripts/docker/docker-compose.atlas.yml
430
442
  - scripts/docker/docker-compose.test.yml
443
+ - scripts/docker/docker-compose.verifyemail.yml
431
444
  - scripts/docker/mongo-init.js
445
+ - scripts/docker/preflight.sh
432
446
  - scripts/eval_mcp_with_lm_studio.rb
433
447
  - scripts/start-parse.sh
434
448
  - scripts/start_mcp_server.rb