Skip to content

Comments

feat(repository): support existing blob refs for profile avatar/banner#138

Open
aspiers wants to merge 1 commit intodevelopfrom
profile-blob-input
Open

feat(repository): support existing blob refs for profile avatar/banner#138
aspiers wants to merge 1 commit intodevelopfrom
profile-blob-input

Conversation

@aspiers
Copy link
Contributor

@aspiers aspiers commented Feb 18, 2026

Summary

Ports the remaining profile-related changes from PR #123 (banner branch) onto a fresh branch off develop.

  • Add BlobInput = Blob | JsonBlobRef union type; all profile param types (CreateBskyProfileParams, UpdateBskyProfileParams, CreateCertifiedProfileParams, UpdateCertifiedProfileParams) now accept either a new Blob to upload or an existing JsonBlobRef to reuse without re-uploading
  • ProfileOperationsImpl.applyImageField updated to handle the JsonBlobRef path: converts to BlobRef via BlobRef.fromJsonRef() (required for AT Protocol lexicon validators) and stores directly
  • Add blobRefToJsonRef(blobRef: BlobRef): JsonBlobRef helper (wraps blobRef.ipld())
  • Remove stale BlobUploadResult interface (was already @deprecated; blobs.upload() returns BlobRef from @atproto/lexicon)

Test coverage

Two new test cases added to ProfileOperationsImpl.test.ts:

  • updateBskyProfile with existing JsonBlobRef avatar → no upload, stored as BlobRef instance
  • updateCertifiedProfile with existing JsonBlobRef avatar → no upload, stored wrapped in smallImage

Related

Extracted from #123. Also see #136, #137 for other extractions from the same PR.

Summary by CodeRabbit

  • New Features

    • Support for reusing existing blob references in profile avatar and banner fields, eliminating unnecessary re-uploads.
    • New utility to convert blob references to JSON format.
  • Breaking Changes

    • Deprecated BlobUploadResult interface removed; migrate to BlobRef from @atproto/lexicon instead.

- Add BlobInput = Blob | JsonBlobRef type; profile param types updated accordingly
- ProfileOperationsImpl.applyImageField now accepts BlobInput: existing
  JsonBlobRef is converted to BlobRef via fromJsonRef() and stored without
  re-uploading; new Blob is uploaded as before
- Add blobRefToJsonRef() helper in types.ts (BlobRef.ipld())
- Remove stale BlobUploadResult interface (was @deprecated; actual return
  type of blobs.upload() is BlobRef from @atproto/lexicon)
Copilot AI review requested due to automatic review settings February 18, 2026 19:47
@changeset-bot
Copy link

changeset-bot bot commented Feb 18, 2026

🦋 Changeset detected

Latest commit: 196fde2

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@hypercerts-org/sdk-core Minor
@hypercerts-org/sdk-react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

The changes introduce support for reusing existing JsonBlobRef references in profile avatar and banner fields. A new BlobInput type alias unifies Blob and JsonBlobRef inputs, and a blobRefToJsonRef utility function converts BlobRef to JsonBlobRef. The deprecated BlobUploadResult interface is removed.

Changes

Cohort / File(s) Summary
Interface & Type Definitions
packages/sdk-core/src/repository/interfaces.ts, packages/sdk-core/src/repository/types.ts
Introduces BlobInput type alias (Blob | JsonBlobRef), updates profile parameter types to accept BlobInput for avatar/banner fields, adds blobRefToJsonRef conversion utility, and removes deprecated BlobUploadResult interface.
Profile Operations Implementation
packages/sdk-core/src/repository/ProfileOperationsImpl.ts
Extends applyImageField to accept BlobInput alongside null/undefined, adds type guard isJsonBlobRef to differentiate blob reference types, converts JsonBlobRef to BlobRef for direct storage (avoiding re-upload), and preserves separate formatting for Bluesky vs Certified profiles.
Test Coverage
packages/sdk-core/tests/repository/ProfileOperationsImpl.test.ts
Adds three tests verifying JsonBlobRef instances are reused without triggering uploads and are correctly formatted as simple BlobRef for Bluesky or wrapped smallImage for Certified profiles.
Changeset Documentation
.changeset/profile-blob-input.md
Documents breaking change regarding BlobUploadResult removal and migration path for affected code.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A blob by any other ref would reuse just as sweet,
No more uploads of the same—efficiency's a treat!
JsonBlobRef dances with the newly minted Blob,
ProfileOperationsImpl lets old friends skip the job! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: introducing support for existing blob references in profile avatar and banner fields.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch profile-blob-input

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds support for using existing blob references (JsonBlobRef) in profile avatar and banner fields, eliminating unnecessary re-uploads when updating profiles with already-uploaded images. This is part of a larger effort extracted from PR #123.

