Conversation
PostgresDB joins JsonDB/SQLite/MongoDB/DynamoDB as a first-class
backend. Targets Neon, Aurora Serverless v2, and self-hosted PG 15+.
- jvspatial/db/postgres.py — asyncpg pool with auto-tuned sizing
(Lambda: 0/3, long-running: 2/10), session/transaction pooler modes,
schema-scoped tables, JSONB storage, vector column support, native
$near peeling, multi-tenant RLS via current_tenant contextvar,
transactional save/get/find/delete via PostgresTransaction, and
drop_deprecated_indexes for migrations.
- jvspatial/db/_postgres_translate.py — Mongo-style query → SQL
translator with positional-placeholder builder.
- jvspatial/db/factory.py — register "postgres"/"postgresql" with
env-driven defaults (DSN, pool sizes, pooler mode).
- jvspatial/db/transaction.py — backend-agnostic transaction_context.
- docker/ — local dev pgvector container + .env example.
- docs/md/postgres-guide.md, neon-deployment.md,
aurora-serverless-deployment.md, multi-tenant-rls.md,
cursor-pagination.md, schema-migrations.md, vector-store.md.
- tests/db/test_postgres_{unit,integration,translate}.py,
tests/db/test_cursor_pagination.py.
Companion changes pulled in:
- jvspatial/core/migrations.py + tests — record-level schema migrations.
- jvspatial/core/validate.py + tests — graph-shape validation.
- jvspatial/core/entities/walker_components/trail_store.py + tests —
pluggable walker trail persistence.
- jvspatial/api/auth/_session_store.py + tests — pluggable auth session
store (in-process / Redis) for multi-worker deployments.
- jvspatial/observability/tracing.py + tests — OTel span helpers.
- jvspatial/cli.py — `jvspatial migrate` CLI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Authlib-based OAuth 2.1 AS+RS layered on jvspatial's existing auth. RS256+JWKS access tokens, DCR, PRM/AS metadata, audience binding. Foundation for OAuth-secured MCP (M2 = Integral MCP endpoint, M3 = UI). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add OAuthSigningKey Object model and keys.py module: RSA-2048 keypair generation, persist/load via Object.find, idempotent ensure_signing_key(), and JWKS builder (public keys only). All three TDD tests pass. PyJWT 2.x to_jwk requires cryptography key object; PEM loaded via serialization.load_pem_public_key before conversion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add oauth_enabled, oauth_issuer_url, oauth_prefix, oauth_supported_scopes, oauth_dcr_enabled, oauth_access_token_ttl_minutes, oauth_code_ttl_seconds, and accept_oauth_bearer to AuthConfig. All default off/empty so existing apps are unaffected. Covered by two-test TDD suite in tests/api/auth/oauth/test_oauth_authconfig.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements OAuthClientAdapter wrapping OAuthClient so Authlib can validate redirect URIs, grant/response types, scope filtering, and token endpoint auth methods (public PKCE + confidential secret check). All three tests (redirect/grant checks, public auth method + scope filter, confidential secret check) pass; black/isort/flake8/mypy clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…io-bridged) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds RequiredS256CodeChallenge subclass that overrides the authorize-step hook to reject missing code_challenge (regardless of client auth method) and reject code_challenge_method=plain. Registers it in build_authorization_server instead of CodeChallenge(required=True), which only enforced PKCE for public clients (auth_method=="none"). Two regression tests added: confidential client without PKCE is rejected, and plain code_challenge_method is rejected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire _generate_refresh_token into JvSpatialJWTTokenGenerator so Authlib
emits an opaque rt_ token when client.check_grant_type("refresh_token")
is true; implement save_token to persist the plaintext (hashed) via
refresh_store.mint_refresh_token through a positional async shim
(_persist_refresh) compatible with call_async's positional-only contract.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-use code
Three hardening fixes from the M1b-2 spec §6:
1. scope∩permissions — `save_authorization_code` now reads `permissions`
from `request.user` (when the grant_user dict carries it) and stores
only the intersection of the client-allowed scope and the user's
permissions in `AuthorizationCode.scope`. Back-compat: callers that
pass `{"id":...}` without `permissions` keep the full client scope.
Because `create_token_response` reads scope from
`authorization_code.get_scope()`, the intersected scope propagates
to the token `scope` claim automatically with no generator changes.
2. nbf claim — `JvSpatialJWTTokenGenerator.get_extra_claims` overrides
the Authlib base (which returns `{}`) to inject `"nbf": int(time.time())`.
`access_token_generator` merges extra claims via `token_data.update(...)`,
so `nbf` lands in every issued JWT. super() is called first so any
future upstream additions are preserved.
3. atomic single-use — `query_authorization_code` calls `_consume_code`
(sets `consumed=True` + saves) *before* returning the `StoredAuthCode`
to the grant. A concurrent duplicate exchange sees `consumed=True` on
the first load and is rejected. `delete_authorization_code` remains
idempotent (Authlib still calls it after token issuance; the record is
already consumed at that point, which is harmless).
All 9 server-flow tests pass (incl. 2 new: scope-intersection+nbf,
back-compat no-permissions); all 24 OAuth tests pass.
…ckout) Move single-use enforcement from query_authorization_code (runs before redirect_uri + PKCE validation) to delete_authorization_code (runs only after all validation passes on the success path, per authlib 1.7.2 authorization_code.py:create_token_response). A request with a wrong code_verifier or wrong redirect_uri previously burned the code, locking out the legitimate client's retry. Now query_authorization_code is read-only; delete_authorization_code is the single-use point. Also corrects the atomicity docstring: single-use is serialized by the single-process anyio bridge, not a DB-level CAS; multi-worker deployment will require a conditional-update primitive (tracked for a later phase). Adds regression test test_failed_verifier_does_not_burn_code_for_retry proving a failed verifier does NOT lock out a correct retry, and confirms the existing single-use replay test still rejects a replayed code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements OAuth 2.1 §rotation family kill (review finding I-3). When a rotated/revoked refresh token is replayed, the entire rotation family is immediately revoked (including the live child token), and the request is rejected. Normal rotation is unaffected. - OAuthRefreshToken: +family_id field (shared across rotations of one grant) - refresh_store: mint_refresh_token gains family_id param; add find_any (lookup without is_active filter for replay detection) and revoke_family (kill all tokens in a family) - server.py: authenticate_refresh_token stashes rec.family_id on request._oauth_family_id; save_token forwards it to _persist_refresh so the rotated token inherits the family; fresh uuid4 family_id generated at first mint (auth-code path). Replay of revoked token triggers revoke_family.
Implements JvSpatialClientRegistrationEndpoint (open registration, MCP zero-config) + async_register_client on the authorization server. JSON body wired via BasicOAuth2Payload on request.payload before dispatching; public clients (auth_method=none) receive no client_secret in the 201 response and store no secret hash. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure builder function ``build_as_metadata`` constructs the RFC 8414 Authorization Server metadata document (issuer, endpoints, PKCE, grant types). Validated against ``authlib.oauth2.rfc8414.AuthorizationServerMetadata`` in the accompanying TDD test. Route to serve the document at ``/.well-known/oauth-authorization-server`` is deferred to M1b-3b.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-limit) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… key hook Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…known metadata-prefix bug) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… test build_as_metadata gains api_prefix kwarg (default "" for back-compat); the well-known handler now passes APIRoutes.PREFIX so token/registration/revocation/ authorization endpoints are advertised as https://as.example/api/oauth/… instead of missing the /api segment. register route pre-reads JSON body then builds a StarletteOAuth2Request with empty form to avoid double-read error. New test_oauth_http_flow.py covers both assertions over TestClient. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implement the real GET/POST /oauth/authorize flow, replacing the HTML placeholder. GET renders a consent page (client name + requested scopes + approve/deny form re-carrying the OAuth params); POST builds grant_user from the bearer-authenticated session user ONLY — its id plus effective permissions via get_effective_permissions(roles, permissions, role_permission_mapping) — and calls async_create_authorization_response so the authorization server intersects scope with those permissions. Deny redirects with error=access_denied. Permissions are never read from client/request input, making the scope∩permissions narrowing the trust boundary. - Thread get_current_user from the auth configurator into build_oauth_routers and Depends() it on both authorize routes (401 without bearer). - Add JvSpatialAuthorizationServer.async_get_consent_grant to validate the authorize request off-thread (anyio bridge) and surface the client/scope for rendering; invalid requests yield a 400 page (no open redirect). - Optional auth_config.oauth_consent_handler override; 503 when no get_current_user is wired. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add `oauth_dcr_rate_limit_per_minute` knob (default 12) to AuthConfig and wire a tight rate-limit override onto `/api/oauth/register` at OAuth-configure time — before `get_app()` builds the rate-limit middleware — so the MemoryRateLimitBackend caps DCR bursts with a 429. When the server operator has not enabled rate limiting globally, the flag is implicitly set to True so the override takes effect. Existing auth and OAuth tests (42) remain green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rect) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…earer) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds `build_prm()` and `www_authenticate_header()` to metadata.py. Mounts `/.well-known/oauth-protected-resource` on `well_known_router` (active only when `oauth_enabled=True`). Exempts the PRM path from the bearer-auth middleware unconditionally so FastAPI returns a natural 404 when OAuth is disabled, matching the existing AS-metadata + JWKS pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t_oauth_bearer) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… discovery) When `accept_oauth_bearer=True`, the middleware now attaches `WWW-Authenticate: Bearer resource_metadata="<issuer>/.well-known/oauth-protected-resource"` to every 401 response (missing bearer AND failed-auth paths), so MCP clients can auto-discover the Authorization Server per RFC 9728 §5.1 / RFC 6750 §3. Header is absent when `accept_oauth_bearer=False` (no change for non-OAuth servers). Import is lazy/guarded to avoid coupling when the oauth subpackage is unavailable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gin SPA When AuthConfig.oauth_authorize_login_redirect is set, an unauthenticated GET /oauth/authorize now 302-redirects the browser to that SPA login URL with the original OAuth query string appended, instead of returning the legacy 401 JSON. This makes the OAuth authorization-code browser flow completable. Empty (default) preserves the legacy 401 behavior. The GET handler uses a non-raising optional-user seam (_optional_current_user) that invokes get_current_user imperatively and converts its 401 to None, so it can branch on the unauthenticated case. POST /authorize and the authed-GET consent page are unchanged (still bearer-required). The redirect target is ONLY the trusted config base + the original query — never a request-derived host — so it cannot be turned into an open redirect. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dening Make the authorization-code scope grant secure-by-default once a scope ceiling is declared, while staying a strict no-op for callers that do not declare one. - build_authorization_server / JvSpatialAuthorizationServer gain an optional supported_scopes; stored as self._supported_scopes (defaults to []). routes.py wires auth_config.oauth_supported_scopes through. - save_authorization_code intersects the permission-narrowed scope against the supported set when non-empty (empty => skip, back-compat). - _intersect_scope: permissions is None => no narrowing (absent key, back-compat); '*' in permissions => authorize all requested scope (admin wildcard, fixes the inverted-admin footgun); else membership filter. A present-but-empty [] now narrows to nothing. - save_authorization_code no longer collapses []->None, so an explicit empty permission set denies all scope. BEHAVIOUR CHANGE (for CHANGELOG in M4-T6): a present-but-empty request.user['permissions'] == [] now grants NO scope (was: full requested scope). Absent key still preserves full scope. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When the authorization server declares a supported-scope ceiling (server._supported_scopes non-empty), the DCR endpoint now silently intersects a client's requested scope against that set in extract_client_metadata, so a client cannot self-register an unsupported or elevated scope. Unsupported tokens are dropped (RFC 7591 permits the AS to filter) rather than rejected, keeping zero-config MCP registration working. Filtering in extract_client_metadata (not save_client) keeps a single source of truth so both the persisted record and the echoed 201 scope reflect the filtered value. Back-compat: an empty/undeclared ceiling is a no-op — requested scope is persisted verbatim. This is defense-in-depth; the authorize-time ceiling in JvSpatialAuthCodeGrant.save_authorization_code (M4-T1) remains the primary bound on issued-token scope. scopes_supported is deliberately NOT advertised in get_server_metadata: Authlib's get_claims_options turns it into a hard issuperset validation that would 400 an out-of-set scope before our silent filter runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the blind `consumed = True; save()` in _consume_code with an atomic compare-and-swap (find_one_and_update predicated on consumed=False), mirroring the work_claim.py idiom. On a lost race (None result) the consume raises InvalidGrantError — the same error a replayed/consumed code produces via query_authorization_code — so the loser never gets a second token. Consume still runs after PKCE/redirect validation (failed verifier does not burn the code). Atomic on postgres (SELECT ... FOR UPDATE) and mongo (native findOneAndUpdate); best-effort on sqlite/json (no regression vs the prior blind write). Drop the consumed record from the GraphContext cache so a stale cached entry can't let a replay slip through. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7a — Drop the attacker-controlled User-Agent from the unauthenticated
rate-limit bucket key. The key is now the source IP alone (sha256-hashed):
folding in the UA let an attacker rotate the header per request to mint a
fresh bucket and evade the per-IP cap. Authenticated `user:{id}` path is
unchanged. (X-Forwarded-For trusted-proxy parsing remains deferred as 7b;
`request.client.host` stays as-is, documented as a known proxy limitation.)
3 — Declare `rate_limit_backend` as a real Optional[RateLimitBackend] field
on ServerConfig (default None -> process-local MemoryRateLimitBackend, behaviour
unchanged), replacing the prior getattr lookup. ServerConfig now allows
arbitrary types so the Protocol-typed backend can be stored. Documents the
multi-worker limitation (in-memory counter is per-process, so effective cap is
N x configured under N workers — including the DCR /oauth/register cap) on the
field, the server_configurator selection site, and _wire_dcr_rate_limit.
Tests: UA-independence of the identifier + same-IP shared bucket (no UA-rotation
evasion); rate_limit_backend field declared/defaults-None/accepts-instance and
server_configurator consumes a supplied backend (falls back to Memory otherwise).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Access tokens are stateless RS256 JWTs that previously stayed valid until their exp regardless of /oauth/revoke. Add a per-token jti denylist so a holder can revoke a specific access token before expiry: - OAuthRevokedToken Object (jti + self-expiring expires_at = token exp) - denylist.py store: revoke_jti (idempotent) / is_jti_revoked - resource.verify_oauth_access_token: require jti + reject denylisted jtis (Authlib's RFC-9068 generator always stamps jti; verified by decoding a freshly-minted token). One indexed lookup per RS validation (hot-path note; in-proc cache is a future optimization). - revocation endpoint: when an access-token JWT is presented (or token_type_hint=access_token), validate it via the same verify path and denylist its jti. Refresh-token revocation unchanged. Still requires client-auth + token possession (check_client on the client_id claim), so a caller can only revoke a token they hold. Per-token only; mass revocation (per-(user,client) revoked-after watermark) is a separate future item since stateless jtis can't be enumerated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the five M4 fixes (scope ceiling, DCR scope filter, auth-code CAS, IP-only rate-limit key + pluggable backend, jti denylist), the two intentional behavior changes (permissions:[] => deny scope; IP-only rate-limit key), the holistic adversarial review verdict (no over-grant on any branch, no fail-open, back-compat preserved, no cross-cutting bypass), and the two documented deferrals (Item 5 RFC-8707 per-request resource->aud; Item 7b trusted-XFF). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nsent ceiling fix 1bb5f5a) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ferences jvspatial is a generic application framework; remove the two remaining authoritative references to a specific downstream app (integral): - db/postgres.py: partial-filter docstring now says 'jvspatial and its downstream applications' instead of naming jvagent/integral - docs/md/multi-tenant-rls.md: RLS is 'a load-bearing isolation primitive for any multi-tenant platform built on it', not 'the Integral platform' Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mes in jvspatial) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nion to the COMPLETE record) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n and standard json backends
Make pyproject.toml the single source of build metadata and fix the metadata/packaging issues found in the pre-release review. Packaging: - Remove setup.py; resolve version dynamically via [tool.setuptools.dynamic] from jvspatial/version.py. Fixes a dual-source conflict where the wheel published Requires-Python >=3.8 (EOL) while the project targets >=3.9. - Add the `cache` extra (redis[hiredis]) so `pip install jvspatial[cache]` works; it backs jvspatial.cache.redis. - Complete the `all` extra: lambda, postgres, pgvector, otel, cache, scheduler. - Remove the redundant top-level requirements*.txt files; deps live solely in pyproject.toml extras. CI / release: - publish.yml: switch from API-token upload to PyPI Trusted Publishing (OIDC) via pypa/gh-action-pypi-publish; add id-token: write; drop dead setup.py/requirements config-change grep. - security.yml: drop the now-inert requirements*.txt path filters. Docs / templates: - CHANGELOG: promote [Unreleased] to [0.0.9] - 2026-06-14 with a Packaging section. - bug_report.md: drop downstream Jaclang/Jivas fields; add OS, DB backend, serverless mode. - Update workflow docs (README/VERSIONING/WORKFLOW_SUMMARY), RELEASING.md, and docs/md to drop setup.py/requirements*.txt references. Verified: clean sdist+wheel build, twine check PASSED, fresh-venv install + import + CLI all green; wheel METADATA shows version 0.0.9 and Requires-Python >=3.9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…x auth docs
Security:
- jvspatial/cache/redis.py: in the default `json` serialization mode the
deserializer fell through to pickle.loads on any unprefixed value, so
anyone able to write to the Redis keyspace could achieve RCE even with
the "safe" default. JSON mode now refuses non-JSON values (raised and
caught by get() as a cache miss -> recompute). Legacy pickle entries are
readable only via explicit opt-in (allow_legacy_pickle=True /
JVSPATIAL_REDIS_ALLOW_LEGACY_PICKLE=true) on a trusted keyspace.
Explicit `pickle` mode is unchanged.
Docs:
- README + docs/md/{authentication,api-keys,migration}.md used a flat
`auth_enabled=True` kwarg that ServerConfig silently ignores (auth
stayed disabled). Corrected to the nested `auth=dict(...)` form.
Cleanup:
- jvspatial/cache/layered.py: replace print() Redis-unavailable warnings
with logging.warning.
- .env.example: document JVSPATIAL_REDIS_ALLOW_LEGACY_PICKLE.
- tests: cover safe-default rejection, opt-in legacy read, and env toggle.
CHANGELOG: 0.0.9 Security/Fixed entries.
Benchmark comparisonThreshold: ±25% (informational, does not block merge)
|
Two independent CI failures on the 0.0.9 PR.
1. Starlette >=0.52 broke auth/OpenAPI route introspection (4 failures).
Newer Starlette wraps `app.include_router(...)` in an `_IncludedRouter`
object instead of flattening the included APIRoutes into `app.routes`.
jvspatial's auth resolver only scanned top-level APIRoute instances, so
included routes (e.g. the file-storage GET/DELETE endpoints) looked
unregistered -> deny-by-default -> spurious 401s; OpenAPI security
padlocks were also dropped. Added `jvspatial/api/_route_utils.py`
(`iter_api_routes`) which recurses through `_IncludedRouter`
(`.original_router.routes`) and mounts, and routed every route-table
scan through it: endpoint auth resolver (3 sites), OpenAPI security
wiring, dynamic-walker registration, and duplicate-route detection.
Compatible with old and new Starlette. Reproduced and verified fixed
in a python:3.12-slim container (fastapi 0.137 / starlette 0.52);
does not regress the older local stack (fastapi 0.119 / starlette 0.48).
2. Postgres unit tests errored when asyncpg absent (14 failures).
`tests/db/test_postgres_unit.py` now `importorskip("asyncpg")` so it
skips cleanly instead of erroring; CI installs the postgres+pgvector
extras so the unit tests actually run (translator pushdown, pool
auto-tuning, vector encoding). Live-DB integration tests still skip
without JVSPATIAL_POSTGRES_TEST_DSN.
CHANGELOG: 0.0.9 Fixed entry for the Starlette compatibility fix.
Benchmark comparisonThreshold: ±25% (informational, does not block merge)
|
Starlette 0.52.x carries PYSEC-2026-161 (fixed in 1.0.1); the previous `starlette<1.0.0` cap pinned installs to the vulnerable line and failed the pip-audit CI gate. The cap's original rationale (Starlette 1.0 drops Router on_startup/on_shutdown) does not apply: jvspatial wires lifecycle hooks through its own LifecycleManager (FastAPI lifespan), and route introspection is version-agnostic via _route_utils.iter_api_routes. Dependency is now `starlette>=0.46.0` (matches FastAPI's own floor, no upper cap). Fresh installs resolve to a patched Starlette (verified 1.3.1). Full suite green against Starlette 1.3.1; pip-audit reports no vulnerabilities with the CI-exact install.
Benchmark comparisonThreshold: ±25% (informational, does not block merge)
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type of Change
What type of change does this PR introduce? Mark all that apply:
Summary
What does this PR address?
Cuts the 0.0.9 release. It bundles a new production database backend, three new persistence/observability subsystems, a library-wide audit remediation (Waves 1–5), hot-path performance work, a Redis RCE fix, and packaging cleanup that makes
pyproject.tomlthe single source of build truth.asyncpg+ JSONB, RLS, pgvector) — the new recommended production backend.pickle.loadsin the default JSON mode).setup.py/requirements*.txt, fixesRequires-Python, adds missing extras, switches publishing to OIDC Trusted Publishing.Description
Bug Fixes:
Security / correctness (highlights — full list in CHANGELOG
[0.0.9]):jsonserialization mode, unprefixed values fell through topickle.loads, giving RCE to anyone able to write to the Redis keyspace. JSON mode now refuses non-JSON blobs (cache miss → recompute); legacy pickle reads require explicit opt-in on a trusted keyspace.__entity_name__corruption:save_objectregenerated IDs throughcls.__name__, silently corrupting IDs for any entity using the override on every save. Edge lookups/queries similarly ignored the override.==; nowhmac.compare_digest.LocalFileInterfaceversion methods bypassed sanitization —../../etc/passwdescaped the storage root. Now routed through a sanitized base.compare_digestalways returned False.awaits andasync def __init__leaked coroutines in webhook/file-delete paths; blockingPathI/O in async JsonDB/graph-export bodies now offloaded toasyncio.to_thread.InfiniteLoopError/WalkerTimeoutError/…) instead of silently swallowing; queue front/middle inserts now respectmax_size.auth_enabled=Truekwarg thatServerConfigsilently ignores (auth stayed off); corrected to nestedauth=dict(...)in README +docs/md/{authentication,api-keys,migration}.md.Root cause theme: most findings trace to (a)
cls.__name__used where the per-subclass__entity_name__discriminator was required, (b)==/ unawaited coroutines / blocking I/O on async paths, and (c) protection/security defaults that failed open or were silently bypassed.Feature Request:
jvspatial/db/postgres.py): JSONB schema per collection, serverless-aware pool auto-tuning, near-100% Mongo-operator pushdown (_postgres_translate.py), atomicfind_one_and_*viaRETURNING ... FOR UPDATE,COPY-based bulk writes, walker BFS via recursive CTE, multi-tenant Row-Level Security, and pgvector hybrid KG+metadata+vector queries ($near). Registered ascreate_database("postgres"|"postgresql"). Extras:[postgres],[pgvector].jvspatial/core/migrations.py):__schema_version__+@migrationdecorator + MRO-walking chain resolution; load-path hook;jvspatial migrateCLI.Database.find_iter/Object.find_iter): constant-memory keyset iteration across all backends with opaque resumable cursors; native PG implementation.TrailStore(in-memory/DB) +Walker.restore()cold-start resume; sharedSessionStore(in-process/Redis) so token revocations propagate across workers/Lambda; no-op-safe OpenTelemetry tracing; read-onlyvalidate_graph()invariant checker.Refactor Request:
_get_class_hierarchy_fields()~7×, attribute set ~11×, instantiation ~1.8×.JsonDBusesorjsonwhen available + compact writes (~1.6× scans).@on_visitcollection unified into_visit_hooks.register_visit_hooks; bool parsing consolidated toenv.parse_boolacross four modules.JsonDBTransaction+best_effortbuffered-commit mode (dev-only, weaker-than-ACID); Python 3.8 support;aiofilesruntime dep; deadJSONTransaction.Changes Made
High-Level Summary:
[postgres]/[pgvector]extras.jvspatial migrateCLI.find_iter) across all backends.setup.py/requirements*.txt, single-sourcepyproject.toml, fixRequires-Python >=3.9, addcache/completeallextras, switch publishing to OIDC Trusted Publishing.Checklist
Mark all that apply:
Steps to Test
pip install -e '.[dev,test]'thenpytest -q— full suite passes (PG live-DB tests skip gracefully without a reachable Postgres; benchmarks excluded by default).python -m build && twine check dist/*— both artifacts PASS; confirm wheelMETADATAshowsVersion: 0.0.9andRequires-Python: >=3.9. Confirmpip install 'dist/….whl[cache]'resolves redis.pytest tests/db/test_postgres_integration.py— CRUD, operator pushdown, walker traverse, RLS tenant isolation, pgvector ANN + hybrid queries.jvspatial migrate --collection <c> --import-module <m> --dry-runthen--apply.get()returns a miss (no unpickle); setJVSPATIAL_REDIS_ALLOW_LEGACY_PICKLE=trueand confirm legacy reads work.auth=dict(auth_enabled=True, jwt_secret=...)example and confirm/auth/*endpoints register and protected routes 401 without a token.Additional Context
file:lineand audit-section references is inCHANGELOG.mdunder## [0.0.9].pyproject.tomlis now the single metadata source; version is read fromjvspatial/version.py; merge tomainauto-publishes via OIDC.TrueSelph, repojvspatial, workflowpublish.yml, env blank) or the OIDC publish step will fail.Questions or Concerns
JsonDBTransactionremoved; Python 3.8 dropped; deferred-invoke route fails closed without a secret;ObjectPagerno longer caches;Walker.skip()raisesTraversalSkipped. Confirm downstreams are prepared.blacklist_fail_closed=False); (2) open Dynamic Client Registration (/oauth/register, unauthenticated + rate-limited, opt-in for MCP).0.0.8line should appear in the changelog — it currently jumps0.0.9 → 0.0.7.Want this written to a file (e.g.
.github/PR_0.0.9.md) or opened directly as a PR withgh pr create(dev → main)?