Skip to content

node: storage program read RPCs, /health, rate-limit headers, ACL hardening - #797

Merged
tcsenpai merged 14 commits into
stabilisationfrom
fix/storage-rpc-and-health-stabilisation
May 6, 2026
Merged

node: storage program read RPCs, /health, rate-limit headers, ACL hardening#797
tcsenpai merged 14 commits into
stabilisationfrom
fix/storage-rpc-and-health-stabilisation

Conversation

@tcsenpai

@tcsenpai tcsenpai commented May 5, 2026

Copy link
Copy Markdown
Contributor

Replaces #796 (closed — wrong base branch).

Lands the storage-program read RPC family, /health endpoint, rate-limit
client signal, and the security/correctness review fixes from PR #796
all adapted to stabilisation's modular handler-registry architecture.

What's in this PR

Features

  • 9 storage-program read RPCs (getStorageProgram, getStorageProgramAll,
    getStorageProgramFields, getStorageProgramFieldType, getStorageProgramItem,
    getStorageProgramValue, getStorageProgramsByOwner, hasStorageProgramField,
    searchStoragePrograms) registered via a new storageProgramHandlers module
    in the modular dispatch registry.
  • getTransactionStatus RPC for transaction lifecycle tracking (pending /
    included / unknown), with sha256-hex regex validation and case-insensitive
    normalised lookup.
  • GET /health endpoint reporting {version, version_name, accepting, mempool_size, uptime_s}. Returns HTTP 503 when the node isn't accepting
    traffic or the mempool DB is unreachable, so LB/k8s probes can detect
    unhealthy nodes by status code alone.
  • X-RateLimit-{Limit,Remaining,Reset} headers on POST / responses so
    SDK clients can self-throttle.
  • /health and /version exempt from the global rate limiter so probes
    can't be 429-throttled under load.

Fixes (security / correctness)

  • Unknown-message dispatcher: returns HTTP 404 with {error, message}
    instead of HTTP 200 with a Python-dict-shaped string. SDK clients can now
    detect unsupported methods.
  • ACL bypass (was P1): the !requesterAddress || ... === owner pattern
    treated anonymous (undefined or empty-string) callers as the owner and
    leaked owner/restricted programs. Fixed in both the new RPC handler and
    the older HTTP route handler.
  • Post-pagination ACL filter: searchStorageProgramsByName paginated at
    the SQL layer then JS-filtered, producing short pages and hidden
    accessible rows. Now uses a jsonb WHERE predicate that mirrors
    checkReadPermission exactly, so LIMIT/OFFSET produce full pages.

Hardening

  • jsonResponse accepts extraHeaders and forces Content-Type last so
    caller-supplied headers can't override the JSON content type.
  • Storage program error logs pass the error object as a separate argument
    instead of string-concatenating, preserving non-Error throws as
    diagnostic data instead of [object Object].

Performance — SQL ACL filter design notes

The post-pagination ACL fix is the only place where the perf surface
materially changed. Highlights:

  • Owner fast-path: requesterAddress === owner skips the jsonb
    predicate entirely, uses idx_gcr_storageprogram_owner. Same plan as
    before for the common owner-lists-own-programs case.
  • Anonymous fast-path: predicate collapses to acl->>'mode' = 'public'
    — single text comparison, no jsonb_each subquery, no parameter bind.
  • No GIN index added on acl: the query is already gated by ILIKE /
    owner / isDeleted, so the predicate runs on a small post-prune candidate
    set. Adding a GIN would slow every storage-program write (a hot path)
    for no win on this query.
  • JS-side: strictly less work — only accessible rows are hydrated
    through the ORM and mapped through toStorageProgramListItem.

Verification

  • ESLint on changed files: 0 errors, 6 pre-existing result.program!
    non-null-assertion warnings (project-wide style)
  • tsc --noEmit on changed files: clean — pre-existing errors in
    chainBlocks.ts, chainTransactions.ts, FHE/ZK tests are not
    introduced by this work (verified by stash-and-recheck)

Tests

Regression tests added for:

  • short-page case (one accessible row in the SQL window)
  • owner fast-path (uses repo.find not QueryBuilder)
  • anonymous-only-sees-public (no requester bind)
  • SQL-injection safety (predicate uses bound :requesterAddress parameter)

History

Originally targeted testnet as PR #796. Closed and reopened against
stabilisation per direction that stabilisation will replace testnet.
The original testnet branch (claude/checkout-branches-WQCIg) is
preserved at commit 8c9f6628 for history.

Summary by CodeRabbit

  • New Features

    • Transaction status RPC; storage program read/search/value/field endpoints; GET /health reports uptime and mempool size; rate-limit headers added to responses.
  • Bug Fixes

    • Storage program pagination and ACL enforcement moved to the database layer to preserve correct results and pagination.
  • Refactor

    • Improved structured error for unknown RPC messages.
  • Documentation

    • Added planning document outlining SDK capability-detection approach.
  • Tests

    • Expanded tests for storage program ACL and query behavior.

tcsenpai added 4 commits May 5, 2026 18:03
… fix, capability detection plan

Bundles testnet's storage-program-read PR work, adapted to stabilisation's
modular handler-registry architecture. This commit lands:

  - 9 read RPC handlers for the StorageProgram subsystem:
      getStorageProgram, getStorageProgramAll, getStorageProgramFields,
      getStorageProgramFieldType, getStorageProgramItem,
      getStorageProgramValue, getStorageProgramsByOwner,
      hasStorageProgramField, searchStoragePrograms
  - getTransactionStatus RPC for transaction lifecycle tracking
    (pending / included / unknown), with sha256-hex regex validation
    and case-insensitive normalised lookup
  - shared module storageProgramShared.ts with the validate-resolve-
    require-jsonObject envelope and a withFieldRead higher-order helper
    that dedupes the simple field-read handlers (getValue, getFieldType)
  - 404 dispatcher fix: registry-miss path now returns HTTP 404 with a
    structured {error, message} body instead of HTTP 200 with a
    Python-dict-shaped string. SDK clients can now detect unsupported
    methods.
  - SDK capability detection implementation plan in history/planning/

ACL semantics are preserved: each handler enforces checkReadPermission
via the shared getAccessibleProgram resolver, with anonymous callers
limited to public programs.
…ites

Registers the 10 new RPC handlers in the modular dispatch registry, adds
the Chain / Mempool / sharedState building blocks they (and the upcoming
/health endpoint) depend on:

  - handlers/storageProgramHandlers.ts: registers the 9 storage RPCs
  - handlers/transactionHandlers.ts: adds getTransactionStatus
  - handlers/index.ts: spreads storageProgramHandlers into the registry
  - blockchain/chainTypes.ts: TxStatus discriminated type
  - blockchain/chainTransactions.ts: getTransactionStatus(hash) — checks
    mempool first, then transactions table, returns pending/included/
    unknown
  - blockchain/chain.ts: re-exports TxStatus, exposes
    Chain.getTransactionStatus on the facade
  - blockchain/mempool.ts: count() for cheap mempool size lookups (used
    by /health) and findByHash() for case-insensitive hash lookup (used
    by getTransactionStatus)
  - utilities/sharedState.ts: nodeStartTime + getUptimeSeconds() for the
    /health endpoint
