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/api/hooks.rb
CHANGED
|
@@ -7,15 +7,52 @@ module Parse
|
|
|
7
7
|
module Hooks
|
|
8
8
|
# @!visibility private
|
|
9
9
|
HOOKS_PREFIX = "hooks/"
|
|
10
|
-
# The allowed set of Parse triggers.
|
|
11
|
-
|
|
10
|
+
# The allowed set of Parse webhook triggers. Mirrors Parse Server's
|
|
11
|
+
# `triggers.Types` so registration of the auth / LiveQuery / password-
|
|
12
|
+
# reset hooks is no longer pre-rejected by the SDK.
|
|
13
|
+
#
|
|
14
|
+
# NOTE: this allowlist gates *registration* only. The webhook router in
|
|
15
|
+
# {Parse::Webhooks} currently shapes payloads for the object triggers
|
|
16
|
+
# (before/after save/delete/find); the login / connect / subscribe /
|
|
17
|
+
# password-reset payloads carry a different shape (no `object`) and
|
|
18
|
+
# their first-class routing is a follow-up. `beforeConnect` is a
|
|
19
|
+
# connection-global trigger whose Parse-canonical className is the
|
|
20
|
+
# `@Connect` sentinel; file triggers use `@File`. Both are accepted by
|
|
21
|
+
# the trigger-className validator ({Parse::API::PathSegment.trigger_class_name!}).
|
|
22
|
+
TRIGGER_NAMES = [
|
|
23
|
+
:afterDelete, :afterFind, :afterSave,
|
|
24
|
+
:beforeDelete, :beforeFind, :beforeSave,
|
|
25
|
+
:beforeLogin, :afterLogin, :afterLogout, :beforePasswordResetRequest,
|
|
26
|
+
:beforeConnect, :beforeSubscribe, :afterEvent,
|
|
27
|
+
].freeze
|
|
12
28
|
# @!visibility private
|
|
13
|
-
TRIGGER_NAMES_LOCAL = [
|
|
29
|
+
TRIGGER_NAMES_LOCAL = [
|
|
30
|
+
:after_delete, :after_find, :after_save,
|
|
31
|
+
:before_delete, :before_find, :before_save,
|
|
32
|
+
:before_login, :after_login, :after_logout, :before_password_reset_request,
|
|
33
|
+
:before_connect, :before_subscribe, :after_event,
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
# `beforeCreate` / `afterCreate` are NOT Parse Server trigger types —
|
|
37
|
+
# Parse Server rejects them ("invalid hook declaration"). They exist only
|
|
38
|
+
# as Parse-Stack ActiveModel callbacks (`before_create` / `after_create`),
|
|
39
|
+
# which the webhook router runs INSIDE the `beforeSave` / `afterSave`
|
|
40
|
+
# handler for new objects (gated on `original.nil?`). So there is nothing
|
|
41
|
+
# to register for them — register `beforeSave` / `afterSave` instead and
|
|
42
|
+
# the create callbacks fire within it.
|
|
14
43
|
# @!visibility private
|
|
15
44
|
def _verify_trigger(triggerName)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
45
|
+
camel = triggerName.to_s.camelize(:lower).to_sym
|
|
46
|
+
if %i[beforeCreate afterCreate].include?(camel)
|
|
47
|
+
save = camel == :beforeCreate ? "beforeSave" : "afterSave"
|
|
48
|
+
callback = camel == :beforeCreate ? "before_create" : "after_create"
|
|
49
|
+
raise ArgumentError,
|
|
50
|
+
"Parse Server has no #{camel} webhook trigger. Register a " \
|
|
51
|
+
"#{save} webhook instead — Parse Stack runs your #{callback} " \
|
|
52
|
+
"ActiveModel callbacks within the #{save} handler for new objects."
|
|
53
|
+
end
|
|
54
|
+
raise ArgumentError, "Invalid trigger name #{camel}" unless TRIGGER_NAMES.include?(camel)
|
|
55
|
+
camel
|
|
19
56
|
end
|
|
20
57
|
|
|
21
58
|
# Fetch all defined cloud code functions.
|
|
@@ -74,7 +111,7 @@ module Parse
|
|
|
74
111
|
# @see TRIGGER_NAMES
|
|
75
112
|
def fetch_trigger(triggerName, className)
|
|
76
113
|
triggerName = _verify_trigger(triggerName)
|
|
77
|
-
safe_class = Parse::API::PathSegment.
|
|
114
|
+
safe_class = Parse::API::PathSegment.trigger_class_name!(className, kind: "class name")
|
|
78
115
|
request :get, "#{HOOKS_PREFIX}triggers/#{safe_class}/#{triggerName}"
|
|
79
116
|
end
|
|
80
117
|
|
|
@@ -98,7 +135,7 @@ module Parse
|
|
|
98
135
|
# @see Parse::API::Hooks::TRIGGER_NAMES
|
|
99
136
|
def update_trigger(triggerName, className, url)
|
|
100
137
|
triggerName = _verify_trigger(triggerName)
|
|
101
|
-
safe_class = Parse::API::PathSegment.
|
|
138
|
+
safe_class = Parse::API::PathSegment.trigger_class_name!(className, kind: "class name")
|
|
102
139
|
request :put, "#{HOOKS_PREFIX}triggers/#{safe_class}/#{triggerName}", body: { url: url }
|
|
103
140
|
end
|
|
104
141
|
|
|
@@ -109,7 +146,7 @@ module Parse
|
|
|
109
146
|
# @see Parse::API::Hooks::TRIGGER_NAMES
|
|
110
147
|
def delete_trigger(triggerName, className)
|
|
111
148
|
triggerName = _verify_trigger(triggerName)
|
|
112
|
-
safe_class = Parse::API::PathSegment.
|
|
149
|
+
safe_class = Parse::API::PathSegment.trigger_class_name!(className, kind: "class name")
|
|
113
150
|
request :put, "#{HOOKS_PREFIX}triggers/#{safe_class}/#{triggerName}", body: { __op: "Delete" }
|
|
114
151
|
end
|
|
115
152
|
end
|
data/lib/parse/api/objects.rb
CHANGED
|
@@ -84,8 +84,15 @@ module Parse
|
|
|
84
84
|
# @param body [Hash] the body of the request.
|
|
85
85
|
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
86
86
|
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
87
|
+
# @param context [Hash, nil] an optional caller context forwarded as the
|
|
88
|
+
# +X-Parse-Cloud-Context+ header. Parse Server maps it to
|
|
89
|
+
# +req.info.context+ inside beforeSave/afterSave cloud triggers.
|
|
90
|
+
# Omit or pass +nil+ to leave behavior unchanged.
|
|
87
91
|
# @return [Parse::Response]
|
|
88
|
-
def create_object(className, body = {}, headers: {}, **opts)
|
|
92
|
+
def create_object(className, body = {}, headers: {}, context: nil, **opts)
|
|
93
|
+
unless context.nil?
|
|
94
|
+
headers = headers.merge(Parse::Protocol::CLOUD_CONTEXT => context.to_json)
|
|
95
|
+
end
|
|
89
96
|
response = request :post, uri_path(className), body: body, headers: headers, opts: opts
|
|
90
97
|
response.parse_class = className if response.present?
|
|
91
98
|
response
|
|
@@ -135,8 +142,15 @@ module Parse
|
|
|
135
142
|
# @param body [Hash] The key value pairs to update.
|
|
136
143
|
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
137
144
|
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
145
|
+
# @param context [Hash, nil] an optional caller context forwarded as the
|
|
146
|
+
# +X-Parse-Cloud-Context+ header. Parse Server maps it to
|
|
147
|
+
# +req.info.context+ inside beforeSave/afterSave cloud triggers.
|
|
148
|
+
# Omit or pass +nil+ to leave behavior unchanged.
|
|
138
149
|
# @return [Parse::Response]
|
|
139
|
-
def update_object(className, id, body = {}, headers: {}, **opts)
|
|
150
|
+
def update_object(className, id, body = {}, headers: {}, context: nil, **opts)
|
|
151
|
+
unless context.nil?
|
|
152
|
+
headers = headers.merge(Parse::Protocol::CLOUD_CONTEXT => context.to_json)
|
|
153
|
+
end
|
|
140
154
|
response = request :put, uri_path(className, id), body: body, headers: headers, opts: opts
|
|
141
155
|
response.parse_class = className if response.present?
|
|
142
156
|
response
|
|
@@ -45,6 +45,39 @@ module Parse
|
|
|
45
45
|
s
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Parse trigger className pattern: a normal identifier, OR one of Parse
|
|
49
|
+
# Server's `@`-prefixed pseudo-classes (`@File` for file triggers,
|
|
50
|
+
# `@Connect` for the connection-global LiveQuery trigger). The optional
|
|
51
|
+
# leading `@` is the only relaxation; the rest stays path-safe (no `/`,
|
|
52
|
+
# `.`, or `..`).
|
|
53
|
+
TRIGGER_CLASS_PATTERN = /\A@?[A-Za-z_][A-Za-z0-9_]*\z/.freeze
|
|
54
|
+
|
|
55
|
+
# Validate a className used in a webhook-trigger path
|
|
56
|
+
# (`hooks/triggers/<className>/<trigger>`). Same as {.identifier!} but
|
|
57
|
+
# additionally accepts the `@File` / `@Connect` pseudo-classes that Parse
|
|
58
|
+
# Server uses for file and connection triggers. `create_trigger` carries
|
|
59
|
+
# the className in the request BODY (not the path), so it already accepts
|
|
60
|
+
# these; this keeps fetch / update / delete symmetric with create.
|
|
61
|
+
#
|
|
62
|
+
# @param value the className to validate.
|
|
63
|
+
# @param kind [String] human-readable name for error messages.
|
|
64
|
+
# @return [String] the validated className.
|
|
65
|
+
# @raise [ArgumentError] if blank or otherwise fails the pattern.
|
|
66
|
+
def trigger_class_name!(value, kind: "class name")
|
|
67
|
+
s = value.to_s
|
|
68
|
+
if s.empty?
|
|
69
|
+
raise ArgumentError, "#{kind} must not be empty"
|
|
70
|
+
end
|
|
71
|
+
unless TRIGGER_CLASS_PATTERN.match?(s)
|
|
72
|
+
raise ArgumentError,
|
|
73
|
+
"#{kind} #{s.inspect} contains characters that are not allowed in " \
|
|
74
|
+
"a Parse trigger class name. Names must match " \
|
|
75
|
+
"/\\A@?[A-Za-z_][A-Za-z0-9_]*\\z/ (an identifier, optionally an " \
|
|
76
|
+
"@-prefixed pseudo-class such as @File or @Connect)."
|
|
77
|
+
end
|
|
78
|
+
s
|
|
79
|
+
end
|
|
80
|
+
|
|
48
81
|
# Validate and percent-encode a less-restrictive path segment, used
|
|
49
82
|
# for file names which can contain hyphens, periods, and other
|
|
50
83
|
# filename-safe characters but must never contain a literal `/`,
|
data/lib/parse/api/server.rb
CHANGED
|
@@ -59,6 +59,100 @@ module Parse
|
|
|
59
59
|
server_info.present? ? @server_info[:parseServerVersion] : nil
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# The `features` block advertised by `GET /serverInfo`. Parse Server
|
|
63
|
+
# surfaces coarse capability groups here (`globalConfig`, `hooks`,
|
|
64
|
+
# `cloudCode`, `logs`, `push`, `schemas`), each a Hash of booleans.
|
|
65
|
+
# This is authoritative where present but intentionally coarse — it
|
|
66
|
+
# does NOT carry fine-grained behavior flags like "public explain" or
|
|
67
|
+
# the LiveQuery `keys` rename. Those are resolved by version inference
|
|
68
|
+
# in {#server_supports?}.
|
|
69
|
+
# @return [Hash] the advertised features block, or `{}` if unavailable.
|
|
70
|
+
def server_features
|
|
71
|
+
info = server_info
|
|
72
|
+
return {} unless info.is_a?(Hash)
|
|
73
|
+
feats = info[:features]
|
|
74
|
+
feats.is_a?(Hash) ? feats : {}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Capability table consumed by {#server_supports?}. Each entry is
|
|
78
|
+
# version-inferred (we cannot read these off the coarse `features`
|
|
79
|
+
# block) with one of two predicates:
|
|
80
|
+
#
|
|
81
|
+
# - `since:` — the capability EXISTS on this version and newer. An
|
|
82
|
+
# unknown/unparseable server version resolves to `true`
|
|
83
|
+
# (fail-open-to-modern: assume the current server line, matching the
|
|
84
|
+
# deprecation gate's posture).
|
|
85
|
+
# - `until:` — the capability existed BELOW this version and was
|
|
86
|
+
# removed/restricted at it. Unknown version resolves to `false`
|
|
87
|
+
# (the modern server no longer offers it).
|
|
88
|
+
#
|
|
89
|
+
# `feature:` (a `[group, flag]` pair) lets a future capability prefer
|
|
90
|
+
# the advertised `features` block when Parse Server genuinely surfaces
|
|
91
|
+
# it there; absent that, the version predicate decides.
|
|
92
|
+
# @!visibility private
|
|
93
|
+
CAPABILITIES = {
|
|
94
|
+
# LiveQuery subscription field projection: the `fields` option was
|
|
95
|
+
# renamed `keys` in Parse Server 7.0.0 (DEPPS9 / #8852). The SDK
|
|
96
|
+
# emits both, so this is informational rather than gating.
|
|
97
|
+
livequery_keys_option: { since: "7.0.0" },
|
|
98
|
+
# Cloud functions encode returned Parse.Object values as `__type`
|
|
99
|
+
# dictionaries: default flipped to `true` in 8.0.0, made
|
|
100
|
+
# unconditional (option removed) in 9.0.0.
|
|
101
|
+
cloud_object_encoding: { since: "8.0.0" },
|
|
102
|
+
# Non-master `explain` on a query: `allowPublicExplain` defaulted to
|
|
103
|
+
# `false` in 9.0.0, so a session-scoped explain that worked on 8.x
|
|
104
|
+
# is rejected on 9.x unless the operator re-enables it.
|
|
105
|
+
public_explain: { until: "9.0.0" },
|
|
106
|
+
# Aggregation `rawValues` / `rawFieldNames` options added in 9.9.0
|
|
107
|
+
# (#10438).
|
|
108
|
+
aggregate_raw_values: { since: "9.9.0" },
|
|
109
|
+
}.freeze
|
|
110
|
+
|
|
111
|
+
# Capability probe against the connected Parse Server. Builds on the
|
|
112
|
+
# already-memoized {#server_info} (no extra round-trip beyond the one
|
|
113
|
+
# `serverInfo` fetch) and the coarse `features` block, falling back to
|
|
114
|
+
# version inference for behavior flags the `features` block does not
|
|
115
|
+
# carry.
|
|
116
|
+
#
|
|
117
|
+
# Fails OPEN to the modern server line: when the server version cannot
|
|
118
|
+
# be determined (offline unit tests, a `serverInfo` outage, a wire
|
|
119
|
+
# surprise), a `since:` capability resolves `true` and an `until:`
|
|
120
|
+
# capability resolves `false` — i.e. "assume the current server",
|
|
121
|
+
# mirroring {#warn_if_deprecated_server_version!}.
|
|
122
|
+
#
|
|
123
|
+
# @example
|
|
124
|
+
# client.server_supports?(:public_explain) # => false on PS 9.x
|
|
125
|
+
# client.server_supports?(:aggregate_raw_values)
|
|
126
|
+
# @param feature [Symbol] a key of {CAPABILITIES}.
|
|
127
|
+
# @return [Boolean] whether the connected server supports the feature.
|
|
128
|
+
# @raise [ArgumentError] for an unknown capability key (typo guard).
|
|
129
|
+
def server_supports?(feature)
|
|
130
|
+
spec = CAPABILITIES[feature]
|
|
131
|
+
raise ArgumentError, "Unknown Parse Server capability #{feature.inspect}" if spec.nil?
|
|
132
|
+
|
|
133
|
+
# Prefer the advertised features block when a capability declares a
|
|
134
|
+
# `[group, flag]` path AND the server actually surfaces it.
|
|
135
|
+
if (path = spec[:feature])
|
|
136
|
+
group, flag = path
|
|
137
|
+
advertised = server_features.dig(group.to_s, flag.to_s)
|
|
138
|
+
advertised = server_features.dig(group, flag) if advertised.nil?
|
|
139
|
+
return advertised == true unless advertised.nil?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
version = server_version.to_s
|
|
143
|
+
if (floor = spec[:since])
|
|
144
|
+
# Supported on `floor` and newer. Unknown version => assume modern => true.
|
|
145
|
+
return true if version.empty?
|
|
146
|
+
!server_version_below?(version, floor)
|
|
147
|
+
elsif (ceiling = spec[:until])
|
|
148
|
+
# Supported strictly below `ceiling`. Unknown version => assume modern => false.
|
|
149
|
+
return false if version.empty?
|
|
150
|
+
server_version_below?(version, ceiling)
|
|
151
|
+
else
|
|
152
|
+
false
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
62
156
|
private
|
|
63
157
|
|
|
64
158
|
# One-shot deprecation warning. The check runs once per client
|
data/lib/parse/api/users.rb
CHANGED
|
@@ -14,7 +14,11 @@ module Parse
|
|
|
14
14
|
# @!visibility private
|
|
15
15
|
LOGIN_PATH = "login"
|
|
16
16
|
# @!visibility private
|
|
17
|
+
VERIFY_PASSWORD_PATH = "verifyPassword"
|
|
18
|
+
# @!visibility private
|
|
17
19
|
REQUEST_PASSWORD_RESET = "requestPasswordReset"
|
|
20
|
+
# @!visibility private
|
|
21
|
+
VERIFICATION_EMAIL_REQUEST = "verificationEmailRequest"
|
|
18
22
|
|
|
19
23
|
# Fetch a {Parse::User} for a given objectId.
|
|
20
24
|
# @param id [String] the user objectid
|
|
@@ -130,6 +134,26 @@ module Parse
|
|
|
130
134
|
response
|
|
131
135
|
end
|
|
132
136
|
|
|
137
|
+
# Request that Parse Server (re)send the email-address verification email
|
|
138
|
+
# for a registered, not-yet-verified user. Requires the server to have an
|
|
139
|
+
# email adapter and `verifyUserEmails` enabled; otherwise Parse Server
|
|
140
|
+
# responds with an error. Rate-limited per email like password reset.
|
|
141
|
+
#
|
|
142
|
+
# @param email [String] the Parse user email.
|
|
143
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
144
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
145
|
+
# @return [Parse::Response]
|
|
146
|
+
def request_email_verification(email, headers: {}, **opts)
|
|
147
|
+
rate_key = "emailverify:#{email}"
|
|
148
|
+
check_login_rate_limit!(rate_key)
|
|
149
|
+
body = { email: email }
|
|
150
|
+
response = request :post, VERIFICATION_EMAIL_REQUEST, body: body, opts: opts, headers: headers
|
|
151
|
+
# Indistinguishable found/not-found response, like password reset — count
|
|
152
|
+
# every attempt toward backoff so probing can't reset the counter.
|
|
153
|
+
track_login_attempt(rate_key, false)
|
|
154
|
+
response
|
|
155
|
+
end
|
|
156
|
+
|
|
133
157
|
# Login a user. Implements client-side rate limiting with exponential
|
|
134
158
|
# backoff after repeated failures to mitigate brute force attacks.
|
|
135
159
|
# @param username [String] the Parse user username.
|
|
@@ -180,6 +204,37 @@ module Parse
|
|
|
180
204
|
response
|
|
181
205
|
end
|
|
182
206
|
|
|
207
|
+
# Verify a user's credentials against Parse Server without minting a session.
|
|
208
|
+
# This is the canonical step-up / re-authentication primitive: it confirms
|
|
209
|
+
# that the username + password combination is correct without producing a
|
|
210
|
+
# new session token on success.
|
|
211
|
+
#
|
|
212
|
+
# Uses the +POST /parse/verifyPassword+ endpoint (credentials in the request
|
|
213
|
+
# BODY, mirroring +login+) rather than the +GET+ form. Parse Server accepts
|
|
214
|
+
# both (same handler, neither master-key gated; the POST variant landed in
|
|
215
|
+
# 7.1.0), but POST keeps the plaintext password out of the URL — and
|
|
216
|
+
# therefore out of server access logs, reverse-proxy logs, the +Referer+
|
|
217
|
+
# header, and the SDK's URL-keyed response cache.
|
|
218
|
+
#
|
|
219
|
+
# On success Parse Server returns the user object (HTTP 200) with the same
|
|
220
|
+
# shape as a login response (minus +sessionToken+). On failure it returns a
|
|
221
|
+
# 4xx with an error body, most commonly:
|
|
222
|
+
# - code 101 (+ERROR_OBJECT_NOT_FOUND+) for an unknown username or wrong password.
|
|
223
|
+
# - code 205 (+ERROR_EMAIL_NOT_FOUND+) when +preventLoginWithUnverifiedEmail+
|
|
224
|
+
# is enabled and the account's email has not been verified.
|
|
225
|
+
#
|
|
226
|
+
# @param username [String] the Parse user username.
|
|
227
|
+
# @param password [String] the Parse user's associated password.
|
|
228
|
+
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
229
|
+
# @param opts [Hash] additional options to pass to the {Parse::Client} request.
|
|
230
|
+
# @return [Parse::Response]
|
|
231
|
+
def verify_password(username, password, headers: {}, **opts)
|
|
232
|
+
body = { username: username, password: password }
|
|
233
|
+
response = request :post, VERIFY_PASSWORD_PATH, body: body, headers: headers, opts: opts
|
|
234
|
+
response.parse_class = Parse::Model::CLASS_USER
|
|
235
|
+
response
|
|
236
|
+
end
|
|
237
|
+
|
|
183
238
|
# Logout a user by deleting the associated session.
|
|
184
239
|
# @param session_token [String] the Parse user session token to delete.
|
|
185
240
|
# @param headers [Hash] additional HTTP headers to send with the request.
|
|
@@ -223,7 +278,7 @@ module Parse
|
|
|
223
278
|
LOGIN_RATE_LIMIT_TTL = 600
|
|
224
279
|
|
|
225
280
|
# Checks if a login attempt is allowed for the given username.
|
|
226
|
-
# @raise [
|
|
281
|
+
# @raise [Parse::Error::AccountLockoutError] if the account is temporarily locked out.
|
|
227
282
|
def check_login_rate_limit!(username)
|
|
228
283
|
@login_rate_limit_mutex ||= Mutex.new
|
|
229
284
|
@login_rate_limit_mutex.synchronize do
|
|
@@ -231,7 +286,8 @@ module Parse
|
|
|
231
286
|
return unless entry
|
|
232
287
|
if entry[:locked_until] && Time.now < entry[:locked_until]
|
|
233
288
|
wait = (entry[:locked_until] - Time.now).ceil
|
|
234
|
-
raise
|
|
289
|
+
raise Parse::Error::AccountLockoutError,
|
|
290
|
+
"Login rate limited for '#{username}'. Try again in #{wait} seconds."
|
|
235
291
|
end
|
|
236
292
|
end
|
|
237
293
|
end
|
data/lib/parse/atlas_search.rb
CHANGED
|
@@ -105,10 +105,10 @@ module Parse
|
|
|
105
105
|
|
|
106
106
|
# @!attribute [rw] role_cache_ttl
|
|
107
107
|
# TTL (seconds) for {Session}'s user-id → role-name cache.
|
|
108
|
-
# Default:
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
108
|
+
# Default: 30. Short on purpose: stale role data yields
|
|
109
|
+
# incorrect ACL decisions, so the cache is sized to amortize
|
|
110
|
+
# within a single request/turn but expire well inside the
|
|
111
|
+
# response time the operator notices a role grant or revoke.
|
|
112
112
|
# @return [Integer]
|
|
113
113
|
attr_accessor :role_cache_ttl
|
|
114
114
|
|
|
@@ -141,7 +141,7 @@ module Parse
|
|
|
141
141
|
# @param session_cache_ttl [Integer] session-token cache TTL
|
|
142
142
|
# (seconds). Default: 3600.
|
|
143
143
|
# @param role_cache_ttl [Integer] role-name cache TTL (seconds).
|
|
144
|
-
# Default:
|
|
144
|
+
# Default: 30.
|
|
145
145
|
# @example
|
|
146
146
|
# Parse::AtlasSearch.configure(enabled: true, default_index: "default")
|
|
147
147
|
def configure(enabled: true,
|
|
@@ -195,7 +195,7 @@ module Parse
|
|
|
195
195
|
@allow_raw = default_allow_raw
|
|
196
196
|
@require_session_token = false
|
|
197
197
|
@session_cache_ttl = 3600
|
|
198
|
-
@role_cache_ttl =
|
|
198
|
+
@role_cache_ttl = 30
|
|
199
199
|
@session_cache = Session::MemoryCache.new
|
|
200
200
|
@role_cache = Session::MemoryCache.new
|
|
201
201
|
@master_warned = false
|
|
@@ -1015,7 +1015,7 @@ module Parse
|
|
|
1015
1015
|
@allow_raw = nil
|
|
1016
1016
|
@require_session_token = false
|
|
1017
1017
|
@session_cache_ttl = 3600
|
|
1018
|
-
@role_cache_ttl =
|
|
1018
|
+
@role_cache_ttl = 30
|
|
1019
1019
|
@session_cache = Session::MemoryCache.new
|
|
1020
1020
|
@role_cache = Session::MemoryCache.new
|
|
1021
1021
|
@master_warned = false
|
|
@@ -74,6 +74,11 @@ module Parse
|
|
|
74
74
|
Parse::Protocol::MASTER_KEY,
|
|
75
75
|
Parse::Protocol::API_KEY,
|
|
76
76
|
Parse::Protocol::SESSION_TOKEN,
|
|
77
|
+
# Caller-supplied Cloud Code context (X-Parse-Cloud-Context) carries
|
|
78
|
+
# `context.to_json`, which may hold PII / request metadata. Redact it in
|
|
79
|
+
# the header log; the body/as_json log path scrubs sensitive sub-values
|
|
80
|
+
# of context separately.
|
|
81
|
+
Parse::Protocol::CLOUD_CONTEXT,
|
|
77
82
|
"X-Parse-JavaScript-Key",
|
|
78
83
|
"Authorization",
|
|
79
84
|
"Cookie",
|
|
@@ -31,6 +31,10 @@ module Parse
|
|
|
31
31
|
# The request header field for MongoDB read preference.
|
|
32
32
|
# Supported values: PRIMARY, PRIMARY_PREFERRED, SECONDARY, SECONDARY_PREFERRED, NEAREST
|
|
33
33
|
READ_PREFERENCE = "X-Parse-Read-Preference"
|
|
34
|
+
# The request header field for threading a caller-supplied context object
|
|
35
|
+
# through a write or cloud-function call. Parse Server maps this header to
|
|
36
|
+
# +req.info.context+ and flows it through beforeSave/afterSave triggers.
|
|
37
|
+
CLOUD_CONTEXT = "X-Parse-Cloud-Context"
|
|
34
38
|
|
|
35
39
|
# Valid read preference values for MongoDB
|
|
36
40
|
READ_PREFERENCES = %w[PRIMARY PRIMARY_PREFERRED SECONDARY SECONDARY_PREFERRED NEAREST].freeze
|
data/lib/parse/client.rb
CHANGED
|
@@ -1414,9 +1414,53 @@ module Parse
|
|
|
1414
1414
|
# Unwrap the `{ "result" => ... }` envelope from a successful cloud-code response.
|
|
1415
1415
|
# Guards against unusual server payloads (non-Hash bodies) by returning the raw
|
|
1416
1416
|
# result rather than raising TypeError on `String#[]`/`Integer#[]`.
|
|
1417
|
+
#
|
|
1418
|
+
# Parse Server 8.0 flipped `encodeParseObjectInCloudFunction` to true and 9.0
|
|
1419
|
+
# removed the opt-out, so a cloud function that returns a Parse object now
|
|
1420
|
+
# yields a `__type`-encoded dictionary (`{"__type":"Object","className":...}`)
|
|
1421
|
+
# where a pre-8.x caller received a plain attribute Hash. We decode those
|
|
1422
|
+
# self-describing envelopes back into `Parse::Object` / `Parse::Pointer` so the
|
|
1423
|
+
# value a caller sees is consistent across server versions and matches what
|
|
1424
|
+
# every other Parse SDK returns. Decoding is conservative: only a fully-shaped
|
|
1425
|
+
# Object/Pointer envelope is converted, and an Object of an UNregistered class
|
|
1426
|
+
# is left as a raw Hash (building it would degrade to a field-less Pointer).
|
|
1427
|
+
# Plain Hashes and arbitrary `__type` app data pass through untouched.
|
|
1417
1428
|
def self._extract_cloud_result(response)
|
|
1418
1429
|
r = response.result
|
|
1419
|
-
r.is_a?(Hash) ? r["result"] : r
|
|
1430
|
+
value = r.is_a?(Hash) ? r["result"] : r
|
|
1431
|
+
_decode_cloud_value(value)
|
|
1432
|
+
end
|
|
1433
|
+
|
|
1434
|
+
# @!visibility private
|
|
1435
|
+
# Recursively decode Parse-encoded values in a cloud-code result. See
|
|
1436
|
+
# {._extract_cloud_result} for the rationale and the conservatism rules.
|
|
1437
|
+
def self._decode_cloud_value(value)
|
|
1438
|
+
case value
|
|
1439
|
+
when Array
|
|
1440
|
+
value.map { |v| _decode_cloud_value(v) }
|
|
1441
|
+
when Hash
|
|
1442
|
+
type = value["__type"] || value[:__type]
|
|
1443
|
+
class_name = value["className"] || value[:className]
|
|
1444
|
+
object_id = value["objectId"] || value[:objectId]
|
|
1445
|
+
if type == Parse::Model::TYPE_POINTER && class_name && object_id
|
|
1446
|
+
# Pointers carry no attributes, so building one is lossless even for
|
|
1447
|
+
# an unregistered class (yields a Parse::Pointer).
|
|
1448
|
+
Parse::Object.build(value)
|
|
1449
|
+
elsif type == Parse::Model::TYPE_OBJECT && class_name && object_id &&
|
|
1450
|
+
Parse::Model.find_class(class_name)
|
|
1451
|
+
# Only build a full object when the class is registered; otherwise
|
|
1452
|
+
# Parse::Object.build collapses to a field-less Pointer and we'd lose
|
|
1453
|
+
# the attributes — better to hand back the raw Hash.
|
|
1454
|
+
Parse::Object.build(value)
|
|
1455
|
+
else
|
|
1456
|
+
# Plain Hash, partial envelope, or non-object `__type` (Date/GeoPoint/
|
|
1457
|
+
# File/Bytes, or literal app data): leave the node shape intact and
|
|
1458
|
+
# only recurse into nested values so embedded objects still decode.
|
|
1459
|
+
value.transform_values { |v| _decode_cloud_value(v) }
|
|
1460
|
+
end
|
|
1461
|
+
else
|
|
1462
|
+
value
|
|
1463
|
+
end
|
|
1420
1464
|
end
|
|
1421
1465
|
|
|
1422
1466
|
# Helper method to trigger cloud jobs and get results.
|
|
@@ -1491,6 +1535,9 @@ module Parse
|
|
|
1491
1535
|
# @option opts [Symbol] :client The client connection to use.
|
|
1492
1536
|
# @option opts [Boolean] :raw Whether to return the raw response object.
|
|
1493
1537
|
# @option opts [Boolean] :master_key Whether to use the master key for this request.
|
|
1538
|
+
# @option opts [Hash, nil] :context An optional caller context forwarded as the
|
|
1539
|
+
# +X-Parse-Cloud-Context+ header. Parse Server maps it to +req.info.context+
|
|
1540
|
+
# in the function handler and flows it through beforeSave/afterSave triggers.
|
|
1494
1541
|
# @return [Object] the result data of the response. nil if there was an error.
|
|
1495
1542
|
def self.call_function(name, body = {}, **opts)
|
|
1496
1543
|
conn = opts[:session] || opts[:client] || :default
|
|
@@ -1500,7 +1547,13 @@ module Parse
|
|
|
1500
1547
|
request_opts[:session_token] = opts[:session_token] if opts[:session_token]
|
|
1501
1548
|
request_opts[:master_key] = opts[:master_key] if opts[:master_key]
|
|
1502
1549
|
|
|
1503
|
-
|
|
1550
|
+
# Build call kwargs; only forward context: when explicitly supplied so
|
|
1551
|
+
# call sites that do not use context produce the same opts hash that
|
|
1552
|
+
# existing mock expectations match against.
|
|
1553
|
+
call_kwargs = { opts: request_opts }
|
|
1554
|
+
call_kwargs[:context] = opts[:context] unless opts[:context].nil?
|
|
1555
|
+
|
|
1556
|
+
response = Parse::Client.client(conn).call_function(name, body, **call_kwargs)
|
|
1504
1557
|
return response if opts[:raw].present?
|
|
1505
1558
|
if response.error?
|
|
1506
1559
|
Parse::Client._safe_warn("CloudCodeError", response, name: name)
|