parse-stack-next 5.0.1 → 5.1.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/.github/ISSUE_TEMPLATE/bug_report.yml +105 -0
- data/.github/ISSUE_TEMPLATE/feature_request.yml +67 -0
- data/.github/dependabot.yml +13 -0
- data/.github/workflows/codeql.yml +1 -1
- data/.github/workflows/docs.yml +3 -3
- data/.github/workflows/release.yml +14 -3
- data/.github/workflows/ruby.yml +1 -1
- data/.gitignore +1 -0
- data/.yardopts +19 -0
- data/CHANGELOG.md +792 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +8 -5
- data/README.md +15 -0
- data/Rakefile +5 -1
- data/docs/acl_clp_guide.md +553 -0
- data/docs/atlas_vector_search_guide.md +123 -22
- data/docs/client_sdk_guide.md +201 -5
- data/docs/usage_guide.md +21 -0
- data/docs/yard-template/default/fulldoc/html/css/common.css +1222 -0
- data/docs/yard-template/default/fulldoc/html/css/full_list.css +387 -0
- data/lib/parse/agent/tools.rb +153 -1
- data/lib/parse/cache/redis.rb +53 -0
- data/lib/parse/client/caching.rb +18 -1
- data/lib/parse/client.rb +79 -12
- data/lib/parse/embeddings/cohere.rb +143 -6
- data/lib/parse/embeddings/provider.rb +20 -2
- data/lib/parse/embeddings/voyage.rb +102 -0
- data/lib/parse/embeddings.rb +332 -1
- data/lib/parse/live_query/client.rb +167 -4
- data/lib/parse/live_query/configuration.rb +12 -0
- data/lib/parse/live_query/subscription.rb +55 -2
- data/lib/parse/live_query.rb +123 -1
- data/lib/parse/lock.rb +342 -0
- data/lib/parse/lock_backend.rb +308 -0
- data/lib/parse/model/classes/audience.rb +5 -0
- data/lib/parse/model/classes/installation.rb +122 -0
- data/lib/parse/model/classes/job_schedule.rb +3 -1
- data/lib/parse/model/classes/job_status.rb +4 -1
- data/lib/parse/model/classes/push_status.rb +4 -1
- data/lib/parse/model/classes/session.rb +7 -0
- data/lib/parse/model/classes/user.rb +204 -0
- data/lib/parse/model/core/create_lock.rb +28 -146
- data/lib/parse/model/core/embed_managed.rb +162 -13
- data/lib/parse/model/core/parse_reference.rb +17 -1
- data/lib/parse/model/core/querying.rb +26 -2
- data/lib/parse/model/file.rb +523 -18
- data/lib/parse/query.rb +31 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +98 -1
- data/parse-stack-next.gemspec +2 -2
- metadata +17 -7
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,797 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.1.0
|
|
4
|
+
|
|
5
|
+
#### `Parse::File` — URL normalization, presigned-URL stash, leak hardening
|
|
6
|
+
|
|
7
|
+
The model is hardened so signed URLs never persist in `@url` — they
|
|
8
|
+
get stripped to a canonical bare URL and stashed separately with a
|
|
9
|
+
data-driven expiry parsed from the URL's own query parameters. The
|
|
10
|
+
single URL normalization point applies uniformly to every writer
|
|
11
|
+
(caller-side `url=`, hydration `attributes=`) so the rule is the same
|
|
12
|
+
whether the file pointer arrived from Parse Server's REST surface
|
|
13
|
+
(which may include a freshly-signed URL when Parse Server's
|
|
14
|
+
S3FilesAdapter is configured with `presignedUrl: true`) or from a
|
|
15
|
+
direct caller-side assignment. The change also lays groundwork for
|
|
16
|
+
pluggable storage-adapter work in a later release without committing
|
|
17
|
+
that surface area in this one.
|
|
18
|
+
|
|
19
|
+
**Migration callout — `@url` value change:** the `@url` field now
|
|
20
|
+
drops signed-URL query parameters before storage. Any application
|
|
21
|
+
code that assigned a presigned URL directly to `Parse::File#url=`
|
|
22
|
+
(uncommon, but possible when wiring up custom file-serve flows) will
|
|
23
|
+
find that `file.url` now returns the bare canonical URL; the
|
|
24
|
+
original signed URL is available via the new `file.presigned_url`
|
|
25
|
+
accessor with its expiry in `file.presigned_url_expires_at`. The
|
|
26
|
+
`Parse::File#to_s` / `<%= file %>` ERB rendering path is unchanged
|
|
27
|
+
in shape (still returns `@url`), but the value emitted is now the
|
|
28
|
+
canonical bare URL rather than whatever was assigned — apps relying
|
|
29
|
+
on inline ERB to render a freshly-signed URL must switch to
|
|
30
|
+
`file.presigned_url` (or the new
|
|
31
|
+
`Parse::File#presigned_url_valid?` predicate) explicitly.
|
|
32
|
+
|
|
33
|
+
**Migration callout — error reporter payload shape:**
|
|
34
|
+
`Parse::File#inspect` no longer includes the URL at all. Anything
|
|
35
|
+
that captures exception inspect output — Sentry, Honeybadger,
|
|
36
|
+
Rollbar, Bugsnag, Rails' default error pages, custom log scrubbers
|
|
37
|
+
— will see a different payload shape the day an app upgrades to
|
|
38
|
+
5.1.0. Tests that pattern-matched on `@url='https://...'` in
|
|
39
|
+
inspect output will need to be updated, and dashboards / alerts
|
|
40
|
+
that grouped errors by inspect string fingerprints will see a
|
|
41
|
+
one-time shift in fingerprint values. The new format emits
|
|
42
|
+
`@name`, `@mime_type`, `@contents` (presence), and `@url=set|blank`
|
|
43
|
+
— enough to debug, none of the URL content.
|
|
44
|
+
|
|
45
|
+
- **NEW**: `Parse::File#url=` and `Parse::File#attributes=` now route
|
|
46
|
+
through a single private normalization point. When the incoming URL
|
|
47
|
+
carries a recognized signed-URL query parameter
|
|
48
|
+
(`X-Amz-Signature`, `X-Amz-Credential`, `X-Amz-Security-Token`,
|
|
49
|
+
`AWSAccessKeyId`, `Key-Pair-Id`), the query string is stripped
|
|
50
|
+
entirely; the bare canonical URL is stored in `@url`; the original
|
|
51
|
+
signed URL is stashed in `@presigned_url` with its expiry parsed
|
|
52
|
+
into `@presigned_url_expires_at`. The expiry is data-driven —
|
|
53
|
+
computed from `X-Amz-Date + X-Amz-Expires` (SigV4) or `Expires`
|
|
54
|
+
(SigV2 / CloudFront) — never hardcoded SDK-side. The `@url`
|
|
55
|
+
invariant is now structural: the field never holds a short-TTL
|
|
56
|
+
signed URL. (`lib/parse/model/file.rb`)
|
|
57
|
+
- **NEW**: `Parse::File#presigned_url` and
|
|
58
|
+
`Parse::File#presigned_url_expires_at` accessors expose the
|
|
59
|
+
stashed signed URL and its parsed expiry. Useful today for apps
|
|
60
|
+
with Parse Server's `S3FilesAdapter` configured with
|
|
61
|
+
`presignedUrl: true`: the SDK normalizes the URL Parse Server
|
|
62
|
+
hands back on every read, the bare canonical value lands in
|
|
63
|
+
`@url`, and the freshly-signed URL is available via
|
|
64
|
+
`file.presigned_url` until `file.presigned_url_expires_at`. The
|
|
65
|
+
stash is invalidated automatically on any URL reassignment
|
|
66
|
+
(signed → canonical, signed → signed, or assignment to nil), so
|
|
67
|
+
callers reading `file.presigned_url` are never handed a value
|
|
68
|
+
staler than `@url`. (`lib/parse/model/file.rb`)
|
|
69
|
+
- **NEW**: `Parse::File#presigned_url_valid?(buffer: 60)` returns
|
|
70
|
+
true when `@presigned_url` is set and `@presigned_url_expires_at`
|
|
71
|
+
is at least `buffer` seconds in the future. Default 60 seconds —
|
|
72
|
+
a margin that absorbs network RTT, client clock skew, and one
|
|
73
|
+
retry. Eliminates the hand-rolled `expires_at && expires_at -
|
|
74
|
+
Time.now.utc > N` pattern every caller would otherwise write to
|
|
75
|
+
gate a refetch. (`lib/parse/model/file.rb`)
|
|
76
|
+
- **NEW**: `Parse::File.signed_url_policy` global accessor controls
|
|
77
|
+
how the URL normalization point reacts to incoming signed URLs.
|
|
78
|
+
Values: `:strip` (default — strip and stash, the pragmatic
|
|
79
|
+
behavior for any deployment where Parse Server's S3FilesAdapter
|
|
80
|
+
returns presigned URLs on read) or `:raise` (refuse the
|
|
81
|
+
assignment with `SignedUrlError`). Strict mode for apps that can
|
|
82
|
+
guarantee Parse Server is NOT issuing signed URLs and want a
|
|
83
|
+
loud failure on any signed-URL assignment instead of silent
|
|
84
|
+
normalization. The policy applies uniformly to both
|
|
85
|
+
caller-side `url=` and hydration `attributes=` — asymmetric
|
|
86
|
+
writer behavior was an explicit anti-goal. (`lib/parse/model/file.rb`)
|
|
87
|
+
- **NEW**: `Parse::File.parse_presigned_expiry(url)` class method
|
|
88
|
+
extracts the expiry time (UTC) from any signed URL by parsing its
|
|
89
|
+
own query parameters. Supports SigV4 and SigV2 / CloudFront
|
|
90
|
+
shapes. Returns nil for URLs without parseable presigned-URL
|
|
91
|
+
expiry data. (`lib/parse/model/file.rb`)
|
|
92
|
+
- **FIXED**: `Parse::File#saved?` basename comparison now strips the
|
|
93
|
+
URL's query string before computing basename, so short-TTL
|
|
94
|
+
presigned URLs that Parse Server's S3FilesAdapter returns on every
|
|
95
|
+
read (`https://bucket.s3.../doc.pdf?X-Amz-Signature=...`) don't
|
|
96
|
+
break `saved?` on reload. The signature bytes used to leak into
|
|
97
|
+
the comparison and cause false negatives.
|
|
98
|
+
(`lib/parse/model/file.rb`)
|
|
99
|
+
- **FIXED**: `Hash#parse_file?` strips the URL's query string before
|
|
100
|
+
the basename equality check so presigned URLs round-trip cleanly
|
|
101
|
+
through file-pointer recognition. Previously the signature bytes
|
|
102
|
+
leaked into the comparison and could cause false negatives.
|
|
103
|
+
(`lib/parse/model/file.rb`)
|
|
104
|
+
- **FIXED**: `Parse::File#save` now routes the file-create response
|
|
105
|
+
URL through the same normalization point as `url=` / `attributes=`.
|
|
106
|
+
Parse Server's S3FilesAdapter can return a freshly-signed URL in the
|
|
107
|
+
create response (not only on read); the save writer previously
|
|
108
|
+
assigned it verbatim to `@url` — and baked the signature query
|
|
109
|
+
string into `@name` via `File.basename` when the response omitted a
|
|
110
|
+
name — bypassing the `@url`-is-always-canonical invariant and the
|
|
111
|
+
`signed_url_policy = :raise` guard. The save writer now strips and
|
|
112
|
+
stashes like every other writer, derives any fallback name from the
|
|
113
|
+
canonical URL, and honors strict mode. (`lib/parse/model/file.rb`)
|
|
114
|
+
- **CHANGED**: `Parse::File#inspect` no longer includes the full
|
|
115
|
+
`@url` string. Inspect output lands in exception messages, Rails
|
|
116
|
+
error pages, log captures, and every error reporter the app uses
|
|
117
|
+
(Sentry / Honeybadger / Rollbar / Bugsnag); defaulting to URL
|
|
118
|
+
emission is a future leak waiting to happen even after the
|
|
119
|
+
normalization guarantees `@url` is canonical. The new format
|
|
120
|
+
emits `@name`, `@mime_type`, `@contents` (presence), and
|
|
121
|
+
`@url=set|blank`. See the migration callout above for the
|
|
122
|
+
error-reporter payload shift.
|
|
123
|
+
(`lib/parse/model/file.rb`)
|
|
124
|
+
- **NEW**: `Parse::File::SignedUrlError` is raised when
|
|
125
|
+
`signed_url_policy = :raise` is set and an incoming URL carries
|
|
126
|
+
a signed-URL signature parameter. Apps that want strict-mode
|
|
127
|
+
enforcement no longer need to subclass or monkey-patch — flip
|
|
128
|
+
the policy and the SDK's normalization point does the work.
|
|
129
|
+
`Parse::File.url_signature_param?(url_string)` and the
|
|
130
|
+
`SIGNATURE_QUERY_PARAMS` constant remain public for caller-side
|
|
131
|
+
custom detection logic. (`lib/parse/model/file.rb`)
|
|
132
|
+
- **NEW**: `Parse::File.log_filter` returns a frozen `Regexp` that
|
|
133
|
+
matches any plain-text HTTP(S) URL carrying an unambiguously
|
|
134
|
+
AWS-style signed-URL parameter — SigV4 (`X-Amz-Signature`,
|
|
135
|
+
`X-Amz-Credential`, `X-Amz-Security-Token`, `X-Amz-Algorithm`,
|
|
136
|
+
`X-Amz-Date`, `X-Amz-Expires`, `X-Amz-SignedHeaders`), legacy
|
|
137
|
+
SigV2 (`AWSAccessKeyId`), or CloudFront (`Key-Pair-Id`). Designed
|
|
138
|
+
to be plugged into `lograge` / `semantic_logger` / custom log
|
|
139
|
+
scrubbers so accidental `Rails.logger.info(file_url)` calls do
|
|
140
|
+
not leak read capabilities into log aggregators. Bare
|
|
141
|
+
`Signature=` / `Policy=` query params are intentionally NOT
|
|
142
|
+
matched on their own — they collide with too many unrelated app
|
|
143
|
+
conventions (webhook signatures, privacy_policy form fields);
|
|
144
|
+
CloudFront URLs always carry `Key-Pair-Id` alongside `Signature`
|
|
145
|
+
/ `Policy`, which IS matched. (`lib/parse/model/file.rb`)
|
|
146
|
+
- **NEW**: `Parse::File.log_filter_strict` returns the same
|
|
147
|
+
signature-detection regex but ALSO accepts the JSON-encoded
|
|
148
|
+
query separator (`\u0026` for `&`). Required for scrubbing
|
|
149
|
+
error-reporter event bodies (Sentry, Honeybadger, Rollbar,
|
|
150
|
+
Bugsnag) where the URL string has been JSON-encoded once before
|
|
151
|
+
reaching the scrubber and the literal `&` appears as `\u0026`.
|
|
152
|
+
The default `log_filter` would silently miss those — operators
|
|
153
|
+
shipping to a JSON-encoding error reporter should wire
|
|
154
|
+
`log_filter_strict` into the before-send hook. (`lib/parse/model/file.rb`)
|
|
155
|
+
- **NEW**: `Parse::File.filter_parameter_names` returns an
|
|
156
|
+
`Array<Regexp>` for `Rails.application.config.filter_parameters`.
|
|
157
|
+
Defaults to AWS-prefixed names only (`X-Amz-*`, `AWSAccessKeyId`,
|
|
158
|
+
`Key-Pair-Id`) so the list never over-redacts a Rails app's
|
|
159
|
+
privacy_policy / e-signature / policy_id form fields. Companion
|
|
160
|
+
`Parse::File.cloudfront_signed_param_names` returns the bare
|
|
161
|
+
`Signature` / `Policy` / `Expires` regexes as an opt-in
|
|
162
|
+
extension for CloudFront-heavy deployments that have confirmed
|
|
163
|
+
no app-side collision. (`lib/parse/model/file.rb`)
|
|
164
|
+
- **CHANGED**: `Parse::File#to_s` is deliberately left returning
|
|
165
|
+
`@url` unchanged — ERB templates and `<img src="<%= file %>">`
|
|
166
|
+
callers continue to work. Combined with the URL normalization
|
|
167
|
+
above, this makes the "@url is canonical" invariant structural
|
|
168
|
+
rather than convention-based — `to_s` cannot leak a signed URL
|
|
169
|
+
because such a URL is stripped before reaching `@url`.
|
|
170
|
+
|
|
171
|
+
#### `Parse::Lock` — public TTL-bounded mutual-exclusion primitive
|
|
172
|
+
|
|
173
|
+
- **NEW**: `Parse::Lock.acquire(key, ttl:, wait:, on_degraded:) { … }`
|
|
174
|
+
exposes the Redis-backed lock previously hidden inside
|
|
175
|
+
`first_or_create!` / `create_or_update!` as a first-class
|
|
176
|
+
primitive. TTL-bounded (1..30s, default 3s), with in-process
|
|
177
|
+
`Mutex` fallback when the configured cache is process-local
|
|
178
|
+
(Moneta `Memory` / `Null` / nil), and fails closed —
|
|
179
|
+
acquisition errors are caught, treated as "not acquired", and
|
|
180
|
+
surface as `Parse::Lock::TimeoutError` once the `wait:` budget
|
|
181
|
+
elapses. Block-form only (no token-based `try_acquire`); release
|
|
182
|
+
is automatic on normal return, exception, `throw`/`break`, or
|
|
183
|
+
any `ensure`-path exit. Keys are SHA-256-hashed before hitting
|
|
184
|
+
the store so sensitive identifiers (user IDs, request IDs,
|
|
185
|
+
webhook idempotency keys) don't appear verbatim in `KEYS *`
|
|
186
|
+
output (obfuscation, not authentication — see the YARD for the
|
|
187
|
+
guessable-input-space caveat). Documented use cases: bulk-import
|
|
188
|
+
dedup, cron-job singletons, external-API idempotency, anywhere
|
|
189
|
+
two processes might race the same logical operation. Built on
|
|
190
|
+
`Parse::LockBackend` (see below); namespace prefix
|
|
191
|
+
`parse-stack:lock:v1:` is distinct from `first_or_create!`'s
|
|
192
|
+
`parse-stack:foc:v1:`, so the two APIs cannot collide even on
|
|
193
|
+
literally-equal-named keys. (`lib/parse/lock.rb`)
|
|
194
|
+
- **NEW**: `Parse::Lock::TimeoutError` and
|
|
195
|
+
`Parse::Lock::UnavailableError` — namespaced under `Parse::Lock`
|
|
196
|
+
so the peer-not-base relationship to
|
|
197
|
+
`Parse::CreateLockTimeoutError` / `Parse::CreateLockUnavailableError`
|
|
198
|
+
is unambiguous in the name itself (a caller seeing
|
|
199
|
+
`Parse::Lock::TimeoutError` cannot reasonably read it as a base
|
|
200
|
+
of `Parse::CreateLockTimeoutError`). Both inherit from
|
|
201
|
+
`Parse::Error`; rescue chains targeting `Parse::Error` continue
|
|
202
|
+
to catch them. (`lib/parse/lock.rb`)
|
|
203
|
+
- **NEW**: `Parse::LockBackend` — `@api private` module hosting the
|
|
204
|
+
shared lock primitives (`lock_store`, `degraded_store?`,
|
|
205
|
+
`handle_degraded`, `try_acquire`, `release`, `poll_interval`,
|
|
206
|
+
`process_mutex`, `lock_secret_for`, `configured_secret`,
|
|
207
|
+
`auto_secret`, `warn_plain_sha_once`). Both `Parse::Lock` and
|
|
208
|
+
`Parse::CreateLock` consume the backend directly instead of one
|
|
209
|
+
reaching into the other's privates. The extraction eliminates
|
|
210
|
+
the `.send(:private)` coupling that the v5.1.0 round-2 review
|
|
211
|
+
called out as fragile — any future refactor of the SETNX
|
|
212
|
+
semantics, the degraded-detection heuristic, the in-process-Mutex
|
|
213
|
+
fallback registry, OR the HMAC secret resolution happens in
|
|
214
|
+
exactly one place. `Parse::CreateLock` retains only its
|
|
215
|
+
CreateLock-specific helpers (`clamp` for input range).
|
|
216
|
+
(`lib/parse/lock_backend.rb`, `lib/parse/model/core/create_lock.rb`)
|
|
217
|
+
- **NEW**: `Parse::Lock.acquire(secret:)` — HMAC keying option.
|
|
218
|
+
`:auto` (default) picks up the operator-configured
|
|
219
|
+
`PARSE_STACK_LOCK_SECRET` / `Parse.synchronize_create_secret`,
|
|
220
|
+
auto-derives a per-process secret for degraded stores, falls
|
|
221
|
+
back to plain SHA-256 with a one-time `[Parse::Lock:SECURITY]`
|
|
222
|
+
warn for cross-process stores without a configured secret. A
|
|
223
|
+
`String` value overrides the resolution per call; `nil`
|
|
224
|
+
explicitly opts out of HMAC (no warn — the opt-out is
|
|
225
|
+
deliberate). Closes the parity gap with `Parse::CreateLock`: an
|
|
226
|
+
operator setting one secret hardens both APIs without a second
|
|
227
|
+
config knob. Different secrets isolate locks on the same raw
|
|
228
|
+
key — useful for multi-tenant deployments sharing one Redis
|
|
229
|
+
where tenants must not block each other on coincidentally-equal
|
|
230
|
+
lock names. Real-Redis integration coverage in
|
|
231
|
+
`test/lib/parse/lock_redis_integration_test.rb` (Queue-gated to
|
|
232
|
+
eliminate sleep-based race flakes: HMAC-keyed entry shape,
|
|
233
|
+
plain-SHA opt-out, cross-process contention, fast-fail under
|
|
234
|
+
`wait: 0`, different-secret isolation, shared-secret-with-CreateLock
|
|
235
|
+
via env var, namespace separation from `first_or_create!`, atomic
|
|
236
|
+
compare-and-delete under a simulated lease-expiry race, and the
|
|
237
|
+
TTL-overrun warning). Explicit `secret:` kwarg
|
|
238
|
+
values are length-validated at the boundary —
|
|
239
|
+
`Parse::Lock::SECRET_MIN_BYTES` (= 16) is the floor for any
|
|
240
|
+
caller-supplied HMAC key. A `secret: "a"` misconfiguration is
|
|
241
|
+
refused with `ArgumentError` rather than silently degrading the
|
|
242
|
+
lock-pinning resistance HMAC keying is supposed to provide. The
|
|
243
|
+
operator-configured `PARSE_STACK_LOCK_SECRET` path is not
|
|
244
|
+
length-checked (different threat model — process-boot
|
|
245
|
+
configuration, not per-call argument). The `on_degraded:` YARD
|
|
246
|
+
now documents the asymmetric-degradation residual risk: if two
|
|
247
|
+
processes target the same Redis but disagree on degraded
|
|
248
|
+
detection, they derive different store keys for the same raw
|
|
249
|
+
key and silently fail to mutually exclude — mitigated by
|
|
250
|
+
uniform `Parse.synchronize_create_store` configuration or
|
|
251
|
+
`on_degraded: :raise`. (`lib/parse/lock.rb`)
|
|
252
|
+
- **NEW**: `Parse::Lock` and `Parse::LockBackend` are autoloaded —
|
|
253
|
+
`Parse::Lock.acquire(…)` works without an explicit
|
|
254
|
+
`require 'parse/lock'`. (`lib/parse/stack.rb`)
|
|
255
|
+
- **FIXED**: lock release is now an **atomic compare-and-delete**.
|
|
256
|
+
`Parse::Cache::Redis` gains raw-Redis `lock_acquire` (`SET NX EX`)
|
|
257
|
+
and `lock_release` (a Lua compare-and-delete), and `Parse::LockBackend`
|
|
258
|
+
routes both ends through them. The previous release read the owner
|
|
259
|
+
token and deleted in two separate commands; a holder whose lease
|
|
260
|
+
expired and was re-acquired by another holder between the two could
|
|
261
|
+
delete the new holder's live lock. The Lua CAD makes a stale owner's
|
|
262
|
+
release a guaranteed no-op. The raw path also uses plain-string keys
|
|
263
|
+
and values (bypassing Moneta's marshal transformers) so acquire and
|
|
264
|
+
release share one encoding and the keys are human-inspectable in
|
|
265
|
+
Redis. Non-Redis (raw-Moneta) stores keep the documented best-effort
|
|
266
|
+
GET-then-DEL bounded by the short TTL.
|
|
267
|
+
(`lib/parse/cache/redis.rb`, `lib/parse/lock_backend.rb`)
|
|
268
|
+
- **FIXED**: `Parse::Lock.acquire` no longer over-promises exactly-once
|
|
269
|
+
execution. The contract is now documented as mutual exclusion with a
|
|
270
|
+
DEADLINE: if the critical section outruns `ttl:`, the lease expires
|
|
271
|
+
mid-block and a second caller can acquire concurrently. The block now
|
|
272
|
+
receives its owner token (`acquire(key) { |token| … }`) for callers
|
|
273
|
+
who want to fence against a token-checking resource, and a
|
|
274
|
+
`[Parse::Lock]` warning is emitted on release when the section
|
|
275
|
+
overran its TTL (mutual exclusion was not guaranteed for the overrun
|
|
276
|
+
window). The misleading "two webhook deliveries can't double-charge"
|
|
277
|
+
example is replaced with an idempotency-required example.
|
|
278
|
+
(`lib/parse/lock.rb`)
|
|
279
|
+
|
|
280
|
+
#### LiveQuery — BREAKING: ACL-scoped by default; plus ergonomics (autoload, error context, signal-safe shutdown)
|
|
281
|
+
|
|
282
|
+
- **NEW**: `Parse::LiveQuery` is now autoloaded — `Parse::LiveQuery.configure { … }`
|
|
283
|
+
works without an explicit `require 'parse/live_query'`. The
|
|
284
|
+
autoload is purely a file-loading convenience and does NOT open
|
|
285
|
+
any network connection; a WebSocket only opens when
|
|
286
|
+
`Parse.live_query_enabled = true` AND a
|
|
287
|
+
`Parse::LiveQuery::Client` is instantiated (typically via
|
|
288
|
+
`Klass.subscribe { … }`). The opt-in toggle's security shape is
|
|
289
|
+
preserved. (`lib/parse/stack.rb`)
|
|
290
|
+
- **BREAKING**: LiveQuery connections are now **ACL-scoped by
|
|
291
|
+
default** — the connect frame no longer carries the master key
|
|
292
|
+
merely because one is configured. Parse Server resolves
|
|
293
|
+
master-key (ACL/CLP-bypass) authorization once, per CONNECTION,
|
|
294
|
+
from the connect frame (`_handleConnect` → `client.hasMasterKey`);
|
|
295
|
+
once set, EVERY subscription on that socket bypasses ACL/CLP and
|
|
296
|
+
returns every matching object regardless of its ACL. Prior
|
|
297
|
+
versions sent the master key on the connect frame whenever one was
|
|
298
|
+
present, so a `Parse.setup(master_key: …)` process silently
|
|
299
|
+
elevated session-token subscriptions the caller believed were
|
|
300
|
+
ACL-scoped. To get the old admin/event-tap behavior, build an
|
|
301
|
+
explicit admin connection:
|
|
302
|
+
`Parse::LiveQuery::Client.new(use_master_key: true)` or
|
|
303
|
+
`Parse::LiveQuery.configure { |c| c.use_master_key = true }`.
|
|
304
|
+
Admin connections emit a one-time `[Parse::LiveQuery:SECURITY]`
|
|
305
|
+
warning at connect. For a process that needs both scoped and
|
|
306
|
+
admin streams, use two separate clients.
|
|
307
|
+
(`lib/parse/live_query/client.rb`,
|
|
308
|
+
`lib/parse/live_query/configuration.rb`)
|
|
309
|
+
- **NEW**: `Parse::LiveQuery::Client.new(use_master_key: true)`,
|
|
310
|
+
the `config.use_master_key` toggle, and the
|
|
311
|
+
`Client#use_master_key` / `Client#admin_connection?` predicates
|
|
312
|
+
make the admin (ACL-bypassing) posture explicit and inspectable.
|
|
313
|
+
`admin_connection?` is the single source of truth for "will this
|
|
314
|
+
socket bypass ACL/CLP" — true only when the opt-in is set AND a
|
|
315
|
+
usable master key is present.
|
|
316
|
+
(`lib/parse/live_query/client.rb`,
|
|
317
|
+
`lib/parse/live_query/configuration.rb`)
|
|
318
|
+
- **CHANGED**: `Query#subscribe` / `Klass.subscribe` / `Client#subscribe`
|
|
319
|
+
still accept `use_master_key:`, but it is now an **intent assertion**,
|
|
320
|
+
not a per-subscription wire credential. Parse Server has no
|
|
321
|
+
per-subscription master key, so the subscribe frame NEVER carries
|
|
322
|
+
`masterKey` (sending it was a no-op that put a privileged credential
|
|
323
|
+
on the wire for zero effect). The flag is satisfied only on an admin
|
|
324
|
+
connection (where the whole socket is already elevated); on a
|
|
325
|
+
non-admin connection, `use_master_key: true` emits a one-time
|
|
326
|
+
`[Parse::LiveQuery:SECURITY]` warning and the subscription stays
|
|
327
|
+
ACL-scoped. Passing a `session_token:` on an admin connection
|
|
328
|
+
likewise warns — those results are NOT scoped to that token.
|
|
329
|
+
(`lib/parse/model/core/querying.rb`, `lib/parse/query.rb`,
|
|
330
|
+
`lib/parse/live_query/client.rb`, `lib/parse/live_query/subscription.rb`)
|
|
331
|
+
- **FIXED**: `Parse::LiveQuery::Client#inspect` and
|
|
332
|
+
`Subscription#inspect` now redact credentials. The default `inspect`
|
|
333
|
+
dumped every instance variable, exposing `@master_key`, `@client_key`,
|
|
334
|
+
and per-subscription `@session_token` in plaintext anywhere an object
|
|
335
|
+
was rendered — a log line, a backtrace, a Rails error page, or an
|
|
336
|
+
APM/error reporter (Sentry / Honeybadger / Rollbar / Bugsnag). The
|
|
337
|
+
custom `inspect` emits only non-secret diagnostics (url, state,
|
|
338
|
+
`admin_connection`, subscription count, request id, class name) and
|
|
339
|
+
`[REDACTED]` for any secret, matching the redaction
|
|
340
|
+
`Configuration#to_h` already applied. (`lib/parse/live_query/client.rb`,
|
|
341
|
+
`lib/parse/live_query/subscription.rb`)
|
|
342
|
+
- **NEW**: `Klass.subscribe`, `Query#subscribe`, and
|
|
343
|
+
`Parse::LiveQuery::Client#subscribe` all accept an optional `&block`
|
|
344
|
+
yielded the freshly-constructed `Subscription` **before** the
|
|
345
|
+
subscribe frame is sent to the server, so callbacks registered
|
|
346
|
+
inside the block (`sub.on(:create) { … }`) are wired before any
|
|
347
|
+
server event can arrive on the request_id. Order matters and is
|
|
348
|
+
tested — yielding AFTER the wire send would race a fast server
|
|
349
|
+
response against the callback registration on a hot socket. The
|
|
350
|
+
capture-then-wire form (`sub = Post.subscribe(…); sub.on(…)`)
|
|
351
|
+
still works for callers that prefer it. Matches the Parse JS
|
|
352
|
+
client's block-form convention. **If the block raises, the
|
|
353
|
+
subscription is rolled back out of the client's internal
|
|
354
|
+
`@subscriptions` registry before the exception propagates** —
|
|
355
|
+
without the rollback, the next reconnect's `resubscribe_all`
|
|
356
|
+
would silently wire-send the ghost subscription to the server
|
|
357
|
+
(round-3 review finding). (`lib/parse/live_query/client.rb`,
|
|
358
|
+
`lib/parse/query.rb`, `lib/parse/model/core/querying.rb`)
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
Post.subscribe(where: { published: true }) do |sub|
|
|
362
|
+
sub.on(:create) { |obj| puts "new: #{obj.id}" }
|
|
363
|
+
sub.on(:update) { |obj, _prev| puts "updated: #{obj.id}" }
|
|
364
|
+
end
|
|
365
|
+
```
|
|
366
|
+
- **CHANGED**: `Parse::LiveQuery::SubscriptionError` now carries
|
|
367
|
+
`request_id` and `class_name` as structured attributes, and the
|
|
368
|
+
`message` is auto-prefixed with `request_id=<n> class=<X>` when
|
|
369
|
+
the constructor receives either. `Subscription#fail!` promotes
|
|
370
|
+
String errors from the server (e.g. `"Permission denied (code: 101)"`)
|
|
371
|
+
to typed instances carrying both the request id and the class
|
|
372
|
+
the subscription targeted — a single-line log captures enough
|
|
373
|
+
operational context to debug a permission denial without
|
|
374
|
+
re-correlating the raw server string against the subscription
|
|
375
|
+
registry. Backwards compatible — bare `SubscriptionError.new("…")`
|
|
376
|
+
callers (no context) preserve the verbatim message.
|
|
377
|
+
(`lib/parse/live_query.rb`, `lib/parse/live_query/subscription.rb`)
|
|
378
|
+
- **NEW**: `Parse::LiveQuery.run_until_signal!(client:, signals:,
|
|
379
|
+
shutdown_timeout:, poll_interval:) { |client| … }` is a
|
|
380
|
+
signal-safe shutdown helper for long-running subscribe sessions
|
|
381
|
+
(rake-task-style consumers, `rake livequery:tail`, etc.). The
|
|
382
|
+
raw idiom — calling `client.unsubscribe` / `client.close` from
|
|
383
|
+
inside a `Signal.trap` block — raises `ThreadError: can't be
|
|
384
|
+
called from trap context` on macOS / MRI on platforms that
|
|
385
|
+
enforce `:signal_safe?`, because the trap context cannot
|
|
386
|
+
acquire the client's internal `Monitor`. This helper bundles
|
|
387
|
+
the safe pattern: install minimal trap handlers that only push
|
|
388
|
+
a sentinel onto a `Queue`, poll the sentinel from the main
|
|
389
|
+
thread, and run `client.shutdown(timeout:)` on the main thread
|
|
390
|
+
in an `ensure` block. Restores prior trap handlers on exit so
|
|
391
|
+
re-running the helper (in tests, or in a parent process that
|
|
392
|
+
traps SIGINT itself) does not leak our handler. Defaults to
|
|
393
|
+
trapping `INT` and `TERM`; configurable via `signals:`.
|
|
394
|
+
Yields the client to the block before the wait loop starts so
|
|
395
|
+
subscription setup is not racing the trap installation.
|
|
396
|
+
(`lib/parse/live_query.rb`)
|
|
397
|
+
|
|
398
|
+
#### MCP — `structuredContent` outputSchemas for 5 more tools
|
|
399
|
+
|
|
400
|
+
- **NEW**: `output_schema` declarations on five additional built-in
|
|
401
|
+
tools so the MCP dispatcher auto-mirrors their result Hash into
|
|
402
|
+
`structuredContent` per MCP 2025-06-18: `aggregate`,
|
|
403
|
+
`export_data`, `atlas_text_search`, `atlas_autocomplete`,
|
|
404
|
+
`atlas_faceted_search`. Each schema is `type: "object"` with
|
|
405
|
+
every nested `type: "array"` declaring `items:` (so OpenAI's
|
|
406
|
+
strict tool-list validation and MCP client outputSchema
|
|
407
|
+
validation both accept them), and uses `additionalProperties: true`
|
|
408
|
+
on result-row entries to remain honest about the open shape of
|
|
409
|
+
arbitrary `$project` / `$group` / `$lookup` output. Brings the
|
|
410
|
+
built-in MCP tool coverage to sixteen of the catalog;
|
|
411
|
+
`call_method` (structurally polymorphic per application return)
|
|
412
|
+
and `explain_query` (MongoDB-version-dependent shape) remain
|
|
413
|
+
text-only by design. End-to-end emission coverage in
|
|
414
|
+
`test/lib/parse/agent/mcp_dispatcher_test.rb` (five new
|
|
415
|
+
`test_builtin_<tool>_emits_structuredContent` cases drive each
|
|
416
|
+
tool's `tools/call` path through the dispatcher); static
|
|
417
|
+
validity coverage in `test/lib/parse/agent/tools_schema_validity_test.rb`
|
|
418
|
+
walks the new schemas with the existing JSON-Schema-object-root
|
|
419
|
+
and array-items-present invariants. Builds on the eleven
|
|
420
|
+
v5.0.0 tools (`count_objects`, `get_object`, `get_objects`,
|
|
421
|
+
`get_sample_objects`, `distinct`, `group_by`, `group_by_date`,
|
|
422
|
+
`list_tools`, `get_all_schemas`, `get_schema`, `query_class`).
|
|
423
|
+
(`lib/parse/agent/tools.rb`)
|
|
424
|
+
|
|
425
|
+
#### Caching — tenant-aware namespacing
|
|
426
|
+
|
|
427
|
+
- **NEW**: `Parse.with_cache_tenant(scope) { … }` sets an ambient
|
|
428
|
+
cache-tenant scope for the duration of the block;
|
|
429
|
+
`Parse.current_cache_tenant` reads it. When set, the
|
|
430
|
+
`Parse::Middleware::Caching` middleware composes the tenant
|
|
431
|
+
into the cache key as `<base-namespace>:T:<tenant>:…` so a
|
|
432
|
+
multi-tenant Parse application sharing one Redis (or any
|
|
433
|
+
Moneta-backed cache) gets per-tenant key isolation without
|
|
434
|
+
per-tenant `Parse::Client.new` plumbing. A SCAN-delete over
|
|
435
|
+
`<base-namespace>:T:<tenant>:*` evicts exactly one tenant
|
|
436
|
+
cleanly; the existing `<base-namespace>:*` SCAN still evicts
|
|
437
|
+
the whole app. The `T:` discriminator is unambiguously
|
|
438
|
+
distinguishable from session-token hex prefixes (32-char hex)
|
|
439
|
+
and `mk:`, so legacy cache entries written before this feature
|
|
440
|
+
cannot re-hydrate into a tenanted request and vice versa.
|
|
441
|
+
Fiber-local — composes safely with `async` and concurrent web
|
|
442
|
+
frameworks; restored on block exit even when the block raises,
|
|
443
|
+
even when the owning Thread is killed mid-block (`Thread#kill`
|
|
444
|
+
runs `ensure` clauses, which matters for Puma's recycled thread
|
|
445
|
+
pool). Scope set in Fiber A is NOT visible to a concurrently-
|
|
446
|
+
running Fiber B; scope set in Thread A is NOT visible to Thread
|
|
447
|
+
B or the main thread — both explicitly tested.
|
|
448
|
+
AS::N payload (`parse.cache.{hit,miss,store,delete,error}`)
|
|
449
|
+
carries `:cache_tenant` so subscribers can budget cache
|
|
450
|
+
performance per tenant. Strictly a key-namespacing mechanism —
|
|
451
|
+
no access-control semantics; tenant isolation at the data
|
|
452
|
+
layer is the job of `agent_tenant_scope` and ACL/CLP.
|
|
453
|
+
(`lib/parse/stack.rb`, `lib/parse/client/caching.rb`)
|
|
454
|
+
|
|
455
|
+
#### Image embedding — `embed_image` DSL + Voyage multimodal-3 (URL-only)
|
|
456
|
+
|
|
457
|
+
The setup order is **(1) `Parse::Embeddings.allowed_image_hosts = […]` →
|
|
458
|
+
(2) `Parse::Embeddings.trust_provider_url_fetch = "PROVIDER_EGRESS_VERIFIED"`
|
|
459
|
+
→ (3) declare `embed_image` on the model**. Skipping the allowlist or
|
|
460
|
+
the sentinel raises a typed error from the validator at save time;
|
|
461
|
+
each error message tells the operator which prerequisite is missing.
|
|
462
|
+
|
|
463
|
+
- **NEW**: `Parse::Embeddings::Cohere#embed_image(sources, input_type:, allow_insecure:)`
|
|
464
|
+
routes image URLs through Cohere's `/v2/embed` multimodal endpoint
|
|
465
|
+
for the `embed-v4.0` model (1536 native dim, Matryoshka-capable;
|
|
466
|
+
shares vector space with the text-input path on the same model).
|
|
467
|
+
Wire shape uses OpenAI-style nested
|
|
468
|
+
`{ type: "image_url", image_url: { url: ... } }` content rows —
|
|
469
|
+
different from {Voyage#embed_image}'s flat-String form, identical
|
|
470
|
+
high-level SDK contract (caller passes `Array<String>` URLs).
|
|
471
|
+
Refuses v3 models (text-only) with `BadRequestError` before any
|
|
472
|
+
network call; guards oversized batches (>96 per Cohere docs);
|
|
473
|
+
validates every URL up-front via
|
|
474
|
+
`Parse::Embeddings.validate_image_url!`. Internal
|
|
475
|
+
`Cohere#post_embeddings` grows a `path:` kwarg so the text path
|
|
476
|
+
continues to use `/v1/embed` while images route to `/v2/embed`.
|
|
477
|
+
(`lib/parse/embeddings/cohere.rb`)
|
|
478
|
+
- **NEW**: `Parse::Embeddings::Voyage#embed_image(sources, input_type:, allow_insecure:)`
|
|
479
|
+
routes image URLs through Voyage's `/v1/multimodalembeddings`
|
|
480
|
+
endpoint for the `voyage-multimodal-3` model (1024-dim, shares
|
|
481
|
+
vector space with the text-input path that already shipped). The
|
|
482
|
+
SDK does NOT download image bytes — URL-only is the v5.1 path
|
|
483
|
+
(bytes-fetch with MIME-sniff + EXIF stripping is the v5.3 path).
|
|
484
|
+
Calling `embed_image` on a text-only model raises a clear
|
|
485
|
+
`BadRequestError` before any network call. The provider reports
|
|
486
|
+
`modalities == %i[text image]` for the multimodal model and
|
|
487
|
+
`[:text]` for text-only models. (`lib/parse/embeddings/voyage.rb`)
|
|
488
|
+
- **NEW**: `Parse::Embeddings.validate_image_url!(url, allow_insecure:)`
|
|
489
|
+
is the canonical URL validator used by every `embed_image` path.
|
|
490
|
+
Layered checks, ordered cheap-first: (1) sentinel-gated
|
|
491
|
+
`trust_provider_url_fetch` opt-in must be set; (2) URL parses as
|
|
492
|
+
`https://` (or `http://` with `allow_insecure: true`, for local
|
|
493
|
+
dev only); (3) no userinfo; (4) host extracted via `uri.hostname`
|
|
494
|
+
so IPv6 literals are unbracketed and compare uniformly; (5) the
|
|
495
|
+
host is not an obfuscated IP form (`0x7f.0.0.1`, `127.1`,
|
|
496
|
+
`2130706433` — all rejected with `:host_blocked` BEFORE reaching
|
|
497
|
+
the resolver to keep operator logs honest about the failure mode);
|
|
498
|
+
(6) host matches `Parse::Embeddings.allowed_image_hosts` (string
|
|
499
|
+
match, no syscall — runs before the resolver hop so non-allowlisted
|
|
500
|
+
hosts can't amplify DNS traffic); (7) port in
|
|
501
|
+
`Parse::File.allowed_remote_ports`; (8) host resolves only to
|
|
502
|
+
addresses outside `Parse::File::BLOCKED_CIDRS` — delegated to
|
|
503
|
+
`Parse::File.assert_host_allowed!` so the SSRF mechanism is
|
|
504
|
+
shared, not parallelized. Returns the canonicalized URL String so
|
|
505
|
+
callers store/forward exactly what was validated. Failures raise
|
|
506
|
+
`Parse::Embeddings::InvalidImageURL` carrying a `:reason` Symbol
|
|
507
|
+
(`:scheme`, `:port`, `:userinfo`, `:host_blocked`,
|
|
508
|
+
`:host_not_allowlisted`, `:parse`); sentinel-off raises
|
|
509
|
+
`Parse::Embeddings::ConfirmationRequired`.
|
|
510
|
+
(`lib/parse/embeddings.rb`)
|
|
511
|
+
- **NEW**: `Parse::Embeddings.trust_provider_url_fetch=` sentinel-
|
|
512
|
+
gated opt-in for forwarding image URLs to embedding providers.
|
|
513
|
+
Assigning the exact frozen String `"PROVIDER_EGRESS_VERIFIED"`
|
|
514
|
+
unlocks; any other value (`true`, `"true"`, `1`, a non-matching
|
|
515
|
+
String) raises `Parse::Embeddings::ConfirmationRequired`. Mirrors
|
|
516
|
+
the `acl: :off` sentinel pattern — an operator unintentionally
|
|
517
|
+
flipping the gate via `ENV` interpolation is refused, making
|
|
518
|
+
accidental enablement impossible. Threat model: image-URL
|
|
519
|
+
forwarding hands an attacker-controlled URL (chat input, agent
|
|
520
|
+
tool argument, user-submitted document field) to a third-party
|
|
521
|
+
provider that will then issue an HTTP request from its own
|
|
522
|
+
network. Even with the CIDR / port / host allowlist enforced at
|
|
523
|
+
SDK-validation time, the provider's actual fetch happens later
|
|
524
|
+
(DNS-rebinding window) and can follow redirects the SDK never
|
|
525
|
+
saw — operators must consciously acknowledge the residual risk.
|
|
526
|
+
(`lib/parse/embeddings.rb`)
|
|
527
|
+
- **NEW**: `Parse::Embeddings.allowed_image_hosts=` allowlist
|
|
528
|
+
defining which CDN hostnames `validate_image_url!` will accept.
|
|
529
|
+
Entries beginning with `.` match suffixes (`.cloudfront.net`
|
|
530
|
+
matches `foo.cloudfront.net` and `cloudfront.net`); entries
|
|
531
|
+
without a leading `.` are exact. **Empty allowlist denies every
|
|
532
|
+
host** — opposite default from `Parse::File.allowed_remote_hosts`
|
|
533
|
+
(where empty means "any public host"). The asymmetry is
|
|
534
|
+
deliberate: image URLs that reach this validator typically
|
|
535
|
+
originate from attacker-controlled inputs, so opening the
|
|
536
|
+
surface requires an explicit operator declaration of which
|
|
537
|
+
CDNs are trusted. Frozen after assignment, case-insensitive
|
|
538
|
+
matching, reset by `Parse::Embeddings.reset!`.
|
|
539
|
+
(`lib/parse/embeddings.rb`)
|
|
540
|
+
- **NEW**: `embed_image source_field, into: :vector_property, input_type:
|
|
541
|
+
:search_document, digest_field: nil, allow_insecure: false` class
|
|
542
|
+
macro on `Parse::Object` subclasses. Mirrors `embed` but for
|
|
543
|
+
`:file`-typed sources. The source
|
|
544
|
+
property must be `:file` (text sources go through `embed`); the
|
|
545
|
+
target must be a declared `:vector` property with `provider:`
|
|
546
|
+
metadata. On `before_save`: extracts the file's URL, runs it
|
|
547
|
+
through `validate_image_url!`, and calls `Provider#embed_image`.
|
|
548
|
+
**Digest is the SHA-256 of the URL String, not the file bytes** —
|
|
549
|
+
replacing the `Parse::File` with one pointing at a different URL
|
|
550
|
+
re-embeds; resaving the same URL is a no-op (zero provider calls).
|
|
551
|
+
Cloud-stored Parse files have stable URLs unless overwritten, so
|
|
552
|
+
this matches typical upload behavior. If you mutate bytes at the
|
|
553
|
+
same URL (PUT-replace on S3 without renaming), null the digest
|
|
554
|
+
field to force re-embed. Reuses the existing `EmbedManaged` writer
|
|
555
|
+
guard, before_save registration, and protected-field semantics —
|
|
556
|
+
direct assignment to the managed vector raises `ProtectedFieldError`
|
|
557
|
+
as with text `embed`. (`lib/parse/model/core/embed_managed.rb`)
|
|
558
|
+
- **NEW**: `Parse::Core::EmbedManaged::EmbedDirective` gains
|
|
559
|
+
`modality:` (`nil`/`:text` for `embed`, `:image` for `embed_image`)
|
|
560
|
+
and `allow_insecure:` fields. `recompute_embedding!` dispatches on
|
|
561
|
+
modality, calling either `embed_text` or `embed_image`. The
|
|
562
|
+
source-input builder splits into `build_source_text` (existing,
|
|
563
|
+
concatenates text fields) and the new image-URL path (extracts
|
|
564
|
+
`file.url` and returns it raw — validation runs once, inside the
|
|
565
|
+
provider's `embed_image` call, to avoid double-resolving every
|
|
566
|
+
URL through DNS). Backwards compatible — every existing `embed`
|
|
567
|
+
directive continues to use the text path with no behavior change.
|
|
568
|
+
(`lib/parse/model/core/embed_managed.rb`)
|
|
569
|
+
- **CHANGED**: Base `Parse::Embeddings::Provider#embed_image` signature is
|
|
570
|
+
now `(sources, input_type:, allow_insecure: false, **opts)`.
|
|
571
|
+
`allow_insecure:` is documented as a contract kwarg —
|
|
572
|
+
`EmbedManaged.call_provider` unconditionally forwards it from the
|
|
573
|
+
directive, so future provider overrides must accept it (explicitly
|
|
574
|
+
or via `**opts`) or the managed-embedding save path will raise
|
|
575
|
+
`ArgumentError: unknown keyword`. Existing `Voyage#embed_image`
|
|
576
|
+
already accepts `allow_insecure:` explicitly. No other built-in
|
|
577
|
+
provider overrides `embed_image` yet, so this is a forward-compat
|
|
578
|
+
contract, not a breaking change. (`lib/parse/embeddings/provider.rb`)
|
|
579
|
+
- **NEW**: Voyage `embed_image` refuses oversized batches
|
|
580
|
+
(`sources.length > @embed_batch_size`, default 128) before any
|
|
581
|
+
validation or network call, with a clear "split and retry" error.
|
|
582
|
+
The text path goes through `embed_text_batched` which chunks
|
|
583
|
+
automatically; the image path has no chunker in v5.1, so a
|
|
584
|
+
direct-API caller passing 200 URLs gets a typed error instead of
|
|
585
|
+
a silent 400 from Voyage. (`lib/parse/embeddings/voyage.rb`)
|
|
586
|
+
- **NEW**: Integration coverage for the image-embedding save
|
|
587
|
+
round-trip — `test/lib/parse/embed_managed_image_integration_test.rb`
|
|
588
|
+
exercises Parse::File upload to the Docker Parse Server, the
|
|
589
|
+
before_save → validate → provider → vector-persist path, idempotent
|
|
590
|
+
no-op on unchanged URL, re-embed on file reassignment with a
|
|
591
|
+
different URL, the writer guard against a live server, and clean
|
|
592
|
+
save abort (no half-written record) when the sentinel is unset
|
|
593
|
+
or the URL is not in the allowlist. Unit coverage in
|
|
594
|
+
`test/lib/parse/embeddings_image_url_validation_test.rb` (36
|
|
595
|
+
cases: sentinel gate, allowlist semantics, every validator
|
|
596
|
+
failure mode including obfuscated-IP forms and the
|
|
597
|
+
allowlist-before-resolve ordering),
|
|
598
|
+
`test/lib/parse/embeddings_voyage_image_test.rb` (15 cases:
|
|
599
|
+
multimodal-model gating, wire envelope, canonicalized-URL
|
|
600
|
+
forwarding, allow_insecure precedence, batch-size guard),
|
|
601
|
+
`test/lib/parse/embeddings_cohere_image_test.rb` (16 cases:
|
|
602
|
+
parallel coverage for Cohere `embed-v4.0` plus the nested
|
|
603
|
+
`image_url: { url: }` envelope assertion, `/v2/embed` endpoint
|
|
604
|
+
routing, and an AS::N billed-input-tokens passthrough),
|
|
605
|
+
`test/lib/parse/embed_managed_image_test.rb` (24 cases:
|
|
606
|
+
declaration validation including `:file`-only source check,
|
|
607
|
+
digest semantics, writer guard, security wiring,
|
|
608
|
+
`embed` + `embed_image` co-declaration on the same record).
|
|
609
|
+
|
|
610
|
+
#### Client setup fixes — `Parse.setup` and `live_query_url`
|
|
611
|
+
|
|
612
|
+
- **FIXED**: `Parse.setup` (the module-level helper) silently no-op'd on every
|
|
613
|
+
call after the first. The implementation routed through `Parse::Client.new`,
|
|
614
|
+
whose constructor registers itself with `Parse::Client.clients[:default] ||= self`
|
|
615
|
+
— so once a default was set, subsequent `Parse.setup` invocations built a
|
|
616
|
+
new client, ran all the Faraday and LiveQuery configuration, and then
|
|
617
|
+
threw the result away because `||=` would not overwrite. The class-level
|
|
618
|
+
`Parse::Client.setup` uses `=` and did overwrite, so the two entry points
|
|
619
|
+
behaved differently despite being documented as equivalent. `Parse.setup`
|
|
620
|
+
now delegates to `Parse::Client.setup`, so re-configuring the default
|
|
621
|
+
client (Rake tasks that need to point at a prod URL after a development
|
|
622
|
+
initializer ran, multi-tenant boot, test isolation) works without
|
|
623
|
+
manually clearing `Parse::Client.clients[:default]` first. The `||=`
|
|
624
|
+
guard in `Parse::Client#initialize` is preserved so ad-hoc
|
|
625
|
+
`Parse::Client.new(...)` for secondary clients still does not hijack
|
|
626
|
+
the `:default` slot. (`lib/parse/client.rb`)
|
|
627
|
+
- **FIXED**: Passing `live_query_url:` (or any `live_query: {...}` options)
|
|
628
|
+
to `Parse.setup` / `Parse::Client.new` raised
|
|
629
|
+
`ArgumentError: wrong number of arguments (given 1, expected 0)`.
|
|
630
|
+
`Parse::Client#configure_live_query` was calling
|
|
631
|
+
`Parse::LiveQuery.configure(url:, application_id:, client_key:, master_key:, **opts)`
|
|
632
|
+
with keyword arguments, but `Parse::LiveQuery.configure` takes no
|
|
633
|
+
arguments and only yields a configuration block. The configuration is
|
|
634
|
+
now applied through the block form, assigning each option via the
|
|
635
|
+
`Parse::LiveQuery::Configuration` setters. Boot-time LiveQuery
|
|
636
|
+
configuration via `Parse.setup(live_query_url: ...)` now matches the
|
|
637
|
+
documented behavior. (`lib/parse/client.rb`)
|
|
638
|
+
- **FIXED**: `live_query_url:` (top-level) now correctly wins over
|
|
639
|
+
`live_query: { url: ... }` when both are passed. The first pass of
|
|
640
|
+
the block-form rewrite iterated `live_query_opts` after applying the
|
|
641
|
+
resolved URL, so the loop would re-write `config.url` from the hash
|
|
642
|
+
and silently invert the documented precedence. The hash's `:url`
|
|
643
|
+
key is now skipped in the loop and the resolved URL is applied last.
|
|
644
|
+
(`lib/parse/client.rb`)
|
|
645
|
+
- **NEW**: `Parse::Client#configure_live_query` now refuses an explicit
|
|
646
|
+
`ws://` URL against a non-loopback host unless
|
|
647
|
+
`live_query: { allow_insecure: true }` is also passed. The downstream
|
|
648
|
+
`Parse::LiveQuery::Client#derive_websocket_url` path already enforced
|
|
649
|
+
this for URLs derived from a Parse Server `http://` URL, but an
|
|
650
|
+
explicit `live_query: { url: "ws://prod-host" }` (or top-level
|
|
651
|
+
`live_query_url: "ws://prod-host"` / `PARSE_LIVE_QUERY_URL=ws://...`)
|
|
652
|
+
bypassed the check. The connect frame carries the master key and any
|
|
653
|
+
session token in cleartext on a non-TLS socket, so the explicit-URL
|
|
654
|
+
path now applies the same guard with the same `LOOPBACK_HOSTS`
|
|
655
|
+
exemption (`localhost`, `127.0.0.1`, `::1`, `[::1]`, `0.0.0.0`) and
|
|
656
|
+
the same `allow_insecure` escape hatch. (`lib/parse/client.rb`)
|
|
657
|
+
- **NEW**: `Parse::Client#configure_live_query` now warns on unknown
|
|
658
|
+
`live_query: { ... }` keys instead of silently dropping them. The
|
|
659
|
+
pre-fix kwargs form raised `ArgumentError: unknown keyword` on a
|
|
660
|
+
typo, so e.g. `live_query: { ssl_min_versoin: :TLSv1_3 }` would have
|
|
661
|
+
failed loudly; the block-form rewrite silently dropped them, leaving
|
|
662
|
+
the operator's intent invisible. The warning enumerates the unknown
|
|
663
|
+
keys and lists the valid setter surface; the call still proceeds so
|
|
664
|
+
this is a soft failure, not a hard one. (`lib/parse/client.rb`)
|
|
665
|
+
|
|
666
|
+
#### `Parse::Installation` and `Parse::User` — `user` pointer association
|
|
667
|
+
|
|
668
|
+
- **NEW**: `Parse::Installation` now declares `belongs_to :user`,
|
|
669
|
+
exposing the `user` pointer that Parse Server populates on
|
|
670
|
+
`_Installation` when the row is created or updated by an
|
|
671
|
+
authenticated client. The association is purely ergonomics — read
|
|
672
|
+
`installation.user` to find which user a device is currently signed
|
|
673
|
+
in as, write `installation.user = user; installation.save` from a
|
|
674
|
+
master-key context for targeted push grouping. The YARD prose calls
|
|
675
|
+
out the existing caveat from the class-level CLP notes: the `user`
|
|
676
|
+
pointer is not a reliable owner identity (devices outlive sessions
|
|
677
|
+
and can change users), so it should not be used for ACL or CLP
|
|
678
|
+
scoping. (`lib/parse/model/classes/installation.rb`)
|
|
679
|
+
- **NEW**: `Parse::User` now declares `has_many :installations, as: :installation`
|
|
680
|
+
as the query-form symmetric association. Each access issues a
|
|
681
|
+
`find` against `_Installation` for `where(user: self)`. Because
|
|
682
|
+
Parse Server hardcodes `_Installation` `find` to master-key-only at
|
|
683
|
+
the REST layer, this association only returns rows under a
|
|
684
|
+
master-key client; sessioned / sessionless clients get an empty
|
|
685
|
+
array (or fail closed under scoped agents). Useful for targeted
|
|
686
|
+
push — finding every device a user is signed into. The YARD
|
|
687
|
+
documents both the master-key requirement and the owner-identity
|
|
688
|
+
caveat. (`lib/parse/model/classes/user.rb`)
|
|
689
|
+
|
|
690
|
+
#### `_User` field-visibility DSL — `master_only_fields` and `self_visible_fields`
|
|
691
|
+
|
|
692
|
+
- **NEW**: `Parse::User.master_only_fields(*fields)` declares fields that
|
|
693
|
+
should be hidden from query/get responses for every non-master caller,
|
|
694
|
+
including the owning user themselves. Useful for admin-only metadata
|
|
695
|
+
living on `_User` (e.g. internal scoring, moderation notes). Expands
|
|
696
|
+
internally to a `protect_fields "*"` entry. Effective only when Parse
|
|
697
|
+
Server is started with `protectedFieldsOwnerExempt: false` — the
|
|
698
|
+
default `true` exempts the owning user from every `protectedFields`
|
|
699
|
+
rule on `_User` and would silently negate the protection. The SDK
|
|
700
|
+
documents the dependency on the helper's YARD and surfaces it in the
|
|
701
|
+
one-time advisory described below. (`lib/parse/model/classes/user.rb`)
|
|
702
|
+
- **NEW**: `Parse::User.self_visible_fields(*fields, via: :self)`
|
|
703
|
+
declares fields that should be hidden from public, role, and other-
|
|
704
|
+
user callers but visible to the owning user on their own row.
|
|
705
|
+
Expands internally to a `protect_fields "*"` plus a
|
|
706
|
+
`protect_fields "userField:<via>"` pair whose intersection resolves
|
|
707
|
+
to "owner sees the field, nobody else does". Requires (a) Parse
|
|
708
|
+
Server option `protectedFieldsOwnerExempt: false` and (b) a
|
|
709
|
+
self-pointer field on `_User` (default field name `:self`) populated
|
|
710
|
+
by a `beforeSave('_User')` Cloud Code trigger. The SDK cannot
|
|
711
|
+
install either — both are server-side configuration — and the helper
|
|
712
|
+
documents both prerequisites inline. (`lib/parse/model/classes/user.rb`)
|
|
713
|
+
- **NEW**: One-time process-scoped advisories when the new DSL helpers
|
|
714
|
+
or raw `protect_fields` are called on `Parse::User`:
|
|
715
|
+
- First invocation of `master_only_fields` or `self_visible_fields`
|
|
716
|
+
surfaces the `protectedFieldsOwnerExempt: false` server-option
|
|
717
|
+
prerequisite. With the default `true`, the owning user is silently
|
|
718
|
+
exempted from every `protectedFields` rule on `_User`, so a field
|
|
719
|
+
declared master-only would still be visible to the user themselves
|
|
720
|
+
on their own row. The SDK cannot introspect Parse Server's startup
|
|
721
|
+
options, so the advisory fires at class declaration so it's
|
|
722
|
+
surfaceable before deploy.
|
|
723
|
+
- First invocation of `self_visible_fields` also surfaces the
|
|
724
|
+
self-pointer prerequisite: the `via:` field has to exist on `_User`
|
|
725
|
+
and be populated by a `beforeSave('_User')` Cloud Code trigger,
|
|
726
|
+
AND pre-existing user rows need a one-shot backfill before the
|
|
727
|
+
`userField:<via>` group matches them.
|
|
728
|
+
- Direct calls to `protect_fields` on `Parse::User` outside the
|
|
729
|
+
helpers point the caller at `master_only_fields` /
|
|
730
|
+
`self_visible_fields` plus the same `protectedFieldsOwnerExempt`
|
|
731
|
+
reminder. Behavior is otherwise unchanged. The helpers themselves
|
|
732
|
+
set a class-level bypass flag so the raw-protect_fields advisory
|
|
733
|
+
does not double-fire. Internal SDK callers (e.g. the
|
|
734
|
+
`parse_reference` embedded-reference DSL auto-install in
|
|
735
|
+
`lib/parse/model/core/parse_reference.rb`) also bypass the
|
|
736
|
+
raw-protect_fields advisory so gem boot stays quiet on apps that
|
|
737
|
+
use embedded references on `_User`. (`lib/parse/model/classes/user.rb`,
|
|
738
|
+
`lib/parse/model/core/parse_reference.rb`)
|
|
739
|
+
|
|
740
|
+
#### `_Installation` CLP advisory
|
|
741
|
+
|
|
742
|
+
- **NEW**: One-time process-scoped advisory emitted from
|
|
743
|
+
`Parse::Installation` when any of `set_clp`, `set_class_access`,
|
|
744
|
+
`set_read_user_fields`, or `set_write_user_fields`
|
|
745
|
+
is invoked on the class. Parse Server hardcodes `find` and `delete`
|
|
746
|
+
on `_Installation` to master-key-only at the REST layer
|
|
747
|
+
(`SharedRest.js`), and gates `create` / `update` on the
|
|
748
|
+
`X-Parse-Installation-Id` header rather than CLP — so most CLP
|
|
749
|
+
changes on `_Installation` either do nothing or break the SDK's
|
|
750
|
+
device-registration flow. The advisory enumerates which operations
|
|
751
|
+
CLP actually controls on this class (`get`, `count`, `addField`,
|
|
752
|
+
`protectedFields`) and points the caller at the
|
|
753
|
+
`beforeSave('_Installation')` Cloud Code pattern for login-required
|
|
754
|
+
write policy. Behavior is otherwise unchanged. (`lib/parse/model/classes/installation.rb`)
|
|
755
|
+
|
|
756
|
+
#### Documentation
|
|
757
|
+
|
|
758
|
+
- **NEW**: `docs/acl_clp_guide.md` is the canonical reference for ACL,
|
|
759
|
+
CLP, `protectedFields`, role hierarchy, and field-guard write
|
|
760
|
+
protection across parse-stack-next. Covers the five enforcement
|
|
761
|
+
layers (CLP, ACL, `protectedFields`, field guards, master-key
|
|
762
|
+
bypass); the system-class CLP matrix (which classes actually honor
|
|
763
|
+
CLP versus the ones hardcoded master-key-only at the REST layer:
|
|
764
|
+
`_JobStatus`, `_PushStatus`, `_Hooks`, `_GlobalConfig`,
|
|
765
|
+
`_GraphQLConfig`, `_JobSchedule`, `_Audience`, `_Idempotency`,
|
|
766
|
+
`_Join:*`); the `_Installation` hardcoded asymmetry; the `_User`
|
|
767
|
+
field-visibility recipe with `protectedFieldsOwnerExempt` and the
|
|
768
|
+
self-pointer pattern; role hierarchy direction (the
|
|
769
|
+
`inherits_capabilities_from!` vs `add_child_role` distinction); the
|
|
770
|
+
field-guard modes and their webhook dependency; the REST-aggregate
|
|
771
|
+
vs `Parse::MongoDB.aggregate` enforcement asymmetry (REST aggregate
|
|
772
|
+
is master-key-only and enforces NEITHER CLP nor ACL nor
|
|
773
|
+
`protectedFields`); Atlas Search inheriting SDK-side enforcement
|
|
774
|
+
through the mongo-direct path; and a pitfalls section. (`docs/acl_clp_guide.md`)
|
|
775
|
+
- **CHANGED**: `docs/client_sdk_guide.md` §4 now opens with a banner
|
|
776
|
+
pointing readers at the new comprehensive ACL/CLP guide. Sections
|
|
777
|
+
added in this release for `_Installation` CLP semantics (§6.3),
|
|
778
|
+
the full system-class CLP matrix (§6.5), and the `_User` field-
|
|
779
|
+
visibility recipe with the intersection-resolution table (§6.6)
|
|
780
|
+
remain in the SDK guide as a client-mode quickstart. (`docs/client_sdk_guide.md`)
|
|
781
|
+
- **CHANGED**: YARD `@note` on `Parse::JobStatus`, `Parse::PushStatus`,
|
|
782
|
+
`Parse::Audience`, and `Parse::JobSchedule` now states that the
|
|
783
|
+
class is hardcoded master-key-only at Parse Server's REST layer and
|
|
784
|
+
that CLP changes are ignored. YARD on `Parse::Session` documents the
|
|
785
|
+
non-master find auto-scoping to `user = <current user>`, so CLP
|
|
786
|
+
cannot grant cross-user session visibility. YARD on
|
|
787
|
+
`Parse::Installation` carries the full operation-by-operation CLP
|
|
788
|
+
effectiveness table. (`lib/parse/model/classes/job_status.rb`,
|
|
789
|
+
`lib/parse/model/classes/push_status.rb`,
|
|
790
|
+
`lib/parse/model/classes/audience.rb`,
|
|
791
|
+
`lib/parse/model/classes/job_schedule.rb`,
|
|
792
|
+
`lib/parse/model/classes/session.rb`,
|
|
793
|
+
`lib/parse/model/classes/installation.rb`)
|
|
794
|
+
|
|
3
795
|
### 5.0.1
|
|
4
796
|
|
|
5
797
|
#### Redis cache wrapper compatibility with `Parse::CreateLock`
|