fix(auth): upgrade decentraland-crypto-fetch to 3 - #3469
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
decentraland-bot
left a comment
There was a problem hiding this comment.
Request changes
- P0 — CI is failing:
auditandtestare failing. - P1 — Old signer remains transitively installed: the lockfile still includes
node_modules/decentraland-dapps/node_modules/decentraland-crypto-fetch@2.0.1, even though the top-level dependency moved to 3.0.0. If any bundled path resolves throughdecentraland-dapps, Builder can still emit the legacy signed-fetch format against upgraded services. - Suggested fix: bump/dedupe/override
decentraland-dappsso only the intended crypto-fetch version is bundled, and re-run tests. - Security review: no secrets/injection issues found.
Reviewed by Jarvis 🤖 · Requested by Lautaro Petaccio (<@U025WCHLMN3>) via Slack
9f8b83d to
af666bd
Compare
Coverage Report for CI Build 33000209744Coverage increased (+0.002%) to 53.524%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats💛 - Coveralls |
decentraland-bot
left a comment
There was a problem hiding this comment.
Approved. I re-checked the crypto-fetch v3 bump and the hand-rolled payload change in src/lib/api/auth.ts.
Findings:
- No P0/P1 issues found in this changeset.
- The repo already requires Node 24, so the new
decentraland-crypto-fetch/@dcl/cryptoNode >=22 dependency floor is covered. - Security review: no new security issues found in this PR. The no-metadata legacy signing path remains compatible with the current server fallback; the metadata path now preserves metadata bytes for the v6 verifier format.
- Public API/consumer impact: no exported API shape changes; this is an internal auth-header generation and dependency update.
CI: passing.
Reviewed by Jarvis 🤖 · Requested by Lautaro Petaccio (<@U025WCHLMN3>) via Slack
af666bd to
c39b49f
Compare
decentraland-bot
left a comment
There was a problem hiding this comment.
I found one blocking auth-format issue in the updated helper.
-
P1 —
createAuthHeadersstill emits the old format when metadata is omitted (src/lib/api/auth.ts):decentraland-crypto-fetch@3.0.0signs[method.toLowerCase(), path.toLowerCase(), timestamp, JSON.stringify(metadata || {})].join(':')and always sendsx-identity-timestampplusx-identity-metadata.@dcl/crypto-middleware@6also verifies using timestamp/metadata and expiration unconditionally. The current helper only appends timestamp/metadata whenmetadatais truthy, so calls likecreateAuthHeaders(method, path)still sign justmethod:pathand will fail against v6 middleware.Suggested fix: default missing metadata to
{}, compute the JSON once, include timestamp/metadata in the payload unconditionally, and always emit both headers for authenticated requests. For example:const timestamp = Date.now() const metadataJSON = JSON.stringify(metadata ?? {}) const payload = `${method.toLowerCase()}:${path.toLowerCase()}:${timestamp}:${metadataJSON}` const authChain = Authenticator.signPayload(identity, payload) // ...auth-chain headers... headers[AUTH_CHAIN_TIMESTAMP_HEADER] = timestamp.toString() headers[AUTH_CHAIN_METADATA_HEADER] = metadataJSON
-
P2 — Add focused regression coverage for
src/lib/api/auth.ts: this is signature-sensitive behavior and currently not directly tested. Please add tests that mockDate.now()andAuthenticator.signPayloadfor both omitted metadata and mixed-case metadata, asserting that the exact signed payload matches the emitted metadata header.
Security review: no secrets, injection, or sensitive-data exposure introduced. The main security/correctness concern is the signature payload mismatch above.
Public API/consumer impact: no exported TypeScript API shape changes, but this changes the auth wire protocol used by Builder/Worlds API calls; the no-metadata call sites need to follow the v3/v6 format too.
CI: passing.
Reviewed by Jarvis 🤖 · Requested by Lautaro Petaccio (<@U025WCHLMN3>) via Slack
c39b49f to
c23bdf4
Compare
Updated
|
| override | status | evidence |
|---|---|---|
decentraland-crypto-fetch |
redundant | with dapps on ^3.0.0, the tree resolves a single 3.0.0 instance without it |
@dcl/hooks |
still load-bearing | removing it fails the install: ERESOLVE could not resolve — @dcl/hooks@1.5.0 peers on ^2.0.1 and arrives transitively via decentraland-ui2 |
I kept the redundant one deliberately. If the dapps pin moves back to a release still on ^2.0.1 — which is exactly what happens if someone swaps the commit build for today's latest — dropping it would silently reintroduce the nested 2.0.1 signer. That is the precise defect this branch exists to remove, and it would come back without any error. The override costs nothing and turns a silent regression into a loud one.
@dcl/hooks stays until hooks#52 widens its peer range; that PR is still open.
Verified
npm cipasses- exactly one
decentraland-crypto-fetchinnode_modules, at 3.0.0 - installed dapps reports
decentraland-crypto-fetch: ^3.0.0
Earlier in this PR
Also rebased onto current master, which had moved to decentraland-dapps@^29.6.2 and conflicted with this branch. The lockfile was regenerated from master's rather than hand-merged across 587 conflicted lines; auth.ts applied cleanly.
Version 2 built the signed payload as
`[method, path, timestamp, data].join(':').toLowerCase()`, folding the
metadata along with everything else while sending `x-identity-metadata`
verbatim. That left the metadata's casing outside the signature. Version
3 lowercases only the method and the path and joins the metadata as it
will be sent, which is what services on @dcl/crypto-middleware 6 verify.
`src/lib/api/auth.ts` built the same folded payload by hand and is
updated to match.
One override remains, narrowed to the single package that still needs it.
@dcl/hooks 1.6.0 declares `decentraland-crypto-fetch ^2.0.1` as a peer
and arrives transitively through decentraland-ui2, so without it npm can
satisfy neither range and the install fails with ERESOLVE. Verified by
removing it: the resolution fails outright.
The flat `decentraland-crypto-fetch` override this branch also carried is
gone. It existed because the published decentraland-dapps still depended
on `^2.0.1` and npm was installing a second, nested copy of the signer --
any signed request made from dapps code would have kept emitting the old
format from inside this app. Master now depends on decentraland-dapps
29.7.0, which depends on `^3.0.0` directly, so the nested copy no longer
appears and the override no longer does anything. Confirmed by removing
it: the tree still resolves exactly one instance, at 3.0.0.
The two overrides were redundant with each other rather than each doing
half the job -- either alone resolves the tree, and removing both is what
fails. Keeping the narrower one states which dependency is actually
holding this up, so it can be deleted the moment @dcl/hooks publishes a
release accepting 3.x. That fix is merged there but has only reached the
`next` tag so far; `latest` is still 1.6.0.
`npm ci` passes and installs one crypto-fetch, at 3.0.0.
Rebased onto current master, and the dependency picture simplifiedMaster has moved to The commit-build pin is goneThis branch previously pinned One override removed, one kept — and I tested whichThe two overrides turned out to be redundant with each other, not each doing half the job:
So exactly one is needed. I kept the narrower one. Dropped — the flat Kept — That fix is merged in decentraland/hooks#52 but has only reached the Verified
Earlier probe correctionMy first attempt at these probes used |
bcfa522 to
5b9fd80
Compare
…tires The override existed because @dcl/hooks declared `decentraland-crypto-fetch ^2.0.1` as a peer and arrived transitively through decentraland-ui2, so npm could satisfy neither range once this app moved to the 3.x signer and every install failed with ERESOLVE. Both ends of that are now fixed upstream. hooks 2.0.0 widened its peer to `>=2.0.0`, and decentraland-ui2 3.22.1 widened the range it accepts to `^1.2.1 || ^2.0.0` -- without which hooks 2 was unreachable from here, no matter what this manifest declared. Taking 3.22.1 lets npm resolve hooks 2.0.0 and the override has nothing left to do. Verified by removing it: `npm ci` passes and the tree holds exactly one decentraland-crypto-fetch, at 3.0.0, with hooks resolved to 2.0.0. The lockfile needed the stale `@dcl/hooks` entry dropped before npm would re-resolve it. `^1.2.1 || ^2.0.0` still admits the 1.6.0 the lockfile recorded, so npm kept it and the ERESOLVE survived the ui2 bump -- the range was already right and the pin was what held it back. Worth knowing for the other consumers of this chain: widening a range does not by itself move a dependency a lockfile has already pinned. No crypto-fetch or hooks entries remain in `overrides`. The ones left are pre-existing and unrelated to this change.
The override is gone —
|
decentraland-bot
left a comment
There was a problem hiding this comment.
Review — fix(auth): upgrade decentraland-crypto-fetch to 3
Summary
Clean, well-documented change that upgrades decentraland-crypto-fetch from v2 to v3 and aligns the hand-rolled signing in src/lib/api/auth.ts with @dcl/crypto-middleware 6.x. The core fix — removing the redundant trailing .toLowerCase() — is correct: method and path are still lowercased, but metadata is now signed verbatim.
Findings
No P0 or P1 issues found. All issues identified are P2 (pre-existing patterns not introduced by this PR).
[P2] Pre-existing: timestamp not included without metadata (src/lib/api/auth.ts)
When metadata is undefined (all builder.ts call sites), the signed payload is method:path with no timestamp. This means those signatures are replay-able. This is a pre-existing pattern (identical on master) and not introduced by this PR, but worth noting for future hardening. Consider including the timestamp unconditionally in a follow-up.
[P2] Pre-existing: no error handling around signPayload (src/lib/api/auth.ts:31)
If the identity from localStorage is corrupt or expired, Authenticator.signPayload will throw and propagate unhandled. Pre-existing — not introduced here.
[P2] Pre-existing: metadata && (headers[X] = Y) style (src/lib/api/auth.ts:33-34)
The side-effect expressions outside the if (metadata) guard work correctly but are fragile if the function is later refactored. An explicit if block would be clearer. Pre-existing pattern.
Dependency resolution ✅
The lockfile resolves cleanly to a single decentraland-crypto-fetch 3.0.0 at the top level — the nested copy under decentraland-dapps (which was already 3.0.0) was hoisted. No dual-copy risk. Transitive changes are safe:
@dcl/hooks1.6.0 → 2.0.0 (peer dep, range widened to>=2.0.0)decentraland-ui23.22.0 → 3.22.1 (patch; widens hooks peer range)core-js-pureremoved (was a dependency of crypto-fetch v2, no longer needed)
Deploy ordering ✅
The PR body correctly documents that every signed metadata value in this repo is already all-lowercase (WorldPermissionType values, empty {}, or undefined). Since .toLowerCase() on already-lowercase strings is a no-op, both the old and new payload formats produce identical bytes. Safe to deploy before or after the backend middleware upgrade.
Consumer impact ✅
createAuthHeaders is internal to this repo. The signed requests target the builder server and worlds content server, which are being upgraded to @dcl/crypto-middleware 6.x in parallel. No breaking API surface change.
Security ✅
No secrets exposure, injection vulnerabilities, or auth bypass risks introduced by this change. The case-sensitivity contract shift is the intended behavior and all current call sites are unaffected (all-lowercase metadata).
CI ✅
All checks passing (test, audit, Vercel deployment).
Git conventions ✅
PR title fix(auth): upgrade decentraland-crypto-fetch to 3 and branch fix/crypto-fetch-3 follow ADR-6.
Reviewed by Jarvis 🤖 · Requested by Lautaro Petaccio (<@U025WCHLMN3>) via Slack
Upgrades
decentraland-crypto-fetchfrom^2.0.1to^3.0.0, and brings this repo's own hand-rolled signed-request payload in line with the same format.What changed
decentraland-crypto-fetch3.0.0 changes the signed-fetch payload format to match@dcl/crypto-middleware6.x:[method, path, timestamp, metadata].join(':').toLowerCase()— the whole payload lowercased, metadata included.[method.toLowerCase(), path.toLowerCase(), timestamp, metadata].join(':')— only method and path are lowercased; the metadata is joined verbatim, so its casing is covered by the signature.This repo signs requests two ways, and both needed attention:
1.
decentraland-crypto-fetchitself — used insrc/modules/deployment/sagas.tstoDELETEa world entity from the worlds content server. The format lives inside the package, so the bump covers it.2. A hand-rolled payload in
src/lib/api/auth.ts—Authorization.createAuthHeadersbuilds thex-identity-auth-chain-*/x-identity-timestamp/x-identity-metadataheaders itself viaAuthenticator.signPayload, and it was applying the old whole-payload.toLowerCase():The trailing
.toLowerCase()is now dropped, so the method and path are still lowercased but the metadata is signed verbatim — identical to what the package produces. This helper is used bysrc/lib/api/builder.tsandsrc/lib/api/worlds.ts.Deploy ordering — this one is inert
This repo now signs the new format, so its signed requests only verify against services already running
@dcl/crypto-middleware6.x, and services are being upgraded in parallel. Where every piece of signed metadata is all-lowercase, both formats produce identical bytes and the change makes no observable difference.That is the case here — every signed request this repo sends carries all-lowercase metadata or none at all:
decentraland-crypto-fetchDELETE /entities/:world(worlds content server)createAuthHeaderssrc/lib/api/builder.ts(all calls)createAuthHeadersPOST /world/:name/permissions/:names{ type: WorldPermissionType }createAuthHeadersPUT/DELETEpermissions,GET /wallet/contribute{}WorldPermissionTypeis'unrestricted' | 'shared-secret' | 'nft-ownership' | 'allow-list'— all lowercase, under a lowercasetypekey. Method and path are lowercased under both the old and the new format, so they are not a factor.So this PR can deploy before or after the services it calls without producing
401s. It is being made now so the client is correct once metadata that is not all-lowercase is eventually signed.Note on the lockfile
decentraland-crypto-fetch3.0.0 is hoisted to the top level, butdecentraland-dappsstill declares^2.0.1, so a nested 2.x copy remains until decentraland/decentraland-dapps#818 lands and is picked up here. That nested copy is only reached throughdecentraland-dapps' own clients, and it is on the old format — which is likewise inert, for the same all-lowercase reason, except formarkNotificationsAsRead. See that PR for details.Testing
npx tsc(the typecheck fromnpm run build,noEmit: true) passes.npm run lintpasses.Full test suite not run — this is a frontend repo with a slow suite.
src/lib/api/auth.tshas no direct test; the specs that touch it (src/lib/api/worlds.spec.ts,src/modules/worlds/sagas.spec.ts) mockcreateAuthHeaders, so they are unaffected.🤖 Generated with Claude Code