parse-stack-next 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/CHANGELOG.md +461 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +12 -4
- data/README.md +160 -3
- data/Rakefile +52 -3
- data/docs/atlas_vector_search_guide.md +86 -2
- data/docs/client_sdk_guide.md +5 -0
- data/docs/mcp_guide.md +59 -4
- data/docs/mongodb_direct_guide.md +93 -1
- data/docs/usage_guide.md +11 -1
- data/docs/webhooks_guide.md +418 -0
- data/examples/README.md +46 -0
- data/examples/basic_client.rb +93 -0
- data/examples/basic_server.rb +109 -0
- data/examples/live_query_listener.rb +98 -0
- data/examples/rag_chatbot.rb +221 -0
- data/examples/webhook_server.rb +111 -0
- data/lib/parse/agent/mcp_rack_app.rb +285 -62
- data/lib/parse/agent/tools.rb +45 -5
- data/lib/parse/api/aggregate.rb +7 -1
- data/lib/parse/api/cloud_functions.rb +12 -4
- data/lib/parse/api/hooks.rb +46 -9
- data/lib/parse/api/objects.rb +16 -2
- data/lib/parse/api/path_segment.rb +33 -0
- data/lib/parse/api/server.rb +94 -0
- data/lib/parse/api/users.rb +58 -2
- data/lib/parse/atlas_search.rb +7 -7
- data/lib/parse/client/body_builder.rb +5 -0
- data/lib/parse/client/protocol.rb +4 -0
- data/lib/parse/client.rb +55 -2
- data/lib/parse/embeddings/spend_cap.rb +255 -0
- data/lib/parse/embeddings.rb +1 -0
- data/lib/parse/live_query/client.rb +3 -1
- data/lib/parse/live_query/subscription.rb +32 -5
- data/lib/parse/model/acl.rb +4 -2
- data/lib/parse/model/classes/audience.rb +52 -4
- data/lib/parse/model/classes/user.rb +180 -3
- data/lib/parse/model/core/embed_managed.rb +113 -0
- data/lib/parse/model/core/querying.rb +3 -1
- data/lib/parse/model/core/vector_searchable.rb +161 -0
- data/lib/parse/model/object.rb +28 -5
- data/lib/parse/mongodb.rb +7 -1
- data/lib/parse/pipeline_security.rb +5 -3
- data/lib/parse/query/constraints.rb +29 -0
- data/lib/parse/query.rb +265 -27
- data/lib/parse/retrieval/agent_tool.rb +49 -0
- data/lib/parse/retrieval/reranker/cohere.rb +218 -0
- data/lib/parse/retrieval/reranker.rb +157 -0
- data/lib/parse/retrieval/retriever.rb +110 -23
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +17 -0
- data/lib/parse/two_factor_auth/user_extension.rb +123 -31
- data/lib/parse/vector_search/hybrid.rb +578 -0
- data/lib/parse/webhooks/payload.rb +252 -7
- data/lib/parse/webhooks/trigger_audit.rb +502 -0
- data/lib/parse/webhooks.rb +215 -3
- data/scripts/docker/Dockerfile.parse +5 -1
- data/scripts/docker/docker-compose.test.yml +31 -0
- data/scripts/docker/docker-compose.verifyemail.yml +4 -0
- data/scripts/docker/preflight.sh +76 -0
- data/scripts/start-parse.sh +52 -4
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3568759074455238e0addc957d2b46db3616a26057eb4d1fdd8ec70c1be577c7
|
|
4
|
+
data.tar.gz: c31228426dfa4dbbcb49ab5df47662104e1d91100e2a92040f699ce92b09db7a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54e3ecd41d651aa918809f247799e00edcb79ad5da331ad2b33f669b3bdb989b727b4cb3cf39f8e1150c9778ca4326084cc7d8c31994ab0c2e5c04b82247833a
|
|
7
|
+
data.tar.gz: fbb24fcacd678d5ad060d4e1ce772c646aed2ff61bcaaa232be42903eee416965cd06b9eab7b4f2f68b1980272ed059fd4e216e78c8896b77741bdf1d3377d69
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,466 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.4.0
|
|
4
|
+
|
|
5
|
+
#### Parse Server 8.x / 9.x compatibility fixes
|
|
6
|
+
|
|
7
|
+
A set of latent correctness fixes for behaviors that changed across Parse
|
|
8
|
+
Server versions, plus a capability-detection layer so future server changes
|
|
9
|
+
can be feature-gated rather than discovered by breakage.
|
|
10
|
+
|
|
11
|
+
- **FIXED**: `Query#read_pref` now rides the REST query body
|
|
12
|
+
(`readPreference`), not just the `X-Parse-Read-Preference` header. Parse
|
|
13
|
+
Server reads read preference from request options and maps no such header,
|
|
14
|
+
so over REST the header alone was silently ignored and every read hit the
|
|
15
|
+
primary. Scoped reads now route correctly; the mongo-direct path (which
|
|
16
|
+
passes the preference straight to the driver) was already correct. The
|
|
17
|
+
normalized token (`PRIMARY`, `SECONDARY_PREFERRED`, …) is emitted verbatim,
|
|
18
|
+
matching Parse Server's accepted values.
|
|
19
|
+
- **FIXED**: LiveQuery field projection now emits the `keys` subscription
|
|
20
|
+
option (and keeps `fields` for older servers). Parse Server 7.0 renamed the
|
|
21
|
+
projection option from `fields` to `keys`; a frame carrying only `fields`
|
|
22
|
+
was ignored on 7.0+, so projected subscriptions silently received every
|
|
23
|
+
column. `subscribe(keys: […])` is accepted on `Query#subscribe`,
|
|
24
|
+
`Klass.subscribe`, and the LiveQuery client, with `fields:` retained as an
|
|
25
|
+
alias.
|
|
26
|
+
- **BREAKING**: cloud-function results now decode `__type`-encoded Parse objects
|
|
27
|
+
back into `Parse::Object` / `Parse::Pointer`. Parse Server 8.0 began encoding
|
|
28
|
+
returned objects and 9.0 made it unconditional, so `Parse.call_function`
|
|
29
|
+
returned an encoded dictionary where callers expected attribute access.
|
|
30
|
+
Decoding is conservative: only a fully-shaped Object/Pointer envelope is
|
|
31
|
+
converted, an Object of an unregistered class is left as a raw Hash (so no
|
|
32
|
+
attributes are lost), and plain data passes through untouched. This changes the
|
|
33
|
+
read type of typed fields for **in-process Ruby callers**: a returned object of
|
|
34
|
+
a *registered* class is now an ORM instance, so reading a field goes through the
|
|
35
|
+
property getter and carries its ORM type — an `enum` / `symbolize:` property
|
|
36
|
+
yields a Symbol (`:active`, not `"active"`), a date yields a `Parse::Date`, and
|
|
37
|
+
a pointer field yields a `Parse::Pointer`, where a pre-5.4.0 caller read the raw
|
|
38
|
+
JSON String/Hash. HTTP/JSON consumers are unaffected — JSON has no symbols, so
|
|
39
|
+
values re-serialize to strings on the way out. Migration: a cloud function whose
|
|
40
|
+
JSON shape is a contract should return an explicit plain Hash and coerce typed
|
|
41
|
+
fields (`status: obj.status.to_s`) rather than returning whole `Parse::Object`s
|
|
42
|
+
— returning objects (and `as_json`, which still emits the `__type` envelope) is
|
|
43
|
+
what triggers the client-side rebuild; reading `obj.as_json` back through a
|
|
44
|
+
caller does not escape it. Ruby callers that read typed fields off a result
|
|
45
|
+
should expect the ORM type or normalize at the read site. (`lib/parse/client.rb`)
|
|
46
|
+
- **IMPROVED**: `Query#explain` surfaces actionable guidance when it hits a
|
|
47
|
+
permission error — Parse Server 9.0 defaults `allowPublicExplain` to false,
|
|
48
|
+
so a non-master explain now reports that it requires the master key or
|
|
49
|
+
`allowPublicExplain: true` instead of a bare 403.
|
|
50
|
+
- **NEW**: `Parse.server_supports?(:capability)` and `Parse.server_features`
|
|
51
|
+
(and the client-level equivalents) expose a capability probe built on the
|
|
52
|
+
memoized `serverInfo` fetch. It prefers the advertised `features` block where
|
|
53
|
+
present and falls back to version inference for behavior flags the block does
|
|
54
|
+
not carry, failing open to the current server line when the version is
|
|
55
|
+
unknown.
|
|
56
|
+
- **IMPROVED**: the webhook trigger allowlist now mirrors Parse Server's full
|
|
57
|
+
set of trigger types, so registering `beforeLogin`, `afterLogin`,
|
|
58
|
+
`afterLogout`, `beforePasswordResetRequest`, `beforeConnect`,
|
|
59
|
+
`beforeSubscribe`, and `afterEvent` hooks is no longer pre-rejected by the
|
|
60
|
+
SDK.
|
|
61
|
+
- **NEW**: first-class webhook routing for the non-object trigger shapes — the
|
|
62
|
+
authentication triggers (`beforeLogin`, `afterLogin`, `afterLogout`,
|
|
63
|
+
`beforePasswordResetRequest`) and the LiveQuery triggers (`beforeConnect`,
|
|
64
|
+
`beforeSubscribe`, `afterEvent`). `Parse::Webhooks::Payload` gains matching
|
|
65
|
+
predicates (`before_login?` … `after_event?`, plus `auth_trigger?` /
|
|
66
|
+
`live_query_trigger?`), an `event` accessor (the `afterEvent` event type) and
|
|
67
|
+
`clients` / `subscriptions` connection counters, and captures the top-level
|
|
68
|
+
`sessionToken` that connect/subscribe carry into `#session_token` (so
|
|
69
|
+
`#user_client` / `#user_agent` work, while keeping the token out of `as_json`
|
|
70
|
+
and the request log). Dispatch matches Parse Server's response contract: the
|
|
71
|
+
response body is ignored for all seven triggers, so a handler that returns
|
|
72
|
+
`false` from a `before*` variant — which Parse Server would otherwise resolve
|
|
73
|
+
as `{success:false}` and **allow** — is converted to a rejection (`error!`
|
|
74
|
+
remains the explicit, message-carrying form), while a returned object is
|
|
75
|
+
normalized to a success no-op rather than serialized back. None of these run
|
|
76
|
+
ActiveModel `save` / `create` / `destroy` callbacks, even though the auth
|
|
77
|
+
triggers carry a `_User` / `_Session`. `@Connect` / `@File` trigger paths now
|
|
78
|
+
route correctly. LiveQuery triggers are delivered over HTTP only in a
|
|
79
|
+
co-located single-process setup; `beforeConnect` is effectively in-process
|
|
80
|
+
only.
|
|
81
|
+
- **NEW**: file (`@File`) and connection (`@Connect`) triggers now have a full
|
|
82
|
+
register/fetch/delete lifecycle through the SDK.
|
|
83
|
+
`Parse::API::PathSegment.trigger_class_name!` accepts Parse Server's
|
|
84
|
+
`@`-prefixed pseudo-classes for trigger paths (previously `fetch_trigger` /
|
|
85
|
+
`delete_trigger` rejected the leading `@`, so an `@File` / `@Connect` trigger
|
|
86
|
+
could be created but not managed).
|
|
87
|
+
- **CHANGED**: `beforeCreate` / `afterCreate` are no longer presented as
|
|
88
|
+
registerable webhook triggers — Parse Server has no such trigger type and
|
|
89
|
+
rejects them. They remain ActiveModel callbacks (`before_create` /
|
|
90
|
+
`after_create`) that run inside the `beforeSave` / `afterSave` handler for new
|
|
91
|
+
objects, so registering `beforeSave` / `afterSave` enables both the save and
|
|
92
|
+
create callbacks. Attempting to register a create trigger now raises a clear
|
|
93
|
+
error pointing to the save trigger instead of failing with a server-side
|
|
94
|
+
"invalid hook declaration".
|
|
95
|
+
- **FIXED**: `beforeFind` / `afterFind` webhook triggers now route. Parse Server
|
|
96
|
+
omits the class name from the find payload body entirely (the matched
|
|
97
|
+
`objects` carry no `className` and there is no top-level one), so the SDK could
|
|
98
|
+
not resolve `parse_class` for a find request and the dispatcher never invoked
|
|
99
|
+
the registered handler. The class is now threaded from the webhook URL path
|
|
100
|
+
(`<endpoint>/<trigger>/<className>`) into the `Parse::Webhooks::Payload`, so
|
|
101
|
+
find handlers fire. This also matters for correctness, not just feature
|
|
102
|
+
completeness: an unrouted `afterFind` returned `{"success": true}` (not an
|
|
103
|
+
objects array), which Parse Server rejects — so a registered `afterFind`
|
|
104
|
+
previously broke every matching query with a connection error rather than
|
|
105
|
+
no-op'ing. The path segment is charset-validated before use as a routing key.
|
|
106
|
+
- **FIXED**: `:vector` columns are now stripped from `afterFind` webhook payload
|
|
107
|
+
`objects`. Because the find payload carries no class name, the route-derived
|
|
108
|
+
class is the only way to resolve the model and its declared `:vector` fields;
|
|
109
|
+
the previous per-element `className` lookup found nothing and left embeddings
|
|
110
|
+
in the payload. (`vector_visibility :public` classes keep them, as elsewhere.)
|
|
111
|
+
- **IMPROVED**: `Query#explain` now warns proactively (one-shot) when a clearly
|
|
112
|
+
non-master explain runs against a server known to restrict it (Parse Server
|
|
113
|
+
9.0+ defaults `allowPublicExplain` to false), and the `explain_query` agent
|
|
114
|
+
tool surfaces the same guidance on a permission error — both in addition to
|
|
115
|
+
the existing reactive message. The warning is suppressed for master-key and
|
|
116
|
+
unknown-version calls to avoid noise on a flag `/serverInfo` does not expose.
|
|
117
|
+
- **DOCS**: added a Cloud Code Webhooks guide and a runnable
|
|
118
|
+
`examples/webhook_server.rb`, plus a README section on how ActiveModel
|
|
119
|
+
callbacks relate to Parse Server trigger types, the synchronous-latency model
|
|
120
|
+
for `afterSave`, and the inbound replay/freshness protection.
|
|
121
|
+
|
|
122
|
+
#### `exclude_keys` honored on the direct-MongoDB read path
|
|
123
|
+
|
|
124
|
+
- **IMPROVED**: `Query#exclude_keys` now takes effect on the mongo-direct read
|
|
125
|
+
path (`results_direct`, `first_direct`, and an aggregation that auto-promotes
|
|
126
|
+
to direct MongoDB, such as an `$inQuery`/`$notInQuery` pointer constraint).
|
|
127
|
+
MongoDB's `$project` is an allowlist with no denylist equivalent, so the
|
|
128
|
+
excluded fields were previously dropped silently and the full object came
|
|
129
|
+
back. The SDK now applies the denylist as a post-fetch sanitize over the
|
|
130
|
+
decoded results — the MongoDB query itself is unchanged. On this path the
|
|
131
|
+
strip is recursive by field name (it removes the field at every depth,
|
|
132
|
+
including inside included/nested objects), which is broader than the REST
|
|
133
|
+
path's top-level/dotted `excludeKeys` scoping. Decode-critical reserved
|
|
134
|
+
fields (`objectId`, `className`, `__type`, `createdAt`, `updatedAt`, `ACL`,
|
|
135
|
+
and their Mongo storage-form names) are never stripped, so excluding one of
|
|
136
|
+
them is a no-op rather than a way to break object reconstruction.
|
|
137
|
+
`exclude_keys` remains a result-shaping convenience, not an ACL/CLP boundary —
|
|
138
|
+
use `keys` or `protectedFields` to keep a field from leaving the database.
|
|
139
|
+
|
|
140
|
+
#### Webhook handler blocks support explicit `return`
|
|
141
|
+
|
|
142
|
+
- **IMPROVED**: a registered webhook handler (`Parse::Webhooks.route`,
|
|
143
|
+
`webhook`, `webhook_function`) can now use an explicit `return value` to set
|
|
144
|
+
its result. Previously the block ran through `instance_exec`, so a bare
|
|
145
|
+
`return` raised `LocalJumpError: unexpected return` whenever the handler was
|
|
146
|
+
defined inside a method (an initializer, a class body, a config block) — the
|
|
147
|
+
only way to return a value was to make it the last expression. Handlers now
|
|
148
|
+
run as a method on the request payload, giving `return` ordinary
|
|
149
|
+
method semantics:
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
Parse::Webhooks.route :before_save, :Post do
|
|
153
|
+
post = parse_object
|
|
154
|
+
return post if post.title.present? # now works
|
|
155
|
+
error! "title required"
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
The legacy idioms are unchanged — the last expression's value, `next value`,
|
|
160
|
+
and `break value` all still set the result — and `self` is still the payload,
|
|
161
|
+
so `parse_object`, `params`, and `error!` resolve directly inside the block.
|
|
162
|
+
`raise` / `error!` and returning `false` from a `before_save` halt the save
|
|
163
|
+
exactly as before. `return` ends the handler, so it cannot be followed by more
|
|
164
|
+
work in the same block; to run work after the response, use `after_response`
|
|
165
|
+
(below).
|
|
166
|
+
- **NEW**: `payload.after_response { … }` (alias `defer`) registers work to run
|
|
167
|
+
**after** the webhook response has been sent, off the client's critical path —
|
|
168
|
+
for search indexing, cache warming, or fan-out that should not add latency to
|
|
169
|
+
the save/function the client is waiting on. Under a server that exposes
|
|
170
|
+
`rack.after_reply` (Puma, Unicorn) the block runs once the response is flushed
|
|
171
|
+
to the socket on the same worker thread; otherwise it falls back to a detached
|
|
172
|
+
thread. Multiple callbacks run in registration order and each is isolated, so
|
|
173
|
+
one raising affects neither the response nor the others. Callbacks are
|
|
174
|
+
dispatched only on the success path (a rejected `before_save` does not trigger
|
|
175
|
+
follow-up work) and only when the payload is processed through the mounted
|
|
176
|
+
`Parse::Webhooks` Rack app. The work runs after the response is flushed, which
|
|
177
|
+
is not a guarantee about when the row commits, and it runs in-process (it does
|
|
178
|
+
not survive a worker restart) — for work that *must* happen, use a durable job
|
|
179
|
+
queue.
|
|
180
|
+
|
|
181
|
+
#### Webhook trigger coverage audit
|
|
182
|
+
|
|
183
|
+
- **NEW**: `Parse::Webhooks.trigger_audit` — a master-key operator audit that
|
|
184
|
+
cross-references three sources of trigger truth across every registered class
|
|
185
|
+
and reports where they drift: a model's ActiveModel callbacks
|
|
186
|
+
(`before_save` / `after_save` / `after_create` / ...), the locally registered
|
|
187
|
+
webhook blocks (`Parse::Webhooks.routes`), and the triggers actually
|
|
188
|
+
registered with Parse Server (`hooks/triggers`). It surfaces the non-obvious
|
|
189
|
+
rule that a callback runs server-side for non-Ruby clients only when both a
|
|
190
|
+
local webhook block and the matching server trigger are registered — a
|
|
191
|
+
callback declared on its own is inert for JS/Swift/REST/Dashboard writes.
|
|
192
|
+
Findings include `callbacks_inert` (callbacks that will not run for non-Ruby
|
|
193
|
+
clients), `route_not_registered` (a local block with no server trigger),
|
|
194
|
+
`orphan_server_trigger` (a server trigger with no local handler), and
|
|
195
|
+
`local_only_callbacks` (`*_update` / `*_validation` callbacks that no server
|
|
196
|
+
trigger can ever run). Framework-internal callbacks are filtered out by source
|
|
197
|
+
location so the report shows only app-defined logic. Returns a Hash by
|
|
198
|
+
default, a human-readable summary with `pretty: true`, and `network: false`
|
|
199
|
+
audits callbacks against local routes without a master key. See the Cloud Code
|
|
200
|
+
Webhooks guide for details.
|
|
201
|
+
|
|
202
|
+
#### Parse Server feature-coverage additions
|
|
203
|
+
|
|
204
|
+
Closes a set of backend capabilities the SDK did not previously surface.
|
|
205
|
+
|
|
206
|
+
- **NEW**: `context` propagation. Pass `context:` to `create_object` /
|
|
207
|
+
`update_object`, `call_function` / `call_function_with_session`, and
|
|
208
|
+
`Parse.call_function`; it is serialized to the `X-Parse-Cloud-Context` header
|
|
209
|
+
(`Parse::Protocol::CLOUD_CONTEXT`) and made available to Cloud Code triggers.
|
|
210
|
+
On the receive side, `Parse::Webhooks::Payload#context` exposes the incoming
|
|
211
|
+
context (not credential-scrubbed). Backward compatible — omitting `context:`
|
|
212
|
+
sends nothing.
|
|
213
|
+
- **NEW**: `Parse::User#verify_password(password)` and
|
|
214
|
+
`Parse::API::Users#verify_password(username, password)` validate credentials
|
|
215
|
+
via `POST /verifyPassword` (credentials in the request body, mirroring
|
|
216
|
+
`login`) without minting a session — a step-up / re-authentication primitive.
|
|
217
|
+
POST is used over the GET form so the plaintext password stays out of the URL
|
|
218
|
+
(and therefore out of access logs, proxy logs, and the response cache key).
|
|
219
|
+
- **NEW**: `Parse::Error::EmailNotVerifiedError` is raised from
|
|
220
|
+
`Parse::User.login!` when Parse Server rejects a login because the account's
|
|
221
|
+
email is unverified (`preventLoginWithUnverifiedEmail`; Parse Server returns
|
|
222
|
+
code 205 in this context). It subclasses `Parse::Error::AuthenticationError`,
|
|
223
|
+
so existing `rescue Parse::Error::AuthenticationError` handlers keep catching
|
|
224
|
+
the unverified-email case (no breaking change — it was a plain
|
|
225
|
+
`AuthenticationError` before); callers who want to distinguish "verify your
|
|
226
|
+
email" from "bad credentials" rescue the narrower subclass first.
|
|
227
|
+
- **NEW**: `Query#exclude_keys(*fields)` emits the Parse Server `excludeKeys`
|
|
228
|
+
parameter (a server-side field denylist, the complement of `keys`) — fetch a
|
|
229
|
+
row minus large columns (e.g. a managed `:vector`) without enumerating every
|
|
230
|
+
field you do want.
|
|
231
|
+
- **NEW**: LiveQuery `watch` — `subscribe(watch: [...])` (on `Klass.subscribe`,
|
|
232
|
+
`Query#subscribe`, and the LiveQuery client) requests update events only when
|
|
233
|
+
the named fields change, cutting event volume on busy subscriptions. Emitted
|
|
234
|
+
as the `watch` subscription option (Parse Server 7.0+).
|
|
235
|
+
- **NEW**: `Query#aggregate(pipeline, raw_values:, raw_field_names:)` forwards
|
|
236
|
+
the Parse Server 9.9.0 `rawValues` / `rawFieldNames` aggregation options
|
|
237
|
+
through the REST aggregate path.
|
|
238
|
+
- **NEW**: `Query#hint(index_name)` forces a specific index. Emitted in the
|
|
239
|
+
compiled REST query body and forwarded to the mongo-direct path
|
|
240
|
+
(`Parse::MongoDB.aggregate` / `find` `hint:`), so a bad plan diagnosed with
|
|
241
|
+
`explain` can be corrected without dropping to `mongosh`.
|
|
242
|
+
- **NEW**: `:field.contained_by => [...]` (`$containedBy`) query constraint —
|
|
243
|
+
matches when the array field's values are all within the supplied set (the
|
|
244
|
+
inverse of `$all`), rounding out array-operator coverage.
|
|
245
|
+
|
|
246
|
+
#### Hybrid search and reranking for RAG
|
|
247
|
+
|
|
248
|
+
- **NEW**: `Class.hybrid_search(text:, lexical:, vector:, k:, fusion:)` fuses a
|
|
249
|
+
lexical Atlas Search branch with a `$vectorSearch` branch using
|
|
250
|
+
reciprocal-rank fusion (RRF). Lexical search captures exact-token matches
|
|
251
|
+
(proper nouns, codes); vector search captures paraphrase; fusing the two beats
|
|
252
|
+
either alone on most workloads. Each branch enforces ACL/CLP/`protectedFields`
|
|
253
|
+
independently before fusion, so results are already access-filtered — there is
|
|
254
|
+
no separate hydration fetch to secure.
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
Article.hybrid_search(
|
|
258
|
+
text: "how do I reset my password",
|
|
259
|
+
lexical: { index: "article_search" },
|
|
260
|
+
vector: { num_candidates: 200 },
|
|
261
|
+
k: 20,
|
|
262
|
+
fusion: { k_constant: 60, weights: { lexical: 0.4, vector: 0.6 } },
|
|
263
|
+
)
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Returned objects carry `#hybrid_score`, `#hybrid_ranks`, and (when the branch
|
|
267
|
+
contributed) `#vector_score` / `#search_score`.
|
|
268
|
+
- **NEW**: `Parse::VectorSearch::Hybrid.rrf` exposes the pure RRF fusion math,
|
|
269
|
+
and `Parse::VectorSearch::Hybrid.rank_fusion_supported?` detects Atlas 8.0+
|
|
270
|
+
native `$rankFusion` via a cached behavioural probe (1-hour TTL) rather than
|
|
271
|
+
version-string parsing.
|
|
272
|
+
- **NEW**: `Parse::Retrieval::Reranker` cross-encoder reranking protocol with a
|
|
273
|
+
deterministic `Reranker::Fixture` (zero-network, for tests) and a
|
|
274
|
+
`Reranker::Cohere` adapter (`/v2/rerank`). A reranker reorders retrieved
|
|
275
|
+
documents by relevance before chunking.
|
|
276
|
+
- **NEW**: `Parse::Retrieval.retrieve` now accepts `hybrid:` (route through
|
|
277
|
+
`hybrid_search`) and `rerank:` (a reranker that reorders documents and sets the
|
|
278
|
+
chunk score to the cross-encoder relevance). Both kwargs were previously
|
|
279
|
+
reserved and raised `NotImplementedError`. When `tenant_scope:` is supplied, the
|
|
280
|
+
tenant constraint is enforced authoritatively in BOTH hybrid branches: a
|
|
281
|
+
caller-supplied `vector_filter` / `lexical` filter can narrow the result set but
|
|
282
|
+
can no longer replace (and thereby escape) the tenant scope.
|
|
283
|
+
- **NEW**: `Parse::Embeddings::SpendCap` adds an opt-in per-tenant cumulative
|
|
284
|
+
embedding token cap with hard-refuse semantics. The `semantic_search` agent
|
|
285
|
+
tool charges the estimated query tokens against the caller's tenant budget on
|
|
286
|
+
every call (attacker-controlled chat input embeds text); a breach surfaces as a
|
|
287
|
+
rate-limited tool error. Disabled by default; admin agents are exempt. The token
|
|
288
|
+
estimate takes the larger of a character- and a byte-based heuristic so
|
|
289
|
+
multibyte input (e.g. CJK, emoji) is not undercounted — the chars/4 ratio only
|
|
290
|
+
holds for ASCII, and this estimate is the sole basis for the refuse decision.
|
|
291
|
+
- **CHANGED**: `PipelineSecurity::ALLOWED_STAGES` and `STAGE0_ONLY_ATLAS_STAGES`
|
|
292
|
+
admit `$rankFusion` (Atlas 8.0+ native server-side RRF) — a read-only,
|
|
293
|
+
stage-0 Atlas operator like `$vectorSearch`.
|
|
294
|
+
- **NOTE**: Hybrid fusion runs client-side by default. The native single-roundtrip
|
|
295
|
+
`$rankFusion` path is opt-in (`fusion: { method: :rrf_native }`) and falls back
|
|
296
|
+
to client-side fusion when the cluster does not support it; detection and the
|
|
297
|
+
native pipeline shape ship, but live results route through the always-enforced
|
|
298
|
+
two-aggregate client path unless native is explicitly requested. When native
|
|
299
|
+
fusion does execute, top-level rows are re-verified against the scope's `_rperm`
|
|
300
|
+
after the fusion stage and fail closed (a row must carry an `_rperm` that
|
|
301
|
+
explicitly satisfies the scope), and `numCandidates` is clamped to Atlas's
|
|
302
|
+
`[limit, 10000]` range to match `Parse::VectorSearch`.
|
|
303
|
+
|
|
304
|
+
#### RAG completeness: bulk embed, vector visibility, webhook redaction
|
|
305
|
+
|
|
306
|
+
- **NEW**: `Class.embed_pending!` backfills embeddings for records whose managed
|
|
307
|
+
`:vector` field is still null, using objectId-cursor pagination (robust to the
|
|
308
|
+
result set shrinking as records embed). Intended as a master-key maintenance
|
|
309
|
+
operation; supports `field:`, `batch_size:`, `limit:`, and `where:`.
|
|
310
|
+
- **NEW**: `Parse::Object#compute_embedding!` forces an in-place recompute of a
|
|
311
|
+
record's managed embedding(s) without a save (digest-tracked — a provider call
|
|
312
|
+
happens only when the source changed).
|
|
313
|
+
- **NEW**: `vector_visibility :owner_only | :public` class-level DSL controls
|
|
314
|
+
whether a class's `:vector` properties are included in `as_json` by default
|
|
315
|
+
(`:owner_only` omits, the safe default; `:public` includes). An explicit
|
|
316
|
+
`include_vectors:` in the `as_json` call always wins.
|
|
317
|
+
- **IMPROVED**: Webhook trigger payloads now strip declared `:vector` columns from
|
|
318
|
+
`object` / `original` / `update` / `objects` by default, mirroring the `as_json`
|
|
319
|
+
default. A class that opts into `vector_visibility :public` keeps its vectors in
|
|
320
|
+
the payload. Embeddings are large and leak ML signal; a handler has no reason to
|
|
321
|
+
receive them.
|
|
322
|
+
|
|
323
|
+
#### Fix `Parse::Audience` hash-query persistence
|
|
324
|
+
|
|
325
|
+
- **FIXED**: `Parse::Audience#query` is now stored as a JSON string on the wire,
|
|
326
|
+
matching Parse Server's built-in `_Audience.query` column (which is typed
|
|
327
|
+
`String`). The property previously serialized as an object, so every save of
|
|
328
|
+
an audience with a hash query was rejected by the server with a schema
|
|
329
|
+
mismatch (`expected String but got Object`). The public API is unchanged —
|
|
330
|
+
assign a `Hash` and read a `Hash` back; the value is encoded to JSON on save
|
|
331
|
+
and decoded on load, reading back as a `HashWithIndifferentAccess` so both
|
|
332
|
+
string and symbol keys resolve.
|
|
333
|
+
|
|
334
|
+
#### `Parse::MFA` write, status, and disable fixes
|
|
335
|
+
|
|
336
|
+
- **FIXED**: `Parse::User#setup_mfa!`, `#setup_sms_mfa!`, `#confirm_sms_mfa!`,
|
|
337
|
+
`#disable_mfa!`, and `#disable_mfa_master_key!` raised an `ArgumentError`
|
|
338
|
+
(nested `opts:`) before reaching the server. Each passed its session token or
|
|
339
|
+
master-key flag wrapped in an `opts:` hash, which the current `Parse::Client`
|
|
340
|
+
request layer rejects; the credentials are now passed as direct keyword
|
|
341
|
+
arguments so MFA enrollment and disable calls work.
|
|
342
|
+
- **FIXED**: `Parse::User#mfa_enabled?` and `#mfa_status` now report correctly
|
|
343
|
+
after an ordinary fetch. The SDK strips `authData` from fetched users to avoid
|
|
344
|
+
leaking the TOTP secret and recovery codes that Parse Server returns there;
|
|
345
|
+
the strip now preserves a non-sensitive `{ "status" => "enabled" }` projection
|
|
346
|
+
(and nothing else — the secret and recovery codes are still removed), so the
|
|
347
|
+
status methods read true instead of always reporting "not enabled".
|
|
348
|
+
- **FIXED**: `Parse::User#disable_mfa!` (self-service disable) now works. Parse
|
|
349
|
+
Server's TOTP adapter has no first-class self-disable, so the SDK first proves
|
|
350
|
+
possession of the current code, then unlinks the MFA provider. A wrong code is
|
|
351
|
+
rejected with `Parse::MFA::VerificationError` and leaves MFA enabled. The
|
|
352
|
+
current-code step is now classified positively — a rejected code raises
|
|
353
|
+
`VerificationError`, while any other failure (transport, session, server error)
|
|
354
|
+
surfaces as a `Parse::Client::ResponseError` instead of being mislabeled a
|
|
355
|
+
verification failure. The disable is confirmed authoritatively from the
|
|
356
|
+
server's own view (a disabled account's own session-token read returns no
|
|
357
|
+
`authData.mfa`) rather than from the in-memory projection, and the local
|
|
358
|
+
`mfa_enabled?` / `mfa_status` state is cleared to reflect the disable so a
|
|
359
|
+
subsequent read on the same object does not report a stale `enabled`.
|
|
360
|
+
- **FIXED**: `Parse::User#disable_mfa_master_key!` now clears the in-memory MFA
|
|
361
|
+
status after disabling, so `mfa_enabled?` / `mfa_status` report the truth on
|
|
362
|
+
the same object without requiring a fresh load.
|
|
363
|
+
- **BREAKING**: `Parse::User#disable_mfa_master_key!` now fails closed. Because it
|
|
364
|
+
bypasses MFA verification entirely via the master key, it refuses to run without
|
|
365
|
+
an authorization signal: pass `admin_role:` for the library to verify the
|
|
366
|
+
operator's role membership, or `allow_unverified: true` to explicitly assert that
|
|
367
|
+
the caller has already authorized the operator out-of-band. Callers that
|
|
368
|
+
previously passed only `authorized_by:` now raise `Parse::MFA::ForbiddenError`;
|
|
369
|
+
add `admin_role:` or `allow_unverified: true` to migrate. `authorized_by:`
|
|
370
|
+
remains required and is still validated first.
|
|
371
|
+
- **FIXED**: `Parse::User#mfa_enabled?` / `#mfa_status` no longer report `enabled`
|
|
372
|
+
for a user whose `authData.mfa` carries an explicit non-`enabled` status with a
|
|
373
|
+
stale residual secret or recovery code; an explicit status is now authoritative.
|
|
374
|
+
|
|
375
|
+
#### Interactive console MFA login
|
|
376
|
+
|
|
377
|
+
- **NEW**: `rake client:console` now logs in MFA-enrolled accounts. When the
|
|
378
|
+
server reports that an additional MFA factor is required, the console prompts
|
|
379
|
+
for a TOTP / recovery code (or reads `PARSE_LOGIN_MFA` for non-interactive
|
|
380
|
+
use) and completes the login via `Parse::User.login_with_mfa`. A
|
|
381
|
+
password-only login of a non-enrolled account is unaffected.
|
|
382
|
+
|
|
383
|
+
#### Request email-address verification
|
|
384
|
+
|
|
385
|
+
- **NEW**: `Parse::User.request_email_verification(email)` and the instance
|
|
386
|
+
`Parse::User#request_email_verification` ask Parse Server to (re)send the
|
|
387
|
+
address-verification email for a registered, not-yet-verified user (the
|
|
388
|
+
`POST /verificationEmailRequest` endpoint). The server must have an email
|
|
389
|
+
adapter and `verifyUserEmails` enabled. Mirrors `request_password_reset`:
|
|
390
|
+
rate-limited per email, returns a Boolean, and raises
|
|
391
|
+
`Parse::Error::ServiceUnavailableError` on a misconfigured server.
|
|
392
|
+
|
|
393
|
+
#### Faster AtlasSearch role-cache expiry
|
|
394
|
+
|
|
395
|
+
- **CHANGED**: `Parse::AtlasSearch` `role_cache_ttl` now defaults to 30 seconds
|
|
396
|
+
(was 120). The shorter TTL expires cached user-to-role mappings sooner, so a
|
|
397
|
+
role grant or revoke is reflected in `$search` ACL decisions faster, at the
|
|
398
|
+
cost of slightly more frequent role lookups. Override via
|
|
399
|
+
`Parse::AtlasSearch.configure(role_cache_ttl:)`.
|
|
400
|
+
|
|
401
|
+
#### MCP Streamable HTTP transport switch
|
|
402
|
+
|
|
403
|
+
- **NEW**: `Parse::Agent::MCPRackApp.new(transport: :streamable_http)` (and the
|
|
404
|
+
`Parse::Agent.rack_app(transport: :streamable_http)` convenience) enables the
|
|
405
|
+
full MCP 2025-06-18 Streamable HTTP transport with one switch — POST→SSE
|
|
406
|
+
streaming plus the server→client `GET /` notification stream — instead of
|
|
407
|
+
setting `streaming: true, notifications: true` separately. Streamable HTTP is
|
|
408
|
+
now documented as the primary transport for embedded Rack deployments.
|
|
409
|
+
|
|
410
|
+
```ruby
|
|
411
|
+
mcp_app = Parse::Agent.rack_app(transport: :streamable_http) do |env|
|
|
412
|
+
# auth factory returning a Parse::Agent
|
|
413
|
+
end
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
`transport:` is a closed enum (`:streamable_http`, `:legacy`, or `nil`).
|
|
417
|
+
`resource_subscriptions: true` may be combined with `:streamable_http` to
|
|
418
|
+
upgrade the server→client bus to its LiveQuery-backed resource-subscription
|
|
419
|
+
posture.
|
|
420
|
+
- **CHANGED**: Passing `transport: :streamable_http` together with an explicit
|
|
421
|
+
`streaming:` or `notifications:` raises `ArgumentError` (the switch already
|
|
422
|
+
owns those toggles); any `transport:` value outside the closed enum also
|
|
423
|
+
raises.
|
|
424
|
+
- **NOTE**: The default transport is unchanged — an existing
|
|
425
|
+
`Parse::Agent.rack_app { ... }` keeps its non-streaming buffered-JSON
|
|
426
|
+
behavior until it opts in. The switch requires a streaming-capable Rack server
|
|
427
|
+
(Puma, Falcon, Unicorn) and has no effect under the WEBrick-backed
|
|
428
|
+
`MCPServer`, which cannot stream.
|
|
429
|
+
- **CHANGED**: `Parse::Agent::MCPRackApp` `max_concurrent_dispatchers:` now
|
|
430
|
+
defaults to a finite **100** (`DEFAULT_MAX_CONCURRENT_DISPATCHERS`) instead of
|
|
431
|
+
`nil` (unlimited). Enabling a streaming surface is now bounded out of the box:
|
|
432
|
+
once the cap is reached, a new SSE request or `GET /` listening stream is
|
|
433
|
+
refused with a `503` JSON-RPC `-32000` ("server busy") rather than spawning an
|
|
434
|
+
unbounded number of orphan-prone threads. The cap applies separately to
|
|
435
|
+
request-scoped SSE and listening streams (effective ceiling up to 2x). Pass an
|
|
436
|
+
explicit positive integer to resize it, or `max_concurrent_dispatchers: nil`
|
|
437
|
+
to knowingly run uncapped (which logs a one-time construction warning). A
|
|
438
|
+
non-positive or non-integer value now raises `ArgumentError`.
|
|
439
|
+
- **NEW**: Observability for SSE dispatchers abandoned by a client disconnect.
|
|
440
|
+
`Parse::Agent::MCPRackApp.abandoned_dispatcher_count` is a process-wide
|
|
441
|
+
cumulative counter, and each abandonment emits a
|
|
442
|
+
`parse.agent.mcp_dispatcher_abandoned` `ActiveSupport::Notifications` event
|
|
443
|
+
(`reason:`, `dispatcher_alive:`, `request_id:`) so operators can detect
|
|
444
|
+
disconnect-against-slow-tool pressure. On disconnect the dispatcher's
|
|
445
|
+
cancellation token is tripped (cooperative exit) and its lifetime is bounded
|
|
446
|
+
by the per-tool `Timeout` plus the clean MongoDB/REST I/O deadlines; the
|
|
447
|
+
orphan is intentionally NOT force-killed, because a `Thread#kill` would skip
|
|
448
|
+
the database driver's connection-invalidation and risk returning a half-used
|
|
449
|
+
pooled connection to a later request.
|
|
450
|
+
- **CHANGED**: Custom tools registered via `Parse::Agent::Tools.register` now
|
|
451
|
+
have their declared `timeout:` (default 30s) actually enforced —
|
|
452
|
+
`Tools.invoke` wraps the handler in `Timeout.timeout`, raising
|
|
453
|
+
`Parse::Agent::ToolTimeoutError` when it is exceeded (previously the stored
|
|
454
|
+
timeout was not applied to the custom-handler path, so a blocking or looping
|
|
455
|
+
handler ran unbounded and could hold an MCP streaming dispatcher slot after a
|
|
456
|
+
client disconnect). Built-in tools are unaffected (they already self-applied
|
|
457
|
+
their timeout). **Migration:** a custom tool that legitimately runs longer
|
|
458
|
+
than 30s must now declare an explicit `timeout:` (e.g.
|
|
459
|
+
`register(..., timeout: 120)`); a tool that exceeds its budget will otherwise
|
|
460
|
+
raise `ToolTimeoutError`. `register` now also rejects a non-positive
|
|
461
|
+
`timeout:` with `ArgumentError` (a `0` would make `Timeout.timeout` a no-op
|
|
462
|
+
and silently disable the bound).
|
|
463
|
+
|
|
3
464
|
### 5.3.0
|
|
4
465
|
|
|
5
466
|
#### Run webhook handlers as the calling user
|
data/Gemfile
CHANGED
|
@@ -32,5 +32,12 @@ group :test, :development do
|
|
|
32
32
|
gem "puma"
|
|
33
33
|
gem "sinatra"
|
|
34
34
|
gem "rack-test"
|
|
35
|
+
# MFA / TOTP test infrastructure (Parse::MFA, two_factor_auth).
|
|
36
|
+
# rotp: generates TOTP secrets and time-based codes so the MFA unit and
|
|
37
|
+
# integration tests can enroll and log in against Parse Server's
|
|
38
|
+
# TOTP adapter (SHA1 / 6 digits / 30s — rotp's defaults match).
|
|
39
|
+
# rqrcode: renders the provisioning QR code exercised by Parse::MFA.qr_code.
|
|
40
|
+
gem "rotp"
|
|
41
|
+
gem "rqrcode"
|
|
35
42
|
# gem "thin" # for yard server - disabled due to eventmachine compilation issues
|
|
36
43
|
end
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
parse-stack-next (5.
|
|
4
|
+
parse-stack-next (5.4.0)
|
|
5
5
|
activemodel (>= 6.1, < 9)
|
|
6
6
|
activesupport (>= 6.1, < 9)
|
|
7
7
|
connection_pool (>= 2.2, < 4)
|
|
@@ -39,6 +39,7 @@ GEM
|
|
|
39
39
|
bundler-audit (0.9.3)
|
|
40
40
|
bundler (>= 1.2.0)
|
|
41
41
|
thor (~> 1.0)
|
|
42
|
+
chunky_png (1.4.0)
|
|
42
43
|
coderay (1.1.3)
|
|
43
44
|
concurrent-ruby (1.3.6)
|
|
44
45
|
connection_pool (3.0.2)
|
|
@@ -54,7 +55,7 @@ GEM
|
|
|
54
55
|
faraday-net_http (>= 2.0, < 3.5)
|
|
55
56
|
json
|
|
56
57
|
logger
|
|
57
|
-
faraday-net_http (3.4.
|
|
58
|
+
faraday-net_http (3.4.4)
|
|
58
59
|
net-http (~> 0.5)
|
|
59
60
|
faraday-net_http_persistent (2.3.1)
|
|
60
61
|
faraday (~> 2.5)
|
|
@@ -72,7 +73,7 @@ GEM
|
|
|
72
73
|
prism (>= 1.3.0)
|
|
73
74
|
rdoc (>= 4.0.0)
|
|
74
75
|
reline (>= 0.4.2)
|
|
75
|
-
json (2.19.
|
|
76
|
+
json (2.19.8)
|
|
76
77
|
logger (1.7.0)
|
|
77
78
|
method_source (1.1.0)
|
|
78
79
|
minitest (6.0.6)
|
|
@@ -104,7 +105,7 @@ GEM
|
|
|
104
105
|
coderay (~> 1.1)
|
|
105
106
|
method_source (~> 1.0)
|
|
106
107
|
reline (>= 0.6.0)
|
|
107
|
-
psych (5.
|
|
108
|
+
psych (5.4.0)
|
|
108
109
|
date
|
|
109
110
|
stringio
|
|
110
111
|
puma (8.0.2)
|
|
@@ -133,6 +134,11 @@ GEM
|
|
|
133
134
|
connection_pool
|
|
134
135
|
reline (0.6.3)
|
|
135
136
|
io-console (~> 0.5)
|
|
137
|
+
rotp (6.3.0)
|
|
138
|
+
rqrcode (3.2.0)
|
|
139
|
+
chunky_png (~> 1.0)
|
|
140
|
+
rqrcode_core (~> 2.0)
|
|
141
|
+
rqrcode_core (2.1.0)
|
|
136
142
|
ruby-progressbar (1.13.0)
|
|
137
143
|
rufo (0.18.2)
|
|
138
144
|
securerandom (0.4.1)
|
|
@@ -181,6 +187,8 @@ DEPENDENCIES
|
|
|
181
187
|
rake
|
|
182
188
|
redcarpet
|
|
183
189
|
redis
|
|
190
|
+
rotp
|
|
191
|
+
rqrcode
|
|
184
192
|
rufo
|
|
185
193
|
sinatra
|
|
186
194
|
webrick
|