diff --git a/.gitignore b/.gitignore index f381003b..f3bd65a7 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ coverage.json npm-debug.log* yarn-debug.log* yarn-error.log* +package-lock.json .DS_Store /.idea stats.html diff --git a/packages/sdk-core/package.json b/packages/sdk-core/package.json index e1e6a9b2..470f8918 100644 --- a/packages/sdk-core/package.json +++ b/packages/sdk-core/package.json @@ -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" } } diff --git a/packages/sdk-core/src/core/types.ts b/packages/sdk-core/src/core/types.ts index a2b88f24..7fde3f63 100644 --- a/packages/sdk-core/src/core/types.ts +++ b/packages/sdk-core/src/core/types.ts @@ -26,7 +26,7 @@ export type DID = string; * Validates that a string is a valid DID format. * * DIDs must follow the format: `did::` - * 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 @@ -44,9 +44,9 @@ export type DID = string; */ export function isValidDid(did: string): boolean { // DID format: did:: - // 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); } /** diff --git a/packages/sdk-core/src/index.ts b/packages/sdk-core/src/index.ts index 1e2afc52..5f5889e4 100644 --- a/packages/sdk-core/src/index.ts +++ b/packages/sdk-core/src/index.ts @@ -99,6 +99,7 @@ export type { ContributorIdentityParams, CreateContributorInformationParams, ResolvedContributorIdentity, + BlobInput, } from "./repository/interfaces.js"; // ============================================================================ diff --git a/packages/sdk-core/src/repository/ProfileOperationsImpl.ts b/packages/sdk-core/src/repository/ProfileOperationsImpl.ts index 1cd9b2dc..8a8f4583 100644 --- a/packages/sdk-core/src/repository/ProfileOperationsImpl.ts +++ b/packages/sdk-core/src/repository/ProfileOperationsImpl.ts @@ -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).$type === "blob" && - "ref" in value && - "mimeType" in value && - "size" in value - ); + if (typeof value !== "object" || value === null) { + return false; + } + + const record = value as Record; + + 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; + return typeof refRecord.$link === "string"; } /** diff --git a/packages/sdk-core/tests/core/types.test.ts b/packages/sdk-core/tests/core/types.test.ts index 71b5de48..7c132a16 100644 --- a/packages/sdk-core/tests/core/types.test.ts +++ b/packages/sdk-core/tests/core/types.test.ts @@ -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", () => { @@ -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); }); diff --git a/packages/sdk-core/tests/repository/ProfileOperationsImpl.test.ts b/packages/sdk-core/tests/repository/ProfileOperationsImpl.test.ts index 5fb5d109..5eac1dbe 100644 --- a/packages/sdk-core/tests/repository/ProfileOperationsImpl.test.ts +++ b/packages/sdk-core/tests/repository/ProfileOperationsImpl.test.ts @@ -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", () => { @@ -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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b3291103..73fafbbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,7 +72,7 @@ importers: specifier: ^5.4.1 version: 5.4.1 zod: - specifier: ^3.24.4 + specifier: ^3.25.76 version: 3.25.76 devDependencies: '@rollup/plugin-commonjs':