parse-stack-next 5.8.0 → 5.8.2
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/CHANGELOG.md +630 -0
- data/README.md +31 -19
- data/docs/caching.md +90 -5
- data/docs/mcp_guide.md +22 -5
- data/docs/mongodb_direct_guide.md +29 -0
- data/docs/webhooks_guide.md +94 -9
- data/lib/parse/acl_scope.rb +71 -9
- data/lib/parse/agent/constraint_translator.rb +22 -21
- data/lib/parse/agent/describe.rb +8 -2
- data/lib/parse/agent/errors.rb +17 -0
- data/lib/parse/agent/mcp_dispatcher.rb +8 -1
- data/lib/parse/agent/mcp_server.rb +47 -6
- data/lib/parse/agent.rb +521 -70
- data/lib/parse/api/batch.rb +70 -2
- data/lib/parse/atlas_search/index_manager.rb +5 -1
- data/lib/parse/atlas_search.rb +50 -1
- data/lib/parse/authorization.rb +235 -3
- data/lib/parse/cache/sub_cache.rb +26 -0
- data/lib/parse/client/authentication.rb +19 -1
- data/lib/parse/client/batch.rb +263 -11
- data/lib/parse/client/body_builder.rb +100 -15
- data/lib/parse/client/caching.rb +53 -3
- data/lib/parse/client/request.rb +59 -0
- data/lib/parse/client/response.rb +63 -5
- data/lib/parse/client.rb +173 -33
- data/lib/parse/clp_scope.rb +37 -3
- data/lib/parse/live_query/client.rb +142 -11
- data/lib/parse/lock_backend.rb +4 -3
- data/lib/parse/model/associations/collection_proxy.rb +19 -11
- data/lib/parse/model/associations/has_many.rb +11 -0
- data/lib/parse/model/associations/pointer_collection_proxy.rb +36 -0
- data/lib/parse/model/associations/relation_collection_proxy.rb +97 -12
- data/lib/parse/model/classes/role.rb +23 -0
- data/lib/parse/model/classes/session.rb +307 -19
- data/lib/parse/model/classes/user.rb +33 -12
- data/lib/parse/model/core/actions.rb +41 -5
- data/lib/parse/model/core/fetching.rb +29 -2
- data/lib/parse/model/core/field_guards.rb +16 -8
- data/lib/parse/model/file.rb +96 -19
- data/lib/parse/model/object.rb +52 -1
- data/lib/parse/model/push.rb +11 -1
- data/lib/parse/mongodb.rb +43 -2
- data/lib/parse/pipeline_security.rb +157 -21
- data/lib/parse/query/constraint.rb +1 -1
- data/lib/parse/query/constraints.rb +665 -41
- data/lib/parse/query.rb +802 -60
- data/lib/parse/stack/tasks.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +23 -0
- data/lib/parse/vector_search/hybrid.rb +78 -16
- data/lib/parse/vector_search.rb +215 -7
- data/lib/parse/webhooks/payload.rb +82 -8
- data/lib/parse/webhooks/registration.rb +26 -6
- data/lib/parse/webhooks/replay_protection.rb +19 -9
- data/lib/parse/webhooks.rb +329 -13
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d49d6371c1e6873ea48401f789d9087e23870530ae405bf2629ea9db67360995
|
|
4
|
+
data.tar.gz: 9589dfb1ec150a78d527c6ad0097a3ee4263701b104baf7b54afb73e73ec3bdf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22998b27d0f277d174759d29c5087675628eee3a9a24ed460c82c87deda6a28252ed1d1ad78daee9a2438b2a7b1c4f8995197450d9eb5922b5689e58bda52bdb
|
|
7
|
+
data.tar.gz: 5ef7ef2f07269ca95bbd985c806250ca7d93d02c7c6bccd83a9d72a0c9a57b990bac63e977ddfbd1c601481562fb12d5c0272639ec415787cef0cc76fc4dd3c0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,635 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.8.2
|
|
4
|
+
|
|
5
|
+
A security and correctness patch. No new features beyond the `session:`
|
|
6
|
+
arguments and the cache opt-in the fixes need; intentional master-key and
|
|
7
|
+
analytics access is unchanged. Outcomes that change are listed under
|
|
8
|
+
Behavior Notes.
|
|
9
|
+
|
|
10
|
+
#### Transport and MCP authentication
|
|
11
|
+
|
|
12
|
+
- **FIXED**: A whitespace-only MCP API key (`api_key: " "`,
|
|
13
|
+
`MCP_API_KEY=" "`, or a non-breaking-space key) now counts as no key.
|
|
14
|
+
Before, it passed the check that refuses a non-loopback bind without a key
|
|
15
|
+
and then disabled request authentication, leaving a public MCP endpoint
|
|
16
|
+
open. The configured key and the provided `X-MCP-API-Key` header are
|
|
17
|
+
stripped the same way, an explicit blank `api_key:` no longer hides
|
|
18
|
+
`MCP_API_KEY`, and a non-loopback bind with a key shorter than 16
|
|
19
|
+
characters warns.
|
|
20
|
+
- **FIXED**: HTTPS and WebSocket scheme checks parse the URL and compare the
|
|
21
|
+
scheme case-insensitively. Before, `HTTP://` passed `require_https`,
|
|
22
|
+
`HTTPS://` with TLS verification turned off was not refused, and `WS://` got
|
|
23
|
+
past the plaintext LiveQuery guard.
|
|
24
|
+
- **FIXED**: The TLS guard also refuses `ssl: { verify_mode:
|
|
25
|
+
OpenSSL::SSL::VERIFY_NONE }`, `ssl: { verify_hostname: false }`, and a
|
|
26
|
+
`Faraday::SSLOptions` with verification off on an https server URL. Before,
|
|
27
|
+
only a Hash with `verify: false` was caught. A `faraday:` option passed as
|
|
28
|
+
`Faraday::ConnectionOptions` gets the same TLS and proxy guards as a Hash,
|
|
29
|
+
and any other non-Hash value raises.
|
|
30
|
+
- **FIXED**: `Parse::LiveQuery::Client.new` validates an explicit or
|
|
31
|
+
configured URL, as `Parse.setup` does. Plaintext `ws://` to a routable host
|
|
32
|
+
is refused without `allow_insecure`, and a URL whose scheme is not `ws`,
|
|
33
|
+
`wss`, `http`, or `https` raises `ArgumentError`. **CHANGED**: an
|
|
34
|
+
`https://` LiveQuery URL is used as `wss://` (it used to be opened without
|
|
35
|
+
TLS), and an `http://` URL as `ws://` under the same loopback rule, each
|
|
36
|
+
with a one-time deprecation warning.
|
|
37
|
+
- **FIXED**: `Parse::File` URLs hydrated from Parse JSON no longer skip the
|
|
38
|
+
`trusted_url_hosts` check when the scheme is uppercase (`HTTPS://`), the
|
|
39
|
+
URL has leading whitespace, or the URL is malformed (`https:host`,
|
|
40
|
+
`https:\\host`, `https:///host`), all of which browsers resolve to the
|
|
41
|
+
remote host. Under
|
|
42
|
+
`untrusted_url_policy = :raise` or `:strip`, a malformed http(s) URL is
|
|
43
|
+
treated as untrusted. `force_ssl` upgrades `HTTP://` URLs too. Hydration
|
|
44
|
+
reads a URL the way a browser does (tabs and newlines removed, leading
|
|
45
|
+
control characters stripped), checks scheme-relative values (`//host`,
|
|
46
|
+
`\\host`) against `trusted_url_hosts`, and treats `javascript:`, `data:`,
|
|
47
|
+
and other non-http(s) URLs as untrusted.
|
|
48
|
+
- **NEW**: `Parse::File.trust_legacy_tfss_on_any_host = false` requires
|
|
49
|
+
`tfss-` file URLs to come from `trusted_url_hosts`. The default (`true`)
|
|
50
|
+
keeps accepting them from any host.
|
|
51
|
+
- **FIXED**: `Parse::Response#inspect` prints only the status and the
|
|
52
|
+
result's shape, and `#to_s` replaces credential fields with `[FILTERED]`:
|
|
53
|
+
session tokens, passwords, `authData`, access and refresh tokens, API,
|
|
54
|
+
master, client, and JavaScript keys, MFA recovery codes and secrets
|
|
55
|
+
(`recovery`, `recoveryCodes`, `secret`, `authDataResponse`), and
|
|
56
|
+
storage-form columns (`_session_token`, `_hashed_password`,
|
|
57
|
+
`_email_verify_token`, `_perishable_token`, `_password_history`,
|
|
58
|
+
`_auth_data_*`). The same list drives log redaction, which also covers
|
|
59
|
+
header-shaped text (`X-Parse-Master-Key: ...`, `X-Parse-REST-API-Key`,
|
|
60
|
+
`X-Parse-Session-Token`, `X-Parse-Javascript-Key`, `X-Parse-Client-Key`,
|
|
61
|
+
`X-Parse-Webhook-Key`, `Authorization: Bearer ...`). `to_s` also filters
|
|
62
|
+
credential text inside string values (`password=...`), and its error form
|
|
63
|
+
redacts the request line and error text. Text patterns run on string values
|
|
64
|
+
before encoding, and a quoted value is redacted whole (spaces included,
|
|
65
|
+
and after an auth scheme: `password="a b"`, `Authorization: Bearer "x"`),
|
|
66
|
+
so the output stays valid JSON; log redaction of JSON bodies works the same
|
|
67
|
+
way. `#result` is unchanged.
|
|
68
|
+
- **IMPROVED**: Webhook endpoint registration accepts the scheme in any case
|
|
69
|
+
and warns when an `http://` endpoint on a routable host would carry the
|
|
70
|
+
webhook key in cleartext.
|
|
71
|
+
|
|
72
|
+
#### Batches and transactions
|
|
73
|
+
|
|
74
|
+
- **FIXED**: A batch keeps each write's credentials. Parse Server runs every
|
|
75
|
+
sub-request of a `POST /batch` under that call's own credentials and ignores
|
|
76
|
+
per-sub-request headers. Before, `Array#save`, `Array#destroy`,
|
|
77
|
+
`Parse.batch`, and `Parse::Object.transaction` sent everything through the
|
|
78
|
+
default client, so a write built for a user session (a class bound to
|
|
79
|
+
`Parse.client.become(token)`, or a `Parse::Request` with `session_token:` /
|
|
80
|
+
`use_master_key: false`) ran with the master key and bypassed ACLs and CLPs.
|
|
81
|
+
Writes are now sent through their own client and options, and a batch with
|
|
82
|
+
mixed credentials goes out as separate calls with responses in request
|
|
83
|
+
order. Credentials resolve exactly as for a single request: a request's
|
|
84
|
+
`session_token:` / `use_master_key:` options, its `X-Parse-Session-Token`
|
|
85
|
+
header, and its master-key suppression header (which keeps the master key
|
|
86
|
+
off even with `use_master_key: true`). The same rules apply to direct
|
|
87
|
+
`Parse::Client#batch_request` calls. A client chosen explicitly
|
|
88
|
+
(`BatchOperation#client=`, or the client `batch_request` is called on) is
|
|
89
|
+
used for every request, as `Parse::Client#request` uses it for a single
|
|
90
|
+
request; an object's class client applies only to implicit batches
|
|
91
|
+
(`Array#save`, `Array#destroy`, `Parse.batch`). A `use_master_key:` that is
|
|
92
|
+
not exactly `true` or `false` is treated as unset, a `session_token:` that
|
|
93
|
+
resolves to no token fails closed, and narrowing `batch_request` options
|
|
94
|
+
(`use_master_key: false`) override a request's own `use_master_key: true`.
|
|
95
|
+
- **FIXED**: `Parse::Object.transaction` runs with its objects' class client.
|
|
96
|
+
A transaction that mixes credentials raises
|
|
97
|
+
`Parse::BatchOperation::MixedAuthorityError` before anything is sent,
|
|
98
|
+
because Parse Server runs a transaction under one credential and it cannot
|
|
99
|
+
be split. Clients are compared by their credentials (server, application,
|
|
100
|
+
keys, and bound session), not by object identity, so classes whose clients
|
|
101
|
+
were configured identically, such as before and after a second
|
|
102
|
+
`Parse.setup`, still share one transaction. Requests that name their own
|
|
103
|
+
session token are compared without the master key or bound session, so
|
|
104
|
+
`transaction(session:)` across clients that differ only in those is sent as
|
|
105
|
+
one call.
|
|
106
|
+
- **FIXED**: `Array#destroy(session:)` on sessions looks up their tokens and
|
|
107
|
+
owners as that user, so deleting session ids the user cannot see no longer
|
|
108
|
+
evicts other users' cached identities or triggers a reset. A session the
|
|
109
|
+
caller could not read is still treated as deleted when its delete succeeds:
|
|
110
|
+
its recorded owner is invalidated, or the rate-limited reset applies. A
|
|
111
|
+
denied or not-found delete of such a row forgets nothing.
|
|
112
|
+
- **NEW**: `Array#save(session:)`, `Array#destroy(session:)`, and
|
|
113
|
+
`Parse::Object.transaction(session:)` send every write in the call as the
|
|
114
|
+
given user.
|
|
115
|
+
|
|
116
|
+
#### Agents
|
|
117
|
+
|
|
118
|
+
- **FIXED**: A session-token agent whose token could not be resolved when it
|
|
119
|
+
was built (for example, Parse Server unreachable) is no longer treated as
|
|
120
|
+
master-key posture by the SDK-side permission checks. The scope is resolved
|
|
121
|
+
again on first use; if it still cannot be, the call fails with
|
|
122
|
+
`Parse::Agent::UnresolvedIdentity` (reported as `:access_denied`,
|
|
123
|
+
`kind: :unresolved_identity`) instead of skipping the CLP check. Parse
|
|
124
|
+
Server already validated the token on REST calls.
|
|
125
|
+
- **FIXED**: `Parse::Agent#inspect`, `to_s`, and `pp` print a redacted summary
|
|
126
|
+
and no longer show the session token, the client's keys, the conversation,
|
|
127
|
+
or the last request and response.
|
|
128
|
+
- **FIXED**: `Parse::Agent#as_json`, `#to_json`, and `#to_yaml`, and the same
|
|
129
|
+
methods on `Parse::Client`, return a redacted summary, so JSON log
|
|
130
|
+
formatters and error trackers that serialize an agent or client no longer
|
|
131
|
+
emit its session token, master key, or REST key.
|
|
132
|
+
- **FIXED**: A sub-agent that holds its parent's session token on the
|
|
133
|
+
parent's client reuses the parent's resolved scope instead of resolving it again. A failed resolution
|
|
134
|
+
of a different child token is reported as `Parse::Agent::UnresolvedIdentity`,
|
|
135
|
+
not as an attempt to widen the parent's scope.
|
|
136
|
+
- **FIXED**: Lazy session resolution, `impersonate`, and `refresh_scope!` are
|
|
137
|
+
serialized per agent, so a concurrent `impersonate` cannot leave one
|
|
138
|
+
token's scope bound to another token. A failed lazy resolution is retried
|
|
139
|
+
after 5 seconds, not on every call. `refresh_scope!` resolves or raises for
|
|
140
|
+
a session agent instead of returning nil, and an invalid or expired token
|
|
141
|
+
gets its own error message. `acl_scope` checks and reads the scope under
|
|
142
|
+
the same lock, and `acl_permission_strings` never returns nil for a
|
|
143
|
+
session-token agent.
|
|
144
|
+
- **FIXED**: A session agent whose token cannot be resolved records the
|
|
145
|
+
failure at construction too, so the first use inside the 5-second window is
|
|
146
|
+
refused without a second lookup, and a token Parse Server rejected is not
|
|
147
|
+
retried. The lookup runs outside the agent's scope lock, so other threads
|
|
148
|
+
using the agent are not blocked for a request timeout, and the result binds
|
|
149
|
+
only if the token is unchanged. A sub-agent reuses its parent's scope only
|
|
150
|
+
from a token-and-scope snapshot taken under the parent's lock.
|
|
151
|
+
- **FIXED**: `Parse::Client` and `Parse::Agent` refuse `Marshal.dump`
|
|
152
|
+
(`TypeError`), so a client's master key or an agent's session token cannot
|
|
153
|
+
land in a marshaled cache or job payload.
|
|
154
|
+
- **CHANGED**: `Parse::Agent#stop_impersonating!` is a no-op on an agent that
|
|
155
|
+
is not impersonating, instead of dropping a constructor-supplied session
|
|
156
|
+
token and leaving master-key posture.
|
|
157
|
+
|
|
158
|
+
#### Response cache
|
|
159
|
+
|
|
160
|
+
- **FIXED**: Reads made with a session token are no longer served from the
|
|
161
|
+
response cache. A cached answer was returned without contacting Parse
|
|
162
|
+
Server, so a revoked session (logout, `Parse::Session#destroy`,
|
|
163
|
+
`logout_all!`, a password change), or a user who lost a role or row access,
|
|
164
|
+
kept reading the cached rows until the entry expired. Session reads now
|
|
165
|
+
always reach Parse Server, whether the session came from `session_token:`,
|
|
166
|
+
`Parse.with_session`, or a session-bound client. Master-key and anonymous
|
|
167
|
+
reads are cached as before, and writes made with a session still invalidate
|
|
168
|
+
cached entries.
|
|
169
|
+
- **NEW**: `Parse::Middleware::Caching.cache_session_requests = true` (or
|
|
170
|
+
per client, `Parse.setup(cache_session_requests: true)`, which overrides the
|
|
171
|
+
class default either way) opts back into caching session reads, for apps
|
|
172
|
+
that accept revocation being bounded by the cache TTL. The
|
|
173
|
+
`parse.cache.bypass` event includes `cache_tenant`.
|
|
174
|
+
|
|
175
|
+
#### Regex validation and direct reads
|
|
176
|
+
|
|
177
|
+
- **FIXED**: A raw `$regex` in a where hash
|
|
178
|
+
(`Model.query(name: { "$regex" => ... })`, `:field.eq`, `:field.not`,
|
|
179
|
+
`$elemMatch`, and `:or` branches) goes through the same ReDoS check as
|
|
180
|
+
`:field.like`. Before, only the regex constraints themselves were checked.
|
|
181
|
+
Ruby `Regexp` and BSON regex values under `$not`, `$in`, `$nin`, and `$all`
|
|
182
|
+
are checked too.
|
|
183
|
+
- **FIXED**: Mongo-direct filters and pipelines (`results_direct`,
|
|
184
|
+
`count_direct`, `Query#aggregate`, `Parse::MongoDB.aggregate` and `find`,
|
|
185
|
+
Atlas Search and vector filters, LiveQuery `where`, and the agent
|
|
186
|
+
`aggregate` tool) run every `$regex`, Ruby `Regexp`, BSON regex, and
|
|
187
|
+
`$regexMatch` / `$regexFind` / `$regexFindAll` pattern through the ReDoS
|
|
188
|
+
check. Before, only the pattern length was capped. The agent constraint
|
|
189
|
+
translator applies the same check.
|
|
190
|
+
- **FIXED**: `Parse::RegexSecurity` parses each caller-supplied pattern
|
|
191
|
+
instead of matching a few substrings. It refuses a group repeated more than
|
|
192
|
+
once whose body holds a variable-count quantifier (including an optional
|
|
193
|
+
`?`), an alternation, or a backreference (`(a+)+`, `(a|aa)+$`, `((a+))+`,
|
|
194
|
+
`(a+){2,}`, `(a+){20}`, `^(a?){100}a{100}$`), inline comments
|
|
195
|
+
`(?#...)`, extended mode, and patterns it cannot read. Before, several of
|
|
196
|
+
these shapes passed. Repeats of a single atom (`^.{1,255}$`, `\d{1,100}`)
|
|
197
|
+
and simple lookarounds (`^(?!test)`) are accepted; the old checker refused
|
|
198
|
+
them for `:field.like`. Escaped literal patterns
|
|
199
|
+
built by `contains`, `starts_with`, and `ends_with` get a longer length cap,
|
|
200
|
+
so long values keep compiling. It also refuses subroutine calls (`\g<1>`,
|
|
201
|
+
`\g'1'`), more than three unbounded or wide quantifiers in a row with no
|
|
202
|
+
separating literal they cannot match (`\d+\d+\d+\d+`, `.*a.*a.*a.*a`),
|
|
203
|
+
and whitespace inside a repeat count (`{ 2,}`), and reads `[:` that is not a
|
|
204
|
+
complete POSIX class as a literal, as PCRE2 does. A repeated group is
|
|
205
|
+
allowed when each repeat cannot split its input two ways: fixed
|
|
206
|
+
alternatives whose first characters differ even ignoring case
|
|
207
|
+
(`^(foo|bar)+$`, but not `(?i)^(a|Aa)+$`) or a literal
|
|
208
|
+
separator next to one run that cannot match it
|
|
209
|
+
(`^[a-z0-9]+(?:-[a-z0-9]+)*$`, `(\.[\w-]+)+`, `([\w-]+\.)+`).
|
|
210
|
+
- **FIXED**: `$regexMatch`, `$regexFind`, and `$regexFindAll` in
|
|
211
|
+
caller-supplied pipelines require a literal `regex` (a String that is not a
|
|
212
|
+
field path or variable, a Regexp, or a BSON regex). An expression or
|
|
213
|
+
`"$field"` operand raises `Parse::PipelineSecurity::Error`
|
|
214
|
+
(`reason: :regex_not_literal`).
|
|
215
|
+
- **IMPROVED**: Mongo-direct reads (`Parse::MongoDB.aggregate` and `find`,
|
|
216
|
+
`results_direct`, `count_direct`, Atlas Search, and vector and hybrid
|
|
217
|
+
search) whose filter or pipeline carries a regex that is not escaped literal
|
|
218
|
+
text get a server-side `max_time_ms` of
|
|
219
|
+
`Parse::PipelineSecurity.default_regex_max_time_ms` (5000 ms) when the
|
|
220
|
+
caller passed none. Set it to nil to turn this off; an explicit
|
|
221
|
+
`max_time_ms:` always wins. For REST queries, set Parse Server's
|
|
222
|
+
`databaseOptions.maxTimeMS`.
|
|
223
|
+
- **FIXED**: `Parse::MongoDB.find` raises `Parse::ACLScope::ACLRequired` inside
|
|
224
|
+
`Parse.without_master_key` instead of reading raw documents unscoped.
|
|
225
|
+
`Parse::MongoDB.indexes` (metadata only) stays available.
|
|
226
|
+
|
|
227
|
+
#### Webhooks
|
|
228
|
+
|
|
229
|
+
- **FIXED**: A webhook request's `master` flag is trusted only when the
|
|
230
|
+
request is authenticated: the webhook key matched, or a configured
|
|
231
|
+
signature verified. Under `Parse::Webhooks.allow_unauthenticated` with no
|
|
232
|
+
signature, a body claiming `"master": true` no longer passes master-only
|
|
233
|
+
field guards, ACL owner resolution, or handler `payload.master?` checks, and
|
|
234
|
+
no longer triggers the Ruby-initiated callback dedup, so a forged `_RB_`
|
|
235
|
+
request id cannot make a write skip model callbacks (including a
|
|
236
|
+
`before_save` that rejects it). Only a JSON `true` counts as master.
|
|
237
|
+
`payload.authenticated?` reports whether the request was authenticated, and
|
|
238
|
+
`payload.claimed_master?` exposes the raw claim; both are for diagnostics
|
|
239
|
+
only. Signature verification and the authenticated flag come from one read
|
|
240
|
+
of the signing secret.
|
|
241
|
+
|
|
242
|
+
#### Behavior Notes
|
|
243
|
+
|
|
244
|
+
- **Transport.** A LiveQuery `https://` URL is used as `wss://` (and a
|
|
245
|
+
loopback `http://` as `ws://`) with a deprecation warning; configure the
|
|
246
|
+
`ws(s)://` URL directly. Other LiveQuery schemes raise. `HTTP://` with
|
|
247
|
+
`require_https`, and an https server URL with TLS verification disabled
|
|
248
|
+
(`verify: false`, `verify_mode`, `verify_hostname`, or `SSLOptions`), raise.
|
|
249
|
+
Loopback hosts for these checks are `localhost`, `127.0.0.0/8`, `::1`, and
|
|
250
|
+
`0.0.0.0`, parsed as addresses (`127.999.1.1` and `localhost.` are not
|
|
251
|
+
loopback). A padded MCP API key is enforced without its surrounding
|
|
252
|
+
whitespace. Hydrated `Parse::File` URLs with an uppercase scheme or leading
|
|
253
|
+
whitespace are checked against `trusted_url_hosts`; malformed http(s) URLs
|
|
254
|
+
are refused or stripped under `:raise` / `:strip` and warn under `:warn`,
|
|
255
|
+
as are non-http(s) schemes (`javascript:`, `data:`, `file:`) and
|
|
256
|
+
scheme-relative values pointing at an untrusted host.
|
|
257
|
+
- **Printing.** `Parse::Response#to_s` replaces credential-bearing values with
|
|
258
|
+
`"[FILTERED]"` and `#inspect` no longer shows values; read `#result` (or
|
|
259
|
+
`#result.to_json`) for raw data. `to_s` may also filter text in string
|
|
260
|
+
values that looks like a credential (`password=...`). `Parse::Agent` and `Parse::Client`
|
|
261
|
+
`inspect`, `to_s`, `as_json`, `to_json`, and `to_yaml` print redacted
|
|
262
|
+
summaries.
|
|
263
|
+
- **Agents.** A session-token agent that cannot resolve its token refuses
|
|
264
|
+
tool calls that need its permissions. `Agent#acl_scope` can now raise
|
|
265
|
+
`Parse::Agent::UnresolvedIdentity`, and `acl_scope?` is true for any
|
|
266
|
+
session-token agent; custom handlers that test `agent.acl_scope` for
|
|
267
|
+
truthiness should use `agent.acl_scope?`. A sub-agent of an unresolved
|
|
268
|
+
parent can inherit the parent's token, but one with a different identity
|
|
269
|
+
raises until the parent's token resolves. A session agent built while Parse
|
|
270
|
+
Server is unreachable refuses calls for 5 seconds before retrying.
|
|
271
|
+
Marshaling a `Parse::Client` or `Parse::Agent` raises `TypeError`; store the
|
|
272
|
+
configuration and build a new client instead.
|
|
273
|
+
- **Response cache.** Queries that set `cache: true` (or run under
|
|
274
|
+
`Parse.default_query_cache = true`) with a session token no longer get cache
|
|
275
|
+
hits. Set `cache_session_requests` to `true` (only a literal `true`
|
|
276
|
+
enables it) to restore the old behavior, and keep `expires:` short if you
|
|
277
|
+
do.
|
|
278
|
+
- **Regex.** A raw `$regex`, or a `Regexp` in a mongo-direct filter, that the
|
|
279
|
+
ReDoS check rejects now raises: `ArgumentError` on REST queries,
|
|
280
|
+
`Parse::PipelineSecurity::Error` (or `Parse::MongoDB::DeniedOperator`) on
|
|
281
|
+
direct paths. Escaped literal patterns, including those built by
|
|
282
|
+
`starts_with`, `ends_with`, and `contains`, pass the shape check and get a
|
|
283
|
+
length cap about twice the normal one. Caller-supplied `$options` accept
|
|
284
|
+
`i`, `m`, `s`, and `u`; an unknown flag, a non-String value, the extended
|
|
285
|
+
flag `x`, or a Regexp built with `Regexp::EXTENDED` raises. The agent
|
|
286
|
+
translator now accepts `s` and `u` and no longer accepts `x`. A pattern with
|
|
287
|
+
more than three unbounded quantifiers in a row and no literal between them
|
|
288
|
+
(for example `^\w+\s+\w+\s+\w+\s+\w+$`) is refused; add a literal
|
|
289
|
+
separator or split the query. A mongo-direct read carrying a non-literal
|
|
290
|
+
regex and no `max_time_ms` times out after 5 seconds with
|
|
291
|
+
`Parse::MongoDB::ExecutionTimeout`; pass `max_time_ms:` or set
|
|
292
|
+
`Parse::PipelineSecurity.default_regex_max_time_ms` to change it.
|
|
293
|
+
- **Direct reads.** Code that calls `Parse::MongoDB.find` inside
|
|
294
|
+
`Parse.without_master_key` should wrap it in `Parse.with_master_key` or use
|
|
295
|
+
a scoped read. `Parse::MongoDB.collection` returns raw driver access with no
|
|
296
|
+
scope and is not refused inside the block.
|
|
297
|
+
- **Batches.** A batch or transaction of objects whose class is bound to a
|
|
298
|
+
session client now runs as that user, as a single save does. A transaction
|
|
299
|
+
mixing credentials raises `MixedAuthorityError`, and a non-transactional
|
|
300
|
+
batch with mixed credentials is sent as several calls. An explicitly chosen
|
|
301
|
+
batch client (`client=` or the `batch_request` receiver) is used for every
|
|
302
|
+
request instead of each object's class client.
|
|
303
|
+
`Client#batch_request` accepts `session_token:` and `use_master_key:`. A
|
|
304
|
+
blank `session:`, or a user with no session token, raises `ArgumentError`.
|
|
305
|
+
- **Webhooks.** `payload.master?` and `payload.claimed_master?` no longer
|
|
306
|
+
treat the string `"true"` as master; Parse Server always sends a JSON
|
|
307
|
+
boolean. Under `Parse::Webhooks.allow_unauthenticated` with no signing
|
|
308
|
+
secret, `payload.master?` is false even for a master-key request Parse
|
|
309
|
+
Server forwards, so master-only field guards and handler `master?` checks
|
|
310
|
+
refuse it, and saves made by the SDK itself run their model callbacks in
|
|
311
|
+
process and again through the webhook. Configure a webhook key
|
|
312
|
+
(`PARSE_SERVER_WEBHOOK_KEY` on Parse Server and `Parse::Webhooks.key` in the
|
|
313
|
+
app) or a signing secret to restore master trust and dedup.
|
|
314
|
+
|
|
315
|
+
### 5.8.1
|
|
316
|
+
|
|
317
|
+
A correctness release for webhooks, sessions, associations, queries, search,
|
|
318
|
+
and agent policies. No new defaults and no API changes; every corrected
|
|
319
|
+
outcome is listed under Behavior Notes.
|
|
320
|
+
|
|
321
|
+
#### Webhooks
|
|
322
|
+
|
|
323
|
+
- **FIXED**: A client create through a `before_save` webhook whose handler
|
|
324
|
+
returns `parse_object` now gets the ACL its class policy resolves, exactly
|
|
325
|
+
as an SDK-side create would. Owner-based policies (`:owner_else_private`,
|
|
326
|
+
the default, plus `:owner_else_public` and `:owner_but_public_read`) take the
|
|
327
|
+
owner from the declared `owner:` field, or from the requesting user when that
|
|
328
|
+
field is empty. Before, a signed-in client got the policy's no-owner
|
|
329
|
+
fallback (`{}` under `:owner_else_private`) and could not read its own
|
|
330
|
+
record. A request without a user, or a master-key request, gets the policy
|
|
331
|
+
fallback. A `_User` class declared with `owner: :self` is owned by the new
|
|
332
|
+
user, never the requester. An ACL the client sent is kept, and an ACL the
|
|
333
|
+
handler sets always wins (through `acl=` or mass assignment), even when it
|
|
334
|
+
equals the class default. A `guard :acl, :master_only` revert on a create
|
|
335
|
+
returns the ACL to the class policy instead of `{}`. A handler-assigned ACL
|
|
336
|
+
is written whatever the handler returns (`parse_object`, `true`, `nil`, or
|
|
337
|
+
a Hash that does not set `ACL` itself).
|
|
338
|
+
- **FIXED**: When a `before_save` handler changes a record, a sub-document the
|
|
339
|
+
client changed is written back as one-level dotted paths (`"meta.count":
|
|
340
|
+
{...}`) instead of the whole field, so concurrent writes to other sub-keys
|
|
341
|
+
survive. A removed sub-key is written as a `Delete`. Dotted keys in a
|
|
342
|
+
returned Hash are folded into the field or `field.sub` write they belong to,
|
|
343
|
+
with operators (`Increment`, `Add`, `AddUnique`, `Remove`, `Delete`) applied
|
|
344
|
+
rather than stored. Hash keys may use Ruby names.
|
|
345
|
+
|
|
346
|
+
#### Sessions and master-key scope
|
|
347
|
+
|
|
348
|
+
- **FIXED**: Deleting `Parse::Session` or `Parse::User` objects, one at a time
|
|
349
|
+
or with a batch `Array#destroy`, drops their identity-cache entries when the
|
|
350
|
+
delete succeeds or reports the row already gone. A denied delete leaves the
|
|
351
|
+
cache alone. A session that does not carry its token or owner (built from
|
|
352
|
+
its objectId alone, fetched without the master key, or fetched with `keys:`
|
|
353
|
+
leaving them out) has them looked up first, in one query per batch: with the
|
|
354
|
+
master key when the client has one, otherwise with the delete's `session:`.
|
|
355
|
+
A lookup that finds no row leaves nothing to forget; only a lookup that
|
|
356
|
+
fails resets the client's identity cache, at most once every 5 seconds per
|
|
357
|
+
client. When the lookup finds no row, the delete uses the owner recorded
|
|
358
|
+
when a `_Session` query or `Parse::Session.session` last fetched that
|
|
359
|
+
session through the same client (only the user id, never the token, in a
|
|
360
|
+
store separate from the identity plane,
|
|
361
|
+
`client.authorization.session_owner_cache`). Without one, a batch delete
|
|
362
|
+
that applied or reported the row gone falls back to the rate-limited reset;
|
|
363
|
+
a denied delete never resets, and a single `destroy` never resets. The
|
|
364
|
+
looked-up token is used only for the delete, is never kept on the object,
|
|
365
|
+
and never enters the response cache. A token resolution in flight when an
|
|
366
|
+
invalidation lands, in this process or, on the Redis identity plane, in any
|
|
367
|
+
other, does not cache its answer: the resolver checks a shared invalidation
|
|
368
|
+
marker before and after writing and evicts its own write when the marker
|
|
369
|
+
moved.
|
|
370
|
+
- **FIXED**: `Parse::User#signup!`, signup on save, and `upgrade_anonymous!`
|
|
371
|
+
store `created_at` / `updated_at` as `Parse::Date` values. Reading
|
|
372
|
+
`updated_at` on a newly signed-up user no longer marks it dirty.
|
|
373
|
+
- **FIXED**: Inside `Parse.without_master_key`, an explicit `master: true`
|
|
374
|
+
passed to `Parse::MongoDB.aggregate`, `Parse::ACLScope`, `Parse::Query`
|
|
375
|
+
direct reads, vector and hybrid search, Atlas Search, or agent tools for
|
|
376
|
+
master-key agents no longer runs as master. The call runs in the public
|
|
377
|
+
scope, as the REST request does once the block strips its master key, and
|
|
378
|
+
the `parse.mongodb.aggregate` notification reports `master_dropped: true`.
|
|
379
|
+
The SDK's own metadata reads keep the master key without lifting the block:
|
|
380
|
+
the class schema and role graph read to enforce a scope, index statistics,
|
|
381
|
+
and the Atlas Search index listing. Before, a schema read refused inside the
|
|
382
|
+
block was cached and denied every scoped mongo-direct read of that class,
|
|
383
|
+
for all callers, for 5 seconds. `Parse::AtlasSearch.faceted_search` raises
|
|
384
|
+
`FacetedSearchNotACLSafe` inside the block, because its bucket counts are
|
|
385
|
+
not ACL-filtered. The metadata marker rides only on the outgoing request
|
|
386
|
+
copy, never on `response.request` or an error's request. A schema read that
|
|
387
|
+
fails inside the block is retried at most every 2 seconds there. A LiveQuery
|
|
388
|
+
admin client that first connected inside the block keeps withholding its
|
|
389
|
+
master key on every reconnect until `allow_master_key_connection!`.
|
|
390
|
+
|
|
391
|
+
#### Agent policies
|
|
392
|
+
|
|
393
|
+
- **FIXED**: A sub-agent's `tools:` and `methods:` filters now only narrow its
|
|
394
|
+
parent's, as `classes:` already did. A child `tools: { only: }` is
|
|
395
|
+
intersected with the parent's allowlist and `except:` sets are unioned; for
|
|
396
|
+
`methods:` the parent's filters stay in force alongside the child's. Before,
|
|
397
|
+
the child's filter replaced the parent's, so a sub-agent could enable a tool
|
|
398
|
+
or `agent_method` within its tier that the parent was not given. A child
|
|
399
|
+
`only:` list with nothing its parent allows raises `ArgumentError` at
|
|
400
|
+
construction.
|
|
401
|
+
- **FIXED**: A sub-agent built with `parent:` uses its parent's approval gate,
|
|
402
|
+
read at call time, unless given its own. Before, it fell back to
|
|
403
|
+
`NullGate`, so a sub-agent built inside an MCP `tools/call` ran tools in
|
|
404
|
+
`require_approval_for` tiers without approval.
|
|
405
|
+
- **FIXED**: Qualified `methods:` entries compare the class by its Parse class
|
|
406
|
+
name, so `"User.x"` and `"_User.x"` (and the `Role`, `Session`, and
|
|
407
|
+
`Installation` aliases) match whichever spelling the entry or the
|
|
408
|
+
`call_method` caller uses. Before, `methods: { except: ["_User.x"] }` could
|
|
409
|
+
be bypassed by calling the method on `"User"`.
|
|
410
|
+
- **FIXED**: A sub-agent can no longer turn on `master_atlas` when its parent
|
|
411
|
+
does not have it. `Parse::Agent.new(parent: scoped_parent, master_atlas:
|
|
412
|
+
true)` raises `ArgumentError`; before, the child's `atlas_faceted_search` ran
|
|
413
|
+
with master-key semantics and returned every row under the parent's scoped
|
|
414
|
+
identity. Omitting `master_atlas` still inherits, and `false` still drops
|
|
415
|
+
it.
|
|
416
|
+
- **FIXED**: The MCP dispatcher snapshots and restores an agent's own approval
|
|
417
|
+
gate, so driving a sub-agent through `tools/call` no longer pins its
|
|
418
|
+
parent's gate onto it. Assigning `approval_gate = nil` makes a sub-agent use
|
|
419
|
+
its parent's gate again.
|
|
420
|
+
- **FIXED**: Qualified `methods:` entries may name a model by its Ruby
|
|
421
|
+
constant (`"Artist.purge"` for a class whose `parse_class` is `"Musician"`)
|
|
422
|
+
and are compared by Parse class name, so such an `except:` entry is no
|
|
423
|
+
longer bypassed through the Parse class name. An entry whose class resolves
|
|
424
|
+
to no loaded model warns at construction.
|
|
425
|
+
- **IMPROVED**: The `parse.agent.tool_call` audit payload adds
|
|
426
|
+
`methods_layers` whenever a sub-agent inherits a `methods:` filter
|
|
427
|
+
(including a single parent layer), and `describe[:methods][:layers]` lists
|
|
428
|
+
every `methods:` filter in force, parent first.
|
|
429
|
+
|
|
430
|
+
#### Associations
|
|
431
|
+
|
|
432
|
+
- **FIXED**: `add!`, `add_unique!`, and `remove!` on a
|
|
433
|
+
`has_many through: :array` collection take the array Parse Server returns,
|
|
434
|
+
as plain array properties already did. A stale local copy is corrected,
|
|
435
|
+
objects already fetched locally are kept, and the field is not marked
|
|
436
|
+
changed. The array is adopted only when every entry is a pointer to the
|
|
437
|
+
declared class; otherwise the operation is applied locally.
|
|
438
|
+
- **FIXED**: Saving a relation collection on its own (`owner.likes.save`)
|
|
439
|
+
sends the staged additions and removals and returns whether they were
|
|
440
|
+
applied. Before, it silently sent nothing.
|
|
441
|
+
- **FIXED**: `Parse::Object#clear_changes!` (also called by `reload!`) drops
|
|
442
|
+
a relation's staged additions and removals and clears array collection
|
|
443
|
+
dirty state. Before, discarded relation changes were sent with the next
|
|
444
|
+
change to that relation. A fetch (`fetch!`, partial fetch, autofetch, with
|
|
445
|
+
or without `preserve_changes:`) keeps a relation's staged additions and
|
|
446
|
+
removals and leaves the relation marked changed, including when the response
|
|
447
|
+
carries the relation's descriptor, so the next save sends them.
|
|
448
|
+
`rollback!` drops a relation's staged additions and removals too, including
|
|
449
|
+
after a fetch.
|
|
450
|
+
- **IMPROVED**: `RelationCollectionProxy#save` accepts `session:` to send the
|
|
451
|
+
staged operations as a given user (`nil` for no session token). Relation
|
|
452
|
+
writes on an owner whose objectId was assigned client-side during a create
|
|
453
|
+
are staged rather than sent until the create returns.
|
|
454
|
+
- **FIXED**: Atomic relation operations (`add!` / `remove!` on a
|
|
455
|
+
`through: :relation` collection) write to the relation's Parse column when
|
|
456
|
+
its name differs from the Ruby name (`has_many :liked_items` maps to
|
|
457
|
+
`likedItems`, or a relation declared with `field:`). Before, they targeted
|
|
458
|
+
the Ruby name.
|
|
459
|
+
|
|
460
|
+
#### Queries
|
|
461
|
+
|
|
462
|
+
- **FIXED**: An OR whose branch has no constraints now matches every row.
|
|
463
|
+
Previously `query | Model.query` and `Parse::Query.or(query, Model.query)`
|
|
464
|
+
dropped that branch and returned only the other side. An empty receiver of
|
|
465
|
+
`|` still starts a new OR, as `or_where` does, so
|
|
466
|
+
`[q1, q2].reduce(Model.query, :|)` builds `q1 OR q2`.
|
|
467
|
+
- **FIXED**: `Model.query(:or => [{...}, {...}])` builds a `$or` group ANDed
|
|
468
|
+
with the query's other constraints, instead of a constraint on a literal
|
|
469
|
+
field named `or`. Branches can be constraint hashes, `Parse::Query` objects
|
|
470
|
+
of the same class, or arrays of constraints.
|
|
471
|
+
- **FIXED**: A constraint that needs the aggregation pipeline or mongo-direct
|
|
472
|
+
(`readable_by`, `:ACL.readable_by`, `:field.array_size`, the mongo-direct geo
|
|
473
|
+
operators) raises `ArgumentError` inside an OR (`:or`, `or_where`, `|`,
|
|
474
|
+
`Parse::Query.or`). Before, the branch was emitted as `{}` and matched every
|
|
475
|
+
row, or lost part of its conditions, including in push targeting, which
|
|
476
|
+
could reach every installation.
|
|
477
|
+
- **FIXED**: `Parse::Query#clone`, `|`, `or_where`, `Parse::Query.or`, `:or`
|
|
478
|
+
branches (including Hash branches with `session:` / `use_master_key:` keys
|
|
479
|
+
or nested scoped queries), and subqueries (`$inQuery`, `$notInQuery`,
|
|
480
|
+
`$select`, `$dontSelect`) keep the query's authority and application:
|
|
481
|
+
session token, a session-bound client from `Parse::Client#become`, scoped
|
|
482
|
+
user or role, master-key flag, read preference, and the Parse application
|
|
483
|
+
the query targets. Queries combined this way must run under the same
|
|
484
|
+
authority against the same application. An unscoped query takes the
|
|
485
|
+
other's authority, and different authorities or applications raise
|
|
486
|
+
`ArgumentError`. Before, a session combined with `scope_to_user` ran as the
|
|
487
|
+
scoped user, a `become`-client query composed into an OR ran as the master
|
|
488
|
+
key, a scoped subquery ran under the outer query's master key, and a clone
|
|
489
|
+
could send one application's session token to the default application.
|
|
490
|
+
Changing the authority or client of a combined query raises and leaves the
|
|
491
|
+
query unchanged. A combined query re-checks its authority before it runs,
|
|
492
|
+
and explicit auth passed to `results_direct`, `count_direct`,
|
|
493
|
+
`distinct_direct`, or `atlas_search` must match it.
|
|
494
|
+
- **FIXED**: LiveQuery subscriptions and push targeting refuse constraints that
|
|
495
|
+
only the aggregation pipeline or mongo-direct can run (`array_size`,
|
|
496
|
+
`set_equals`, `not_empty`, `elem_match`, `readable_by`, mongo-direct geo
|
|
497
|
+
operators). Before, those constraints were dropped and the subscription or
|
|
498
|
+
push reached a wider audience. A subquery with such a constraint is also
|
|
499
|
+
refused, where it used to match every row. `Parse::Query#subscribe` and
|
|
500
|
+
`client.subscribe(query)` run under the query's own session or `become`
|
|
501
|
+
client, and such a subscription is refused with `ArgumentError` when the
|
|
502
|
+
LiveQuery client targets a different Parse application id than the query,
|
|
503
|
+
so one application's session token is never sent to another application's
|
|
504
|
+
LiveQuery server.
|
|
505
|
+
- **FIXED**: A field repeated in `order` is sent once, at its first position
|
|
506
|
+
with the last direction given, matching what MongoDB applies on direct
|
|
507
|
+
reads. String forms such as `"-title"` are parsed as descending, so they
|
|
508
|
+
deduplicate too, and the direct `$sort` stage sorts on the real field.
|
|
509
|
+
`order("title,-plays")` is parsed as two fields.
|
|
510
|
+
- **FIXED**: A bare objectId compared to a declared pointer field
|
|
511
|
+
(`Song.query(:artist => "abc123")`, `:artist.ne`, `:artist.in`) now matches
|
|
512
|
+
on REST, in `Parse::Push` targeting, and in LiveQuery subscriptions from
|
|
513
|
+
`Parse::Query#subscribe`, `Model.subscribe`, and `client.subscribe(query)`.
|
|
514
|
+
Equality and `$in` used to return no rows and `$ne` every row, while direct
|
|
515
|
+
and aggregate reads already matched. The new
|
|
516
|
+
`Parse::Query#compile_rest_where` returns the where clause in that form.
|
|
517
|
+
|
|
518
|
+
#### Search
|
|
519
|
+
|
|
520
|
+
- **FIXED**: Atlas Search (`Parse::AtlasSearch.search`, `search_with_stage`,
|
|
521
|
+
`autocomplete`, `Parse::Query#atlas_search`) and native hybrid search apply
|
|
522
|
+
the CLP `pointerFields` / `readUserFields` ownership constraint before the
|
|
523
|
+
page is cut. For native hybrid, the lexical input filters before its
|
|
524
|
+
per-input `$limit`, and the fused result is filtered again before the final
|
|
525
|
+
`$limit`. Rows used to be filtered after the page was cut, so when the
|
|
526
|
+
top-ranked hits belonged to other users a scoped caller got a short or empty
|
|
527
|
+
page. Inaccessible rows are still never returned.
|
|
528
|
+
- **IMPROVED**: `Parse::VectorSearch.search` and native hybrid search enforce
|
|
529
|
+
the `pointerFields` ownership constraint server-side across the whole
|
|
530
|
+
candidate window. When every owner field is declared as a scalar pointer
|
|
531
|
+
(`belongs_to`) and its storage path (`_p_<field>`) is a `type: "filter"`
|
|
532
|
+
field of the vectorSearch index, the constraint also goes
|
|
533
|
+
into `$vectorSearch.filter`, so other users' rows no longer use up the
|
|
534
|
+
candidate window. The prefilter is used only while the index is READY and
|
|
535
|
+
serving its latest definition. If Atlas refuses it (for example mid-rebuild),
|
|
536
|
+
the search reruns once without it and skips it for the index cache TTL. A
|
|
537
|
+
failed index lookup is also remembered for that TTL.
|
|
538
|
+
|
|
539
|
+
- **FIXED**: A `$vectorSearch` refusal caused by the caller's own
|
|
540
|
+
`vector_filter` on an unindexed path no longer switches off the owner
|
|
541
|
+
prefilter for every caller on that index or clears the index cache; only a
|
|
542
|
+
refusal naming an owner `_p_<field>` path does. Native hybrid search uses
|
|
543
|
+
`Parse::VectorSearch.default_index` when the vector branch names no index.
|
|
544
|
+
|
|
545
|
+
#### Documentation
|
|
546
|
+
|
|
547
|
+
- **FIXED**: README and query YARD examples use `:field.pointer_id` and
|
|
548
|
+
`:field.array_size`, the names the DSL has used since 5.8.0.
|
|
549
|
+
|
|
550
|
+
#### Behavior Notes
|
|
551
|
+
|
|
552
|
+
- An OR that includes an unconstrained branch now returns every row. The one
|
|
553
|
+
exception is an empty receiver of `|`, which starts the OR. Code that passed
|
|
554
|
+
an empty query for "no filter" should leave that branch out. `:or => []`
|
|
555
|
+
matches no rows.
|
|
556
|
+
- A pipeline-only constraint inside an OR now raises `ArgumentError`. Apply it
|
|
557
|
+
outside the OR, or run the branches as separate queries.
|
|
558
|
+
- Combining queries that run under different authorities or against
|
|
559
|
+
different Parse applications now raises `ArgumentError`. That covers OR
|
|
560
|
+
composition, `:or` Hash branches, and subqueries, and includes different
|
|
561
|
+
session tokens, a session against `scope_to_user` / `scope_to_role` / the
|
|
562
|
+
master key, and different scoped users or roles. Setting a new session
|
|
563
|
+
token, master-key flag, scope, or client on a query that took its authority
|
|
564
|
+
from another query also raises, as does passing different explicit auth to
|
|
565
|
+
a direct or Atlas terminal. Query-shaping options (`limit`, `order`, `keys`,
|
|
566
|
+
`skip`, `include`) inside an `:or` branch raise.
|
|
567
|
+
- LiveQuery subscriptions, push targeting, and subqueries with pipeline-only
|
|
568
|
+
constraints raise `ArgumentError`. Subscribing to a `scope_to_user` /
|
|
569
|
+
`scope_to_role` query, or passing a `session_token:` that differs from the
|
|
570
|
+
query's session, raises.
|
|
571
|
+
- REST queries, LiveQuery subscriptions, and push targeting that compare a
|
|
572
|
+
bare objectId to a declared pointer field now return the matching rows.
|
|
573
|
+
Undeclared fields are unchanged. If a model declares `belongs_to` for a
|
|
574
|
+
field whose server column holds a plain String, a bare string compared to
|
|
575
|
+
that field is now sent as a Pointer and stops matching those values;
|
|
576
|
+
declare such a field as a `:string` property instead.
|
|
577
|
+
- Code that relied on `master: true` keeping master authority inside
|
|
578
|
+
`Parse.without_master_key` should wrap that call in `Parse.with_master_key`.
|
|
579
|
+
A dropped `master: true` raises `ACLRequired` under either
|
|
580
|
+
`Parse::ACLScope.require_session_token` or
|
|
581
|
+
`Parse::AtlasSearch.require_session_token`, with a message naming the block,
|
|
582
|
+
and no longer emits the no-ACL banner. The role-graph helpers
|
|
583
|
+
(`users_in_role_subtree` and related) raise `ACLRequired` for
|
|
584
|
+
`master: true` inside the block; pass `as:` instead.
|
|
585
|
+
- A LiveQuery connection that already connected with the master key outside
|
|
586
|
+
`Parse.without_master_key` stays elevated for subscriptions made inside the
|
|
587
|
+
block, because Parse Server authorizes per connection; the SDK warns.
|
|
588
|
+
- `Parse.without_master_key` guards against accidental master-key use and is
|
|
589
|
+
not an isolation boundary.
|
|
590
|
+
- `Parse::Role.all_for_user` and `Parse::Role#all_parent_role_names` without
|
|
591
|
+
`master:` or `as:` return the full role closure inside
|
|
592
|
+
`Parse.without_master_key`, because the role graph is read as SDK metadata.
|
|
593
|
+
- `Parse::Session._preload_identity_for_destroy!` and the
|
|
594
|
+
`_after_batch_destroy` hooks are now private.
|
|
595
|
+
- A session deleted elsewhere whose owner this process never recorded keeps
|
|
596
|
+
resolving until the next rate-limited reset or `identity_cache_ttl`.
|
|
597
|
+
- A custom identity plane gets the cross-process revocation-race guarantee by
|
|
598
|
+
implementing `invalidation_nonce` and `bump_invalidation_nonce`, as the
|
|
599
|
+
built-in Redis plane does. One with generation counters only is covered for
|
|
600
|
+
single-token and per-user invalidations, but a lookup racing another
|
|
601
|
+
process's full reset is bounded by `identity_cache_ttl` when the plane's
|
|
602
|
+
`clear` also deletes its counters.
|
|
603
|
+
- A sub-agent's `tools:` or `methods:` `only:` list that leaves nothing once
|
|
604
|
+
the parent's `only:` and `except:` lists apply now raises `ArgumentError`.
|
|
605
|
+
An explicitly empty `only: []` is accepted. A sub-agent passing
|
|
606
|
+
`master_atlas: true` under a parent without it raises `ArgumentError`.
|
|
607
|
+
- Parse Server sends a `before_save` webhook only the resulting value of a
|
|
608
|
+
dotted operator. When the handler changes the record, the SDK writes back
|
|
609
|
+
changed sub-keys one level deep and deletes removed ones by path. Two
|
|
610
|
+
concurrent writes inside the same sub-key (including deeper paths under it)
|
|
611
|
+
can still overwrite each other, an array is written whole, and a changed
|
|
612
|
+
sub-key carrying a typed value (Date, Bytes, Pointer) makes the whole field
|
|
613
|
+
be written. A client's whole replace of a sub-document becomes a merge. The
|
|
614
|
+
save response reports the dotted keys; the JavaScript SDK applies them, and
|
|
615
|
+
other clients may need a fetch. A handler operator that cannot be folded,
|
|
616
|
+
or two conflicting dotted keys from the handler, fail the save with an
|
|
617
|
+
error.
|
|
618
|
+
- A `before_save` handler that returns `true`, `nil`, or a Hash keeps the
|
|
619
|
+
client's write as sent, so a client create without an ACL through such a
|
|
620
|
+
handler is stored public read and write, as before, unless the handler
|
|
621
|
+
assigns an ACL on `parse_object`.
|
|
622
|
+
- Saving a relation collection on its own sends removals and additions as two
|
|
623
|
+
requests. If the second fails, the first stays applied and the unsent
|
|
624
|
+
operations stay staged.
|
|
625
|
+
- Owner fields stored as arrays of pointers, undeclared, or not filter-indexed
|
|
626
|
+
get no `$vectorSearch` prefilter. Vector search then fills results only from
|
|
627
|
+
its existing candidate window.
|
|
628
|
+
- A custom identity cache with neither `invalidate_value` nor generation
|
|
629
|
+
support (`generation`, `generation_current?`, `bump_generation`) only
|
|
630
|
+
expires a user's tokens at `identity_cache_ttl` after a password change,
|
|
631
|
+
account deletion, `logout_all!`, or a batch destroy of sessions.
|
|
632
|
+
|
|
3
633
|
### 5.8.0
|
|
4
634
|
|
|
5
635
|
#### Breaking Changes
|