Node SDK cloud mode accepts empty projectId, produces 404 URLs
Severity: Critical
Affected repos: middleware-node
Component boundary: middleware-node → API
Symptom
When init({ apiKey: "rc-..." }) is called without a projectId, the SDK silently enters cloud mode with an empty projectId. The transport then POSTs to https://api.recost.dev/projects//telemetry (two slashes). The API returns 404 on every flush. Telemetry is dropped silently.
Evidence
middleware-node/src/core/transport.ts — resolveConfig() defaults projectId to "" rather than failing fast.
middleware-node/src/core/aggregator.ts — the aggregator's _projectId default is "" and is included in every WindowSummary.flush(). Even in local mode (no apiKey), this means every frame carries projectId: "", leaving the would-be extension server with no way to demultiplex.
middleware-node/src/init.ts — init() does not validate that projectId is non-empty when apiKey is set.
The same defect exists in the Python SDK and should be fixed symmetrically.
Impact
- User installs the SDK, sets
apiKey, forgets projectId. SDK appears to work — no errors, no warnings. Dashboard shows no data. User assumes the API is broken.
- Misattribution risk: if the API ever accepts
/projects//telemetry with an empty path segment (it currently 404s), data lands somewhere unintended.
Fix recommendation
Validate at init():
if (config.apiKey && (!config.projectId || config.projectId.trim() === "")) {
throw new Error(
"ReCost: projectId is required when apiKey is set (cloud mode). " +
"Get a project ID from your dashboard at https://recost.dev/dashboard/projects"
);
}
In local mode (no apiKey), projectId should still be required if multiple SDK instances might share the same extension server — see critical/01 for the multi-tenant binding requirement.
Mirror the same check in the Python SDK's _init.py so both SDKs behave identically.
Verification
- Unit test: call
init({ apiKey: "rc-test" }) with no projectId, assert it throws.
- Unit test: call
init({ apiKey: "rc-test", projectId: "" }) with empty string, assert it throws.
- Unit test: call
init({}) (local mode) without projectId, assert it does NOT throw (or document the local-mode policy).
Node SDK cloud mode accepts empty
projectId, produces 404 URLsSeverity: Critical
Affected repos:
middleware-nodeComponent boundary: middleware-node → API
Symptom
When
init({ apiKey: "rc-..." })is called without aprojectId, the SDK silently enters cloud mode with an emptyprojectId. The transport then POSTs tohttps://api.recost.dev/projects//telemetry(two slashes). The API returns 404 on every flush. Telemetry is dropped silently.Evidence
middleware-node/src/core/transport.ts—resolveConfig()defaultsprojectIdto""rather than failing fast.middleware-node/src/core/aggregator.ts— the aggregator's_projectIddefault is""and is included in everyWindowSummary.flush(). Even in local mode (no apiKey), this means every frame carriesprojectId: "", leaving the would-be extension server with no way to demultiplex.middleware-node/src/init.ts—init()does not validate thatprojectIdis non-empty whenapiKeyis set.The same defect exists in the Python SDK and should be fixed symmetrically.
Impact
apiKey, forgetsprojectId. SDK appears to work — no errors, no warnings. Dashboard shows no data. User assumes the API is broken./projects//telemetrywith an empty path segment (it currently 404s), data lands somewhere unintended.Fix recommendation
Validate at
init():In local mode (no
apiKey),projectIdshould still be required if multiple SDK instances might share the same extension server — seecritical/01for the multi-tenant binding requirement.Mirror the same check in the Python SDK's
_init.pyso both SDKs behave identically.Verification
init({ apiKey: "rc-test" })with noprojectId, assert it throws.init({ apiKey: "rc-test", projectId: "" })with empty string, assert it throws.init({})(local mode) withoutprojectId, assert it does NOT throw (or document the local-mode policy).