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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +461 -0
  4. data/Gemfile +7 -0
  5. data/Gemfile.lock +12 -4
  6. data/README.md +160 -3
  7. data/Rakefile +52 -3
  8. data/docs/atlas_vector_search_guide.md +86 -2
  9. data/docs/client_sdk_guide.md +5 -0
  10. data/docs/mcp_guide.md +59 -4
  11. data/docs/mongodb_direct_guide.md +93 -1
  12. data/docs/usage_guide.md +11 -1
  13. data/docs/webhooks_guide.md +418 -0
  14. data/examples/README.md +46 -0
  15. data/examples/basic_client.rb +93 -0
  16. data/examples/basic_server.rb +109 -0
  17. data/examples/live_query_listener.rb +98 -0
  18. data/examples/rag_chatbot.rb +221 -0
  19. data/examples/webhook_server.rb +111 -0
  20. data/lib/parse/agent/mcp_rack_app.rb +285 -62
  21. data/lib/parse/agent/tools.rb +45 -5
  22. data/lib/parse/api/aggregate.rb +7 -1
  23. data/lib/parse/api/cloud_functions.rb +12 -4
  24. data/lib/parse/api/hooks.rb +46 -9
  25. data/lib/parse/api/objects.rb +16 -2
  26. data/lib/parse/api/path_segment.rb +33 -0
  27. data/lib/parse/api/server.rb +94 -0
  28. data/lib/parse/api/users.rb +58 -2
  29. data/lib/parse/atlas_search.rb +7 -7
  30. data/lib/parse/client/body_builder.rb +5 -0
  31. data/lib/parse/client/protocol.rb +4 -0
  32. data/lib/parse/client.rb +55 -2
  33. data/lib/parse/embeddings/spend_cap.rb +255 -0
  34. data/lib/parse/embeddings.rb +1 -0
  35. data/lib/parse/live_query/client.rb +3 -1
  36. data/lib/parse/live_query/subscription.rb +32 -5
  37. data/lib/parse/model/acl.rb +4 -2
  38. data/lib/parse/model/classes/audience.rb +52 -4
  39. data/lib/parse/model/classes/user.rb +180 -3
  40. data/lib/parse/model/core/embed_managed.rb +113 -0
  41. data/lib/parse/model/core/querying.rb +3 -1
  42. data/lib/parse/model/core/vector_searchable.rb +161 -0
  43. data/lib/parse/model/object.rb +28 -5
  44. data/lib/parse/mongodb.rb +7 -1
  45. data/lib/parse/pipeline_security.rb +5 -3
  46. data/lib/parse/query/constraints.rb +29 -0
  47. data/lib/parse/query.rb +265 -27
  48. data/lib/parse/retrieval/agent_tool.rb +49 -0
  49. data/lib/parse/retrieval/reranker/cohere.rb +218 -0
  50. data/lib/parse/retrieval/reranker.rb +157 -0
  51. data/lib/parse/retrieval/retriever.rb +110 -23
  52. data/lib/parse/stack/version.rb +1 -1
  53. data/lib/parse/stack.rb +17 -0
  54. data/lib/parse/two_factor_auth/user_extension.rb +123 -31
  55. data/lib/parse/vector_search/hybrid.rb +578 -0
  56. data/lib/parse/webhooks/payload.rb +252 -7
  57. data/lib/parse/webhooks/trigger_audit.rb +502 -0
  58. data/lib/parse/webhooks.rb +215 -3
  59. data/scripts/docker/Dockerfile.parse +5 -1
  60. data/scripts/docker/docker-compose.test.yml +31 -0
  61. data/scripts/docker/docker-compose.verifyemail.yml +4 -0
  62. data/scripts/docker/preflight.sh +76 -0
  63. data/scripts/start-parse.sh +52 -4
  64. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90a966c230e0a0e2e6fd814170726e3af713dae06c0341a00cd7581eecbe53ee
4
- data.tar.gz: b171ed432c7ea7806ec07653b47122bd6102a16046c980d915fe45f98ffbca32
3
+ metadata.gz: 3568759074455238e0addc957d2b46db3616a26057eb4d1fdd8ec70c1be577c7
4
+ data.tar.gz: c31228426dfa4dbbcb49ab5df47662104e1d91100e2a92040f699ce92b09db7a
5
5
  SHA512:
6
- metadata.gz: 510f3f2deb860cedc73f11455fb5d4789c327923abebad4e26b223ae9c385374ffa23bc9564561e7d4dddb31b0c1a015b7e7222dacd7c0b18bdb01199580d7a0
7
- data.tar.gz: cd0fb4c706b3c33f12059c7431cf75abd792cbd5ba8b193dd43256aaa70c8fcdd3f86e10b3ad8ffb25c8e9ccc3f985a61bcaa8b49d24badf6f8358cec2b16f3d
6
+ metadata.gz: 54e3ecd41d651aa918809f247799e00edcb79ad5da331ad2b33f669b3bdb989b727b4cb3cf39f8e1150c9778ca4326084cc7d8c31994ab0c2e5c04b82247833a
7
+ data.tar.gz: fbb24fcacd678d5ad060d4e1ce772c646aed2ff61bcaaa232be42903eee416965cd06b9eab7b4f2f68b1980272ed059fd4e216e78c8896b77741bdf1d3377d69
data/.gitignore CHANGED
@@ -49,6 +49,8 @@ logs
49
49
  !docs/mongodb_index_optimization_guide.md
50
50
  !docs/atlas_vector_search_guide.md
51
51
  !docs/usage_guide.md
52
+ !docs/webhooks_guide.md
52
53
  !SECURITY.md
53
54
  !docs/client_sdk_guide.md
54
55
  !docs/acl_clp_guide.md
56
+ !examples/README.md
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.3.0)
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.3)
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.5)
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.3.1)
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