…rdening

  - GET /health: reports {version, version_name, accepting, mempool_size,
    uptime_s}. Returns HTTP 503 when the node is not accepting traffic
    (not synced) or when the mempool DB is unreachable, so LB/k8s probes
    can detect unhealthy nodes by status code alone. Mempool.count() is
    isolated in a try/catch so a transient DB outage doesn't 500 the
    probe.
  - Rate limiter: /health and /version are exempt from the global rate
    limiter middleware so liveness/readiness probes can't be 429-throttled
    under load (k8s/ALB hit /health from a single source IP that would
    otherwise blow the per-IP quota).
  - Rate limiter: getCurrentLimits(ip) exposes the current window state
    (limit, remaining, resetEpochSeconds) so server_rpc can surface
    X-RateLimit-{Limit,Remaining,Reset} headers on POST / responses. SDK
    clients can use these to self-throttle.
  - bunServer.jsonResponse: extraHeaders parameter, with Content-Type
    forced last so caller-supplied headers can't accidentally override
    the JSON content type.
…nymous bypass

Closes the security and correctness defects flagged in PR review:

  ACL bypass (security, was P1)
  ─────────────────────────────
  The pattern `!requesterAddress || requesterAddress === owner` treated
  anonymous callers (undefined or empty-string requester) as the owner
  and skipped checkReadPermission entirely, leaking owner/restricted
  programs to unauthenticated callers. This shape was repeated in the
  HTTP route handler (listByOwnerHandler) and the new RPC handler. Both
  now require a strictly defined non-empty requester for the owner
  fast-path; everything else falls through to the SQL ACL filter.

  Post-pagination filter (correctness)
  ────────────────────────────────────
  searchStorageProgramsByName paginated at the SQL layer
  (take(limit).skip(offset)) and then ACL-filtered the result in JS.
  This produced two visible defects:
    - short pages: limit=10 could return 3 if 7 of the rows in the SQL
      window were restricted
    - invisible accessible rows: anything past offset+limit in the SQL
      result was never fetched, so subsequent pages couldn't reveal it
  Now ACL filtering happens in the WHERE clause via a jsonb predicate
  that mirrors checkReadPermission exactly:
    - public + not blacklisted (anonymous always sees public —
      blacklist needs an identity to test)
    - owner mode + requester is the owner
    - restricted + requester is the owner (overrides blacklist)
    - restricted + requester in acl.allowed and not blacklisted
    - restricted + requester is a member of a group with read permission
  The database returns exactly the requested page of accessible rows.

  Performance
  ───────────
  - Owner fast-path: when requesterAddress === owner, skips the jsonb
    predicate entirely and uses the existing owner index
    (idx_gcr_storageprogram_owner). Same plan as before for the common
    owner-lists-own-programs case.
  - Anonymous fast-path: predicate collapses to acl->>'mode' = 'public'
    — single text comparison, no jsonb_each subquery, no parameter bind.
  - Containment uses jsonb @> operators which Postgres can short-circuit
    on absent keys. No GIN index added: the query is already gated by
    ILIKE / owner / isDeleted predicates, so the ACL predicate runs on
    a small post-prune candidate set; a GIN on `acl` would add write
    amplification on every storage-program write (a hot path) for no
    win on this query.
  - JS-side mapping is now strictly less work: only accessible rows are
    hydrated through the ORM and run through toStorageProgramListItem.

Updated all four call sites:
  - src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts
  - src/libs/network/routines/nodecalls/searchStoragePrograms.ts
  - src/libs/network/manageGCRRoutines.ts
  - src/features/storageprogram/routes.ts (HTTP, both byOwner and search)

Tests: regression coverage for the previously-broken short-page case,
the owner fast-path, anonymous-only-sees-public, and parameter binding
(SQL-injection safety).
@coderabbitai

coderabbitai Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@tcsenpai has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 53 minutes and 1 second before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7fb38aca-1c62-4fa9-bce6-a8fabb5dd065

📥 Commits

Reviewing files that changed from the base of the PR and between 6490b96 and 9f7e4a5.

📒 Files selected for processing (6)
  • src/features/storageprogram/routes.ts
  • src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts
  • src/libs/network/manageGCRRoutines.ts
  • src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts
  • src/libs/network/routines/nodecalls/storageProgramShared.ts
  • tests/storageprogram/routines.test.ts

Walkthrough

Storage-program ACL enforcement moved into SQL with new read-side RPC handlers and shared helpers; transaction status lookup API added; health endpoint, uptime tracking, and rate-limit headers implemented; mempool helpers added; and an SDK capability-detection planning doc was introduced.

Changes

Storage Program Access Control

