Skip to content

Commit 3db71cd

Browse files
committed
fix(sdk-core): convert blob upload results to proper JsonBlobRef format
When uploading images for collection avatars/banners, validation failed with: 'ValidationError: Invalid collection record: Record/banner/image should be a blob ref' The issue: blobs.upload() returns { ref, mimeType, size } but AT Protocol lexicons define SmallImage.image and LargeImage.image as BlobRef, which requires the full structure including $type: 'blob'. Two helper methods were passing raw upload results directly: - resolveCollectionImageInput() for avatar/banner images - resolveUriOrBlob() for location/attachment blobs The SDK already had blobToJsonRef() to add the required $type: 'blob', but these methods weren't using it. Other code paths (uploadImageBlob, updateClaim) correctly used blobToJsonRef() and worked fine. Without $type: 'blob', the server rejects the record because the blob reference doesn't match the expected BlobRef schema from @atproto/lexicon.
1 parent fd33ee5 commit 3db71cd

File tree

2 files changed

+30
-32
lines changed

2 files changed

+30
-32
lines changed

packages/sdk-core/src/repository/HypercertOperationsImpl.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -959,7 +959,7 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
959959
const uploadResult = await this.blobs.upload(content);
960960
return {
961961
$type: "org.hypercerts.defs#smallBlob" as const,
962-
blob: uploadResult,
962+
blob: this.blobToJsonRef(uploadResult),
963963
};
964964
}
965965

@@ -974,11 +974,12 @@ export class HypercertOperationsImpl extends EventEmitter<HypercertEvents> imple
974974
}
975975

976976
const uploadResult = await this.blobs.upload(input);
977+
const blobRef = this.blobToJsonRef(uploadResult);
977978
if (isBanner) {
978-
return { $type: "org.hypercerts.defs#largeImage" as const, image: uploadResult };
979+
return { $type: "org.hypercerts.defs#largeImage" as const, image: blobRef };
979980
}
980981

981-
return { $type: "org.hypercerts.defs#smallImage" as const, image: uploadResult };
982+
return { $type: "org.hypercerts.defs#smallImage" as const, image: blobRef };
982983
}
983984

