parse-stack-next 5.3.0 → 5.4.1

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