Skip to content

SDK profile update rejected: 401 invalid signature on PUT /users/<cryptoId>/profile #89

Description

@oxoxDev

Summary

Every wallet profile update — users.updateProfile() (CLI: profile set) — fails on staging with HTTP 401 {"error":"invalid signature"} on PUT /users/<cryptoId>/profile. The endpoint is currently unusable.

Where

Isolation (what is NOT the cause)

The signing machinery is fine — these all pass against staging with the same signFreshCanonicalPayload + directory-auth (X-Agent-ID = publicKeyBase64):

  • card publish, domain primary
  • registry.updateProfileVisibility (body-canonical identity.profile.visibility)
  • POST /users/<cryptoId>/email/verification (startEmailVerification) — same /users/* directory auth, and its canonical (user.email.start) includes harnessKey and still verifies.

So: the /users/* directory auth, the freshness/nonce scheme, and including harnessKey in the canonical are all correct. Only the user.profile canonical on the profile-update endpoint is rejected.

Further narrowing (all return 401):

  • canonical variants: full 8-field, drop harnessKey, minimal {cryptoId, displayName}, action: "users.profile", actorType: "agent" filled, drop avatarEmail/actorType.
  • both X-Agent-ID = publicKeyBase64 and X-Agent-ID = cryptoId.

Conclusion

The backend's userProfilePayload (the string it rebuilds to verify) diverges from the SDK's userProfileSignaturePayload (users.ts:105) in the non-harnessKey fields. Since email.start verifies with harnessKey present, harnessKey is not the culprit. Most likely a field the backend includes or excludes that the SDK doesn't mirror — e.g. the stored email on the profile, or a server-side defaulting of actorType/avatarEmail — or a field-name/casing drift. The fix is to realign the two canonical builders field-for-field.

SDK canonical today

canonicalPayload("user.profile", {
  actorType, avatarEmail, bio, cryptoId, displayName, harnessKey, link, tags
})   // omitted fields -> null; keys sorted; then "\n{ts}\n{nonce}" appended and signed

Repro

const client = makeClient(config, signer);            // staging-api.tiny.place
await client.users.updateProfile(signer.agentId, { displayName: "X" });
// -> HTTP 401 { error: "invalid signature" }

Reproduced on a registered staging agent; resolve @<handle> confirms the wallet exists and other signed writes succeed for the same key.

Impact

Suspected regression window

Commit 2df2d3c (2026-06-15) added harnessKey to the profile canonical — but since email.start verifies with harnessKey, the break is likely in another field of the profile canonical, not harnessKey itself. Needs a field-for-field diff against the backend userProfilePayload.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions