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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +461 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +12 -4
- data/README.md +160 -3
- data/Rakefile +52 -3
- data/docs/atlas_vector_search_guide.md +86 -2
- data/docs/client_sdk_guide.md +5 -0
- data/docs/mcp_guide.md +59 -4
- data/docs/mongodb_direct_guide.md +93 -1
- data/docs/usage_guide.md +11 -1
- data/docs/webhooks_guide.md +418 -0
- data/examples/README.md +46 -0
- data/examples/basic_client.rb +93 -0
- data/examples/basic_server.rb +109 -0
- data/examples/live_query_listener.rb +98 -0
- data/examples/rag_chatbot.rb +221 -0
- data/examples/webhook_server.rb +111 -0
- data/lib/parse/agent/mcp_rack_app.rb +285 -62
- data/lib/parse/agent/tools.rb +45 -5
- data/lib/parse/api/aggregate.rb +7 -1
- data/lib/parse/api/cloud_functions.rb +12 -4
- data/lib/parse/api/hooks.rb +46 -9
- data/lib/parse/api/objects.rb +16 -2
- data/lib/parse/api/path_segment.rb +33 -0
- data/lib/parse/api/server.rb +94 -0
- data/lib/parse/api/users.rb +58 -2
- data/lib/parse/atlas_search.rb +7 -7
- data/lib/parse/client/body_builder.rb +5 -0
- data/lib/parse/client/protocol.rb +4 -0
- data/lib/parse/client.rb +55 -2
- data/lib/parse/embeddings/spend_cap.rb +255 -0
- data/lib/parse/embeddings.rb +1 -0
- data/lib/parse/live_query/client.rb +3 -1
- data/lib/parse/live_query/subscription.rb +32 -5
- data/lib/parse/model/acl.rb +4 -2
- data/lib/parse/model/classes/audience.rb +52 -4
- data/lib/parse/model/classes/user.rb +180 -3
- data/lib/parse/model/core/embed_managed.rb +113 -0
- data/lib/parse/model/core/querying.rb +3 -1
- data/lib/parse/model/core/vector_searchable.rb +161 -0
- data/lib/parse/model/object.rb +28 -5
- data/lib/parse/mongodb.rb +7 -1
- data/lib/parse/pipeline_security.rb +5 -3
- data/lib/parse/query/constraints.rb +29 -0
- data/lib/parse/query.rb +265 -27
- data/lib/parse/retrieval/agent_tool.rb +49 -0
- data/lib/parse/retrieval/reranker/cohere.rb +218 -0
- data/lib/parse/retrieval/reranker.rb +157 -0
- data/lib/parse/retrieval/retriever.rb +110 -23
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +17 -0
- data/lib/parse/two_factor_auth/user_extension.rb +123 -31
- data/lib/parse/vector_search/hybrid.rb +578 -0
- data/lib/parse/webhooks/payload.rb +252 -7
- data/lib/parse/webhooks/trigger_audit.rb +502 -0
- data/lib/parse/webhooks.rb +215 -3
- data/scripts/docker/Dockerfile.parse +5 -1
- data/scripts/docker/docker-compose.test.yml +31 -0
- data/scripts/docker/docker-compose.verifyemail.yml +4 -0
- data/scripts/docker/preflight.sh +76 -0
- data/scripts/start-parse.sh +52 -4
- metadata +15 -1
data/lib/parse/webhooks.rb
CHANGED
|
@@ -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|
|
|
363
|
+
result = registry.map { |hook| invoke_handler(payload, hook) }.last
|
|
223
364
|
else
|
|
224
|
-
result =
|
|
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
|
|
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
|
-
|
|
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,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
|
data/scripts/start-parse.sh
CHANGED
|
@@ -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.
|
|
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
|