Changes:

  • Introduces BlobInput union type (Blob | JsonBlobRef) for avatar/banner fields across all profile parameter types
  • Updates ProfileOperationsImpl.applyImageField() to detect and reuse existing JsonBlobRef objects without re-uploading
  • Adds blobRefToJsonRef() helper function and removes deprecated BlobUploadResult interface

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
packages/sdk-core/src/repository/interfaces.ts Adds BlobInput union type and updates profile parameter types to accept Blob or JsonBlobRef
packages/sdk-core/src/repository/ProfileOperationsImpl.ts Adds isJsonBlobRef type guard and updates applyImageField to handle existing blob refs
packages/sdk-core/src/repository/types.ts Adds blobRefToJsonRef helper and removes deprecated BlobUploadResult interface
packages/sdk-core/tests/repository/ProfileOperationsImpl.test.ts Adds test cases for updateBskyProfile and updateCertifiedProfile with existing JsonBlobRef
.changeset/profile-blob-input.md Documents the feature addition and potential breaking change for BlobUploadResult removal

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +138 to +142
// Untyped/legacy form: { cid: string, mimeType: string }
if (typeof r.cid === "string" && typeof r.mimeType === "string") {
return true;
}
return false;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "legacy form" check for JsonBlobRef appears to be incorrect. The JsonBlobRef type from @atproto/lexicon always has the typed form with $type: "blob", ref, mimeType, and size fields. The "untyped/legacy form" with just cid and mimeType is not a valid JsonBlobRef structure according to the AT Protocol specification.

If you need to support legacy blob references with a different structure, you should either:

  1. Remove this legacy check if it's not actually needed
  2. Document why this legacy form exists and where it comes from
  3. Ensure that BlobRef.fromJsonRef() can actually handle this format (it likely cannot)

The first check (lines 135-137) is sufficient for validating JsonBlobRef objects.

Copilot uses AI. Check for mistakes.
Comment on lines +122 to 124
export function blobRefToJsonRef(blobRef: BlobRef): JsonBlobRef {
return blobRef.ipld();
}
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blobRefToJsonRef helper function is added but not exported from the main index.ts file. If this helper is intended to be part of the public API for SDK consumers to convert BlobRef instances to their JSON representation, it should be exported.

If it's only for internal use, consider moving it to a different location (e.g., a utils file) or marking the file/section as internal-only. The changeset description indicates this is a public feature ("Add blobRefToJsonRef(blobRef: BlobRef): JsonBlobRef helper"), which suggests it should be exported.

Copilot uses AI. Check for mistakes.
/**
* Input type for blob fields — either a new Blob to upload or an existing JsonBlobRef to reuse.
*/
export type BlobInput = Blob | JsonBlobRef;
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The BlobInput type is added to interfaces.ts and documented as a public API type in the changeset, but it's not exported from the main index.ts file. SDK consumers who want to use this type in their TypeScript code for type-checking would not be able to import it.

Consider adding BlobInput to the exports in src/index.ts if it's intended to be part of the public API. This would allow consumers to write type-safe code like:

const avatar: BlobInput = existingBlobRef;
await repo.profile.updateBskyProfile({ avatar });