Layer / File(s) Summary
Data Shape & Signatures
src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts, src/features/storageprogram/routes.ts
getStorageProgramsByOwner and searchStorageProgramsByName accept optional requesterAddress; route handlers now pass requesterAddress and treat empty identity as undefined.
Core SQL ACL Predicate
src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts
New readReachablePredicate(requesterAddress, alias?) constructs JSONB-based SQL ACL predicate for QueryBuilder composition.
Shared Field-Read Infrastructure
src/libs/network/routines/nodecalls/storageProgramShared.ts
Adds RPC envelope helpers, StorageFieldType, getAccessibleProgram, mappers (toStorageProgramData, toStorageProgramListItem), requireJsonObject, and withFieldRead HOF for centralized validation/ACL/error handling.
Node-call Routines
src/libs/network/routines/nodecalls/*
Added multiple read routines (getStorageProgram*, getStorageProgramFields, getStorageProgramItem, getStorageProgramValue, hasStorageProgramField, getStorageProgramsByOwner, searchStoragePrograms) that use the shared helpers and return SDK-shaped RPC responses.
Handler Registration & Wiring
src/libs/network/handlers/storageProgramHandlers.ts, src/libs/network/handlers/index.ts, src/libs/network/manageGCRRoutines.ts
New storageProgramHandlers registered into handlerRegistry; manageGCRRoutines and route layers now rely on GCR routines for ACL-filtered results (removed JS post-filtering).
Tests
tests/storageprogram/routines.test.ts
Expanded tests: mock QueryBuilder helpers and assertions validating fast-path owner behavior, anonymous/public predicate usage, requester-bound ACL predicates, and correct QueryBuilder-based pagination.

Transaction Status Tracking

Layer / File(s) Summary
Type
src/libs/blockchain/chainTypes.ts
New exported TxStatus type: `state: "pending"
Mempool Helpers
src/libs/blockchain/mempool.ts
Added count() and findByHash(hash) (case-insensitive lookup).
Lookup Logic
src/libs/blockchain/chainTransactions.ts
Added getTransactionStatus(hash) checking mempool → DB → unknown and returning TxStatus.
Public API
src/libs/blockchain/chain.ts
Added Chain.getTransactionStatus(hash) and re-exported TxStatus.
RPC Endpoint
src/libs/network/handlers/transactionHandlers.ts, src/libs/network/routines/nodecalls/getTransactionStatus.ts
New RPC handler validates 64-char hex hash, calls Chain.getTransactionStatus, and returns structured RPC responses (200/400/500).

Health Monitoring & Rate Limiting

Layer / File(s) Summary
Uptime Tracking
src/utilities/sharedState.ts
SharedState.nodeStartTime added and getUptimeSeconds() computes process uptime in seconds.
Health Endpoint & Mempool Size
src/libs/network/server_rpc.ts, src/libs/blockchain/mempool.ts
New GET /health returns accepting, mempool_size, uptime, and version; treats mempool count errors as degraded (503).
Rate Limiter
src/libs/network/middleware/rateLimiter.ts
/health and /version bypass rate limiting; added RateLimiter.getCurrentLimits(ip) returning `{ limit, remaining, resetEpochSeconds }
Response Headers
src/libs/network/bunServer.ts, src/libs/network/server_rpc.ts
jsonResponse accepts extraHeaders; RPC POST / responses include X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset when available.

SDK Capability Detection Planning (doc-only)

Layer / File(s) Summary
Plan Document
history/planning/sdk-capability-detection.md
New planning doc describing opt-in strictCapabilities, MethodNotSupportedError, capabilities.ts infra (per-rpcUrl /health version caching with TTL, singleflight, semver checks, deprecation warning dedupe), reactive 404 detection locations, gating table for 10 methods with min-node versions, PR staging, and verification checklist.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (SDK)
    participant RPC as RPC Handler (/ POST)
    participant Routine as Node-call Routine
    participant Query as QueryBuilder
    participant DB as Database

    Client->>RPC: call getStorageProgramByOwner(request)
    RPC->>Routine: invoke getStorageProgramsByOwner(data)
    Routine->>Query: build QueryBuilder with readReachablePredicate(requesterAddress)
    Query->>DB: execute SQL (LIMIT/OFFSET)
    DB-->>Query: rows
    Query-->>Routine: accessible rows
    Routine-->>RPC: rpc(200, mappedList)
    RPC-->>Client: HTTP response with body + X-RateLimit-* headers
Loading

(uses rgba(100,150,240,0.5) style by default in visualization layers)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • kynesyslabs/node#491: Overlapping changes to storage-program handlers and manageNodeCall behavior.
  • kynesyslabs/node#517: Modifies manageNodeCall/sharedState areas touched here; likely related at integration points.
  • kynesyslabs/node#774: Changes the same GCR storage-program routines and may conflict with ACL/query-based changes.

Suggested labels

Review effort 2/5

"I hopped through code and SQL rows so bright,
guarding fields through day and tracing tx by night,
health beeps and rate headers keep the path clear,
SDK dreams planned, every gate drawn near —
a rabbit's cheer for changes, small and dear!" 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title comprehensively summarizes the main changes: storage program read RPCs, health endpoint, rate-limit headers, and ACL security hardening.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/storage-rpc-and-health-stabilisation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Storage program read RPCs, transaction status, health endpoint, and ACL hardening

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Implements 9 storage-program read RPCs with modular handler registry
• Adds getTransactionStatus RPC for transaction lifecycle tracking
• Fixes ACL bypass vulnerability in storage program access control
• Adds /health endpoint with rate-limit headers and infrastructure
• Moves ACL filtering to SQL layer to prevent pagination bugs
Diagram
flowchart LR
  A["9 Storage Program<br/>Read RPCs"] -->|registered via| B["storageProgramHandlers<br/>module"]
  C["getTransactionStatus<br/>RPC"] -->|registered via| D["transactionHandlers<br/>module"]
  B -->|dispatch to| E["Modular Handler<br/>Registry"]
  D -->|dispatch to| E
  E -->|routes to| F["RPC Handlers"]
  F -->|enforce ACL via| G["SQL Predicate<br/>readReachablePredicate"]
  G -->|prevents| H["Post-pagination<br/>filtering bugs"]
  I["Unknown Message<br/>Dispatcher"] -->|returns| J["404 with<br/>structured error"]
  K["/health Endpoint"] -->|reports| L["version, accepting,<br/>mempool_size, uptime"]
  M["Rate Limiter"] -->|exempts| K
  M -->|adds headers| N["X-RateLimit-*<br/>headers"]
Loading

Grey Divider

File Changes

1. src/features/storageprogram/routes.ts 🐞 Bug fix +16/-28

Fix ACL bypass and move filtering to SQL layer

src/features/storageprogram/routes.ts


2. src/libs/blockchain/chain.ts ✨ Enhancement +7/-0

Export TxStatus type and add getTransactionStatus method

src/libs/blockchain/chain.ts


3. src/libs/blockchain/chainTransactions.ts ✨ Enhancement +25/-1

Implement getTransactionStatus with mempool and DB lookup

src/libs/blockchain/chainTransactions.ts


View more (25)
4. src/libs/blockchain/chainTypes.ts ✨ Enhancement +14/-0

Define TxStatus discriminated union type

src/libs/blockchain/chainTypes.ts


5. src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts 🐞 Bug fix +114/-20

Add SQL ACL predicate and refactor storage program queries

src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts


6. src/libs/blockchain/mempool.ts ✨ Enhancement +16/-0

Add count and case-insensitive findByHash methods

src/libs/blockchain/mempool.ts


7. src/libs/network/bunServer.ts ✨ Enhancement +12/-4

Support extraHeaders in jsonResponse with forced Content-Type

src/libs/network/bunServer.ts


8. src/libs/network/handlers/index.ts ✨ Enhancement +2/-0

Register storageProgramHandlers in modular dispatch registry

src/libs/network/handlers/index.ts


9. src/libs/network/handlers/storageProgramHandlers.ts ✨ Enhancement +49/-0

Register 9 storage program read RPC handlers

src/libs/network/handlers/storageProgramHandlers.ts


10. src/libs/network/handlers/transactionHandlers.ts ✨ Enhancement +5/-0

Register getTransactionStatus RPC handler

src/libs/network/handlers/transactionHandlers.ts


11. src/libs/network/manageGCRRoutines.ts 🐞 Bug fix +15/-18

Update HTTP routes to use SQL-layer ACL filtering

src/libs/network/manageGCRRoutines.ts


12. src/libs/network/manageNodeCall.ts 🐞 Bug fix +10/-2

Return structured 404 for unknown RPC messages

src/libs/network/manageNodeCall.ts


13. src/libs/network/middleware/rateLimiter.ts ✨ Enhancement +38/-0

Exempt /health and /version from rate limiting, add header export

src/libs/network/middleware/rateLimiter.ts


14. src/libs/network/routines/nodecalls/getStorageProgram.ts ✨ Enhancement +47/-0

Implement getStorageProgram RPC handler with ACL

src/libs/network/routines/nodecalls/getStorageProgram.ts


15. src/libs/network/routines/nodecalls/getStorageProgramAll.ts ✨ Enhancement +45/-0

Implement getStorageProgramAll RPC handler

src/libs/network/routines/nodecalls/getStorageProgramAll.ts


16. src/libs/network/routines/nodecalls/getStorageProgramFieldType.ts ✨ Enhancement +21/-0

Implement getStorageProgramFieldType RPC handler

src/libs/network/routines/nodecalls/getStorageProgramFieldType.ts


17. src/libs/network/routines/nodecalls/getStorageProgramFields.ts ✨ Enhancement +46/-0

Implement getStorageProgramFields RPC handler

src/libs/network/routines/nodecalls/getStorageProgramFields.ts


18. src/libs/network/routines/nodecalls/getStorageProgramItem.ts ✨ Enhancement +88/-0

Implement getStorageProgramItem RPC handler with array indexing

src/libs/network/routines/nodecalls/getStorageProgramItem.ts


19. src/libs/network/routines/nodecalls/getStorageProgramValue.ts ✨ Enhancement +20/-0

Implement getStorageProgramValue RPC handler

src/libs/network/routines/nodecalls/getStorageProgramValue.ts


20. src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts ✨ Enhancement +70/-0

Implement getStorageProgramsByOwner RPC handler with pagination

src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts


21. src/libs/network/routines/nodecalls/getTransactionStatus.ts ✨ Enhancement +54/-0

Implement getTransactionStatus RPC handler with validation

src/libs/network/routines/nodecalls/getTransactionStatus.ts


22. src/libs/network/routines/nodecalls/hasStorageProgramField.ts ✨ Enhancement +59/-0

Implement hasStorageProgramField RPC handler

src/libs/network/routines/nodecalls/hasStorageProgramField.ts


23. src/libs/network/routines/nodecalls/searchStoragePrograms.ts ✨ Enhancement +82/-0

Implement searchStoragePrograms RPC handler with ACL filtering

src/libs/network/routines/nodecalls/searchStoragePrograms.ts


24. src/libs/network/routines/nodecalls/storageProgramShared.ts ✨ Enhancement +281/-0

Shared utilities for storage program RPC handlers

src/libs/network/routines/nodecalls/storageProgramShared.ts


25. src/libs/network/server_rpc.ts ✨ Enhancement +42/-1

Add /health endpoint and rate-limit headers to RPC responses

src/libs/network/server_rpc.ts


26. src/utilities/sharedState.ts ✨ Enhancement +11/-0

Add node uptime tracking for /health endpoint

src/utilities/sharedState.ts


27. tests/storageprogram/routines.test.ts 🧪 Tests +144/-3

Add regression tests for ACL filtering and pagination

tests/storageprogram/routines.test.ts


28. history/planning/sdk-capability-detection.md 📝 Documentation +598/-0

SDK capability detection implementation plan

history/planning/sdk-capability-detection.md


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. ACL SQL can throw ✓ Resolved 🐞 Bug ☼ Reliability
Description
readReachablePredicate uses jsonb_each(sp.acl->'groups') without guarding for
missing/null/non-object groups, which can cause the underlying query to error for restricted
programs that have no groups. This can make storage-program list/search fail at runtime (500)
instead of returning results.
Code

src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts[R969-975]

+                OR (${a}.acl->>'mode' = 'restricted'
+                    AND NOT COALESCE(${a}.acl->'blacklisted' @> to_jsonb(:requesterAddress::text), false)
+                    AND EXISTS (
+                        SELECT 1 FROM jsonb_each(${a}.acl->'groups') AS grp(name, def)
+                        WHERE def->'members' @> to_jsonb(:requesterAddress::text)
+                          AND def->'permissions' @> '"read"'::jsonb
+                    ))
Evidence
The SQL predicate invokes jsonb_each() on acl->'groups' unconditionally, but the entity schema
makes groups optional, and the in-memory ACL logic explicitly guards access to groups via `if
(acl.groups)` before iterating. That mismatch means DB evaluation can hit jsonb_each(NULL) for valid
rows where groups is absent.

src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts[941-979]
src/model/entities/GCRv2/GCR_StorageProgram.ts[16-25]
src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts[1115-1127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`readReachablePredicate()` builds an SQL ACL WHERE clause that calls `jsonb_each(sp.acl->'groups')`. If `acl.groups` is missing/null (allowed by schema and handled in `checkReadPermission`), Postgres can throw when evaluating the predicate, causing list/search queries to fail.
### Issue Context
- `StorageProgramACL.groups` is optional in the entity type.
- `checkReadPermission()` only iterates groups when `acl.groups` is truthy.
- The SQL predicate should mirror that behavior.
### Fix Focus Areas
- src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts[956-976]
### Suggested fix
Update the SQL to safely handle missing groups, e.g.:
- Use `jsonb_each(COALESCE(sp.acl->'groups', '{}'::jsonb))`
- Optionally add a type guard: `jsonb_typeof(sp.acl->'groups') = 'object'`
This ensures rows without `groups` don’t error and simply don’t match the group-membership branch.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Owner list loads all ✓ Resolved 🐞 Bug ➹ Performance
Description
The getStorageProgramsByOwner RPC clamps limit/offset but still fetches all accessible programs from
the DB and then paginates via Array.slice. This can create unnecessary DB/CPU/memory load for owners
with many programs and undermines the intent of limit/offset.
Code

src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts[R52-65]

+        // ACL filtering happens in SQL — GCRStorageProgramRoutines handles
+        // the owner-fast-path internally when requesterAddress === owner.
+        // Anonymous and non-owner requesters get the SQL ACL predicate so
+        // pagination produces full pages and never leaks restricted rows.
+        const repository = await getStorageProgramRepository()
+        const accessiblePrograms =
+            await GCRStorageProgramRoutines.getStorageProgramsByOwner(
+                owner,
+                repository,
+                requesterAddress,
+            )
+
+        const paginated = accessiblePrograms.slice(offset, offset + limit)
+        return rpc(200, paginated.map(toStorageProgramListItem))
Evidence
The RPC handler calls GCRStorageProgramRoutines.getStorageProgramsByOwner(...), which returns all
matching rows (repo.find(...) or qb.getMany()) and does not accept/apply limit/offset. The handler
then slices the full array in memory to satisfy pagination.

src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts[47-65]
src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts[990-1012]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getStorageProgramsByOwner` (RPC) accepts `limit`/`offset`, but it loads the full accessible list from the database and only then applies pagination via `slice()`. This is correct functionally but can be expensive.
### Issue Context
- `GCRStorageProgramRoutines.getStorageProgramsByOwner()` currently has no pagination parameters and returns all rows.
- The RPC handler slices after the fact.
### Fix Focus Areas
- src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts[47-65]
- src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts[990-1012]
### Suggested fix
Extend `GCRStorageProgramRoutines.getStorageProgramsByOwner()` to accept optional `{ limit, offset }` and apply:
- Owner fast-path: `repository.find({ ..., take: limit, skip: offset })`
- Non-owner path: add `.take(limit).skip(offset)` to the QueryBuilder
Then remove (or keep as defensive) the JS `slice()` in the RPC handler.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@greptile-apps

greptile-apps Bot commented May 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR lands 9 storage-program read RPCs, a getTransactionStatus RPC, a /health endpoint, X-RateLimit-* headers on POST / responses, and a batch of ACL/correctness fixes — all adapted to stabilisation's modular handler-registry architecture.

  • ACL hardening: the !requesterAddress || requesterAddress === owner bypass that leaked restricted programs to anonymous callers is fixed in both the HTTP route handlers and the GCR dispatcher; searchStorageProgramsByName and getStorageProgramsByOwner now push ACL filtering into the SQL WHERE clause so LIMIT/OFFSET pages are always full.
  • New RPC surface: 9 read RPCs plus getTransactionStatus are registered via a new storageProgramHandlers module; shared envelope helpers in storageProgramShared.ts eliminate duplication across handlers.
  • Infrastructure: /health returns HTTP 503 when accepting is false or the mempool DB is unreachable, exempted from the rate limiter alongside /version; unknown-message responses are now proper HTTP 404 JSON rather than the old Python-dict-shaped 200 string.

Confidence Score: 5/5

Safe to merge; the ACL bypass and pagination fixes are well-tested and the new RPC surface is well-structured.

The two highest-risk changes — the SQL ACL predicate and the owner/anonymous fast-paths — both have dedicated regression tests that verify the correct branch is taken and that requesterAddress is bound as a parameter rather than interpolated. The only inconsistency found (empty-string requester not coerced in five single-item handlers) has no access-control impact because checkReadPermission treats empty string identically to anonymous.

The five standalone single-item handlers (getStorageProgram.ts, getStorageProgramAll.ts, getStorageProgramFields.ts, getStorageProgramItem.ts, hasStorageProgramField.ts) apply a looser requester-address guard than the list handlers and withFieldRead.

Important Files Changed

Filename Overview
src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts Major addition: SQL ACL predicate (readReachablePredicate) that mirrors checkReadPermission at the DB layer, plus SQL pagination for getStorageProgramsByOwner and searchStorageProgramsByName; the alias injection-guard and jsonb branch coverage look correct.
src/libs/network/server_rpc.ts Adds /health endpoint with accepting/mempool-size gate returning 503 for LB probes, and surfaces X-RateLimit-* headers on POST / responses via getCurrentLimits.
src/libs/network/middleware/rateLimiter.ts Adds path-based bypass for /health and /version and a getCurrentLimits read-accessor; both additions are self-contained and low risk.
src/libs/network/routines/nodecalls/storageProgramShared.ts New shared helper module — getAccessibleProgram, rpc* helpers, withFieldRead HOF, and mapper functions; centralises ACL enforcement and eliminates duplication across 9 RPC handlers.
src/features/storageprogram/routes.ts ACL bypass fixed via new getRequesterAddress helper (empty post-colon segment → undefined); SQL-layer ACL+pagination for both listByOwnerHandler and searchByNameHandler.
src/libs/blockchain/chainTransactions.ts Adds getTransactionStatus: 1 mempool lookup then 1 tx lookup, returning pending/included/unknown — minimal and correct.
src/libs/blockchain/mempool.ts Adds count() for /health and findByHash() using ILike for case-insensitive lookup; only caller already normalises to lowercase so ILike is redundant but harmless.
src/libs/network/handlers/storageProgramHandlers.ts New modular handler registry for 9 storage-program read RPCs; thin wrappers that delegate to typed nodecall routines.
src/libs/network/routines/nodecalls/getTransactionStatus.ts New RPC with sha256-hex regex validation, lowercase normalisation, and clean 400/200/500 response mapping.
src/libs/network/manageNodeCall.ts Fixes unknown-message response from HTTP 200 with Python-dict string to HTTP 404 with structured JSON.
tests/storageprogram/routines.test.ts New regression tests for SQL ACL path: owner fast-path, anonymous public-only branch, non-owner parameter binding, and pagination correctness — coverage is solid.
src/libs/network/routines/nodecalls/getStorageProgram.ts New single-program read RPC; empty-string requesterAddress not coerced to undefined (inconsistent with list handlers), though no ACL bypass results due to checkReadPermission behaviour.
src/utilities/sharedState.ts Adds nodeStartTime and getUptimeSeconds() used by the /health endpoint; straightforward singleton addition.

Sequence Diagram

sequenceDiagram
    participant SDK as SDK Client
    participant RL as RateLimiter
    participant RPC as server_rpc (POST /)
    participant MNC as manageNodeCall
    participant HR as handlerRegistry
    participant SP as storageProgramHandlers
    participant GCR as GCRStorageProgramRoutines
    participant DB as PostgreSQL

    SDK->>RL: POST / {message, data}
    RL-->>RL: /health or /version? bypass
    RL-->>RL: increment IP counter
    RPC->>MNC: processPayload(payload, sender)
    MNC->>HR: handlerRegistry[message]
    HR->>SP: storageProgramHandlers[message](data)
    SP->>GCR: getStorageProgramsByOwner / searchStorageProgramsByName
    GCR->>DB: SELECT WHERE owner AND isDeleted=false AND ACL predicate LIMIT OFFSET
    DB-->>GCR: rows
    GCR-->>SP: GCRStorageProgram[]
    SP-->>MNC: RPCResponse
    MNC-->>RPC: RPCResponse
    RPC-->>RL: getCurrentLimits(clientIP)
    RL-->>RPC: {limit, remaining, resetEpochSeconds}
    RPC-->>SDK: 200 + X-RateLimit-* headers

    SDK->>RL: GET /health
    RL-->>SDK: bypass (no rate limit)
    RPC->>RPC: getSharedState (accepting, version)
    RPC->>DB: Mempool.count()
    DB-->>RPC: mempoolSize
    RPC-->>SDK: 200/503 {version, accepting, mempool_size, uptime_s}
Loading

Reviews (3): Last reviewed commit: "node: use own-property check in withFiel..." | Re-trigger Greptile

Comment thread src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts Outdated
Comment thread src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
src/libs/network/routines/nodecalls/storageProgramShared.ts (2)

57-63: 💤 Low value

Inconsistent shape vs the other rpc* helpers.

rpcBadRequest and rpcPermissionDenied both put the human message in error and the machine code in errorCode. rpcInternalError instead sets both to "INTERNAL_ERROR" and stuffs the actual message into extra. SDK clients reading the standard (error, errorCode) envelope from this helper get error === errorCode, which loses the original throw context.

Also, the non-Error branch loses information entirely by collapsing to "Unknown error"String(error) would preserve at least the throwable's coerced form.

If the intent is to deliberately not leak internal details to the client, that's defensible — but in that case the shape divergence is worth a // intentional: don't leak server-side details comment so a reader doesn't "fix" it.

♻️ Suggested fix if leak-prevention isn't the intent
 export function rpcInternalError(error: unknown): RPCResponse {
+    const message =
+        error instanceof Error ? error.message : String(error)
     return rpc(
         500,
-        { error: "INTERNAL_ERROR", errorCode: "INTERNAL_ERROR" },
-        `Error: ${error instanceof Error ? error.message : "Unknown error"}`,
+        { error: message, errorCode: "INTERNAL_ERROR" },
     )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/network/routines/nodecalls/storageProgramShared.ts` around lines 57
- 63, rpcInternalError currently returns a different shape than
rpcBadRequest/rpcPermissionDenied (it sets both error and errorCode to
"INTERNAL_ERROR" and puts the real message into the extra field) and it
collapses non-Error throwables to "Unknown error"; update rpcInternalError to
follow the same envelope as the other helpers by returning error: <human
message> and errorCode: "INTERNAL_ERROR" (use the actual message for the human
message), convert non-Error values with String(error) to preserve information,
and if you intentionally want to avoid leaking details add an explicit comment
like "// intentional: don't leak server-side details" above rpcInternalError;
reference the rpcInternalError function and the rpc(...) call/ RPCResponse shape
when making the change.

247-255: ⚡ Quick win

Empty-string requesterAddress is not normalized to undefined here either.

If a caller passes data.requesterAddress = "", the typeof guard lets it through and it propagates into getAccessibleProgramcheckReadPermission(program, ""). For most ACL modes the result matches anonymous behavior, but for public mode the if (requesterAddress && acl.blacklisted?.includes(...)) check is skipped because "" is falsy — same data-visibility but a different code path than a true anonymous caller.

This mirrors the exact inconsistency flagged in src/features/storageprogram/routes.ts. A length check keeps the contract uniform across the RPC and HTTP surfaces:

♻️ Suggested normalization
             const requesterAddress =
-                typeof data?.requesterAddress === "string"
+                typeof data?.requesterAddress === "string" &&
+                data.requesterAddress.length > 0
                     ? data.requesterAddress
                     : undefined
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/network/routines/nodecalls/storageProgramShared.ts` around lines 247
- 255, The requesterAddress string is accepted even when empty, causing
inconsistent behavior vs anonymous callers; update the normalization where
requesterAddress is set in storageProgramShared (the block assigning
requesterAddress from data.requesterAddress) to treat empty strings as undefined
(e.g., only accept when typeof data.requesterAddress === "string" AND
data.requesterAddress.length > 0) so that downstream functions like
getAccessibleProgram and checkReadPermission receive undefined for anonymous
callers rather than an empty string.
tests/storageprogram/routines.test.ts (1)

1095-1150: 💤 Low value

LGTM — solid coverage of the three branches, with one small test rigor opportunity.

The non-owner test at Lines 1133-1150 is the important one for SQL-injection safety since it asserts the requester is bound as a parameter (not concatenated).

Minor: the anonymous test at Lines 1116-1131 only asserts 'public' is present in some andWhere call. Since the non-anonymous predicate also contains 'public', this assertion would pass for non-anonymous too. The sibling search test at Lines 1183-1202 already uses the stricter "includes 'public' AND NOT includes :requesterAddress" pattern — the same shape would tighten this test:

♻️ Suggested test rigor improvement
-            // Anonymous gets the public-only branch, no requesterAddress param.
-            expect(findAndWhereCall(qb, "'public'")).toBeDefined()
+            // Anonymous gets the public-only branch, no requesterAddress param.
+            const hasPublicOnly = qb.andWhere.mock.calls.some(
+                (c: unknown[]) =>
+                    typeof c[0] === "string" &&
+                    (c[0] as string).includes("'public'") &&
+                    !(c[0] as string).includes(":requesterAddress"),
+            )
+            expect(hasPublicOnly).toBe(true)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/storageprogram/routines.test.ts` around lines 1095 - 1150, The
anonymous-branch test for getStorageProgramsByOwner should be tightened to
ensure the ACL predicate is *public-only* (no requester parameter); update the
anonymous test that uses makeMockQueryBuilder()/repo.createQueryBuilder and
findAndWhereCall to assert the found andWhere contains "'public'" AND that
findAndWhereCall(qb, ":requesterAddress") is undefined (or that its params do
not include requesterAddress) so the test fails if the non-anonymous predicate
is used; modify only the assertions in that test (keep use of repo, qb, and
findAndWhereCall).
src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts (1)

941-979: 💤 Low value

LGTM — predicate is parameterized and logic mirrors checkReadPermission.

The five ACL branches correspond 1:1 with the JS implementation, including owner-overrides-blacklist in restricted mode and "no blacklist for anonymous public". The use of to_jsonb(:requesterAddress::text) with TypeORM's named-param binding keeps this safe against SQL injection regardless of caller-supplied requester strings, which is what the regression test at tests/storageprogram/routines.test.ts:1133-1150 exercises.

One minor note: alias is interpolated directly into the SQL. It is only ever called with the default "sp" today, but if a future caller passes user-derived input it would be unsafe. Worth either a private invariant comment, or asserting the alias against a small allow-list before interpolation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts` around
lines 941 - 979, The SQL builds use the alias parameter in
readReachablePredicate (alias) which is interpolated directly into the query and
could be unsafe if a caller ever supplies user-derived input; fix by
constraining/sanitizing alias before interpolation (e.g., assert alias is one of
an allow-list like 'sp' (or a small set you expect) or validate it against a
safe identifier regex /^[A-Za-z_][A-Za-z0-9_]*$/ and throw if it fails), and add
a short private-invariant comment above readReachablePredicate documenting that
alias must be validated/not come from user input; implement the check at the top
of readReachablePredicate and keep the rest of the SQL generation unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/features/storageprogram/routes.ts`:
- Around line 632-657: The parsing that maps an empty identity post-colon
segment (e.g. "ed25519:") to undefined is implemented in listByOwnerHandler but
missing in getRequesterAddress and searchByNameHandler; update
getRequesterAddress to mirror the same logic (split on ":" and return splits[1]
only if non-empty, otherwise undefined) and modify searchByNameHandler to call
getRequesterAddress (or reuse the same normalization logic) instead of inlining
splits[1] so that requesterAddress becomes undefined for empty segments and the
SQL ACL anonymous branch is used consistently (refer to functions
getRequesterAddress, listByOwnerHandler, and searchByNameHandler).

In `@src/libs/network/routines/nodecalls/getStorageProgramItem.ts`:
- Around line 53-55: The check "field in obj" in getStorageProgramItem.ts can
return true for inherited properties; change the presence test to an
own-property check (e.g., use Object.prototype.hasOwnProperty.call(obj, field)
or Object.hasOwn(obj, field)) when evaluating obj and field before returning
rpc(404, ...). Update the conditional that currently reads "if (!(field in
obj))" to use the own-property test so only actual JSON storage keys trigger
existence, leaving the rest of the logic (obj, field, rpc(404,...)) unchanged.

In `@src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts`:
- Around line 56-65: The current code in getStorageProgramsByOwner fetches the
entire ACL-filtered owner set then paginates in memory; change the call to push
pagination into the DB by updating
GCRStorageProgramRoutines.getStorageProgramsByOwner to accept limit and offset
(e.g., add parameters limit, offset) and apply .take(limit).skip(offset) /
equivalent in its query, then call it from this routine passing the existing
repository, requesterAddress and the limit/offset values so the repository-level
query returns only the paginated results which you then map with
toStorageProgramListItem.

In `@src/libs/network/routines/nodecalls/hasStorageProgramField.ts`:
- Around line 50-52: The existence check uses the `in` operator which matches
inherited keys; update the logic in hasStorageProgramField (the block computing
`exists`) to use an own-property check instead: keep the `isJsonObject` guard
and replace `field in (program.data as Record<string, unknown>)` with a safe
hasOwnProperty call such as `Object.prototype.hasOwnProperty.call(program.data,
field)` (or equivalent) to avoid false positives from prototype properties.

---

Nitpick comments:
In `@src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts`:
- Around line 941-979: The SQL builds use the alias parameter in
readReachablePredicate (alias) which is interpolated directly into the query and
could be unsafe if a caller ever supplies user-derived input; fix by
constraining/sanitizing alias before interpolation (e.g., assert alias is one of
an allow-list like 'sp' (or a small set you expect) or validate it against a
safe identifier regex /^[A-Za-z_][A-Za-z0-9_]*$/ and throw if it fails), and add
a short private-invariant comment above readReachablePredicate documenting that
alias must be validated/not come from user input; implement the check at the top
of readReachablePredicate and keep the rest of the SQL generation unchanged.

In `@src/libs/network/routines/nodecalls/storageProgramShared.ts`:
- Around line 57-63: rpcInternalError currently returns a different shape than
rpcBadRequest/rpcPermissionDenied (it sets both error and errorCode to
"INTERNAL_ERROR" and puts the real message into the extra field) and it
collapses non-Error throwables to "Unknown error"; update rpcInternalError to
follow the same envelope as the other helpers by returning error: <human
message> and errorCode: "INTERNAL_ERROR" (use the actual message for the human
message), convert non-Error values with String(error) to preserve information,
and if you intentionally want to avoid leaking details add an explicit comment
like "// intentional: don't leak server-side details" above rpcInternalError;
reference the rpcInternalError function and the rpc(...) call/ RPCResponse shape
when making the change.
- Around line 247-255: The requesterAddress string is accepted even when empty,
causing inconsistent behavior vs anonymous callers; update the normalization
where requesterAddress is set in storageProgramShared (the block assigning
requesterAddress from data.requesterAddress) to treat empty strings as undefined
(e.g., only accept when typeof data.requesterAddress === "string" AND
data.requesterAddress.length > 0) so that downstream functions like
getAccessibleProgram and checkReadPermission receive undefined for anonymous
callers rather than an empty string.

In `@tests/storageprogram/routines.test.ts`:
- Around line 1095-1150: The anonymous-branch test for getStorageProgramsByOwner
should be tightened to ensure the ACL predicate is *public-only* (no requester
parameter); update the anonymous test that uses
makeMockQueryBuilder()/repo.createQueryBuilder and findAndWhereCall to assert
the found andWhere contains "'public'" AND that findAndWhereCall(qb,
":requesterAddress") is undefined (or that its params do not include
requesterAddress) so the test fails if the non-anonymous predicate is used;
modify only the assertions in that test (keep use of repo, qb, and
findAndWhereCall).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de30184e-db82-4e6c-9d8c-6b6e947f62fb

📥 Commits

Reviewing files that changed from the base of the PR and between 28a161f and 7e8460a.

📒 Files selected for processing (28)
  • history/planning/sdk-capability-detection.md
  • src/features/storageprogram/routes.ts
  • src/libs/blockchain/chain.ts
  • src/libs/blockchain/chainTransactions.ts
  • src/libs/blockchain/chainTypes.ts
  • src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts
  • src/libs/blockchain/mempool.ts
  • src/libs/network/bunServer.ts
  • src/libs/network/handlers/index.ts
  • src/libs/network/handlers/storageProgramHandlers.ts
  • src/libs/network/handlers/transactionHandlers.ts
  • src/libs/network/manageGCRRoutines.ts
  • src/libs/network/manageNodeCall.ts
  • src/libs/network/middleware/rateLimiter.ts
  • src/libs/network/routines/nodecalls/getStorageProgram.ts
  • src/libs/network/routines/nodecalls/getStorageProgramAll.ts
  • src/libs/network/routines/nodecalls/getStorageProgramFieldType.ts
  • src/libs/network/routines/nodecalls/getStorageProgramFields.ts
  • src/libs/network/routines/nodecalls/getStorageProgramItem.ts
  • src/libs/network/routines/nodecalls/getStorageProgramValue.ts
  • src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts
  • src/libs/network/routines/nodecalls/getTransactionStatus.ts
  • src/libs/network/routines/nodecalls/hasStorageProgramField.ts
  • src/libs/network/routines/nodecalls/searchStoragePrograms.ts
  • src/libs/network/routines/nodecalls/storageProgramShared.ts
  • src/libs/network/server_rpc.ts
  • src/utilities/sharedState.ts
  • tests/storageprogram/routines.test.ts

Comment thread src/features/storageprogram/routes.ts Outdated
Comment thread src/libs/network/routines/nodecalls/getStorageProgramItem.ts
Comment thread src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts Outdated
Comment thread src/libs/network/routines/nodecalls/hasStorageProgramField.ts
tcsenpai added 5 commits May 5, 2026 19:09
readReachablePredicate's group-membership branch called jsonb_each on
sp.acl->'groups' unconditionally. Postgres errors with "cannot call
jsonb_each on a non-object" when the operand is null or any non-object
jsonb value, which would 500 list/search queries for any restricted
program persisted with groups: null (allowed by the schema —
StorageProgramACL.groups is optional and checkReadPermission only
iterates when truthy).

Add a jsonb_typeof guard before the EXISTS clause so the branch only
evaluates when groups is an object, mirroring the JS check.

Closes myc #47.
`field in obj` matches inherited prototype keys, so a request for
field='toString' or '__proto__' against any program would incorrectly
report the field as present. TypeORM's jsonb columns hydrate to plain
objects with Object.prototype in their chain, so every JS object has
those keys.

Observable effects of the bug:
  - hasStorageProgramField returned {field: 'toString', exists: true}
    when the user has no such field — confirms a non-existent key
  - getStorageProgramItem with field='toString' passed the existence
    check, then failed Array.isArray() and returned the wrong error
    code (INVALID_FIELD_TYPE 400 instead of FIELD_NOT_FOUND 404)

Replace `field in obj` with Object.prototype.hasOwnProperty.call(obj,
field) in both handlers so only real user-written keys count.

Closes myc #50.
The withFieldRead higher-order helper accepted requesterAddress=""
while the sibling handlers (searchStoragePrograms,
getStorageProgramsByOwner) gate on .length > 0. Real-world ACL impact
is nil — no real program has owner='' or allowed.includes('') — but
the inconsistency is a footgun and could matter if a future ACL branch
treats empty string differently from undefined.

Closes myc #51.
The anonymous test asserted only that some andWhere call contained
'public', but the non-anonymous predicate ALSO contains 'public'
(the public-mode + blacklist branch). The assertion would have passed
even if the wrong branch ran.

Use the same shape the search test already uses: assert that some
andWhere call contains 'public' AND does NOT bind :requesterAddress.

Closes myc #52.
readReachablePredicate interpolates `alias` directly into the SQL
strings (`${a}.acl->>'mode'`, etc.). The function is private and only
called internally with the default "sp", so there is no current
exploitable surface — but the parameter exists, and a future caller
passing user-derived input would be a SQL-injection vector.

Add a defense-in-depth identifier-shape check at the top of the
function; throw on anything outside `[A-Za-z_][A-Za-z0-9_]*`. The
throw branch is unreachable for current callers.

Closes myc #53.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts`:
- Around line 941-990: readReachablePredicate currently treats only undefined as
anonymous but checkReadPermission treats an empty string "" as anonymous too;
update the anonymous check in readReachablePredicate (function
readReachablePredicate) to treat "" the same as undefined (e.g., change the
conditional that tests requesterAddress to also consider requesterAddress ===
''), and ensure the anonymous branch still returns the anonymous SQL and empty
params so empty-string callers follow the same ACL path as checkReadPermission.

In `@src/libs/network/routines/nodecalls/storageProgramShared.ts`:
- Around line 268-274: The shared field reader uses the prototype-inclusive
check `field in obj` which can return inherited names; update the check in the
reader (the logic around `const obj = program.data as Record<string, unknown>`
inside withFieldRead/shared field reader) to use an own-property test such as
Object.prototype.hasOwnProperty.call(obj, field) (or Object.hasOwn(obj, field)
if you prefer modern API) and ensure obj is a non-null object before checking so
that inherited properties like toString/__proto__ are not reported as real
storage fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7e8414f0-80ef-4639-b5ac-0300162e3fa7

📥 Commits

Reviewing files that changed from the base of the PR and between 7e8460a and 6490b96.

📒 Files selected for processing (5)
  • src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts
  • src/libs/network/routines/nodecalls/getStorageProgramItem.ts
  • src/libs/network/routines/nodecalls/hasStorageProgramField.ts
  • src/libs/network/routines/nodecalls/storageProgramShared.ts
  • tests/storageprogram/routines.test.ts

Comment thread src/libs/blockchain/gcr/gcr_routines/GCRStorageProgramRoutines.ts Outdated
Comment thread src/libs/network/routines/nodecalls/storageProgramShared.ts
tcsenpai added 5 commits May 6, 2026 15:43
Previously the routine fetched the full ACL-filtered owner result set
and the RPC handler sliced in JS. For owners with hundreds or
thousands of programs this materialised the entire set per request,
wasting DB bandwidth and JS heap.

Apply LIMIT/OFFSET at the SQL layer in both the owner fast-path
(repository.find) and the ACL QueryBuilder branch, mirroring the
sibling searchStorageProgramsByName.

Default limit is 200 today (matches the RPC handler's existing clamp),
clamped to [1, 200]. Documented as scheduled to drop to 100 in a
future release — callers that rely on the implicit cap should pass
explicit pagination.

Updated all four call sites:
  - src/libs/network/routines/nodecalls/getStorageProgramsByOwner.ts
    (drops the JS slice; passes {limit, offset})
  - src/features/storageprogram/routes.ts (HTTP listByOwnerHandler)
    accepts ?limit=&offset= query params, defaults to 200
  - src/libs/network/manageGCRRoutines.ts (legacy gcr_routine path):
    accepts an optional params[2] options object for {limit, offset}

The existing owner-fast-path test still passes because
expect.objectContaining doesn't require exact key match. Test mock
helper now mocks take/skip unconditionally so anonymous/non-owner
tests don't trip on the new SQL pagination calls.

Closes myc #48.
`getRequesterAddress` returned the post-colon segment unconditionally,
so an `identity: "ed25519:"` header (empty after the prefix) yielded
`""` instead of `undefined`. Two route handlers (listByOwnerHandler
and searchByNameHandler) re-implemented the parse inline without that
guard, and only listByOwnerHandler had previously been patched
locally.

Audit of all 7 callers shows the result feeds into
`getAccessibleProgram` -> `checkReadPermission`, which treats `""`
falsy in every branch:
  - public mode: blacklist check is gated on `requesterAddress &&`
  - owner mode: `requesterAddress === program.owner` (empty never
    matches a real owner)
  - restricted mode: early-returns on `if (!requesterAddress)`

So returning undefined for an empty post-colon segment is
behavior-preserving for every existing caller while bringing the
helper in line with the SQL ACL contract elsewhere.

Also drop the inline parses in both handlers and reuse the helper.

Closes myc #49.
…nDenied

The previous shape collapsed both `error` and `errorCode` to the
literal "INTERNAL_ERROR" and stuffed the actual exception message into
the `extra` field. SDK clients reading the standard {error, errorCode}
envelope thus saw error === errorCode and lost the original throw
context — and the SDK's `StorageProgramResponse` type only declares
`error?` and `errorCode?`, never `extra`, so the message was
effectively dead data.

Bring the helper in line with the other rpc* helpers: `error` carries
the human message, `errorCode` carries the stable machine identifier.
For non-Error throws fall back to `String(error)` instead of "Unknown
error" so plain objects/numbers/null still produce useful diagnostics.

This is a wire-shape change for 500 responses but every consumer of
the helpers reads the standardised envelope, so existing failure paths
that were ignoring `extra` start surfacing the real message in the
expected slot.

Closes myc #54.
…te boundary

The SQL ACL primitive strict-equalled `undefined` for the anonymous
branch, but `checkReadPermission` treats `""` as anonymous in every
branch via falsy checks. All current callers already gate on
`.length > 0` before passing through, so the divergence isn't reachable
today — but the function should be self-protecting against future
callers, matching the same defense-in-depth rationale used for the
alias regex check.

Add a one-line normalization at the top of the function and bind the
normalized value as the SQL parameter.

Closes myc #58.
#50 fixed the prototype-matching bug in the standalone handlers
(getStorageProgramItem, hasStorageProgramField) but missed the
withFieldRead higher-order helper, which propagated the same bug to
the two handlers built on it: getStorageProgramValue and
getStorageProgramFieldType.

Effect of the original bug:
  - getStorageProgramValue with field='toString' returned the
    Object.prototype.toString function reference
  - getStorageProgramFieldType with field='toString' returned
    {type: 'object'}
both confirming a non-existent user field exists.

Replace `field in obj` with Object.prototype.hasOwnProperty.call —
same fix as #50.

Closes myc #59.
@sonarqubecloud

sonarqubecloud Bot commented May 6, 2026

Copy link
Copy Markdown

@tcsenpai
tcsenpai merged commit 6bf3707 into stabilisation May 6, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant