diff --git a/.rush/temp/shrinkwrap-deps.json b/.rush/temp/shrinkwrap-deps.json index b386e03..a0f8fbd 100644 --- a/.rush/temp/shrinkwrap-deps.json +++ b/.rush/temp/shrinkwrap-deps.json @@ -1,5 +1,5 @@ { - "../../node": "../../node:hnTERTpEMb2w8HOm+aqGL5QPvYaFE1ypPQ9z8hteNMo=:", + "../../node": "../../node:T9AEHYRQ22jknBJL2JzA1IWbgqqvke1gidVuQnOkgE8=:", "/@babel/code-frame/7.29.0": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "/@babel/compat-data/7.29.0": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "/@babel/core/7.29.0": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", @@ -118,7 +118,7 @@ "/@socket.io/component-emitter/3.1.2": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "/@tootallnate/once/2.0.0": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", "/@tootallnate/quickjs-emscripten/0.23.0": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "/@trpc/client/11.0.0-rc.660_dec9ab05b59cfcb251b83d88cde10054": "sha512-bNpkZEfyMGKHynYFxdLpY8nJ1n7E3JHKcd4Pe2cagmpkzOEF9tFT3kzNf+eLI8XMG8196lTRR0J0W2/1Q8/cug==", + "/@trpc/client/11.0.0-rc.660_@trpc+server@11.0.0-rc.660": "sha512-bNpkZEfyMGKHynYFxdLpY8nJ1n7E3JHKcd4Pe2cagmpkzOEF9tFT3kzNf+eLI8XMG8196lTRR0J0W2/1Q8/cug==", "/@trpc/server/11.0.0-rc.660": "sha512-QUapcZCNOpHT7ng9LceGc9ImkboWd0Go9ryrduZpL+p4jdfaC6409AQ3x4XEW6Wu3yBmZAn4CywCsDrDhjDy/w==", "/@types/babel__core/7.20.5": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "/@types/babel__generator/7.27.0": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", @@ -389,6 +389,7 @@ "/find-up/5.0.0": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "/flat-cache/3.2.0": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "/flatted/3.3.3": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "/follow-redirects/1.15.11": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "/follow-redirects/1.15.11_debug@4.3.4": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "/for-each/0.3.5": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "/form-data-encoder/1.7.2": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", diff --git a/ANALYSIS.md b/ANALYSIS.md index 1ca0c73..11847ea 100644 --- a/ANALYSIS.md +++ b/ANALYSIS.md @@ -16,8 +16,8 @@ Core Node SDK/runtime utility package for Arken protocol, data handling, and gam - `.erb/`: documentation/scaffolding branding assets (currently `img/logo.png`) with low runtime risk but potential UX drift if assets are changed ad hoc. - `coverage/`: generated LCOV/Clover/JSON coverage artifacts; useful diagnostics but should remain generated-only to avoid noisy/manual drift (now documented with local `coverage/{README.md,ANALYSIS.md}`). - `websocket.ts`: lightweight socket helper exposing `emitAll`/`emitDirect` and `getClientSocket`; currently uses untyped emitter params and no explicit reconnect/backoff policy controls at this utility boundary. -- `api.ts`: query-to-Mongo filter adapter (`getFilter`) and HTTP POST helper (`fetch`) used for dynamic filtering and remote query dispatch; id-field normalization applies consistently across `equals`/`contains`/`in` operators, scalar shorthand field values map to equality filters (root + nested logical nodes), non-plain object values (e.g., `Date`, ObjectId-like values) are preserved as equality filters, plain-object values without operator keys are now preserved as direct equality filters (instead of being silently dropped), logical nesting preserves child `OR` groups inside parent `AND` clauses, singleton-object shorthand for logical groups (`OR`/`AND` as object instead of array) is normalized for resilient filter parsing, empty `in: []` clauses are now treated as no-op fragments (avoids accidental always-empty result filters in mixed logical queries), and the HTTP helper now fail-fast validates URL/query payload shape, trims validated URL input before dispatch, and applies a deterministic axios timeout to avoid unbounded hangs. -- `util.ts`: currently re-exports from `'.'`, creating a circular/umbrella alias surface that can obscure intended subpath ownership. +- `api.ts`: query-to-Mongo filter adapter (`getFilter`) and HTTP POST helper (`fetch`) used for dynamic filtering and remote query dispatch; id-field normalization applies consistently across `equals`/`contains`/`in` operators, scalar shorthand field values map to equality filters (root + nested logical nodes), non-plain object values (e.g., `Date`, ObjectId-like values) are preserved as equality filters, plain-object values without operator keys are now preserved as direct equality filters (instead of being silently dropped), logical nesting preserves child `OR` groups inside parent `AND` clauses, singleton-object shorthand for logical groups (`OR`/`AND` as object instead of array) is normalized for resilient filter parsing, root-level array-shaped `where` payloads are now rejected as invalid/no-op filter input (prevents accidental numeric-key filter generation), empty `in: []` and `equals: undefined` clauses are now treated as no-op fragments (avoids malformed/noisy filters in mixed logical queries), and the HTTP helper now fail-fast validates URL/query payload shape, trims validated URL input before dispatch, enforces http(s)-only absolute URL protocols, and applies a deterministic axios timeout to avoid unbounded hangs. +- `util.ts`: now explicitly re-exports utility bridges from `util/api` and `util/rpc`, removing prior package-root circular aliasing and clarifying subpath ownership. - root build/test config (`package.json`, `tsconfig*.json`, `jest.unit.config.js`): defines compile/test pipeline, export surface, and strictness defaults for the whole package. ## Omniverse architecture perspective diff --git a/api.ts b/api.ts index f40de40..d7fac47 100644 --- a/api.ts +++ b/api.ts @@ -13,7 +13,7 @@ function escapeRegExp(s: string) { export function getFilter(query: any): Record { const where = query?.where; - if (!where || typeof where !== 'object') return {}; + if (!where || typeof where !== 'object' || Array.isArray(where)) return {}; // Helper to turn a single field condition into a Mongo filter fragment const buildField = (field: string, cond: any) => { @@ -34,6 +34,7 @@ export function getFilter(query: any): Record { } if ('equals' in cond) { + if (cond.equals === undefined) return undefined; return { [normalizedField]: cond.equals }; } @@ -108,6 +109,17 @@ export async function fetch(url: string, query: FetchQuery): Promise { } const normalizedUrl = url.trim(); + let parsedUrl: URL; + + try { + parsedUrl = new URL(normalizedUrl); + } catch { + throw new Error('Invalid fetch URL'); + } + + if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { + throw new Error('Invalid fetch URL'); + } const res = await axios.post(normalizedUrl, query, { timeout: DEFAULT_FETCH_TIMEOUT_MS, diff --git a/binary.ts b/binary.ts index 1884349..93073ff 100644 --- a/binary.ts +++ b/binary.ts @@ -41,7 +41,10 @@ export function decodePayload(msg) { return json; } catch (err) { - // ... - console.log(err, msg); + const raw = typeof msg === 'string' ? msg : String(msg ?? ''); + const preview = raw.length > 40 ? `${raw.slice(0, 40)}...` : raw; + const errorMessage = err instanceof Error ? err.message : String(err); + console.warn(`decodePayload failed: ${errorMessage}; payloadPreview=${preview}`); + return undefined; } } diff --git a/test/ANALYSIS.md b/test/ANALYSIS.md index ed4e627..8e9c6c2 100644 --- a/test/ANALYSIS.md +++ b/test/ANALYSIS.md @@ -31,5 +31,10 @@ - Added malformed-network-response regression coverage for `httpProvider` to ensure non-Fetch-like response objects (including non-finite status metadata) are rejected with deterministic provider errors before runtime method access faults or invalid status propagation. - Added fetch-rejection normalization coverage for `httpProvider` because runtime fetch implementations may throw primitives or non-standard errors; tests now lock deterministic `RequestError` envelopes for both primitive and Error-like rejection paths. - Added response-body read-failure regression coverage for `httpProvider` because some runtimes can throw on `response.text()` stream reads; tests now lock deterministic `Invalid provider response` envelope normalization. -- Added `api/getFilter` regression coverage to ensure `id` criteria normalize to `_id` across `equals`/`in`/`contains` operators, scalar shorthand conditions (e.g., `{ id: 'abc' }`) are treated as equality checks in both root and nested logical nodes, singleton-object `OR`/`AND` shorthand is normalized into logical clauses, plain-object values without operator keys are preserved as equality filters (instead of being dropped), empty `contains` clauses remain no-op within logical groups, empty `in: []` clauses are also treated as no-op fragments (preventing accidental always-empty filters), and nested logical groups (e.g., `AND` containing `OR`) are preserved in generated Mongo filters. -- Added `api/fetch` regression coverage because helper calls can fail non-deterministically without envelope validation/timeouts; tests now lock fail-fast rejection for blank URL and non-object query payloads, verify URL inputs are trimmed before dispatch, and verify axios requests always include deterministic timeout (`10000ms`). +- Added cache-write failure regression coverage for `httpProvider` because Cache API persistence can fail at runtime even when read paths exist; tests now lock best-effort behavior so transport success is not coupled to cache `put` availability. +- Added constructor-URL validation regression coverage for `httpProvider` because malformed endpoint strings previously surfaced raw runtime URL parsing exceptions; tests now lock deterministic `RequestError` metadata (`-32602`, `Invalid provider URL`) for safer upstream error handling. +- Added constructor protocol-guard regression coverage for `httpProvider` because non-http URL schemes (for example `ws:`) are incompatible with the helper’s HTTP fetch transport path; tests now lock deterministic early rejection (`-32602`) instead of deferred runtime failures. +- Added `api/getFilter` regression coverage to ensure `id` criteria normalize to `_id` across `equals`/`in`/`contains` operators, scalar shorthand conditions (e.g., `{ id: 'abc' }`) are treated as equality checks in both root and nested logical nodes, singleton-object `OR`/`AND` shorthand is normalized into logical clauses, plain-object values without operator keys are preserved as equality filters (instead of being dropped), root array-shaped `where` payloads are now rejected as no-op filters to avoid accidental index-key query fragments, empty `contains` clauses remain no-op within logical groups, empty `in: []` clauses are also treated as no-op fragments (preventing accidental always-empty filters), `equals: undefined` fragments are ignored as no-op filters (preventing malformed `{ field: undefined }` predicates), and nested logical groups (e.g., `AND` containing `OR`) are preserved in generated Mongo filters. +- Added `api/fetch` regression coverage because helper calls can fail non-deterministically without envelope validation/timeouts; tests now lock fail-fast rejection for blank/malformed/non-http(s) URL values and non-object query payloads, verify URL inputs are trimmed before dispatch, and verify axios requests always include deterministic timeout (`10000ms`). +- Added `binary/decodePayload` regression coverage to lock deterministic behavior for malformed binary payloads: decode failures now return `undefined` and log only a short payload preview (instead of dumping the full raw payload), reducing accidental sensitive-data exposure in routine logs while preserving troubleshooting context. +- Added `util` subpath export regression coverage (`test/util.spec.ts`) to verify API/RPC helpers remain available without depending on circular `util.ts -> '.'` package-root re-export coupling. diff --git a/test/README.md b/test/README.md index 3113948..1f3a0db 100644 --- a/test/README.md +++ b/test/README.md @@ -5,6 +5,8 @@ Protocol-focused tests for `@arken/node` socket tRPC wrappers. ## Files - `socketLink.spec.ts`: client transport routing, callback lifecycle, duplicate-delivery idempotency, unsubscribe/teardown-vs-late-response no-op behavior (link + proxy), timeout-vs-late-response race handling, single-terminal-settlement guards for resolve/error permutations (link + proxy paths), callback-boundary resolve-throw fallback behavior for mixed `error`/`result` envelopes, ID-collision guardrails, timeout reqId metadata checks, late response handling, malformed payload permutations, strict response-id handling (non-string/blank IDs), own-property callback matching/prototype-key safety (including inherited and `__proto__` ids), deserialize-failure propagation, immediate same-tick response handling, backend-only path rejection (missing method segment), alternate response IDs, server-push malformed-param resilience (for both `trpc` and `trpcResponse` fallback paths), malformed push-method filtering, and `onAny` support including non-response filtering + fallback/teardown edge paths. - `socketServer.spec.ts`: server dispatch, malformed payload handling (including invalid binary-string decode paths and decoded non-string/blank-string method payloads), response-id normalization checks (trimmed IDs on success; non-string/blank IDs dropped on error paths), whitespace-trimmed valid-method dispatch behavior, prototype/constructor path traversal rejection (including surrounding-whitespace, exact `constructor`, `prototype` segment, nested-segment variants, inherited built-in prototype paths like `core.toString`, inherited array-prototype method paths like `core.list.map`, and expanded inherited typed-array prototype methods like `core.bytes.map` and `core.floats.map`), empty-segment and whitespace-segment method-path rejection (`core..ping`, `core. ping`), missing method behavior, invalid/undecodable `params` payload behavior, listener attach/detach wiring, and safe no-op behavior when sockets do not expose `on`/`off` hooks. -- `httpProvider.spec.ts`: verifies `web3/httpProvider` honors explicit constructor URLs, rejects malformed non-object JSON-RPC payloads with deterministic `-32600` invalid-request errors, normalizes whitespace-padded JSON-RPC method names before network submission, preserves caller-provided JSON-RPC IDs (including explicit `null`, with fallback `56` only when `id` is absent), keeps caller-owned request objects immutable while applying defaults internally, degrades safely to network-only flow when Cache API globals are unavailable, ignores malformed cache hits by refetching from network, rejects hung requests once provider timeout budget is exceeded, aborts in-flight fetches on timeout when abort signaling is available, normalizes abort-triggered fetch rejections into timeout-shaped `RequestError` envelopes, fails closed on 403 when no alternate providers are configured (no unbounded retry recursion), normalizes non-object JSON response envelopes (for example `null`) without runtime `TypeError` crashes, coerces malformed RPC error envelopes into deterministic numeric-code/message fallbacks, rejects malformed non-Fetch-like network response objects (including non-finite status values) with a deterministic provider error envelope, normalizes `response.text()` read/stream failures into deterministic provider errors, and normalizes both primitive and Error-like fetch rejections into stable provider `RequestError` envelopes. -- `api.spec.ts`: verifies `getFilter` maps `id` operator variants (`equals`/`in`/`contains`) to `_id` consistently, supports scalar shorthand equality inputs at root and nested logical nodes (e.g., `{ id: 'abc' }`), normalizes singleton-object logical shorthand (`OR`/`AND` as objects) into valid clauses, preserves non-plain object equality values (e.g., `Date`, `ObjectId`) instead of dropping them, preserves plain-object equality filters when no operator keys are present, drops no-op empty `contains` fragments inside logical OR/AND clauses, drops no-op empty `in: []` fragments inside logical OR/AND clauses, and preserves nested logical composition (e.g., `AND` entries containing `OR` groups). Also covers `api/fetch` fail-fast validation for blank URL/non-object query payloads, validates URL-trimming before dispatch, and validates deterministic axios timeout configuration (`10000ms`). +- `httpProvider.spec.ts`: verifies `web3/httpProvider` honors explicit constructor URLs (including whitespace-padded URL input trimming), rejects malformed/non-http(s) constructor URL values with deterministic `-32602` envelopes, rejects malformed non-object JSON-RPC payloads with deterministic `-32600` invalid-request errors, normalizes whitespace-padded JSON-RPC method names before network submission, preserves caller-provided JSON-RPC IDs (including explicit `null`, with fallback `56` only when `id` is absent), keeps caller-owned request objects immutable while applying defaults internally, degrades safely to network-only flow when Cache API globals are unavailable, ignores malformed cache hits by refetching from network, treats cache-write failures as best-effort (request still resolves), rejects hung requests once provider timeout budget is exceeded, aborts in-flight fetches on timeout when abort signaling is available, normalizes abort-triggered fetch rejections into timeout-shaped `RequestError` envelopes, fails closed on 403 when no alternate providers are configured (no unbounded retry recursion), normalizes non-object JSON response envelopes (for example `null`) without runtime `TypeError` crashes, coerces malformed RPC error envelopes into deterministic numeric-code/message fallbacks, rejects malformed non-Fetch-like network response objects (including non-finite status values) with a deterministic provider error envelope, normalizes `response.text()` read/stream failures into deterministic provider errors, and normalizes both primitive and Error-like fetch rejections into stable provider `RequestError` envelopes. +- `api.spec.ts`: verifies `getFilter` maps `id` operator variants (`equals`/`in`/`contains`) to `_id` consistently, supports scalar shorthand equality inputs at root and nested logical nodes (e.g., `{ id: 'abc' }`), normalizes singleton-object logical shorthand (`OR`/`AND` as objects) into valid clauses, preserves non-plain object equality values (e.g., `Date`, `ObjectId`) instead of dropping them, preserves plain-object equality filters when no operator keys are present, rejects array-shaped root `where` payloads as empty/no-op filters (avoids accidental numeric-key fragments), drops no-op empty `contains` fragments inside logical OR/AND clauses, drops no-op empty `in: []` fragments inside logical OR/AND clauses, and preserves nested logical composition (e.g., `AND` entries containing `OR` groups). Also covers `api/fetch` fail-fast validation for blank/malformed/non-http(s) URLs and non-object query payloads, validates URL-trimming before dispatch, and validates deterministic axios timeout configuration (`10000ms`). +- `binary.spec.ts`: validates `binary/decodePayload` happy-path JSON decode from binary strings and failure-path behavior that returns `undefined` while logging only a short payload preview (reduces raw payload leakage in logs). +- `util.spec.ts`: verifies `@arken/node/util` subpath exports expose API/RPC helpers (`getFilter`, `serialize`, `deserialize`) without relying on circular package-root re-export behavior. - `NOTES.md`: tracking notes for additional test coverage. diff --git a/test/api.spec.ts b/test/api.spec.ts index d8ceda8..9dd3583 100644 --- a/test/api.spec.ts +++ b/test/api.spec.ts @@ -54,6 +54,19 @@ describe('api/getFilter', () => { }); }); + test('ignores undefined equals fragments to avoid malformed filters', () => { + expect( + getFilter({ + where: { + OR: [{ id: { equals: undefined } }, { status: { equals: 'active' } }], + AND: [{ owner: { equals: undefined } }], + }, + }) + ).toEqual({ + $or: [{ status: 'active' }], + }); + }); + test('supports nested OR nodes inside AND clauses', () => { expect( getFilter({ @@ -142,6 +155,14 @@ describe('api/getFilter', () => { metadata: { rarity: 'legendary', flags: ['quest'] }, }); }); + + test('returns empty filter when where is an array', () => { + expect( + getFilter({ + where: [{ id: { equals: 'abc123' } }], + }) + ).toEqual({}); + }); }); describe('api/fetch', () => { @@ -151,6 +172,8 @@ describe('api/fetch', () => { test('rejects invalid URL values before network call', async () => { await expect(apiFetch(' ', { where: {} })).rejects.toThrow('Invalid fetch URL'); + await expect(apiFetch('example.com/graphql', { where: {} })).rejects.toThrow('Invalid fetch URL'); + await expect(apiFetch('javascript:alert(1)', { where: {} })).rejects.toThrow('Invalid fetch URL'); expect(mockedAxiosPost).not.toHaveBeenCalled(); }); diff --git a/test/binary.spec.ts b/test/binary.spec.ts new file mode 100644 index 0000000..c016bd5 --- /dev/null +++ b/test/binary.spec.ts @@ -0,0 +1,32 @@ +import { binaryAgent, decodePayload } from '../binary'; + +const toBinaryString = (value: string): string => + value + .split('') + .map((char) => char.charCodeAt(0).toString(2).padStart(8, '0')) + .join(' '); + +describe('binary/decodePayload', () => { + it('decodes binary-encoded JSON payloads', () => { + const payload = { hello: 'world', count: 2 }; + const encoded = toBinaryString(JSON.stringify(payload)); + + expect(binaryAgent(encoded)).toBe(JSON.stringify(payload)); + expect(decodePayload(encoded)).toEqual(payload); + }); + + it('returns undefined and logs only a short payload preview when decode fails', () => { + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); + const malformed = 'not-binary-json-payload-that-should-be-truncated-in-logs'; + + expect(decodePayload(malformed)).toBeUndefined(); + expect(warnSpy).toHaveBeenCalledTimes(1); + + const warningMessage = warnSpy.mock.calls[0][0] as string; + expect(warningMessage).toContain('decodePayload failed:'); + expect(warningMessage).toContain('payloadPreview=not-binary-json-payload-that-should-be-t...'); + expect(warningMessage).not.toContain('payload-that-should-be-truncated-in-logs'); + + warnSpy.mockRestore(); + }); +}); diff --git a/test/httpProvider.spec.ts b/test/httpProvider.spec.ts index 98f917d..30a7adc 100644 --- a/test/httpProvider.spec.ts +++ b/test/httpProvider.spec.ts @@ -71,6 +71,40 @@ describe('web3/httpProvider', () => { expect(provider.url.toString()).toBe('https://rpc.example.org/custom/path'); }); + test('trims constructor url input before parsing', () => { + const provider = new Provider(' https://rpc.example.org/trimmed/path '); + + expect(provider.host).toBe('rpc.example.org'); + expect(provider.path).toBe('/trimmed/path'); + expect(provider.url.toString()).toBe('https://rpc.example.org/trimmed/path'); + }); + + test('throws deterministic RequestError shape when constructor url is invalid', () => { + try { + new Provider('http://:invalid-url'); + throw new Error('expected constructor to throw'); + } catch (error: any) { + expect(error).toMatchObject({ + code: -32602, + message: 'Invalid provider URL', + data: null, + }); + } + }); + + test('rejects constructor urls with non-http protocols', () => { + try { + new Provider('ws://rpc.example.org/socket'); + throw new Error('expected constructor to throw'); + } catch (error: any) { + expect(error).toMatchObject({ + code: -32602, + message: 'Invalid provider URL', + data: null, + }); + } + }); + test('rejects non-object JSON-RPC request payloads', async () => { const provider = new Provider('https://rpc.example.org'); @@ -223,6 +257,21 @@ describe('web3/httpProvider', () => { expect((global as any).fetch).toHaveBeenCalledTimes(1); }); + test('treats cache write failures as best-effort and still resolves provider response', async () => { + (global as any).caches = { + open: jest.fn(async () => ({ + match: jest.fn(async () => null), + put: jest.fn(async () => { + throw new Error('cache write unavailable'); + }), + })), + }; + + const provider = new Provider('https://rpc.example.org'); + await expect(provider.request({ method: 'eth_chainId', params: [], id: 9012 })).resolves.toBe(9012); + expect((global as any).fetch).toHaveBeenCalledTimes(1); + }); + test('rejects when fetch exceeds provider timeout window', async () => { jest.useFakeTimers(); try { @@ -347,6 +396,28 @@ describe('web3/httpProvider', () => { await expect(provider.request({ method: 'eth_chainId', params: [], id: 1011 })).resolves.toBeUndefined(); }); + test('rejects invalid JSON response bodies with deterministic provider error', async () => { + class InvalidJsonBodyResponse { + ok = true; + status = 200; + statusText = 'OK'; + + async text() { + return 'not-json'; + } + } + + (global as any).fetch = jest.fn(async () => new InvalidJsonBodyResponse()); + + const provider = new Provider('https://rpc.example.org'); + + await expect(provider.request({ method: 'eth_chainId', params: [], id: 1018 })).rejects.toMatchObject({ + code: -32000, + message: 'Invalid provider response', + data: null, + }); + }); + test('normalizes malformed rpc error envelope to deterministic RequestError shape', async () => { class InvalidErrorEnvelopeResponse { ok = true; diff --git a/test/util.spec.ts b/test/util.spec.ts new file mode 100644 index 0000000..3b6152a --- /dev/null +++ b/test/util.spec.ts @@ -0,0 +1,18 @@ +// arken/packages/node/test/util.spec.ts + +import * as util from '../util'; + +describe('util subpath exports', () => { + test('exposes api helpers without circular root re-export dependency', () => { + expect(typeof util.getFilter).toBe('function'); + expect(util.getFilter({ where: { id: { equals: 'abc' } } })).toEqual({ _id: 'abc' }); + }); + + test('exposes rpc helpers', () => { + expect(typeof util.serialize).toBe('function'); + expect(typeof util.deserialize).toBe('function'); + + const encoded = util.serialize({ x: 1 }); + expect(util.deserialize(encoded)).toEqual({ x: 1 }); + }); +}); diff --git a/util.ts b/util.ts index dc0dc89..0381f69 100644 --- a/util.ts +++ b/util.ts @@ -1 +1,11 @@ -export * from '.'; +// arken/node/util.ts + +// Compatibility surface for legacy `@arken/node/util` imports. +// Re-export package-root utilities so callers expecting helpers like +// `getTime`, `random`, etc. continue to work. +export * from './index'; +export { default } from './index'; + +// Keep explicit utility module subpath re-exports available as well. +export * from './util/api'; +export * from './util/rpc'; diff --git a/util/ANALYSIS.md b/util/ANALYSIS.md new file mode 100644 index 0000000..a82b750 --- /dev/null +++ b/util/ANALYSIS.md @@ -0,0 +1,9 @@ +# arken/packages/node/util/ANALYSIS.md + +## Scope +Utility subpath re-export surface for `@arken/node`. + +## Change rationale (2026-02-21) +- Replaced root-level `util.ts` `export * from '.'` pattern with explicit `api` + `rpc` re-exports. +- Added dedicated `util/api.ts` and `util/rpc.ts` bridge files to avoid circular package-root coupling and keep subpath ownership explicit. +- Added targeted test coverage (`test/util.spec.ts`) to lock expected exports. diff --git a/util/README.md b/util/README.md new file mode 100644 index 0000000..fa14c7a --- /dev/null +++ b/util/README.md @@ -0,0 +1,10 @@ +# arken/packages/node/util + +Compatibility re-exports for utility-focused subpath imports. + +## Files +- `api.ts`: re-exports `../api` helpers. +- `rpc.ts`: re-exports `../rpc` helpers. + +## Why this exists +Keeps `@arken/node/util` focused on utility modules without re-exporting from package root (which risked circular export coupling). diff --git a/util/api.ts b/util/api.ts new file mode 100644 index 0000000..3c85cb6 --- /dev/null +++ b/util/api.ts @@ -0,0 +1,3 @@ +// arken/node/util/api.ts + +export * from '../api'; diff --git a/util/rpc.ts b/util/rpc.ts new file mode 100644 index 0000000..0982673 --- /dev/null +++ b/util/rpc.ts @@ -0,0 +1,3 @@ +// arken/node/util/rpc.ts + +export * from '../rpc'; diff --git a/web3/ANALYSIS.md b/web3/ANALYSIS.md index fa4270a..0b68a58 100644 --- a/web3/ANALYSIS.md +++ b/web3/ANALYSIS.md @@ -11,7 +11,9 @@ ## Key findings - Provider fallback pool remains hardcoded to default list (`bsc-dataseed1.ninicoin.io`) when constructor URL is not supplied. -- Constructor now honors explicit `url` input, reducing hidden endpoint drift. +- Constructor now honors explicit `url` input (with surrounding-whitespace trim), reducing hidden endpoint drift and avoiding false-negative URL parsing errors from padded env/config values. +- Constructor URL parsing now normalizes malformed endpoint inputs into deterministic request metadata (`RequestError` `-32602`) rather than surfacing runtime-specific `URL` parser exceptions. +- Constructor URL validation now also enforces http(s)-only protocols (rejects `ws:`/other schemes), which is warranted because downstream transport uses Fetch HTTP semantics and non-http schemes would fail later with less actionable errors. - Request shaping now preserves caller-supplied `request.id` (including explicit `null`); fallback `56` is only applied when the `id` field is absent. - Cache API usage is now runtime-guarded: request flow falls back to network-only mode when `caches`/`Request`/`Response` globals are unavailable. - Browser cache usage is now additionally gated by `BROWSER_CACHE_TTL > 0`; this aligns behavior with current default (`0`) so runtime cache is not populated unless explicitly enabled. @@ -28,10 +30,12 @@ - Method names are now normalized with `trim()` before submission so callers with padded method strings still produce canonical RPC method keys. - Request-default injection now uses a cloned envelope, preventing side-effect mutation of caller-provided JSON-RPC request objects. - Parsed response bodies are now envelope-normalized (`object` only) so primitive JSON payloads (for example `null`) do not trigger `'in'` operator runtime faults during error/result checks. +- Invalid JSON response bodies are now treated as deterministic provider failures (`Invalid provider response`) because silently coercing parse failures to `{}` masked upstream transport corruption and could leak undefined-success semantics to callers. - RPC error envelopes now normalize malformed payload metadata (blank/non-string `message`, non-numeric `code`) into deterministic `RequestError` defaults so upstream handlers do not receive undefined/stringly-typed error codes. - Network fetch results now validate a minimal Fetch-like response shape before status parsing (`ok/status/statusText/text`) and require `status` to be finite, preventing runtime crashes or malformed `NaN` status propagation when custom fetch polyfills return invalid response objects. - Non-RequestError fetch rejections are now normalized into deterministic provider `RequestError` envelopes (`-32000`) so caller behavior is stable even when runtimes throw primitives/non-standard errors. - Response body stream-read failures (`response.text()` throws/rejects) are now normalized to `Invalid provider response` so transport callers receive a deterministic provider-error envelope instead of runtime-specific stream exceptions. +- Cache writes are now explicitly best-effort (wrapped/suppressed) because some worker/browser runtimes expose read-capable cache handles with failing `put` behavior; this avoids surfacing cache persistence failures as transport request failures. ## Risks / gaps - Hardcoded provider endpoint and random re-selection logic reduce explicit environment control. diff --git a/web3/README.md b/web3/README.md index 4b31c0e..4950961 100644 --- a/web3/README.md +++ b/web3/README.md @@ -4,7 +4,9 @@ Legacy-compatible Web3 transport helpers. ## Files - `httpProvider.ts`: custom JSON-RPC provider wrapper with fetch + Cache API usage. - - Constructor now honors explicit URL input before falling back to default provider pool. + - Constructor now honors explicit URL input (after whitespace trim) before falling back to default provider pool. + - Invalid constructor URLs are now normalized into deterministic `RequestError` metadata (`code: -32602`, `message: Invalid provider URL`) instead of leaking raw `URL` parser exceptions. + - Constructor URL validation now explicitly rejects non-http(s) protocols (for example `ws:`), preventing invalid transport schemes from reaching HTTP fetch paths. - Request IDs are preserved when callers provide one (including explicit `null`); fallback ID `56` is only used when the `id` field is absent. - Cache API usage is now runtime-guarded; provider falls back to network-only request flow when `caches`/`Request`/`Response` globals are unavailable. - Browser cache writes/reads are now also gated by `BROWSER_CACHE_TTL > 0`; with the default `0`, requests avoid writing stale entries to runtime cache. @@ -18,10 +20,12 @@ Legacy-compatible Web3 transport helpers. - Whitespace-padded JSON-RPC method names are now normalized (`trim`) before network submission, preventing avoidable upstream method mismatch errors. - Provider request normalization no longer mutates caller-owned request objects while still applying deterministic JSON-RPC defaults (`jsonrpc`, fallback `id=56`). - Parsed non-object JSON response payloads (e.g. `null`) are normalized to an empty envelope, preventing `TypeError` during error/result field checks. + - Malformed JSON response bodies now fail closed with deterministic `Invalid provider response` errors instead of silently coercing to empty envelopes and returning `undefined` results. - Malformed RPC error envelopes now normalize to deterministic `RequestError` metadata (`message` fallback + numeric `code` fallback), avoiding undefined/string code leaks to callers. - Malformed network response objects that do not expose a valid Fetch-like shape (`ok/status/statusText/text`) or provide non-finite `status` values are now rejected early with a deterministic `Invalid provider response` error instead of propagating invalid status metadata. - Raw fetch rejections are normalized into deterministic `RequestError` envelopes (`code: -32000`), preserving Error messages when available and falling back to `Provider request failed` for non-Error throws. - Response body read failures (`response.text()` stream/read errors) are normalized to `Invalid provider response` to avoid leaking runtime-specific stream exceptions to higher-level callers. + - Runtime cache writes are now best-effort: cache `put` failures are swallowed so successful provider responses still resolve (and 403 handling still proceeds) even when Cache API persistence is unavailable. ## Notes - This folder currently exposes one monolithic provider implementation. diff --git a/web3/httpProvider.ts b/web3/httpProvider.ts index 77618a7..f49c229 100644 --- a/web3/httpProvider.ts +++ b/web3/httpProvider.ts @@ -54,10 +54,21 @@ export default class Provider { const providers = JSON.parse(PROVIDERS); + const normalizedInputUrl = typeof url === 'string' ? url.trim() : ''; const resolvedProviderUrl = - typeof url === 'string' && url.trim().length > 0 ? url : providers[Math.floor(Math.random() * providers.length)]; + normalizedInputUrl.length > 0 ? normalizedInputUrl : providers[Math.floor(Math.random() * providers.length)]; + + let parsedUrl: URL; + try { + parsedUrl = new URL(resolvedProviderUrl); + } catch { + throw new RequestError('Invalid provider URL', -32602, null); + } + + if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') { + throw new RequestError('Invalid provider URL', -32602, null); + } - const parsedUrl = new URL(resolvedProviderUrl); this.url = parsedUrl; this.host = parsedUrl.host; this.path = parsedUrl.pathname; @@ -99,6 +110,26 @@ export default class Provider { }; } + private async safeCachePut(cache: any, cacheKey: any, body: string, response: any): Promise { + if (!cache || !cacheKey || typeof Response === 'undefined') { + return; + } + + try { + const cacheHeaders = { 'Cache-Control': `public, max-age=${BROWSER_CACHE_TTL}` }; + await cache.put( + cacheKey, + new Response(body, { + status: response?.status, + statusText: response?.statusText, + headers: cacheHeaders, + }) + ); + } catch { + // Best-effort cache write: request flow should not fail when runtime cache put is unavailable. + } + } + private async fetchWithTimeout(url: string, init: any): Promise { let timeoutHandle: ReturnType | undefined; const canAbort = typeof AbortController !== 'undefined'; @@ -216,11 +247,7 @@ export default class Provider { if (!response.ok) { if (response.status === 403) { - if (cache && cacheKey && typeof Response !== 'undefined') { - const fullBody = JSON.stringify({}); - const cacheHeaders = { 'Cache-Control': `public, max-age=${BROWSER_CACHE_TTL}` }; - await cache.put(cacheKey, new Response(fullBody, { ...response, headers: cacheHeaders })); - } + await this.safeCachePut(cache, cacheKey, JSON.stringify({}), response); const availableProviders: string[] = JSON.parse(PROVIDERS); const currentProvider = this.url.toString(); @@ -251,18 +278,15 @@ export default class Provider { try { responseBody = JSON.parse(responseBody); - } catch (e) { - responseBody = {}; + } catch { + throw new RequestError('Invalid provider response', INVALID_PROVIDER_RESPONSE_ERROR_CODE, null); } const responseEnvelope = responseBody && typeof responseBody === 'object' && !Array.isArray(responseBody) ? responseBody : {}; const fullBody = JSON.stringify(responseEnvelope); - if (cache && cacheKey && typeof Response !== 'undefined') { - const cacheHeaders = { 'Cache-Control': `public, max-age=${BROWSER_CACHE_TTL}` }; - await cache.put(cacheKey, new Response(fullBody, { ...response, headers: cacheHeaders })); - } + await this.safeCachePut(cache, cacheKey, fullBody, response); if ('error' in responseEnvelope) { const errorMessage =