984985
/**

packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -994,7 +994,8 @@ describe("HypercertOperationsImpl", () => {
994994
expect(call.record.location).toEqual({
995995
$type: "org.hypercerts.defs#smallBlob", // Your code wraps it in smallBlob
996996
blob: {
997-
// The actual blob data is nested here
997+
// The actual blob data is nested here with $type: "blob" for AT Protocol compliance
998+
$type: "blob",
998999
ref: { $link: "blob-cid" },
9991000
mimeType: "application/geo+json",
10001001
size: 100,
@@ -1203,7 +1204,7 @@ describe("HypercertOperationsImpl", () => {
12031204
expect(call.record.content).toEqual([
12041205
{
12051206
$type: "org.hypercerts.defs#smallBlob",
1206-
blob: { ref: { $link: "blob-cid" }, mimeType: "application/pdf", size: 100 },
1207+
blob: { $type: "blob", ref: { $link: "blob-cid" }, mimeType: "application/pdf", size: 100 },
12071208
},
12081209
]);
12091210
});
@@ -1266,7 +1267,7 @@ describe("HypercertOperationsImpl", () => {
12661267
});
12671268
expect(call.record.content[1]).toEqual({
12681269
$type: "org.hypercerts.defs#smallBlob",
1269-
blob: { ref: { $link: "blob-cid" }, mimeType: "application/pdf", size: 100 },
1270+
blob: { $type: "blob", ref: { $link: "blob-cid" }, mimeType: "application/pdf", size: 100 },
12701271
});
12711272
});
12721273

@@ -2053,29 +2054,18 @@ describe("HypercertOperationsImpl", () => {
20532054
const logoBlob = new Blob(["logo"], { type: "image/png" });
20542055
const headerBlob = new Blob(["header"], { type: "image/jpeg" });
20552056

2056-
mockAgent.com.atproto.repo.uploadBlob.mockResolvedValueOnce({
2057-
success: true,
2058-
data: {
2059-
blob: {
2060-
$type: "blob",
2061-
ref: { $link: "bafyrei-logo" },
2062-
mimeType: "image/png",
2063-
size: 150,
2064-
},
2065-
},
2066-
});
2067-
2068-
mockAgent.com.atproto.repo.uploadBlob.mockResolvedValueOnce({
2069-
success: true,
2070-
data: {
2071-
blob: {
2072-
$type: "blob",
2073-
ref: { $link: "bafyrei-header" },
2074-
mimeType: "image/jpeg",
2075-
size: 250,
2076-
},
2077-
},
2078-
});
2057+
// Mock blob uploads via BlobOperations (not agent.uploadBlob)
2058+
mockBlobs.upload
2059+
.mockResolvedValueOnce({
2060+
ref: { $link: "bafyrei-logo" },
2061+
mimeType: "image/png",
2062+
size: 150,
2063+
})
2064+
.mockResolvedValueOnce({
2065+
ref: { $link: "bafyrei-header" },
2066+
mimeType: "image/jpeg",
2067+
size: 250,
2068+
});
20792069

20802070
mockAgent.com.atproto.repo.createRecord.mockResolvedValue({
20812071
success: true,
@@ -2091,16 +2081,23 @@ describe("HypercertOperationsImpl", () => {
20912081
});
20922082

20932083
expect(result.uri).toContain("collection");
2084+
expect(mockBlobs.upload).toHaveBeenCalledTimes(2);
20942085
expect(mockAgent.com.atproto.repo.createRecord).toHaveBeenCalledWith(
20952086
expect.objectContaining({
20962087
record: expect.objectContaining({
20972088
title: "Climate Action Project",
20982089
type: "project",
20992090
avatar: expect.objectContaining({
21002091
$type: "org.hypercerts.defs#smallImage",
2092+
image: expect.objectContaining({
2093+
$type: "blob",
2094+
}),
21012095
}),
21022096
banner: expect.objectContaining({
21032097
$type: "org.hypercerts.defs#largeImage",
2098+
image: expect.objectContaining({
2099+
$type: "blob",
2100+
}),
21042101
}),
21052102
}),
21062103
}),
@@ -2683,7 +2680,7 @@ describe("HypercertOperationsImpl", () => {
26832680
const createCall = mockAgent.com.atproto.repo.createRecord.mock.calls[0][0];
26842681
expect(createCall.record.avatar).toEqual({
26852682
$type: "org.hypercerts.defs#smallImage",
2686-
image: { ref: { $link: "avatar-cid" }, mimeType: "image/png", size: 100 },
2683+
image: { $type: "blob", ref: { $link: "avatar-cid" }, mimeType: "image/png", size: 100 },
26872684
});
26882685
});
26892686

@@ -2705,7 +2702,7 @@ describe("HypercertOperationsImpl", () => {
27052702
const createCall = mockAgent.com.atproto.repo.createRecord.mock.calls[0][0];
27062703
expect(createCall.record.banner).toEqual({
27072704
$type: "org.hypercerts.defs#largeImage",
2708-
image: { ref: { $link: "banner-cid" }, mimeType: "image/jpeg", size: 200 },
2705+
image: { $type: "blob", ref: { $link: "banner-cid" }, mimeType: "image/jpeg", size: 200 },
27092706
});
27102707
});
27112708

@@ -3220,7 +3217,7 @@ describe("HypercertOperationsImpl", () => {
32203217
const putCall = mockAgent.com.atproto.repo.putRecord.mock.calls[0][0];
32213218
expect(putCall.record.avatar).toEqual({
32223219
$type: "org.hypercerts.defs#smallImage",
3223-
image: { ref: { $link: "new-avatar-cid" }, mimeType: "image/png", size: 150 },
3220+
image: { $type: "blob", ref: { $link: "new-avatar-cid" }, mimeType: "image/png", size: 150 },
32243221
});
32253222
});
32263223

0 commit comments

Comments
 (0)