parse-stack-next 5.8.2 → 5.8.4
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 +323 -0
- data/README.md +20 -6
- data/bin/parse-console +9 -3
- data/docs/TEST_SERVER.md +1 -1
- data/docs/caching.md +7 -2
- data/docs/mcp_guide.md +34 -0
- data/lib/parse/agent/constraint_translator.rb +66 -2
- data/lib/parse/agent/mcp_rack_app.rb +90 -1
- data/lib/parse/agent/mcp_server.rb +24 -2
- data/lib/parse/agent/mcp_subscriptions.rb +32 -1
- data/lib/parse/agent/tools.rb +3 -3
- data/lib/parse/agent.rb +14 -3
- data/lib/parse/atlas_search/protected_paths.rb +163 -0
- data/lib/parse/atlas_search.rb +62 -0
- data/lib/parse/client.rb +148 -6
- data/lib/parse/clp_scope.rb +53 -2
- data/lib/parse/embeddings/cache.rb +29 -9
- data/lib/parse/graphql/type_generator.rb +184 -5
- data/lib/parse/live_query/client.rb +27 -9
- data/lib/parse/live_query/event.rb +55 -1
- data/lib/parse/live_query/event_queue.rb +6 -1
- data/lib/parse/live_query/health_monitor.rb +1 -1
- data/lib/parse/model/file.rb +37 -3
- data/lib/parse/mongodb.rb +19 -0
- data/lib/parse/pipeline_security.rb +242 -5
- data/lib/parse/query/constraints.rb +101 -8
- data/lib/parse/query/cursor.rb +161 -13
- data/lib/parse/query.rb +42 -0
- data/lib/parse/retrieval/agent_tool.rb +82 -0
- data/lib/parse/retrieval/retriever.rb +14 -2
- data/lib/parse/stack/generators/templates/parse.rb +1 -1
- data/lib/parse/stack/rack_advisory.rb +53 -0
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +106 -2
- data/lib/parse/two_factor_auth/user_extension.rb +41 -5
- data/lib/parse/two_factor_auth.rb +81 -4
- data/lib/parse/vector_search/hybrid.rb +21 -0
- data/lib/parse/vector_search.rb +15 -0
- data/lib/parse/webhooks/replay_protection.rb +5 -1
- data/lib/parse/webhooks.rb +23 -4
- data/parse-stack-next.gemspec +12 -5
- metadata +14 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 278d766a244ba60e85ce4e67d8866df2ce6e6ab78dc76e1f72aedf0b4285d149
|
|
4
|
+
data.tar.gz: 4883c76b576d2a0b726ded45adb1c7a3d569a5d5366f1740da7d44a359d26d53
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2a40e323b870080c02613b30b821b87029bb49ddbba9e31581481ac4d13624aae8f456d8bf1fb55f18642fb40d6f73f5e007902b572254f54190df482d6ea80
|
|
7
|
+
data.tar.gz: 2f8e0d0a6d47dafb547e6edb4663ebc2534e24da79760ed81a9ca612cfab8a466f6e9590987401715b0f7edffc61012c4c006f11767ed2a0c33b8cc8e83ecc69
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,328 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.8.4
|
|
4
|
+
|
|
5
|
+
A security patch across the agent, MCP, Atlas Search, mongo-direct, transport,
|
|
6
|
+
cache, webhook, LiveQuery, GraphQL, MFA, and cursor surfaces, with raised
|
|
7
|
+
dependency floors and a release-workflow fix. Outcomes that change are listed
|
|
8
|
+
under Behavior Notes.
|
|
9
|
+
|
|
10
|
+
#### Agent tools
|
|
11
|
+
|
|
12
|
+
- **FIXED**: `semantic_search` now applies the agent's `classes:` allowlist,
|
|
13
|
+
the CLP `find` check for scoped agents, and the per-agent `filters:` for the
|
|
14
|
+
searched class, as every other read tool does. Before, it checked only the
|
|
15
|
+
global hidden flag and the tenant scope, so an agent limited to one class
|
|
16
|
+
could search any other searchable class, including rows its per-agent filter
|
|
17
|
+
excludes. The per-agent filter is ANDed into the post-search match after the
|
|
18
|
+
caller's filter is checked, so a caller filter on the same field narrows the
|
|
19
|
+
result but cannot replace it. A retrieval profile's hybrid lexical filter is
|
|
20
|
+
likewise ANDed with it instead of merged over it. Per-agent filter fields
|
|
21
|
+
resolve through the class's declared properties, so a declared server name
|
|
22
|
+
such as `PublicState` addresses that column and a pointer uses its `_p_`
|
|
23
|
+
column; a field the class does not declare raises
|
|
24
|
+
`Parse::Agent::ValidationError` instead of being guessed.
|
|
25
|
+
- **FIXED**: A `$relatedTo` constraint built by an agent must name the owning
|
|
26
|
+
object with a plain objectId string. An operator or other non-String value
|
|
27
|
+
(`"objectId" => { "$exists" => true }`) now raises a
|
|
28
|
+
`ConstraintSecurityError` with reason `:invalid_object_id`. Against Parse
|
|
29
|
+
Server below 9.10.5 that shape matches the relation of every owning object
|
|
30
|
+
without checking the owning object's ACL (GHSA-58m4-cqvw-4vgh). The relation
|
|
31
|
+
`key` must be a field name (`:invalid_key`), and a constraint that names
|
|
32
|
+
`object` or `key` under both a String and a Symbol key is refused
|
|
33
|
+
(`:duplicate_key`).
|
|
34
|
+
- **FIXED**: The agent constraint translator refuses a `__type` value (Pointer,
|
|
35
|
+
Date, and the other Parse types) that also carries an operator key, with
|
|
36
|
+
reason `:operator_in_parse_type`. Such a hash was passed through without
|
|
37
|
+
validation, so `{ "__type" => "Pointer", "$relatedTo" => ... }` inside an
|
|
38
|
+
`$or` skipped the operator checks.
|
|
39
|
+
|
|
40
|
+
#### MCP and agent identity
|
|
41
|
+
|
|
42
|
+
- **FIXED**: An agent on a client whose master key is empty or whitespace is
|
|
43
|
+
treated as having no master key, as the HTTP layer already did. Before, it
|
|
44
|
+
took master posture: the `acl_user:` / `acl_role:` and `impersonate`
|
|
45
|
+
refusals were skipped while requests went out without the key.
|
|
46
|
+
- **FIXED**: MCP resource subscriptions apply the agent's tenant scope and
|
|
47
|
+
per-agent filter as the LiveQuery subscription constraint. Before, a
|
|
48
|
+
subscription covered the whole class, so `resources/updated` notifications
|
|
49
|
+
reported writes in other tenants and in rows the filter excludes.
|
|
50
|
+
- **NEW**: A loopback-bound MCP server refuses requests whose `Host` header is
|
|
51
|
+
not a loopback host (DNS-rebinding defense from the MCP specification).
|
|
52
|
+
`allowed_hosts:` on `Parse::Agent::MCPServer` and `MCPRackApp` lists
|
|
53
|
+
additional hosts.
|
|
54
|
+
|
|
55
|
+
#### Atlas Search
|
|
56
|
+
|
|
57
|
+
- **FIXED**: Atlas Search refuses Parse Server's internal columns
|
|
58
|
+
(`_hashed_password`, `_perishable_token`, `_email_verify_token`,
|
|
59
|
+
`_session_token`, `_auth_data_*`, `_rperm`, and the rest of the internal
|
|
60
|
+
field list) when named as a search path, highlight path, sort key,
|
|
61
|
+
autocomplete field, or facet path, for every caller including master. A
|
|
62
|
+
wildcard path glob that matches one of them (`*token`, `?hashed*`) and a
|
|
63
|
+
`queryString` query that names one (`_perishable_token:a*`) are refused the
|
|
64
|
+
same way, including a field name written with Lucene escapes
|
|
65
|
+
(`_hashed\_password:a*`) and globs over the `_auth_data_<provider>`
|
|
66
|
+
columns, and the native hybrid search applies the check to its lexical
|
|
67
|
+
branch. Globs are tested against stored column names only. A glob that
|
|
68
|
+
starts with `_` (which no Parse field does) is refused on every class when
|
|
69
|
+
it reaches a credential column; a glob that starts with a wildcard counts
|
|
70
|
+
credential columns only on `_User` and `_Session`, so `s*` or `*count` on
|
|
71
|
+
another class is not refused. On a dynamically mapped index, a `wildcard` or `regex` operator over
|
|
72
|
+
`_perishable_token` let a caller count matches per prefix and recover a
|
|
73
|
+
password-reset token. On
|
|
74
|
+
`_User` and `_Session`, a non-master caller is also refused a path that
|
|
75
|
+
reaches every field, including a wildcard path object inside a path array,
|
|
76
|
+
since on such an index it includes those columns.
|
|
77
|
+
Refusals raise `Parse::CLPScope::Denied` before the pipeline runs.
|
|
78
|
+
|
|
79
|
+
#### Mongo-direct
|
|
80
|
+
|
|
81
|
+
- **FIXED**: `$jsonSchema` is refused at any depth in mongo-direct filters and
|
|
82
|
+
pipelines, alongside `$where` and `$function`. Its `properties` name fields
|
|
83
|
+
inside the operator value, out of reach of the protectedFields key checks,
|
|
84
|
+
so `{ $jsonSchema: { properties: { email: { pattern: "^a" } } } }` on
|
|
85
|
+
`_User` read a protected field one character at a time. Its `pattern` also
|
|
86
|
+
skipped the SDK's regex checks and the default regex time limit.
|
|
87
|
+
- **FIXED**: `Parse::MongoDB.find` is refused inside `Parse.with_session`
|
|
88
|
+
(with a token or anonymous), as it already was inside
|
|
89
|
+
`Parse.without_master_key`. `find` reads raw documents with no ACL, CLP, or
|
|
90
|
+
protectedFields, so inside a session block it returned every row with
|
|
91
|
+
master authority. A `Parse.with_master_key` block entered inside the session
|
|
92
|
+
block still allows it; one wrapped around the session block does not.
|
|
93
|
+
- **FIXED**: The protectedFields and internal-field checks cover
|
|
94
|
+
`$setWindowFields`, `$densify`, `$fill`, `$bucket`, `$bucketAuto`,
|
|
95
|
+
`$sortByCount`, and a top-level `$search` / `$searchMeta`. A scoped pipeline
|
|
96
|
+
could sort, partition, or fill on a protected field there and read its order.
|
|
97
|
+
Credential columns are refused in those stage arguments for every caller.
|
|
98
|
+
- **FIXED**: The mongo-direct field conversion resolves a pointer's declared
|
|
99
|
+
server name (`belongs_to :owner_ref, field: "OwnerRef"`) to its `_p_OwnerRef`
|
|
100
|
+
column on every path. A constraint keyed by `OwnerRef` reached the wrong
|
|
101
|
+
column, where a `$ne` or `$exists: false` matched every row.
|
|
102
|
+
|
|
103
|
+
#### Schema consistency for scoped reads
|
|
104
|
+
|
|
105
|
+
- **FIXED**: The mongo-direct protected-field check read the class schema
|
|
106
|
+
through the default client and treated an unresolvable schema as having
|
|
107
|
+
no protected fields, while the CLP gate and the protected-field strip read
|
|
108
|
+
it separately through the requesting app's client. Two outcomes followed
|
|
109
|
+
for a scoped caller. If the schema fetch failed and then succeeded between
|
|
110
|
+
the check and the gate, a `$match` on a protected field ran. In a
|
|
111
|
+
multi-app process whose default app had no protectedFields on the class,
|
|
112
|
+
a pipeline such as `$group: { _id: "$ssn" }` returned the protected value,
|
|
113
|
+
since the strip removes only the key. The check now uses the requesting
|
|
114
|
+
app's client, refuses a scoped caller when the schema cannot be resolved,
|
|
115
|
+
and each `Parse::MongoDB.aggregate`, Atlas Search, vector search, and
|
|
116
|
+
hybrid search call reads one schema snapshot for every step.
|
|
117
|
+
|
|
118
|
+
#### Regular-expression validation
|
|
119
|
+
|
|
120
|
+
- **FIXED**: `Parse::RegexSecurity` no longer compiles any part of a caller's
|
|
121
|
+
pattern with Ruby's `Regexp`. Its separator check passed a character class,
|
|
122
|
+
braced escape, or `\Q...\E` atom from the caller's `$regex` to
|
|
123
|
+
`Regexp.new`, so agent and MCP `$regex` arguments, `:field.like` and
|
|
124
|
+
`:field.regex` values, raw `where` hashes, and mongo-direct and Atlas
|
|
125
|
+
Search filters reached Ruby's regular-expression compiler. That compiler
|
|
126
|
+
has memory-safety bugs fixed in Ruby 3.3.13, 3.4.12, and 4.0.9 (GitLab-assigned
|
|
127
|
+
CVE-2026-89078, CVE-2026-93577, CVE-2026-97229, CVE-2026-110264), and
|
|
128
|
+
`\Q...\E` reached it as live syntax because it does not support that
|
|
129
|
+
construct. Character membership is now decided from the validator's own
|
|
130
|
+
parse (literals, `\d \w \s` and their negations, ranges); any construct
|
|
131
|
+
it cannot evaluate counts as a possible match, which refuses the pattern
|
|
132
|
+
rather than passing it. Patterns are still sent to Parse Server and MongoDB
|
|
133
|
+
as strings and compiled only by MongoDB's PCRE engine.
|
|
134
|
+
|
|
135
|
+
#### Transport and caching
|
|
136
|
+
|
|
137
|
+
- **FIXED**: `Parse::Client` refuses an absolute (`https://host/...`, any case)
|
|
138
|
+
or protocol-relative (`//host`, `\\host`) request path with `ArgumentError`
|
|
139
|
+
before sending, so a caller-supplied path cannot send the master key or a
|
|
140
|
+
session token to another host. `connected?` raises on such a path.
|
|
141
|
+
- **FIXED**: `PARSE_REQUIRE_HTTPS` accepts `true`, `1`, or `yes` in any case.
|
|
142
|
+
Before, only the exact string `true` enabled it.
|
|
143
|
+
- **FIXED**: Credentials embedded in the server URL (`https://user:pass@...`)
|
|
144
|
+
are shown as `[FILTERED]` in `inspect`, `as_json`, and warnings, with a
|
|
145
|
+
one-time warning that the URL contains them.
|
|
146
|
+
- **CHANGED**: A `cache:` store that deserializes with Marshal (Moneta's
|
|
147
|
+
default value serializer) over an external backend is refused with
|
|
148
|
+
`ArgumentError`; anyone able to write to that backend could run code on each
|
|
149
|
+
cache read. Use `Parse::Cache::Redis` (or a `redis://` URL string), an
|
|
150
|
+
in-process store, or pass `allow_marshal_cache: true` to accept the risk. The embedding cache's `MonetaStore` applies the same rule with
|
|
151
|
+
`allow_marshal: true`. In-process stores (`:Memory`, `:LRUHash`, `:Null`)
|
|
152
|
+
are accepted.
|
|
153
|
+
|
|
154
|
+
#### File fetches
|
|
155
|
+
|
|
156
|
+
- **FIXED**: Remote `Parse::File` fetches also refuse the NAT64
|
|
157
|
+
(`64:ff9b::/96`) and 6to4 (`2002::/16`) ranges, which embed an IPv4
|
|
158
|
+
address that can be internal on networks that route them.
|
|
159
|
+
|
|
160
|
+
#### Webhooks and LiveQuery
|
|
161
|
+
|
|
162
|
+
- **FIXED**: A whitespace-only webhook signing secret counts as unset, so it
|
|
163
|
+
can no longer serve as a guessable HMAC key that grants master trust. A blank
|
|
164
|
+
`PARSE_SERVER_WEBHOOK_KEY` no longer hides `PARSE_WEBHOOK_KEY`.
|
|
165
|
+
- **FIXED**: The webhook handler refuses a declared `Content-Length` over 1 MB
|
|
166
|
+
before reading, and reads at most 1 MB plus one byte. Before, it read the
|
|
167
|
+
whole body and then checked the size.
|
|
168
|
+
- **FIXED**: LiveQuery events for `_User` and `_Session` have `sessionToken`,
|
|
169
|
+
`authData`, and other credential columns removed before hydration. On a
|
|
170
|
+
Parse Server affected by GHSA-349r-988w-x3jp, an `$or` subscription could
|
|
171
|
+
receive other users' events carrying their session tokens.
|
|
172
|
+
|
|
173
|
+
#### Ambient identity lifetime
|
|
174
|
+
|
|
175
|
+
- **NEW**: `Parse.with_session` documents its identity contract: the session
|
|
176
|
+
token and master-key state last for the block on the current fiber and are
|
|
177
|
+
restored on exit, including on exception, so reused request threads carry
|
|
178
|
+
nothing between requests. Threads and Fibers created inside the block
|
|
179
|
+
inherit the identity for their whole lifetime, which suits short-lived
|
|
180
|
+
work for that user. `Parse.without_ambient_identity { }` runs a block with
|
|
181
|
+
no inherited identity, for workers started inside a session block.
|
|
182
|
+
- **FIXED**: LiveQuery's event, reader, reconnect, and health-check threads
|
|
183
|
+
start without the caller's ambient identity, so callbacks no longer run
|
|
184
|
+
with a session token inherited from whichever block first started the
|
|
185
|
+
client. A thread started from restricted code (a session block or
|
|
186
|
+
`Parse.without_master_key`) keeps the master key suppressed, so callbacks
|
|
187
|
+
never gain authority the starting code lacked;
|
|
188
|
+
`Parse.without_ambient_identity(keep_restrictions: true)` applies the same
|
|
189
|
+
rule to application workers. A reconnect scheduled inside `Parse.without_master_key` keeps the
|
|
190
|
+
master key withheld.
|
|
191
|
+
|
|
192
|
+
#### GraphQL, MFA, and cursors
|
|
193
|
+
|
|
194
|
+
- **FIXED**: `Parse::GraphQL::TypeGenerator` never emits credential fields
|
|
195
|
+
(`session_token`, `auth_data`, `password`, `device_token`, and the internal
|
|
196
|
+
field list). Field resolution bypassed `as_json` redaction, so a generated
|
|
197
|
+
`Session` type served bearer tokens. `include_sensitive: [:field]` opts a
|
|
198
|
+
field back in.
|
|
199
|
+
- **NEW**: The generator takes an explicit field policy: `exclude_fields:`
|
|
200
|
+
(an Array, or a Hash per class) and `protected_fields:`, either a Hash per
|
|
201
|
+
class or `:fetch`, which loads each class's public protectedFields from
|
|
202
|
+
the server and raises `ProtectedFieldsUnavailable` if it cannot. Without
|
|
203
|
+
those options it reads no CLP at all, so the same models always produce
|
|
204
|
+
the same types. Generated types are a schema shape, not an authorization
|
|
205
|
+
layer: resolvers must still enforce ACL, CLP, and protectedFields for the
|
|
206
|
+
requesting user.
|
|
207
|
+
- **FIXED**: `mfa_required?` requires a String username. A Hash value became
|
|
208
|
+
a query operator run under the master key, which let a pre-login endpoint
|
|
209
|
+
report MFA status for an arbitrary matching account.
|
|
210
|
+
- **NEW**: `Parse::MFA.verify(secret, code, after:)` passes `after:` to ROTP,
|
|
211
|
+
and `Parse::MFA.verify_timestamp` returns the accepted time step, so an
|
|
212
|
+
application using `verify` as its own second factor can refuse a code used
|
|
213
|
+
again within the drift window.
|
|
214
|
+
- **NEW**: `Parse::Cursor` state can be signed with HMAC-SHA256
|
|
215
|
+
(`Parse::Cursor.signing_secret=`, or `secret:`), and
|
|
216
|
+
`deserialize(json, expected_class:)` refuses a different class. A signing
|
|
217
|
+
secret must be a String of at least 32 bytes; a blank, short, or non-String
|
|
218
|
+
secret raises `ArgumentError`. Length does not make a key strong, so the
|
|
219
|
+
documentation calls for 32 bytes from a cryptographic random source
|
|
220
|
+
(`SecureRandom.hex(32)`). With a secret configured, `trusted: true` does
|
|
221
|
+
not bypass signature verification. Unsigned state is meant only for trusted
|
|
222
|
+
persistence (state the process stored where clients cannot change it):
|
|
223
|
+
pass `trusted: true` to load it silently. Without that flag it still loads
|
|
224
|
+
in this release with a one-time warning; a future minor release will
|
|
225
|
+
require it. Saved date positions now round-trip as dates.
|
|
226
|
+
- **CHANGED**: `parse-console -m` warns that a master key on the command line
|
|
227
|
+
is visible in the process list and shell history.
|
|
228
|
+
|
|
229
|
+
#### Documentation
|
|
230
|
+
|
|
231
|
+
- **CHANGED**: The MCP guide states that the per-agent rate limiter does not
|
|
232
|
+
limit across requests when an agent is built per request, and that
|
|
233
|
+
deployments must enforce limits per client at the proxy or gateway.
|
|
234
|
+
- **CHANGED**: The MFA documentation states that `Parse::MFA.verify` and the
|
|
235
|
+
SDK's MFA-disable flow are client-side checks, that Parse Server is the
|
|
236
|
+
enforcement boundary (disabling MFA must be guarded server-side), that
|
|
237
|
+
replay protection needs an atomic compare-and-set on the stored
|
|
238
|
+
timestamp, and that `authorized_by:` must be the authenticated operator.
|
|
239
|
+
- **CHANGED**: `Parse::File` documents that a file built from a URL records
|
|
240
|
+
the remote server's Content-Type, which is attacker-controlled when the URL
|
|
241
|
+
is, and that applications decoding stored files must sniff content and
|
|
242
|
+
disable untrusted decoders (for libvips, `VIPS_BLOCK_UNTRUSTED` or
|
|
243
|
+
`Vips.block_untrusted(true)`).
|
|
244
|
+
|
|
245
|
+
#### Dependencies, CI, and release workflow
|
|
246
|
+
|
|
247
|
+
- **CHANGED**: The gemspec requires `faraday` 2.14.3 or later (CVE-2026-54297,
|
|
248
|
+
CVE-2026-33637, GHSA-33mh-2634-fwr2). The 2026-04 rack advisories are fixed
|
|
249
|
+
per branch in 2.2.23, 3.1.21, and 3.2.6, with no fixed 3.0 release. The
|
|
250
|
+
`rack` requirement is 2.2.23 or later, and loading the SDK on a vulnerable
|
|
251
|
+
Rack 3.x release (3.0.x, 3.1.0 to 3.1.20, or 3.2.0 to 3.2.5) prints a
|
|
252
|
+
one-time warning naming the fixed release. RubyGems limits a requirement
|
|
253
|
+
string to 255 characters, too short to exclude each of those releases by
|
|
254
|
+
name. Rack 2.2 applications remain supported. The optional `mongo` guidance is 2.26 or later (CVE-2026-88030)
|
|
255
|
+
and `graphql` 2.6.9 or later.
|
|
256
|
+
- **CHANGED**: The integration test stack runs Parse Server 9.10.7 and Atlas
|
|
257
|
+
Local 8.0.34. CI uses `ruby/setup-ruby` 1.334.0, which installs the patched
|
|
258
|
+
Ruby 3.3.13, 3.4.12, and 4.0.9 for those matrix legs, and pins every action
|
|
259
|
+
to a commit SHA. The Ruby 3.2 leg remains for legacy compatibility; 3.2 is
|
|
260
|
+
end of life and has no release with the October 2026 regular-expression
|
|
261
|
+
compiler fixes.
|
|
262
|
+
- **FIXED**: The release workflow waits up to 45 minutes for Gem Tests on the
|
|
263
|
+
release commit to be created and finish before judging it. A release
|
|
264
|
+
published right after its merge commit landed failed the gate because the
|
|
265
|
+
tests on that commit had not finished yet.
|
|
266
|
+
- **CHANGED**: The local Ruby version is now 3.4.12, which includes the
|
|
267
|
+
October 2026 regular-expression compilation fixes.
|
|
268
|
+
- **NEW**: Gem Tests runs `bundle exec bundle-audit check --update` against
|
|
269
|
+
the committed lockfile on pull requests and pushes to main. Known gem
|
|
270
|
+
vulnerabilities or an advisory-database update failure fail the check,
|
|
271
|
+
and the release workflow's Gem Tests gate includes this job.
|
|
272
|
+
|
|
273
|
+
#### Behavior Notes
|
|
274
|
+
|
|
275
|
+
- A `cache:` store with Moneta's default Marshal serializer over a file or
|
|
276
|
+
network backend now raises `ArgumentError` at `Parse.setup`.
|
|
277
|
+
- `Parse::Client#get`/`post`/`put`/`delete` and `connected?` raise
|
|
278
|
+
`ArgumentError` for an absolute or protocol-relative path.
|
|
279
|
+
- A loopback-bound MCP server refuses requests whose `Host` is not a loopback
|
|
280
|
+
host or listed in `allowed_hosts:`.
|
|
281
|
+
- An application subscribed to its own `_User` through LiveQuery no longer
|
|
282
|
+
receives its `sessionToken` in events.
|
|
283
|
+
- `faraday` below 2.14.3 and `rack` below 2.2.23 no longer satisfy the
|
|
284
|
+
gemspec. A vulnerable `rack` 3.x release still installs and prints a
|
|
285
|
+
load-time warning.
|
|
286
|
+
- `Parse::Cursor` signing secrets shorter than 32 bytes, blank, or not
|
|
287
|
+
Strings raise `ArgumentError`.
|
|
288
|
+
- `mfa_required?(nil)` and other non-String usernames raise `ArgumentError`
|
|
289
|
+
instead of returning false.
|
|
290
|
+
- A per-agent filter (including a `:default` filter) that names a field the
|
|
291
|
+
searched class does not declare makes `semantic_search` raise
|
|
292
|
+
`Parse::Agent::ValidationError`; the other read tools return no rows in
|
|
293
|
+
that case.
|
|
294
|
+
- A scoped mongo-direct, Atlas Search, or vector search call on a class
|
|
295
|
+
whose schema cannot be resolved is refused before it runs.
|
|
296
|
+
- Applications that compile regular expressions from untrusted input
|
|
297
|
+
themselves should run Ruby 3.3.13, 3.4.12, or 4.0.9 or later. The SDK no
|
|
298
|
+
longer does so. Ruby 3.2 remains supported for legacy compatibility only:
|
|
299
|
+
it is end of life and does not receive these fixes.
|
|
300
|
+
- An agent `call_method` handler that runs under a session-scoped agent is
|
|
301
|
+
executed inside `Parse.with_session`, so a handler that calls
|
|
302
|
+
`Parse::MongoDB.find` now raises `Parse::ACLScope::ACLRequired`. Use
|
|
303
|
+
`Parse::Query#results_direct`, which applies the ambient session,
|
|
304
|
+
`Parse::MongoDB.aggregate` with an explicit `session_token:`, or wrap a
|
|
305
|
+
deliberate master read in `Parse.with_master_key`.
|
|
306
|
+
- A pipeline or filter that used `$jsonSchema` on the mongo-direct path now
|
|
307
|
+
raises `Parse::PipelineSecurity::Error` (or
|
|
308
|
+
`Parse::MongoDB::DeniedOperator`).
|
|
309
|
+
- A non-master Atlas Search over `_User` or `_Session` with no `fields:` is
|
|
310
|
+
refused; name the fields to search.
|
|
311
|
+
|
|
312
|
+
### 5.8.3
|
|
313
|
+
|
|
314
|
+
A maintenance release that refreshes locked dependencies to their latest
|
|
315
|
+
compatible versions. No SDK behavior changes.
|
|
316
|
+
|
|
317
|
+
#### Dependency updates
|
|
318
|
+
|
|
319
|
+
- **CHANGED**: Bumped locked dependencies to their latest compatible releases:
|
|
320
|
+
`faraday` 2.14.4 → 2.15.0, `faraday-net_http` 3.4.4 → 3.5.0, `graphql`
|
|
321
|
+
2.6.11 → 2.6.13, `json` 3.0.2 → 3.1.0, `net-http` 0.9.1 → 0.10.0,
|
|
322
|
+
`prettyprint` 0.2.0 → 0.2.1, `pry` 0.16.0 → 0.16.1, `rack-test` 2.2.0 →
|
|
323
|
+
2.3.0, and `uri` 1.1.1 → 1.1.2. `net-http` 0.10.0 adds `net-protocol` 0.4.0
|
|
324
|
+
and `timeout` 0.6.1 as new transitive dependencies.
|
|
325
|
+
|
|
3
326
|
### 5.8.2
|
|
4
327
|
|
|
5
328
|
A security and correctness patch. No new features beyond the `session:`
|
data/README.md
CHANGED
|
@@ -102,7 +102,7 @@ See [CHANGELOG.md](./CHANGELOG.md) for the full 5.1 entry, including breaking ch
|
|
|
102
102
|
- **MCP transport hardening** — Streamable HTTP `Mcp-Session-Id` header (renamed from `X-MCP-Session-Id`, **breaking**), `MCP-Protocol-Version` validation, `DELETE /` session termination, structured-content (`outputSchema`) on built-in tools, optional `health_path:` liveness probe
|
|
103
103
|
- **`Parse::GraphQL::TypeGenerator`** — generate `graphql-ruby` types directly from your `Parse::Object` subclasses (no Parse Server round-trip), with `:vector` columns surfaced as `[Float]` and association registries (`has_one_associations`, `has_many_associations`) populated at DSL time
|
|
104
104
|
- **LiveQuery promoted to stable** — the experimental warning is removed; `Parse.live_query_enabled = true` is retained as a network-egress safety toggle, not a stability gate
|
|
105
|
-
- **Server-version deprecation warning**: a one-shot warning when connecting to a Parse Server older than the configured threshold (default `7.0.0`, override with `PARSE_DEPRECATED_SERVER_VERSION_BELOW`); silence with `Parse.suppress_server_version_warning = true`. The **supported baseline is Parse Server 9.x** (the SDK is developed and tested against a pinned `parse-server:9.10.
|
|
105
|
+
- **Server-version deprecation warning**: a one-shot warning when connecting to a Parse Server older than the configured threshold (default `7.0.0`, override with `PARSE_DEPRECATED_SERVER_VERSION_BELOW`); silence with `Parse.suppress_server_version_warning = true`. The **supported baseline is Parse Server 9.x** (the SDK is developed and tested against a pinned `parse-server:9.10.7`); the default warning threshold is intentionally conservative so older deployments only get an advisory, not a hard break.
|
|
106
106
|
- **`mongo_relation_index :field, dedup: true`** — register a compound `{owningId, relatedId}` UNIQUE on relation join collections to prevent duplicate-pair subscriptions without breaking `has_many` semantics
|
|
107
107
|
|
|
108
108
|
See [CHANGELOG.md](./CHANGELOG.md) for the full 5.0 entry, including security-hardening notes and Ruby 3.x cleanup.
|
|
@@ -286,7 +286,7 @@ Runnable, self-contained scripts live in [`examples/`](examples/) — see
|
|
|
286
286
|
|
|
287
287
|
## Release History
|
|
288
288
|
|
|
289
|
-
**Current version: 5.
|
|
289
|
+
**Current version: 5.8.4** | **Ruby 3.2+ required** (3.3.13, 3.4.12, or 4.0.9 or later recommended for their regular-expression compiler fixes)
|
|
290
290
|
|
|
291
291
|
The 5.0 highlights (vector search / RAG, pooled Redis cache, AS::N instrumentation, MCP transport hardening, GraphQL type generation) are summarized in the [What's new in 5.0](#whats-new-in-50) section above. Earlier releases are recorded below.
|
|
292
292
|
|
|
@@ -689,9 +689,13 @@ A caching adapter of type `Moneta::Transformer`. Caching queries and object fetc
|
|
|
689
689
|
Parse.setup(cache: store, expires: 10, ...)
|
|
690
690
|
```
|
|
691
691
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
692
|
+
Use the wrapper rather than a raw `Moneta.new(:Redis, ...)` store. A store that
|
|
693
|
+
keeps Moneta's default Marshal serializer over an external backend is refused
|
|
694
|
+
with `ArgumentError` unless you pass `allow_marshal_cache: true`. Setting
|
|
695
|
+
`value_serializer: nil` on a raw store is not a substitute: the response cache
|
|
696
|
+
stores Hash values, which a file or Redis backend cannot hold without a
|
|
697
|
+
serializer, so every lookup misses. In-process stores (`:Memory`, `:LRUHash`,
|
|
698
|
+
`:Null`) are accepted as they are.
|
|
695
699
|
|
|
696
700
|
As a shortcut, if you are planning on using REDIS and have configured the use of `redis` in your `Gemfile`, you can just pass the REDIS connection string directly to the cache option. The string form builds a `Parse::Cache::Redis` wrapper for you, so it is JSON-serialized and safe by default.
|
|
697
701
|
|
|
@@ -1220,6 +1224,8 @@ The default MIME type for all files is `image/jpeg`. This can be default can be
|
|
|
1220
1224
|
file = Parse::File.new parse_file
|
|
1221
1225
|
```
|
|
1222
1226
|
|
|
1227
|
+
A URL String passed to `Parse::File.new` or `Parse::File.create`, or assigned to a `:file` property, is downloaded and the remote server's `Content-Type` header is recorded as the file's mime type. That type is controlled by whoever controls the URL. Do not assign request strings to file properties; build files from bytes (`Parse::File.new(name, bytes, type)`) and determine the type server-side. Code that decodes stored files (libvips or ImageMagick, Active Storage variants, document parsers) should treat the stored mime type as untrusted, sniff the content, and disable untrusted decoders (for libvips 8.13 and later, `VIPS_BLOCK_UNTRUSTED` or `Vips.block_untrusted(true)` with ruby-vips 2.2.1 and later). Keep `Parse::File.allowed_remote_hosts` restricted to expected hosts.
|
|
1228
|
+
|
|
1223
1229
|
If you are using displaying these files on a secure site and want to make sure that urls returned by a call to `url` are `https`, you can set `Parse::File.force_ssl` to true.
|
|
1224
1230
|
|
|
1225
1231
|
```ruby
|
|
@@ -1955,6 +1961,14 @@ user.disable_mfa_master_key!(authorized_by: admin_user, admin_role: "Admin")
|
|
|
1955
1961
|
# or: user.disable_mfa_master_key!(authorized_by: admin_user, allow_unverified: true)
|
|
1956
1962
|
```
|
|
1957
1963
|
|
|
1964
|
+
**MFA enforcement boundary:**
|
|
1965
|
+
|
|
1966
|
+
`Parse::MFA.verify`, `Parse::MFA.verify_timestamp` and `disable_mfa!` are client-side checks. Parse Server's MFA adapter enforces MFA at login and is the enforcement boundary. Removing MFA is a user update (`authData: { mfa: nil }`) that any holder of the session token can send to the REST API directly, so protect it server-side, for example with a `beforeSave` trigger on `_User` or a Cloud Code function that requires a fresh code verified on the server.
|
|
1967
|
+
|
|
1968
|
+
When using `Parse::MFA.verify(secret, code, after:)` as an application's own second factor, store the last accepted timestamp per user and update it atomically: a compare-and-set keyed on the previous value, or a database unique constraint on user and time step. Otherwise two concurrent requests with the same code can both succeed.
|
|
1969
|
+
|
|
1970
|
+
`authorized_by:` on `disable_mfa_master_key!` must be the authenticated operator from the caller's verified session, never a user id taken from the request.
|
|
1971
|
+
|
|
1958
1972
|
**SMS MFA (requires Parse Server SMS callback):**
|
|
1959
1973
|
```ruby
|
|
1960
1974
|
# Initiate SMS setup
|
|
@@ -5735,7 +5749,7 @@ For high traffic applications that may be performing several server tasks on sim
|
|
|
5735
5749
|
# Parse::Cache::Redis serializes cached responses as JSON, not Marshal — a raw
|
|
5736
5750
|
# Moneta.new(:Redis) store Marshals values by default and a cache read would
|
|
5737
5751
|
# Marshal.load Redis bytes (RCE if the cache is shared/untrusted). Prefer the
|
|
5738
|
-
# wrapper
|
|
5752
|
+
# wrapper over a raw Moneta-Redis store.
|
|
5739
5753
|
store = Parse::Cache::Redis.new(url: 'redis://localhost:6379')
|
|
5740
5754
|
# use a Redis cache store with an automatic expire of 10 seconds.
|
|
5741
5755
|
Parse.setup(cache: store, expires: 10, ...)
|
data/bin/parse-console
CHANGED
|
@@ -111,13 +111,19 @@ opt_parser = OptionParser.new do |o|
|
|
|
111
111
|
o.separator "#{File.basename($0)} -a APP_ID -m MASTER_KEY https://your-parse-server-url"
|
|
112
112
|
o.separator "#{File.basename($0)} -c ./config.json"
|
|
113
113
|
o.separator ""
|
|
114
|
-
o.separator "
|
|
114
|
+
o.separator "With a master key (-m or -c), parse-console will automatically"
|
|
115
115
|
o.separator "import your schema as ruby models."
|
|
116
116
|
o.separator ""
|
|
117
117
|
o.separator "Options"
|
|
118
118
|
o.on('-a APP_ID', '--appId APP_ID', 'Parse App ID (required)') { |a| opts[:app_id] = a }
|
|
119
119
|
o.on('-k REST_API_KEY', '--restAPIKey REST_API_KEY', 'Parse REST API Key') { |a| opts[:api_key] = a }
|
|
120
|
-
o.on('-m MASTER_KEY', '--masterKey MASTER_KEY', 'Parse Master Key')
|
|
120
|
+
o.on('-m MASTER_KEY', '--masterKey MASTER_KEY', 'Parse Master Key (prefer -c CONFIG_JSON)') do |a|
|
|
121
|
+
# A key on the command line is visible to other local users through the
|
|
122
|
+
# process list and is saved in shell history. Keep accepting it, but say so.
|
|
123
|
+
$stderr.puts "Warning: passing the master key with -m exposes it in the process list " \
|
|
124
|
+
"and shell history. Put it in a config file and use -c CONFIG_JSON instead."
|
|
125
|
+
opts[:master_key] = a
|
|
126
|
+
end
|
|
121
127
|
o.on('-s SERVER_URL', '--serverURL SERVER_URL', 'The Parse server url.', 'Defaults to http://localhost:1337/parse') { |a| opts[:server_url] = a }
|
|
122
128
|
o.on('-v','--[no-]verbose', 'Run verbosely') { |v| opts[:verbose] ||= v }
|
|
123
129
|
o.on('--pry', 'Use Pry instead of IRB') { |v| opts[:pry] = v }
|
|
@@ -212,7 +218,7 @@ Parse.setup server_url: opts[:server_url],
|
|
|
212
218
|
master_key: opts[:master_key]
|
|
213
219
|
Parse.logging = true if opts[:verbose]
|
|
214
220
|
# Both of these can have come from a remote config document, so escape them.
|
|
215
|
-
puts "Server : #{Parse::TerminalSafe.sanitize_line(Parse.client.server_url)}"
|
|
221
|
+
puts "Server : #{Parse::TerminalSafe.sanitize_line(Parse::Client.redact_url_userinfo(Parse.client.server_url))}"
|
|
216
222
|
puts "App Id : #{Parse::TerminalSafe.sanitize_line(Parse.client.app_id)}"
|
|
217
223
|
puts "Master : #{Parse.client.master_key.present?}"
|
|
218
224
|
|
data/docs/TEST_SERVER.md
CHANGED
|
@@ -62,7 +62,7 @@ default.
|
|
|
62
62
|
- **`PSNEXT_PREFIX`** (default `psnext-it`) names the Compose project and every
|
|
63
63
|
container. Set it (e.g. `PSNEXT_PREFIX=psnext-ci`) to run a second, fully
|
|
64
64
|
separate copy.
|
|
65
|
-
- **Versions**: Parse Server is pinned to `parseplatform/parse-server:9.10.
|
|
65
|
+
- **Versions**: Parse Server is pinned to `parseplatform/parse-server:9.10.7`
|
|
66
66
|
(see `scripts/docker/Dockerfile.parse`), MongoDB `mongo:9` (`MONGO_VERSION`), Redis
|
|
67
67
|
`redis:7-alpine`, Dashboard `parseplatform/parse-dashboard:9`.
|
|
68
68
|
- **Database**: Parse uses `parse_stack_next_it`.
|
data/docs/caching.md
CHANGED
|
@@ -87,8 +87,13 @@ Use the wrapper rather than a bare `Moneta.new(:Redis, ...)`. Moneta serializes
|
|
|
87
87
|
values with Marshal by default, so every cache read would `Marshal.load` bytes
|
|
88
88
|
returned by Redis, which is a remote-code-execution primitive when that Redis is
|
|
89
89
|
shared, unauthenticated, or reachable over a plaintext connection. The wrapper
|
|
90
|
-
forces `value_serializer: nil` and encodes values as JSON itself.
|
|
91
|
-
|
|
90
|
+
forces `value_serializer: nil` and encodes values as JSON itself. A raw Moneta
|
|
91
|
+
store that keeps the Marshal serializer over an external backend is refused with
|
|
92
|
+
`ArgumentError` unless you pass `allow_marshal_cache: true`; in-process stores
|
|
93
|
+
(`:Memory`, `:LRUHash`, `:Null`) are accepted. Setting `value_serializer: nil` on
|
|
94
|
+
a raw file or Redis store is not a substitute for the wrapper: the response
|
|
95
|
+
cache stores Hash values, which such a backend cannot hold without a
|
|
96
|
+
serializer, so every lookup misses.
|
|
92
97
|
|
|
93
98
|
The wrapper refuses a Moneta `prefix:` option, because it would rewrite the
|
|
94
99
|
physical key layout underneath the SCAN patterns that scoped clearing depends
|
data/docs/mcp_guide.md
CHANGED
|
@@ -1424,6 +1424,40 @@ The `Parse::Agent::RateLimitExceeded` constant is a stable top-level alias — e
|
|
|
1424
1424
|
|
|
1425
1425
|
Per-user rate limiting follows the same pattern: key the Redis counter on the verified user identity extracted during authentication.
|
|
1426
1426
|
|
|
1427
|
+
### Deployment-layer limits
|
|
1428
|
+
|
|
1429
|
+
The SDK's limiters are not a substitute for limits at the deployment layer.
|
|
1430
|
+
|
|
1431
|
+
- `Parse::Agent::RateLimiter` is created per `Parse::Agent` instance. With an `agent_factory:` that builds an agent per request, it does not limit anything across requests.
|
|
1432
|
+
- `MCPServer` keeps one limiter for the whole server. Every principal (every API key holder, every client IP) draws from that one budget, so a single noisy client can exhaust it for all others, and it does not limit any one client.
|
|
1433
|
+
- `pre_auth_rate_limiter:` is called as `check!` with no arguments, before authentication. It sees no client identity, so it limits total request volume rather than any one caller.
|
|
1434
|
+
|
|
1435
|
+
Production deployments should enforce a per-client limit (keyed on API key, user, or IP) at a reverse proxy or API gateway in front of the MCP endpoint, and also pass a shared `pre_auth_rate_limiter:` so unauthenticated traffic cannot drive unbounded Parse Server session lookups. Back the limiter with a shared store (for example Redis) when more than one process serves the endpoint.
|
|
1436
|
+
|
|
1437
|
+
```ruby
|
|
1438
|
+
$pre_auth_limiter = MyRedisRateLimiter.new(key: "mcp_pre_auth", limit: 600, window: 60)
|
|
1439
|
+
|
|
1440
|
+
# Rack mount (MCPRackApp, or Parse::Agent.rack_app which forwards kwargs)
|
|
1441
|
+
mcp_app = Parse::Agent::MCPRackApp.new(
|
|
1442
|
+
pre_auth_rate_limiter: $pre_auth_limiter,
|
|
1443
|
+
agent_factory: ->(env) {
|
|
1444
|
+
# ... auth ...
|
|
1445
|
+
Parse::Agent.new(permissions: :readonly, rate_limiter: $shared_redis_limiter)
|
|
1446
|
+
},
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
# Standalone server. MCPServer.run does not forward pre_auth_rate_limiter:,
|
|
1450
|
+
# so construct the server directly.
|
|
1451
|
+
Parse::Agent::MCPServer.new(
|
|
1452
|
+
port: 3001,
|
|
1453
|
+
api_key: ENV["MCP_API_KEY"],
|
|
1454
|
+
rate_limiter: $shared_redis_limiter,
|
|
1455
|
+
pre_auth_rate_limiter: $pre_auth_limiter,
|
|
1456
|
+
).start
|
|
1457
|
+
```
|
|
1458
|
+
|
|
1459
|
+
On exhaustion the pre-auth limiter must raise an exception responding to `#retry_after` (such as `Parse::Agent::RateLimitExceeded`); the request is answered with HTTP 429 and a `Retry-After` header.
|
|
1460
|
+
|
|
1427
1461
|
---
|
|
1428
1462
|
|
|
1429
1463
|
## Custom Tools
|
|
@@ -233,8 +233,18 @@ module Parse
|
|
|
233
233
|
|
|
234
234
|
# Translate a hash value (could be operators or a pointer/object)
|
|
235
235
|
def translate_hash_value(hash, depth:, agent: nil)
|
|
236
|
-
# Check if it's a Parse type (Pointer, Date, File, GeoPoint)
|
|
237
|
-
|
|
236
|
+
# Check if it's a Parse type (Pointer, Date, File, GeoPoint). A Parse
|
|
237
|
+
# type value carries no operators, so a `__type` hash that also has a
|
|
238
|
+
# `$` key is refused instead of being passed through unvalidated.
|
|
239
|
+
if parse_type?(hash)
|
|
240
|
+
op_key = hash.keys.find { |k| k.to_s.start_with?("$") }
|
|
241
|
+
return hash if op_key.nil?
|
|
242
|
+
raise ConstraintSecurityError.new(
|
|
243
|
+
"SECURITY: a `__type` value cannot also carry the operator #{op_key}.",
|
|
244
|
+
operator: op_key.to_s,
|
|
245
|
+
reason: :operator_in_parse_type,
|
|
246
|
+
)
|
|
247
|
+
end
|
|
238
248
|
|
|
239
249
|
# Classify each key INDEPENDENTLY rather than branching on whether
|
|
240
250
|
# *every* key is an operator. A hash that mixes an operator key with a
|
|
@@ -366,10 +376,64 @@ module Parse
|
|
|
366
376
|
reason: :cross_class_denied,
|
|
367
377
|
)
|
|
368
378
|
end
|
|
379
|
+
# The owning object must be one concrete object. An operator or any
|
|
380
|
+
# other non-String objectId (`{ "$exists" => true }`) makes Parse
|
|
381
|
+
# Server below 9.10.5 match relations of every owning object, without
|
|
382
|
+
# the owning object's ACL (GHSA-58m4-cqvw-4vgh).
|
|
383
|
+
# A Hash holding both "object" and :object (or "key" and :key) would
|
|
384
|
+
# let the checks read one entry while the other is sent.
|
|
385
|
+
%w[object key].each do |slot|
|
|
386
|
+
next unless val.is_a?(Hash) && val.key?(slot) && val.key?(slot.to_sym)
|
|
387
|
+
raise ConstraintSecurityError.new(
|
|
388
|
+
"SECURITY: $relatedTo names `#{slot}` twice (String and Symbol keys).",
|
|
389
|
+
operator: "$relatedTo",
|
|
390
|
+
reason: :duplicate_key,
|
|
391
|
+
)
|
|
392
|
+
end
|
|
393
|
+
key = val["key"] || val[:key]
|
|
394
|
+
unless key.is_a?(String) && RELATED_TO_KEY_RE.match?(key)
|
|
395
|
+
raise ConstraintSecurityError.new(
|
|
396
|
+
"SECURITY: $relatedTo requires `key` to be a relation field name.",
|
|
397
|
+
operator: "$relatedTo",
|
|
398
|
+
reason: :invalid_key,
|
|
399
|
+
)
|
|
400
|
+
end
|
|
401
|
+
object_id = related_to_object_id(val)
|
|
402
|
+
unless object_id.is_a?(String) && RELATED_TO_OBJECT_ID_RE.match?(object_id)
|
|
403
|
+
raise ConstraintSecurityError.new(
|
|
404
|
+
"SECURITY: $relatedTo requires the owning object's objectId to be a " \
|
|
405
|
+
"plain objectId string.",
|
|
406
|
+
operator: "$relatedTo",
|
|
407
|
+
reason: :invalid_object_id,
|
|
408
|
+
)
|
|
409
|
+
end
|
|
369
410
|
assert_embedded_class_accessible!("$relatedTo", owning_class, agent: agent)
|
|
370
411
|
translate_value(val, depth: depth, agent: agent)
|
|
371
412
|
end
|
|
372
413
|
|
|
414
|
+
# objectId shape accepted for a `$relatedTo` owning object (the same
|
|
415
|
+
# character set the agent tools accept for an object_id argument).
|
|
416
|
+
RELATED_TO_OBJECT_ID_RE = /\A[A-Za-z0-9_-]{1,32}\z/.freeze
|
|
417
|
+
|
|
418
|
+
# Field-name shape accepted for a `$relatedTo` relation key.
|
|
419
|
+
RELATED_TO_KEY_RE = /\A[A-Za-z][A-Za-z0-9_]{0,127}\z/.freeze
|
|
420
|
+
|
|
421
|
+
# The objectId of a `$relatedTo` constraint's owning object, in
|
|
422
|
+
# whichever of the forms {.related_to_owning_class} accepts. Returns
|
|
423
|
+
# the raw value (possibly a Hash) so the caller can reject non-Strings.
|
|
424
|
+
def related_to_object_id(val)
|
|
425
|
+
return nil unless val.is_a?(Hash)
|
|
426
|
+
obj = val["object"] || val[:object]
|
|
427
|
+
return obj.id if obj.respond_to?(:parse_class) && obj.respond_to?(:id)
|
|
428
|
+
case obj
|
|
429
|
+
when Hash
|
|
430
|
+
o = obj.transform_keys(&:to_s)
|
|
431
|
+
o["objectId"]
|
|
432
|
+
when String
|
|
433
|
+
obj.include?("$") ? obj.split("$", 2).last : nil
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
373
437
|
# Extract the Parse class name of a `$relatedTo` constraint's owning
|
|
374
438
|
# object from its `object` slot, which may be a Parse::Pointer, a Parse
|
|
375
439
|
# pointer/relation hash (`{__type:, className:, objectId:}`, string or
|