parse-stack-next 5.0.0 → 5.0.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.
- checksums.yaml +4 -4
- data/.bundle/config +2 -0
- data/.github/workflows/release.yml +32 -0
- data/.vscode/settings.json +3 -0
- data/CHANGELOG.md +10 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/lib/parse/cache/pool.rb +15 -0
- data/lib/parse/cache/redis.rb +61 -2
- data/lib/parse/model/core/create_lock.rb +14 -2
- data/lib/parse/stack/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fa424fed1ae3d44678d301ca6fca95d383deda63edfcc645f1bfaafd030a24f3
|
|
4
|
+
data.tar.gz: b96f56fea9dbbeb76650736bee52be70546c02180be8ead3ef0ef392c041884f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c176aa65bd1694c281f2566e69a554ee2d80d090e69969ee5f26e093d11558bbbcbb6da052b9c56e3b9fe95e9c488657b570bb500bf0e5ed3b3cbd72e9f3da8
|
|
7
|
+
data.tar.gz: 9895c24fdb645b06a1cb799da8ded92dc2a1cb5647d34ea5b0f0fd5bef849ff6768ec16098cb70c7b33a5a53f1773ea5487055f8ad1766ffecfe83a9964548d3
|
data/.bundle/config
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Release Gem
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
push:
|
|
10
|
+
name: Push to RubyGems
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: rubygems
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write
|
|
15
|
+
contents: read
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
# bundler-cache (next step) writes --local path / --local
|
|
20
|
+
# deployment into the tracked `.bundle/config`, which would leave
|
|
21
|
+
# the working tree dirty and trip `rake release`'s
|
|
22
|
+
# `release:guard_clean` check. Tell git to ignore worktree
|
|
23
|
+
# mutations to this file so guard_clean passes while still
|
|
24
|
+
# letting Bundler resolve vendor/bundle in the next step.
|
|
25
|
+
- run: git update-index --skip-worktree .bundle/config
|
|
26
|
+
|
|
27
|
+
- uses: ruby/setup-ruby@v1
|
|
28
|
+
with:
|
|
29
|
+
ruby-version: "3.4"
|
|
30
|
+
bundler-cache: true
|
|
31
|
+
|
|
32
|
+
- uses: rubygems/release-gem@v1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.0.1
|
|
4
|
+
|
|
5
|
+
#### Redis cache wrapper compatibility with `Parse::CreateLock`
|
|
6
|
+
|
|
7
|
+
- **FIXED**: `Parse::CreateLock.synchronize` (and therefore `first_or_create!` / `create_or_update!`) failed to acquire cross-process locks when the configured cache was a `Parse::Cache::Redis` wrapper. The lock implementation calls `store.create(key, owner, expires: ttl)` (Moneta's atomic SETNX), but the wrapper only forwarded `[]`, `key?`, `delete`, and `store` to the pooled Moneta backend. Every acquire raised `NoMethodError: undefined method 'create' for an instance of Parse::Cache::Redis`, which the lock caught, logged as `[Parse::CreateLock] acquire error (NoMethodError)`, and treated as contention — so the call spun on the polling loop until the wait budget elapsed and raised `Parse::CreateLockTimeoutError`. (`lib/parse/cache/redis.rb`, `lib/parse/cache/pool.rb`)
|
|
8
|
+
- **FIXED**: `Parse::CreateLock.degraded_store?` classified the `Parse::Cache::Redis` wrapper as a healthy cross-process store (the wrapper has no Moneta `.adapter` chain to walk and its class name does not match the `Memory`/`Null` heuristic), so the lock never fell back to the in-process `Mutex` path when `#create` was unavailable. The detector now special-cases the `Parse::Cache::Redis` wrapper and additionally treats any store that does not respond to `#create` as degraded, so older custom store implementations that pre-date this requirement degrade gracefully instead of timing out. (`lib/parse/model/core/create_lock.rb`)
|
|
9
|
+
- **NEW**: `Parse::Cache::Redis#create` and `Parse::Cache::Pool#create` forward atomic SETNX semantics to the pooled Moneta-Redis store. `#increment` is forwarded on both for Moneta surface parity so counter / rate-limit use cases work transparently through the pool. (`lib/parse/cache/redis.rb`, `lib/parse/cache/pool.rb`)
|
|
10
|
+
- **CHANGED**: The one-time `[Parse::CreateLock:SECURITY]` warning emitted when no `PARSE_STACK_LOCK_SECRET` is configured against a Redis-backed store now also documents the lock-pinning risk that arises when the response cache and lock store share a Redis DB. Without an HMAC secret the lock keys are a plain SHA256 digest of `(app_id, parse_class, principal, query_attrs)` — guessable for any caller who knows the schema — so an adversary with write access to `Parse.cache` can plant `parse-stack:foc:v1:<sha>` to suppress `first_or_create!` / `create_or_update!` for a tuple until TTL expiry. The warning now tells operators to either set `PARSE_STACK_LOCK_SECRET` or point `Parse.synchronize_create_store` at a separate Redis DB. (`lib/parse/model/core/create_lock.rb`)
|
|
11
|
+
- **NEW**: `Parse::Cache::Redis#clear(scope:)` accepts an explicit `scope:` namespace argument that SCAN-deletes `<scope>:*` regardless of how the wrapper was constructed. This is the targeted escape hatch for ops tooling and multi-tenant deployments where the wrapper was built without a configured `@namespace` but the caller still wants to evict a specific prefix without `FLUSHDB`-ing siblings (or wiping the `parse-stack:foc:v1:*` create-lock keys that live on the same DB). Trailing `:` in the input is stripped so `"tenant_x"` and `"tenant_x:"` are equivalent. The `scope:` argument is strictly validated and raises `ArgumentError` when it is not a `String`, is empty (or `":"` only), or contains Redis SCAN glob metacharacters (`*`, `?`, `[`, `]`, `\`) or a NUL byte — otherwise `scope: "*"` would expand the SCAN pattern and delete every key on the DB, defeating the whole point of keeping `flush_db!` as the explicit wide-blast-radius escape hatch. The no-argument form preserves the previous semantics — namespace-scoped SCAN-delete when `@namespace` is set, full `FLUSHDB` otherwise — so existing `Parse::Client#clear_cache!` callers are unaffected. (`lib/parse/cache/redis.rb`)
|
|
12
|
+
|
|
3
13
|
### 5.0.0
|
|
4
14
|
|
|
5
15
|
#### Client-mode `Parse::Agent`
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -175,7 +175,7 @@ result = Parse.call_function :myFunctionName, {param: value}
|
|
|
175
175
|
|
|
176
176
|
## Release History
|
|
177
177
|
|
|
178
|
-
**Current version: 5.0.
|
|
178
|
+
**Current version: 5.0.1** | **Ruby 3.2+ required**
|
|
179
179
|
|
|
180
180
|
The 5.0 highlights (vector search / RAG, pooled Redis cache, AS::N instrumentation, MCP transport hardening, GraphQL type generation) are summarized in the [What's new in 5.0](#whats-new-in-50) section above. Earlier releases are recorded below.
|
|
181
181
|
|
data/lib/parse/cache/pool.rb
CHANGED
|
@@ -51,6 +51,21 @@ module Parse
|
|
|
51
51
|
@pool.with { |store| store.store(key, value, options) }
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
+
# Atomic SETNX-style write. Required by `Parse::CreateLock` to acquire
|
|
55
|
+
# cross-process locks against Redis-backed stores. Forwards to the
|
|
56
|
+
# underlying Moneta store's `#create`, which returns `true` only if
|
|
57
|
+
# the key was absent and is now set.
|
|
58
|
+
def create(key, value, options = {})
|
|
59
|
+
@pool.with { |store| store.create(key, value, options) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Atomic counter increment. Forwarded for parity with Moneta so
|
|
63
|
+
# callers expecting the full Moneta surface (counters, rate limits)
|
|
64
|
+
# work transparently through the pool.
|
|
65
|
+
def increment(key, amount = 1, options = {})
|
|
66
|
+
@pool.with { |store| store.increment(key, amount, options) }
|
|
67
|
+
end
|
|
68
|
+
|
|
54
69
|
# Clear the underlying backend. Pooled Moneta stores all point at the
|
|
55
70
|
# same Redis DB, so a single checkout suffices — issuing `clear` on
|
|
56
71
|
# one connection flushes the DB for every connection.
|
data/lib/parse/cache/redis.rb
CHANGED
|
@@ -105,6 +105,19 @@ module Parse
|
|
|
105
105
|
@pool.store(key, value, options)
|
|
106
106
|
end
|
|
107
107
|
|
|
108
|
+
# Atomic SETNX. Required so `Parse::CreateLock` can acquire
|
|
109
|
+
# cross-process locks when this wrapper is the configured cache /
|
|
110
|
+
# `synchronize_create_store`. Returns `true` only when the key did
|
|
111
|
+
# not already exist.
|
|
112
|
+
def create(key, value, options = {})
|
|
113
|
+
@pool.create(key, value, options)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Atomic counter increment. Forwarded for Moneta surface parity.
|
|
117
|
+
def increment(key, amount = 1, options = {})
|
|
118
|
+
@pool.increment(key, amount, options)
|
|
119
|
+
end
|
|
120
|
+
|
|
108
121
|
# Clear cached entries belonging to this wrapper. Required for
|
|
109
122
|
# `Parse::Client#clear_cache!` compatibility.
|
|
110
123
|
#
|
|
@@ -115,8 +128,22 @@ module Parse
|
|
|
115
128
|
# the backing DB — same blast radius as previous versions, but
|
|
116
129
|
# only for unnamespaced deployments. To opt into the wide
|
|
117
130
|
# FLUSHDB explicitly (e.g. ops tooling), call {#flush_db!}.
|
|
118
|
-
|
|
119
|
-
|
|
131
|
+
#
|
|
132
|
+
# @param scope [String, nil] explicit namespace prefix to scan-delete.
|
|
133
|
+
# When provided, overrides the wrapper's configured `@namespace` and
|
|
134
|
+
# SCAN-deletes `<scope>:*` regardless of how the wrapper was built.
|
|
135
|
+
# This is the safe escape hatch for tenants that share a non-
|
|
136
|
+
# namespaced wrapper but still want to evict only their own keys
|
|
137
|
+
# without `FLUSHDB`-ing siblings (and without wiping
|
|
138
|
+
# `parse-stack:foc:v1:*` create-lock keys that live on the same DB).
|
|
139
|
+
# The scope must be a non-empty String; the trailing `:` is added
|
|
140
|
+
# automatically and any trailing `:` in the input is stripped so
|
|
141
|
+
# `"tenant_x"` and `"tenant_x:"` are equivalent.
|
|
142
|
+
def clear(scope: nil)
|
|
143
|
+
if scope
|
|
144
|
+
prefix = validate_scope!(scope)
|
|
145
|
+
delete_keys_matching!("#{prefix}:*")
|
|
146
|
+
elsif @namespace
|
|
120
147
|
delete_keys_matching!("#{@namespace}:*")
|
|
121
148
|
else
|
|
122
149
|
@pool.clear
|
|
@@ -185,6 +212,38 @@ module Parse
|
|
|
185
212
|
s = ns.to_s.chomp(":")
|
|
186
213
|
s.empty? ? nil : s
|
|
187
214
|
end
|
|
215
|
+
|
|
216
|
+
# Validate a caller-supplied `scope:` for `clear(scope:)`. Returns the
|
|
217
|
+
# normalized prefix or raises ArgumentError. We enforce:
|
|
218
|
+
#
|
|
219
|
+
# - must be a String (Symbol / Integer / nil would silently `.to_s`
|
|
220
|
+
# under `normalize_namespace` and expand the deletion target —
|
|
221
|
+
# `scope: 0` would clear `0:*`)
|
|
222
|
+
# - must be non-empty after trimming a trailing `:`
|
|
223
|
+
# - must not contain Redis SCAN glob metacharacters (`*`, `?`, `[`,
|
|
224
|
+
# `]`, `\`) — otherwise `scope: "*"` would SCAN-delete the whole
|
|
225
|
+
# DB, defeating the whole point of having `flush_db!` as the
|
|
226
|
+
# explicit wide-blast-radius escape hatch
|
|
227
|
+
# - must not contain a null byte (defense-in-depth against keys
|
|
228
|
+
# crafted to terminate early in some Redis client paths)
|
|
229
|
+
GLOB_METACHARS = /[\*\?\[\]\\\x00]/.freeze
|
|
230
|
+
private_constant :GLOB_METACHARS
|
|
231
|
+
|
|
232
|
+
def validate_scope!(scope)
|
|
233
|
+
unless scope.is_a?(String)
|
|
234
|
+
raise ArgumentError, "scope: must be a String (got #{scope.class})"
|
|
235
|
+
end
|
|
236
|
+
prefix = scope.chomp(":")
|
|
237
|
+
if prefix.empty?
|
|
238
|
+
raise ArgumentError, "scope: must be a non-empty namespace string"
|
|
239
|
+
end
|
|
240
|
+
if prefix.match?(GLOB_METACHARS)
|
|
241
|
+
raise ArgumentError,
|
|
242
|
+
"scope: must not contain Redis SCAN glob characters (*, ?, [, ], \\, or NUL); " \
|
|
243
|
+
"use flush_db! for a full-DB flush"
|
|
244
|
+
end
|
|
245
|
+
prefix
|
|
246
|
+
end
|
|
188
247
|
end
|
|
189
248
|
end
|
|
190
249
|
end
|
|
@@ -255,6 +255,13 @@ module Parse
|
|
|
255
255
|
|
|
256
256
|
def degraded_store?(store)
|
|
257
257
|
return true if store.nil?
|
|
258
|
+
# The Parse::Cache::Redis wrapper (and its Pool) are known
|
|
259
|
+
# cross-process stores even though they don't expose a Moneta
|
|
260
|
+
# `.adapter` chain to walk. Anything that can't forward `#create`
|
|
261
|
+
# cannot serve as a lock store, so fall back to the process-local
|
|
262
|
+
# path rather than spinning until timeout on NoMethodError.
|
|
263
|
+
return false if defined?(Parse::Cache::Redis) && store.is_a?(Parse::Cache::Redis)
|
|
264
|
+
return true unless store.respond_to?(:create)
|
|
258
265
|
bottom = walk_to_adapter(store)
|
|
259
266
|
return true if bottom.nil?
|
|
260
267
|
klass_name = bottom.class.name.to_s
|
|
@@ -366,8 +373,13 @@ module Parse
|
|
|
366
373
|
@plain_sha_warned = true
|
|
367
374
|
warn "[Parse::CreateLock:SECURITY] No PARSE_STACK_LOCK_SECRET configured and Redis-backed store detected. " \
|
|
368
375
|
"Falling back to plain SHA256 for lock-key derivation so cross-process locking actually works. " \
|
|
369
|
-
"
|
|
370
|
-
"
|
|
376
|
+
"Risks of running without an HMAC secret: (1) lock keys are deterministic and may expose query_attrs " \
|
|
377
|
+
"content via Redis MONITOR/snapshots; (2) when the response cache and the lock store share a Redis DB, " \
|
|
378
|
+
"any caller with write access to Parse.cache can plant a `parse-stack:foc:v1:<sha>` key under a guessable " \
|
|
379
|
+
"digest of (app_id, class, principal, query_attrs) and suppress first_or_create!/create_or_update! for " \
|
|
380
|
+
"that tuple until TTL expiry — a targeted DoS / create-pinning primitive. " \
|
|
381
|
+
"Set PARSE_STACK_LOCK_SECRET (or Parse.synchronize_create_secret = '…') to enable HMAC keying, or " \
|
|
382
|
+
"point Parse.synchronize_create_store at a separate Redis DB from the response cache."
|
|
371
383
|
end
|
|
372
384
|
|
|
373
385
|
def instrument(event, key, payload = {})
|
data/lib/parse/stack/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: parse-stack-next
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.0.
|
|
4
|
+
version: 5.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anthony Persaud
|
|
@@ -212,10 +212,12 @@ files:
|
|
|
212
212
|
- ".env.test"
|
|
213
213
|
- ".github/workflows/codeql.yml"
|
|
214
214
|
- ".github/workflows/docs.yml"
|
|
215
|
+
- ".github/workflows/release.yml"
|
|
215
216
|
- ".github/workflows/ruby.yml"
|
|
216
217
|
- ".gitignore"
|
|
217
218
|
- ".ruby-version"
|
|
218
219
|
- ".solargraph.yml"
|
|
220
|
+
- ".vscode/settings.json"
|
|
219
221
|
- CHANGELOG.md
|
|
220
222
|
- Gemfile
|
|
221
223
|
- Gemfile.lock
|