Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ coverage.json
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
.DS_Store
/.idea
stats.html
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,6 @@
"@hypercerts-org/lexicon": "0.10.0-beta.13",
"eventemitter3": "^5.0.1",
"type-fest": "^5.4.1",
"zod": "^3.24.4"
"zod": "^3.25.76"
}
}
6 changes: 3 additions & 3 deletions packages/sdk-core/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export type DID = string;
* Validates that a string is a valid DID format.
*
* DIDs must follow the format: `did:<method>:<method-specific-id>`
* where method is lowercase letters and the identifier contains
* where method is lowercase letters and digits, and the identifier contains
* alphanumeric characters plus `.`, `_`, `:`, `%`, and `-`.
*
* @param did - The string to validate
Expand All @@ -44,9 +44,9 @@ export type DID = string;
*/
export function isValidDid(did: string): boolean {
// DID format: did:<method>:<method-specific-id>
// Method: lowercase letters only
// Method: lowercase letters and digits (per W3C DID Core spec)
// Identifier: alphanumeric plus . _ : % -
return /^did:[a-z]+:[a-zA-Z0-9._:%-]+$/.test(did);
return /^did:[a-z0-9]+:[a-zA-Z0-9._:%-]+$/.test(did);
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/sdk-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export type {
ContributorIdentityParams,
CreateContributorInformationParams,
ResolvedContributorIdentity,
BlobInput,
} from "./repository/interfaces.js";

// ============================================================================
Expand Down
28 changes: 18 additions & 10 deletions packages/sdk-core/src/repository/ProfileOperationsImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,20 +93,28 @@ export class ProfileOperationsImpl implements ProfileOperations {
/**
* Checks if a value is an existing JsonBlobRef.
*
* JsonBlobRef has the structure: { $type: "blob", ref: { $link }, mimeType, size }
* JsonBlobRef has the structure: { $type: "blob", ref: { $link: string }, mimeType, size }
*
* @internal
*/
private isJsonBlobRef(value: unknown): value is JsonBlobRef {
return (
typeof value === "object" &&
value !== null &&
"$type" in value &&
(value as Record<string, unknown>).$type === "blob" &&
"ref" in value &&
"mimeType" in value &&
"size" in value
);
if (typeof value !== "object" || value === null) {
return false;
}

const record = value as Record<string, unknown>;

if (record.$type !== "blob" || !("ref" in record) || !("mimeType" in record) || !("size" in record)) {
return false;
}

const ref = record.ref;
if (typeof ref !== "object" || ref === null) {
return false;
}

const refRecord = ref as Record<string, unknown>;
return typeof refRecord.$link === "string";
}

/**
Expand Down
16 changes: 12 additions & 4 deletions packages/sdk-core/tests/core/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ describe("isValidDid", () => {
it("should accept DID with hyphens and underscores", () => {
expect(isValidDid("did:example:my-test_id")).toBe(true);
});

it("should accept DID with method containing digits", () => {
expect(isValidDid("did:key2:abc123")).toBe(true);
});

it("should accept DID with method containing multiple digits", () => {
expect(isValidDid("did:btc1:xyz789")).toBe(true);
});

it("should accept DID with method that is all digits", () => {
expect(isValidDid("did:123:identifier")).toBe(true);
});
});

describe("invalid DIDs", () => {
Expand Down Expand Up @@ -57,10 +69,6 @@ describe("isValidDid", () => {
expect(isValidDid("did:PLC:abc123")).toBe(false);
});

it("should reject method with numbers", () => {
expect(isValidDid("did:plc2:abc123")).toBe(false);
});

it("should reject random URL", () => {
expect(isValidDid("https://example.com")).toBe(false);
});
Expand Down
90 changes: 89 additions & 1 deletion packages/sdk-core/tests/repository/ProfileOperationsImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import type { Agent } from "@atproto/api";
import { ProfileOperationsImpl } from "../../src/repository/ProfileOperationsImpl.js";
import { NetworkError } from "../../src/core/errors.js";
import type { BlobOperations } from "../../src/repository/interfaces.js";
import type { BlobOperations, BlobInput } from "../../src/repository/interfaces.js";
import { createMockAgent, createMockBlobOperations, TEST_REPO_DID } from "../utils/mocks.js";

describe("ProfileOperationsImpl", () => {
Expand Down Expand Up @@ -476,6 +476,94 @@ describe("ProfileOperationsImpl", () => {
);
});

it("should treat malformed blob ref (missing $link) as regular Blob and upload", async () => {
const malformedRef = {
$type: "blob" as const,
ref: {}, // Missing $link
mimeType: "image/png",
size: 1024,
};

mockBlobs.upload.mockResolvedValue({
ref: { $link: "uploaded-cid" },
mimeType: "image/png",
size: 1024,
});

await profileOps.update({
avatar: malformedRef as unknown as BlobInput,
});

// Should upload because it's not a valid JsonBlobRef
expect(mockBlobs.upload).toHaveBeenCalledWith(malformedRef);
});

it("should treat malformed blob ref (ref is not an object) as regular Blob and upload", async () => {
const malformedRef = {
$type: "blob" as const,
ref: "string-instead-of-object",
mimeType: "image/png",
size: 1024,
};

mockBlobs.upload.mockResolvedValue({
ref: { $link: "uploaded-cid" },
mimeType: "image/png",
size: 1024,
});

await profileOps.update({
avatar: malformedRef as unknown as BlobInput,
});

// Should upload because it's not a valid JsonBlobRef
expect(mockBlobs.upload).toHaveBeenCalledWith(malformedRef);
});

it("should treat malformed blob ref (ref is null) as regular Blob and upload", async () => {
const malformedRef = {
$type: "blob" as const,
ref: null,
mimeType: "image/png",
size: 1024,
};

mockBlobs.upload.mockResolvedValue({
ref: { $link: "uploaded-cid" },
mimeType: "image/png",
size: 1024,
});

await profileOps.update({
avatar: malformedRef as unknown as BlobInput,
});

// Should upload because it's not a valid JsonBlobRef
expect(mockBlobs.upload).toHaveBeenCalledWith(malformedRef);
});

it("should treat malformed blob ref (missing mimeType) as regular Blob and upload", async () => {
const malformedRef = {
$type: "blob" as const,
ref: { $link: "some-cid" },
// Missing mimeType
size: 1024,
};

mockBlobs.upload.mockResolvedValue({
ref: { $link: "uploaded-cid" },
mimeType: "image/png",
size: 1024,
});

await profileOps.update({
avatar: malformedRef as unknown as BlobInput,
});

// Should upload because it's not a valid JsonBlobRef
expect(mockBlobs.upload).toHaveBeenCalledWith(malformedRef);
});

it("should throw NetworkError when getRecord returns success: false", async () => {
mockAgent.com.atproto.repo.getRecord.mockResolvedValue({
success: false,
Expand Down
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.