Skip to content

fix(auth): upgrade decentraland-crypto-fetch to 3 - #3469

Merged
LautaroPetaccio merged 3 commits into
masterfrom
fix/crypto-fetch-3
Aug 26, 2026
Merged

fix(auth): upgrade decentraland-crypto-fetch to 3#3469
LautaroPetaccio merged 3 commits into
masterfrom
fix/crypto-fetch-3

Conversation

@LautaroPetaccio

Copy link
Copy Markdown
Contributor

Upgrades decentraland-crypto-fetch from ^2.0.1 to ^3.0.0, and brings this repo's own hand-rolled signed-request payload in line with the same format.

What changed

decentraland-crypto-fetch 3.0.0 changes the signed-fetch payload format to match @dcl/crypto-middleware 6.x:

  • Old: [method, path, timestamp, metadata].join(':').toLowerCase() — the whole payload lowercased, metadata included.
  • New: [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-fetch itself — used in src/modules/deployment/sagas.ts to DELETE a 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.tsAuthorization.createAuthHeaders builds the x-identity-auth-chain-* / x-identity-timestamp / x-identity-metadata headers itself via Authenticator.signPayload, and it was applying the old whole-payload .toLowerCase():

let fullPath = (method + ':' + path).toLowerCase()
if (metadata) {
  fullPath = `${fullPath}:${timestamp}:${JSON.stringify(metadata)}`
}
const endpoint = fullPath.toLowerCase()   // <- lowercased the metadata too

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 by src/lib/api/builder.ts and src/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-middleware 6.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:

Signer Call Metadata Status
decentraland-crypto-fetch DELETE /entities/:world (worlds content server) none inert
createAuthHeaders src/lib/api/builder.ts (all calls) none inert
createAuthHeaders POST /world/:name/permissions/:names { type: WorldPermissionType } inert
createAuthHeaders PUT / DELETE permissions, GET /wallet/contribute {} inert

WorldPermissionType is 'unrestricted' | 'shared-secret' | 'nft-ownership' | 'allow-list' — all lowercase, under a lowercase type key. 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-fetch 3.0.0 is hoisted to the top level, but decentraland-dapps still 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 through decentraland-dapps' own clients, and it is on the old format — which is likewise inert, for the same all-lowercase reason, except for markNotificationsAsRead. See that PR for details.

Testing

  • npx tsc (the typecheck from npm run build, noEmit: true) passes.
  • npm run lint passes.

Full test suite not run — this is a frontend repo with a slow suite. src/lib/api/auth.ts has no direct test; the specs that touch it (src/lib/api/worlds.spec.ts, src/modules/worlds/sagas.spec.ts) mock createAuthHeaders, so they are unaffected.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
builder Ready Ready Preview Aug 26, 2026 6:34pm

Request Review

@decentraland-bot decentraland-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.

Request changes

  • P0 — CI is failing: audit and test are 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 through decentraland-dapps, Builder can still emit the legacy signed-fetch format against upgraded services.
  • Suggested fix: bump/dedupe/override decentraland-dapps so 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

@coveralls

coveralls commented Aug 21, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 33000209744

Coverage increased (+0.002%) to 53.524%

Details

  • Coverage increased (+0.002%) from the base build.
  • Patch coverage: 3 uncovered changes across 1 file (0 of 3 lines covered, 0.0%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
src/lib/api/auth.ts 3 0 0.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 13374
Covered Lines: 7775
Line Coverage: 58.14%
Relevant Branches: 6019
Covered Branches: 2605
Branch Coverage: 43.28%
Branches in Coverage %: Yes
Coverage Strength: 37.35 hits per line

💛 - Coveralls

@decentraland-bot decentraland-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.

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/crypto Node >=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

@decentraland-bot decentraland-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.

I found one blocking auth-format issue in the updated helper.

  • P1 — createAuthHeaders still emits the old format when metadata is omitted (src/lib/api/auth.ts): decentraland-crypto-fetch@3.0.0 signs [method.toLowerCase(), path.toLowerCase(), timestamp, JSON.stringify(metadata || {})].join(':') and always sends x-identity-timestamp plus x-identity-metadata. @dcl/crypto-middleware@6 also verifies using timestamp/metadata and expiration unconditionally. The current helper only appends timestamp/metadata when metadata is truthy, so calls like createAuthHeaders(method, path) still sign just method:path and 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 mock Date.now() and Authenticator.signPayload for 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

@LautaroPetaccio

Copy link
Copy Markdown
Contributor Author

Updated decentraland-dapps

decentraland-dapps#818 merged, so dapps now depends on decentraland-crypto-fetch: ^3.0.0 instead of ^2.0.1. Taking it here means the signer inside dapps code emits the same payload format this app does, rather than relying on an override to force it.

⚠️ This pins a commit build, not a release. latest is still 29.6.2 and carries ^2.0.1, so there is no stable version with the fix yet. The pin is how this branch gets tested against the real dependency — replace it with the stable 29.6.3 before merging.

The two overrides are no longer equal

I checked both by removing them and observing what happened, rather than reasoning about it:

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 ci passes
  • exactly one decentraland-crypto-fetch in node_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.
@LautaroPetaccio

Copy link
Copy Markdown
Contributor Author

Rebased onto current master, and the dependency picture simplified

Master has moved to decentraland-dapps@^29.7.0 — the stable release that depends on decentraland-crypto-fetch ^3.0.0 directly. Two consequences.

The commit-build pin is gone

This branch previously pinned 29.6.3-20260825210513.commit-82671e6, because at the time no stable dapps release carried crypto-fetch 3. That commit is dropped entirely — master's ^29.7.0 supersedes it, so there's no prerelease left in the manifest.

One override removed, one kept — and I tested which

The two overrides turned out to be redundant with each other, not each doing half the job:

removed result
@dcl/hooks only resolves fine, one crypto-fetch at 3.0.0
flat decentraland-crypto-fetch only resolves fine, one crypto-fetch at 3.0.0
both ERESOLVE

So exactly one is needed. I kept the narrower one.

Dropped — the flat decentraland-crypto-fetch override. It existed because the published dapps still depended on ^2.0.1 and npm was installing a second, nested copy of the signer, meaning signed requests made from dapps code would have kept emitting the old format from inside this app. dapps 29.7.0 depends on ^3.0.0 directly, so that nested copy no longer appears and the override no longer does anything.

Kept — @dcl/hooks. That's the real remaining blocker: @dcl/hooks@1.6.0 declares decentraland-crypto-fetch ^2.0.1 as a peer and arrives transitively through decentraland-ui2. Keeping the narrow one states which dependency is holding this up, so it can be deleted the moment hooks publishes a release accepting 3.x.

That fix is merged in decentraland/hooks#52 but has only reached the next tag so far (1.6.1-20260826143734.commit-05ecd74 declares >=2.0.0); latest is still 1.6.0. Once a stable hooks release lands, this last override comes out and the manifest is clean.

Verified

  • npm ci passes
  • exactly one decentraland-crypto-fetch in node_modules, at 3.0.0
  • installed dapps is 29.7.0, itself declaring ^3.0.0

Earlier probe correction

My first attempt at these probes used npm install --package-lock-only without --ignore-scripts, and the repo's postinstall failed for an unrelated reason (MODULE_NOT_FOUND, since --package-lock-only writes no node_modules). That looked like a resolution failure and wasn't. The table above is from re-running with scripts disabled, where ERESOLVE is the only thing that can produce that result.

LautaroPetaccio and others added 2 commits August 26, 2026 15:28
…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.
@LautaroPetaccio

Copy link
Copy Markdown
Contributor Author

The override is gone — decentraland-ui2@3.22.1 retires it

- "decentraland-ui2": "^3.22.0",
+ "decentraland-ui2": "^3.22.1",

  overrides: {
-   "@dcl/hooks": {
-     "decentraland-crypto-fetch": "$decentraland-crypto-fetch"
-   },

Both ends of the conflict are now fixed upstream:

  • @dcl/hooks@2.0.0 widened its own peer from ^2.0.1 to >=2.0.0
  • 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

So npm can now resolve hooks 2.0.0, and the override has nothing left to do.

Verified

npm ci                          → passes
decentraland-crypto-fetch       → 3.0.0  (exactly one in node_modules)
@dcl/hooks                      → 2.0.0  (peer: >=2.0.0)
decentraland-ui2                → 3.22.1

No crypto-fetch or hooks entries remain in overrides. The ones still there are pre-existing crypto/hash entries unrelated to this work.

One thing worth knowing for the rest of this chain

Bumping ui2 alone did not fix it. The first attempt still failed with:

Could not resolve dependency:
peer decentraland-crypto-fetch@"^2.0.1" from @dcl/hooks@1.6.0

^1.2.1 || ^2.0.0 still admits the 1.6.0 the lockfile had already recorded, so npm kept it and the ERESOLVE survived the bump. The range was already correct; the stale pin was what held it back. I had to drop the @dcl/hooks entry from the lockfile before npm would re-resolve it.

Widening a range does not by itself move a dependency a lockfile has already pinned. Any other repo in this chain upgrading ui2 to clear the same override will need the same nudge — a plain npm install won't do it.

This also corrects something I said on decentraland/ui2#474: I'd tested the range in a clean project with no lockfile, where npm resolved hooks 2.0.0 straight away, and concluded consumers would land there automatically. That holds for a fresh tree, not for one with an existing pin.

@decentraland-bot decentraland-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.

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/hooks 1.6.0 → 2.0.0 (peer dep, range widened to >=2.0.0)
  • decentraland-ui2 3.22.0 → 3.22.1 (patch; widens hooks peer range)
  • core-js-pure removed (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

@LautaroPetaccio
LautaroPetaccio merged commit 27e0180 into master Aug 26, 2026
7 checks passed
@LautaroPetaccio
LautaroPetaccio deleted the fix/crypto-fetch-3 branch August 26, 2026 18:43
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.

3 participants