Copilot uses AI. Check for mistakes.
// If the input is already a JSON blob ref, convert to BlobRef instance for validation
// and store without re-uploading
if (this.isJsonBlobRef(input)) {
const blobRef = LexiconBlobRef.fromJsonRef(input);
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call to LexiconBlobRef.fromJsonRef(input) is not wrapped in error handling. If the input is malformed or doesn't match the expected structure (despite passing the isJsonBlobRef check), this could throw an error that isn't caught and wrapped appropriately.

Consider adding a try-catch block around this conversion to provide clearer error messages if the conversion fails, or ensure that isJsonBlobRef validates all fields that fromJsonRef requires. The current type guard only checks for the presence and types of fields, but doesn't validate the structure of the ref field.

Suggested change
const blobRef = LexiconBlobRef.fromJsonRef(input);
let blobRef: LexiconBlobRef;
try {
blobRef = LexiconBlobRef.fromJsonRef(input as JsonBlobRef);
} catch (err) {
// Wrap low-level conversion errors in a domain-specific validation error
throw new ValidationError(
`Invalid blob reference for field "${field}" in collection "${collection}": ${JSON.stringify(input)}`,
);
}

Copilot uses AI. Check for mistakes.
Comment on lines +124 to +127
* Type guard to check if a value is a JsonBlobRef (already-uploaded blob reference).
*
* Supports both typed form (`{ $type: "blob", ref, mimeType, size }`) and
* untyped/legacy form (`{ cid: string, mimeType: string }`).
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc comment mentions "untyped/legacy form" which is checked in the implementation, but this form is likely not a valid JsonBlobRef according to the AT Protocol specification. This documentation should be updated to either:

  1. Remove the mention of the legacy form if it's not actually supported
  2. Clearly document where this legacy form comes from and why it needs to be supported

The inconsistency between the documentation and the actual JsonBlobRef type could lead to confusion for developers.

Suggested change
* Type guard to check if a value is a JsonBlobRef (already-uploaded blob reference).
*
* Supports both typed form (`{ $type: "blob", ref, mimeType, size }`) and
* untyped/legacy form (`{ cid: string, mimeType: string }`).
* Type guard to check if a value is an AT Protocol `JsonBlobRef` (already-uploaded blob reference).
*
* Primarily supports the spec-compliant typed form (`{ $type: "blob", ref, mimeType, size }`).
* For backward compatibility, it also treats a legacy/untyped form (`{ cid: string, mimeType: string }`)
* as acceptable input. This legacy shape originates from earlier Hypercerts profile image handling
* prior to adopting the formal `JsonBlobRef` schema and is not part of the official AT Protocol spec.

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/sdk-core/src/repository/ProfileOperationsImpl.ts (1)

189-201: The as Blob cast on line 190 is safe but redundant.

After the undefined, null, and isJsonBlobRef() checks, the remaining type in the BlobInput union is already Blob from TypeScript's perspective. The explicit cast input as Blob is harmless and arguably aids readability, but it could mask issues if BlobInput is widened in the future without updating this method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk-core/src/repository/ProfileOperationsImpl.ts` around lines 189 -
201, Remove the redundant type assertion on the Blob upload call: after the
null/undefined/isJsonBlobRef() checks the local variable input is already
narrowed to Blob, so call this.blobs.upload(input) without the "as Blob" cast;
update the usage near blobRef assignment in ProfileOperationsImpl (the block
that creates blobRef and assigns result[field] based on
BSKY_PROFILE_NSID/field/banner) to use the narrowed input directly.
packages/sdk-core/src/repository/interfaces.ts (1)

675-678: BlobInput is wedged between the ProfileOperations JSDoc and the first helper type.

The BlobInput type is inserted directly after the ProfileOperations JSDoc block (line 674), which could make the JSDoc appear to document BlobInput rather than the ProfileOperations interface at line 738. Consider moving BlobInput above the ProfileOperations JSDoc or into a dedicated "Blob Types" section alongside the blob-related definitions.

That said, the pre-existing code already had several types (e.g., BskyProfile, CertifiedProfile) between the JSDoc and the interface, so this is consistent with the current layout.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk-core/src/repository/interfaces.ts` around lines 675 - 678, The
BlobInput type is declared immediately after the JSDoc for the ProfileOperations
interface which can make the JSDoc appear to document BlobInput; move the
BlobInput declaration (export type BlobInput = Blob | JsonBlobRef) so it appears
before the JSDoc for ProfileOperations or relocate it into a dedicated "Blob
Types" section with other blob-related types (e.g., alongside JsonBlobRef), then
verify the ProfileOperations interface declaration remains directly after its
JSDoc and update any nearby type order to keep documentation and code grouping
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/sdk-core/src/repository/interfaces.ts`:
- Around line 675-678: The BlobInput type is declared immediately after the
JSDoc for the ProfileOperations interface which can make the JSDoc appear to
document BlobInput; move the BlobInput declaration (export type BlobInput = Blob
| JsonBlobRef) so it appears before the JSDoc for ProfileOperations or relocate
it into a dedicated "Blob Types" section with other blob-related types (e.g.,
alongside JsonBlobRef), then verify the ProfileOperations interface declaration
remains directly after its JSDoc and update any nearby type order to keep
documentation and code grouping consistent.

In `@packages/sdk-core/src/repository/ProfileOperationsImpl.ts`:
- Around line 189-201: Remove the redundant type assertion on the Blob upload
call: after the null/undefined/isJsonBlobRef() checks the local variable input
is already narrowed to Blob, so call this.blobs.upload(input) without the "as
Blob" cast; update the usage near blobRef assignment in ProfileOperationsImpl
(the block that creates blobRef and assigns result[field] based on
BSKY_PROFILE_NSID/field/banner) to use the narrowed input directly.

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.

1 participant