Skip to content

Fix SDS blob uploads; add RichText utilities#122

Merged
aspiers merged 11 commits into
developfrom
richtext-uploadBlob
Jan 29, 2026
Merged

Fix SDS blob uploads; add RichText utilities#122
aspiers merged 11 commits into
developfrom
richtext-uploadBlob

Conversation

@aspiers
Copy link
Copy Markdown
Contributor

@aspiers aspiers commented Jan 29, 2026

This PR addresses SDS blob upload failures and adds RichText utilities for working with AT Protocol's rich text format.

Bug Fixes

  • Fix SDS blob uploads: Route SDS blob uploads to com.sds.repo.uploadBlob with the required repo query parameter. Previously, all blob uploads used the PDS endpoint regardless of server type, causing failures on SDS servers.
  • Fix profile operations routing: Remove incorrect SDS routing logic from profile operations — profiles use standard ATProto endpoints on both PDS and SDS servers.

New Features

  • RichText utilities (createFacetsFromText, createFacetsFromTextSync): Auto-detect URLs, hashtags, and @mentions from plain text for use in hypercert descriptions. Includes async version with mention-to-DID resolution and sync version for fast detection.

Refactoring

  • DRY blob operations: Extract shared BlobOperations interface with dependency injection, eliminating duplicate blob upload code across HypercertOperationsImpl, ProfileOperationsImpl, and BlobOperationsImpl.
  • Add ProfileOperationsImpl.create(): New method for creating profiles without the read-modify-write pattern.
  • Enhanced JSDoc documentation: Comprehensive examples for collection and project methods including avatar/banner image handling.

Technical Details

The SDS fix uses the agent's fetchHandler directly (same pattern as CollaboratorOperationsImpl and OrganizationOperationsImpl) since com.sds.repo.uploadBlob is not a registered XRPC namespace on the standard AT Protocol Agent.

Files Changed

Area Files
Blob operations BlobOperationsImpl.ts, interfaces.ts
Profile operations ProfileOperationsImpl.ts
Hypercert operations HypercertOperationsImpl.ts
New utility src/lib/rich-text.ts
Tests BlobOperationsImpl.test.ts, ProfileOperationsImpl.test.ts, HypercertOperationsImpl.test.ts

Testing

  • 11 new tests for RichText utilities
  • Updated blob upload tests to mock BlobOperations.upload instead of agent.uploadBlob
  • All existing tests pass with refactored code

Summary by CodeRabbit

  • New Features

    • Rich Text utilities for detecting links, hashtags, and mentions (sync + async)
    • Profile creation API and unified profile update flow
    • Expanded location input support for collections/projects (string, structured, or upload)
    • SDS support for blob uploads
  • Bug Fixes

    • Fixed SDS/PDS routing for blob operations
  • Documentation

    • Expanded JSDoc/examples and session guidance
  • Chores

    • Added format checks to scripts and CI/task configs
  • Tests

    • Added rich-text, SDS blob upload, and location handling tests

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings January 29, 2026 00:59
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jan 29, 2026

🦋 Changeset detected

Latest commit: 4348925

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

@cursor
Copy link
Copy Markdown

cursor Bot commented Jan 29, 2026

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on February 19.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

Adds RichText facet utilities; introduces a BlobOperations abstraction with SDS routing; refactors ProfileOperationsImpl and HypercertOperationsImpl to use BlobOperations (adds profile create/merge logic); updates Repository wiring, tests, mocks, changesets, docs, and package scripts.

Changes

Cohort / File(s) Summary
Rich Text Utilities
packages/sdk-core/src/lib/rich-text.ts, packages/sdk-core/src/index.ts, packages/sdk-core/tests/lib/rich-text.test.ts
Adds createFacetsFromText (async, optional mention resolution), createFacetsFromTextSync, re-exports RichText and facet types, and tests.
Blob Operations Core
packages/sdk-core/src/repository/BlobOperationsImpl.ts, packages/sdk-core/src/repository/types.ts, packages/sdk-core/src/repository/interfaces.ts
Introduces BlobUploadResult/uploadResultToBlobRef, adds isSDS flag and uploadViaSDS path in BlobOperationsImpl, and updates interfaces to surface BlobOperations.
Repository wiring
packages/sdk-core/src/repository/Repository.ts
Repository accessors now construct and inject BlobOperationsImpl (passes isSDS) and supply BlobOperations to Profile/Hypercert implementations.
Hypercerts: blob refactoring & location handling
packages/sdk-core/src/repository/HypercertOperationsImpl.ts, packages/sdk-core/src/services/hypercerts/types.ts
Replaces direct agent blob uploads with blobs.upload(), adds blobToJsonRef, introduces resolveLocationValue supporting string/Blob/object, and updates constructor to accept BlobOperations; adjusts location-related public types.
Profiles: create/update refactor
packages/sdk-core/src/repository/ProfileOperationsImpl.ts, packages/sdk-core/src/repository/interfaces.ts
Constructor now accepts BlobOperations; adds create(params: ProfileParams); update(params: ProfileParams) centralized via mergeParamsIntoProfile with unified blob handling and helpers.
Tests: repository & blob
packages/sdk-core/tests/repository/... (BlobOperationsImpl.test.ts, HypercertOperationsImpl.test.ts, ProfileOperationsImpl.test.ts, HypercertValidation.test.ts)
Tests updated to use MockedBlobOperations and createMockBlobOperations; added SDS-path tests; changed expectations to new flat upload result shape.
Test mocks & utilities
packages/sdk-core/tests/utils/mocks.ts
Adds MockedBlobOperations type, createMockBlobOperations, and fetchHandler support for mocked agents.
Public exports & package scripts
packages/sdk-core/package.json, packages/sdk-react/package.json, packages/sdk-core/src/index.ts
Exports new rich-text APIs/types; prepends format:check to check scripts.
Build orchestration
turbo.json
Removes install and typecheck tasks, adjusts lint/test dependencies to ^build, and adds placeholder format:check and check tasks.
Changesets, docs & beads metadata
.changeset/*, specs/lexicon-sync/*, .beads/issues.jsonl, .gitattributes, .beads/*, AGENTS.md
Adds changesets for rich-text and SDS routing, lexicon sync notes, many .beads files (issues, config, README, gitignore, metadata), and .gitattributes merge driver rule.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Repository
    participant BlobOps as BlobOperations
    participant SDS
    participant PDS

    Client->>Repository: request upload(blob)
    Repository->>BlobOps: upload(data)
    BlobOps->>BlobOps: determine isSDS & derive Content-Type
    alt SDS mode
        BlobOps->>SDS: POST com.sds.repo.uploadBlob?repo=... (Content-Type)
        SDS-->>BlobOps: { ref: { $link }, mimeType, size }
        BlobOps-->>Repository: { ref: { $link }, mimeType, size }
    else PDS mode
        BlobOps->>PDS: agent.com.atproto.repo.uploadBlob (multipart)
        PDS-->>BlobOps: { blob: { ref, mimeType, size } }
        BlobOps-->>Repository: { ref: { $link }, mimeType, size }
    end
    Repository-->>Client: normalized upload result (JsonBlobRef)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • bitbeckers

"I'm a rabbit with a tiny pen,
Facets hop from word to glen.
BlobOps hums and routes with care,
Profiles merge and tests prepare.
CI carrots crunch — hop, code, hooray!" 🐇✨

🚥 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 pull request title clearly and concisely summarizes both main changes: SDS blob upload fixes and RichText utilities addition. It directly reflects the core objectives stated in the PR summary.
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

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.

@aspiers aspiers changed the title Summary Fix SDS blob uploads; add RichText utilities Jan 29, 2026
Copy link
Copy Markdown
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 addresses SDS blob upload failures, adds comprehensive RichText utilities for working with AT Protocol's rich text format, and refactors blob operations using dependency injection for better maintainability.

Changes:

  • Fixed SDS blob uploads by routing to com.sds.repo.uploadBlob with required repo query parameter
  • Added RichText utility functions (createFacetsFromText, createFacetsFromTextSync) for auto-detecting URLs, hashtags, and @mentions
  • Refactored blob operations to use dependency injection, eliminating duplicate code across multiple operation implementations
  • Enhanced JSDoc documentation with comprehensive examples for collection and project methods
  • Added ProfileOperationsImpl.create() method for creating profiles without read-modify-write pattern

Reviewed changes

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

Show a summary per file
File Description
turbo.json Updated task dependency configuration (removed install task, modified check dependencies)
packages/sdk-core/package.json Added format:check to check command
packages/sdk-react/package.json Added format:check to check command
packages/sdk-core/src/repository/BlobOperationsImpl.ts Added SDS blob upload support via uploadViaSDS() method with direct fetchHandler calls
packages/sdk-core/src/repository/ProfileOperationsImpl.ts Refactored to use BlobOperations dependency injection, added create() method, extracted helper methods
packages/sdk-core/src/repository/HypercertOperationsImpl.ts Refactored to use BlobOperations dependency injection, added blobToJsonRef converter, enhanced JSDoc
packages/sdk-core/src/repository/interfaces.ts Added ProfileParams interface for shared profile operation parameters
packages/sdk-core/src/repository/Repository.ts Updated to pass BlobOperations and isSDS flag to operation implementations
packages/sdk-core/src/lib/rich-text.ts New utility module for creating facets from plain text with async/sync variants
packages/sdk-core/src/index.ts Added exports for RichText utilities
packages/sdk-core/tests/**/*.test.ts Updated tests to mock BlobOperations instead of agent.uploadBlob, added comprehensive RichText tests
.changeset/*.md Added three changesets documenting the changes (patch for bug fixes, minor for RichText feature, patch for docs)
AGENTS.md Added "Landing the Plane" section for session completion workflow
.gitattributes Added custom merge strategy for beads JSONL files
.beads/issues.jsonl Issue tracking data (project management)
specs/*.md Updated specification tracking documents

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

Comment thread turbo.json
"dependsOn": ["^build"]
},
"format:check": {},
"check": {},
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The "check" task now has no dependencies, but it likely should still depend on the tasks it's checking (lint, typecheck, build, test). The empty dependencies array means "check" won't wait for or trigger any other tasks. Consider whether this is intentional or if it should maintain some dependency chain.

Suggested change
"check": {},
"check": {
"dependsOn": ["lint", "typecheck", "test", "format:check"]
},

Copilot uses AI. Check for mistakes.
private repoDid: string,
private _serverUrl: string,
private isSDS: boolean,
private repo?: unknown,
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The repo parameter is declared but never used in the class. Consider removing this unused parameter unless it's intended for future use. If it's reserved for future functionality, add a JSDoc comment explaining its purpose.

Copilot uses AI. Check for mistakes.
Comment thread turbo.json
Comment on lines 4 to 7
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The "install" task has been removed from turbo.json, but typical Turborepo setups include this task to handle dependency installation in monorepos. Verify that this removal doesn't break the dependency installation workflow, especially in CI/CD environments or when new developers clone the repository.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts (2)

1203-1231: Inconsistent blob upload mock in mixed content test.

This test still uses mockAgent.com.atproto.repo.uploadBlob (line 1205-1210) while other tests in this file have been updated to use mockBlobs.upload. This inconsistency could cause the test to fail or produce unexpected behavior since HypercertOperationsImpl now uses BlobOperations for uploads.

🔧 Proposed fix
     it("should add attachment with multiple content items (mixed URIs and Blobs)", async () => {
       const blob = new Blob(["evidence data"], { type: "application/pdf" });
-      mockAgent.com.atproto.repo.uploadBlob.mockResolvedValue({
-        success: true,
-        data: {
-          blob: { ref: { $link: "blob-cid" }, mimeType: "application/pdf", size: 100 },
-        },
+      mockBlobs.upload.mockResolvedValue({
+        ref: { $link: "blob-cid" },
+        mimeType: "application/pdf",
+        size: 100,
       });

       const result = await hypercertOps.addAttachment({
@@ -1217,7 +1215,7 @@ describe("HypercertOperationsImpl", () => {
       });

       expect(result.uri).toContain("attachment");
-      expect(mockAgent.com.atproto.repo.uploadBlob).toHaveBeenCalled();
+      expect(mockBlobs.upload).toHaveBeenCalled();
       const call = mockAgent.com.atproto.repo.createRecord.mock.calls[0][0];

1976-2032: Another inconsistent blob upload mock in project creation test.

The test "should create a project with avatar and banner" at lines 1976-2032 uses mockAgent.com.atproto.repo.uploadBlob instead of mockBlobs.upload. This should be updated for consistency with the BlobOperations refactoring.

🔧 Proposed fix
     it("should create a project with avatar and banner", async () => {
       const logoBlob = new Blob(["logo"], { type: "image/png" });
       const headerBlob = new Blob(["header"], { type: "image/jpeg" });

-      mockAgent.com.atproto.repo.uploadBlob.mockResolvedValueOnce({
-        success: true,
-        data: {
-          blob: {
-            $type: "blob",
-            ref: { $link: "bafyrei-logo" },
-            mimeType: "image/png",
-            size: 150,
-          },
-        },
+      mockBlobs.upload.mockResolvedValueOnce({
+        ref: { $link: "bafyrei-logo" },
+        mimeType: "image/png",
+        size: 150,
       });

-      mockAgent.com.atproto.repo.uploadBlob.mockResolvedValueOnce({
-        success: true,
-        data: {
-          blob: {
-            $type: "blob",
-            ref: { $link: "bafyrei-header" },
-            mimeType: "image/jpeg",
-            size: 250,
-          },
-        },
+      mockBlobs.upload.mockResolvedValueOnce({
+        ref: { $link: "bafyrei-header" },
+        mimeType: "image/jpeg",
+        size: 250,
       });
🤖 Fix all issues with AI agents
In `@packages/sdk-core/src/repository/ProfileOperationsImpl.ts`:
- Around line 308-329: In update, check the result of the
com.atproto.repo.getRecord call (existing.success) before accessing
existing.data.value; if existing.success is false (record missing) treat
existingProfile as an empty object rather than indexing existing.data.value,
then call mergeParamsIntoProfile with that safe object and proceed to
com.atproto.repo.putRecord; ensure you reference the getRecord result (existing)
and avoid dereferencing existing.data.value when existing.success is falsy so
update and putRecord won't throw on new accounts.
- Around line 92-105: applyBlobField currently assigns only uploadResult.ref
(the { $link } object) to profile blob fields, but AT Protocol requires the full
blob object { $type: "blob", ref: { $link }, mimeType, size }; update
applyBlobField so after uploading (using this.blobs.upload) you convert
uploadResult into the full blob reference structure before assigning to
result[field] (include $type:"blob", ref:uploadResult.ref,
mimeType:uploadResult.mimeType, size:uploadResult.size). Reuse the existing
HypercertOperationsImpl.blobToJsonRef() implementation or extract that logic
into a shared helper and call it from applyBlobField; ensure null deletes the
field and undefined still returns early, and keep blobs.upload as the upload
call.
🧹 Nitpick comments (10)
.gitattributes (1)

1-3: Custom merge driver requires local configuration.

The merge=beads attribute relies on a custom merge driver that must be configured in each developer's .git/config or global .gitconfig. Without this configuration, Git will use the default merge behavior for the JSONL file, which may cause conflicts on concurrent edits.

Consider documenting the merge driver setup in the README or a contributing guide so that team members can configure it correctly.

packages/sdk-core/src/repository/BlobOperationsImpl.ts (2)

163-195: Consider extracting error details from failed SDS responses.

The current implementation only uses response.statusText for error messages. Parsing the response body on failure could provide more actionable error details.

♻️ Proposed enhancement for better error messages
     if (!response.ok) {
-      throw new NetworkError(`SDS blob upload failed: ${response.statusText}`);
+      let errorDetail = response.statusText;
+      try {
+        const errorBody = await response.json() as { message?: string; error?: string };
+        errorDetail = errorBody.message || errorBody.error || errorDetail;
+      } catch {
+        // Ignore JSON parse errors, use statusText
+      }
+      throw new NetworkError(`SDS blob upload failed: ${errorDetail}`);
     }

69-71: Remove the unused repo parameter from the constructor.

The repo parameter is passed from the Repository class but never referenced within BlobOperationsImpl. Removing it will eliminate dead code.

♻️ Remove the unused parameter
   constructor(
     private agent: Agent,
     private repoDid: string,
     private _serverUrl: string,
     private isSDS: boolean,
-    private repo?: unknown,
   ) {}

Also update the instantiation in Repository.ts line 369:

-      this._blobs = new BlobOperationsImpl(this.agent, this.repoDid, this.serverUrl, this._isSDS, this);
+      this._blobs = new BlobOperationsImpl(this.agent, this.repoDid, this.serverUrl, this._isSDS);
specs/lexicon-sync/v0.10.0-beta.4-v0.10.0-beta.11.md (1)

66-112: Avoid hard‑coding test counts in docs.

Line 85 includes exact test counts, which tend to drift as suites evolve. Consider phrasing as “tests pass” or linking to CI if you want to keep this accurate.

packages/sdk-core/tests/utils/mocks.ts (1)

200-217: Centralize BlobOperations mocks under src/testing.

Since this helper is meant for unit tests, consider moving or re-exporting it via src/testing/ so tests can standardize on that location. As per coding guidelines Use mocks from src/testing/ for unit tests.

.changeset/add-richtext-utility.md (1)

1-12: Add a brief compatibility note for the 0.x minor bump.

Consider stating that this is additive and no breaking changes are expected, since 0.x minor releases can still be perceived as breaking without explicit context. Based on learnings For pre-1.0 libraries (0.x), minor version bumps can introduce breaking changes. In changeset files, note that breaking changes may occur with minor bumps and clearly document any API changes, removal or behavioral changes, and compatibility caveats.

packages/sdk-core/src/index.ts (2)

253-254: Consider renaming rich-text to camelCase for utilities.

The new module is a utility; per convention, a name like richText.ts would align better with the file-naming guideline. As per coding guidelines Use camelCase for file names containing utilities and helper functions.


254-255: Export RichText types via the dedicated types entrypoint.

If RichTextResult and AppBskyRichtextFacet are meant for public use, consider adding them to the types entrypoint and re-exporting from there to keep type exports centralized. As per coding guidelines Export types from dedicated entrypoints (e.g., @hypercerts-org/sdk-core/types).

.changeset/fix-sds-endpoint-routing.md (1)

1-15: Patch bump may undersell the new public API.

This changeset includes a new public method (ProfileOperationsImpl.create) and a new BlobOperations abstraction; consider a minor bump or explicitly calling out the API additions and any compatibility expectations. Based on learnings For pre-1.0 libraries (0.x), minor version bumps can introduce breaking changes. In changeset files, note that breaking changes may occur with minor bumps and clearly document any API changes, removal or behavioral changes, and compatibility caveats.

packages/sdk-core/src/repository/HypercertOperationsImpl.ts (1)

954-968: Unused parameter _fallbackMimeType should be removed.

The _fallbackMimeType parameter is no longer used after the refactoring. Previously it may have been needed for direct blob upload calls, but BlobOperations.upload() handles MIME type detection internally via Blob.type.

♻️ Suggested fix
-  private async resolveUriOrBlob(content: string | Blob, _fallbackMimeType: string) {
+  private async resolveUriOrBlob(content: string | Blob) {
     if (typeof content === "string") {
       const uriRef = {
         $type: "org.hypercerts.defs#uri",
         uri: content,
       } satisfies $Typed<OrgHypercertsDefs.Uri>;
       return uriRef;
     }
 
     const uploadResult = await this.blobs.upload(content);
     return {
       $type: "org.hypercerts.defs#smallBlob" as const,
       blob: uploadResult,
     };
   }

Also update call sites (e.g., lines 990 and 1109) to remove the fallback argument.

Comment thread packages/sdk-core/src/repository/ProfileOperationsImpl.ts
Comment thread packages/sdk-core/src/repository/ProfileOperationsImpl.ts
@aspiers aspiers force-pushed the richtext-uploadBlob branch from 8802b3c to bbc6f44 Compare January 29, 2026 01:10
Copilot AI review requested due to automatic review settings January 29, 2026 01:20
@aspiers aspiers force-pushed the richtext-uploadBlob branch from bbc6f44 to 363bf0e Compare January 29, 2026 01:20
Copy link
Copy Markdown

@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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sdk-core/src/repository/HypercertOperationsImpl.ts (1)

954-986: Ensure uploaded blobs are converted to JsonBlobRef.

BlobOperations.upload returns { ref, mimeType, size } without $type. In resolveUriOrBlob and resolveCollectionImageInput, that object is embedded directly, so records may fail lexicon validation and serialize missing $type: "blob". Convert with blobToJsonRef before embedding.

Suggested fix
-    const uploadResult = await this.blobs.upload(content);
-    return {
-      $type: "org.hypercerts.defs#smallBlob" as const,
-      blob: uploadResult,
-    };
+    const uploadResult = await this.blobs.upload(content);
+    return {
+      $type: "org.hypercerts.defs#smallBlob" as const,
+      blob: this.blobToJsonRef(uploadResult),
+    };

-    const uploadResult = await this.blobs.upload(input);
-    if (isBanner) {
-      return { $type: "org.hypercerts.defs#largeImage" as const, image: uploadResult };
-    }
-    return { $type: "org.hypercerts.defs#smallImage" as const, image: uploadResult };
+    const uploadResult = await this.blobs.upload(input);
+    const imageRef = this.blobToJsonRef(uploadResult);
+    if (isBanner) {
+      return { $type: "org.hypercerts.defs#largeImage" as const, image: imageRef };
+    }
+    return { $type: "org.hypercerts.defs#smallImage" as const, image: imageRef };
🤖 Fix all issues with AI agents
In @.changeset/fix-sds-endpoint-routing.md:
- Around line 1-15: Update the changeset to mark the constructor signature
change as breaking: change the package version bump for
"@hypercerts-org/sdk-core" from patch to minor (per 0.x semantics) and add a
"BREAKING CHANGES" or "breaking" note that the BlobOperations
constructor/signature changed and HypercertOperationsImpl and
ProfileOperationsImpl now require a BlobOperations instance; ensure the
changeset text clearly describes the new required constructor parameter and
recommends consumers update their instantiation code accordingly.

In `@packages/sdk-core/src/repository/ProfileOperationsImpl.ts`:
- Around line 188-239: Update the ProfileParams type to include optional $type
and createdAt fields, then modify mergeParamsIntoProfile to apply defaults using
nullish coalescing so callers can override them while ensuring the record always
includes both; specifically, ensure create() continues to call
mergeParamsIntoProfile({} , params) and that mergeParamsIntoProfile sets
profile.$type = params.$type ?? "app.bsky.actor.profile" (or the canonical
default) and profile.createdAt = params.createdAt ?? new Date().toISOString()
(or repository-generated timestamp) so the resulting record always contains
$type and createdAt.
🧹 Nitpick comments (3)
packages/sdk-core/tests/repository/HypercertValidation.test.ts (1)

6-38: Use mocks from src/testing for unit tests.

createMockBlobOperations is pulled from tests/utils/mocks, but the test guidelines require mocks to come from src/testing/. Please move or re-import the mock from src/testing to keep unit-test utilities centralized.

As per coding guidelines: Use mocks from src/testing/ for unit tests.

packages/sdk-core/src/repository/interfaces.ts (1)

659-727: Add a Zod schema for ProfileParams to enable runtime validation.

You’ve introduced a new public params shape; please add a ProfileParamsSchema (and derive the type from it) or otherwise surface Zod validation for this type to align with the runtime validation guideline.

As per coding guidelines: Use Zod schemas for runtime validation in TypeScript files.

packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts (1)

5-6: Consider re-exporting mock helpers from src/testing/.

The new createMockBlobOperations import is from tests/utils/mocks. If it isn’t already re-exported from src/testing/, consider moving/re-exporting it there to align the test-helper location convention.

As per coding guidelines: Use mocks from src/testing/ for unit tests.

Comment thread .changeset/fix-sds-endpoint-routing.md
Comment thread packages/sdk-core/src/repository/ProfileOperationsImpl.ts
- Add RichText utility (src/lib/rich-text.ts) with createFacetsFromText()
  and createFacetsFromTextSync() for auto-detecting mentions, URLs, hashtags
- Add comprehensive tests for RichText utility (11 tests)
- Export RichText utilities from main index
- Enhance JSDoc for createCollection, updateCollection, createProject,
  updateProject with avatar/banner examples
- Update spec file to mark Change 2 and Change 3 as complete
Copy link
Copy Markdown
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

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


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

Comment thread packages/sdk-core/src/repository/BlobOperationsImpl.ts Outdated
Comment thread turbo.json
Comment on lines +17 to +18
"format:check": {},
"check": {},
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The turbo.json changes remove the install task entirely and simplify task dependencies. However, this creates inconsistency:

  1. The format:check and check tasks no longer have dependsOn arrays (empty objects), while other tasks like lint, test, and typecheck depend on ^build.

  2. The check task has no dependencies defined here, but in package.json files it's defined as a composite command that runs format:check, lint, typecheck, build, and test in sequence.

This could lead to confusion where the task orchestration is split between turbo.json (which handles cross-package dependencies) and package.json (which handles within-package command sequences). Consider documenting this pattern or ensuring consistency.

Suggested change
"format:check": {},
"check": {},
"format:check": {
"dependsOn": ["^build"]
},
"check": {
"dependsOn": ["^build"]
},

Copilot uses AI. Check for mistakes.
data: Uint8Array,
encoding: string,
): Promise<{ ref: { $link: string }; mimeType: string; size: number }> {
const url = `/xrpc/com.sds.repo.uploadBlob?repo=${encodeURIComponent(this.repoDid)}`;
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The SDS blob upload endpoint constructs a URL with the repoDid parameter using encodeURIComponent(), which is good for preventing URL injection. However, there's no validation that this.repoDid is actually a valid DID format before making the request.

While this is likely validated elsewhere during Repository construction, consider adding a comment or assertion to clarify that repoDid is expected to be pre-validated, or add a basic format check (e.g., starts with "did:") to fail fast with a clear error message if the DID is invalid.

Copilot uses AI. Check for mistakes.
aspiers and others added 5 commits January 29, 2026 15:02
Without this patch, blob uploads always use the PDS endpoint
(com.atproto.repo.uploadBlob) regardless of server type, and profile
operations incorrectly route through non-existent com.sds.repo.*
endpoints. This causes blob uploads to fail on SDS servers because the
repository DID is never passed.

This patch solves the problem by:

1. Using com.sds.repo.uploadBlob with the repo query parameter for SDS
   servers, and com.atproto.repo.uploadBlob for PDS servers
2. Removing incorrect SDS routing from profile operations — SDS servers
   override standard ATProto endpoints rather than exposing separate ones
3. Extracting a shared BlobOperations interface with dependency injection,
   eliminating duplicate blob upload code across HypercertOperationsImpl,
   ProfileOperationsImpl, and BlobOperationsImpl
4. Adding a ProfileOperationsImpl.create() method for creating new
   profiles without the read-modify-write pattern
5. Refactoring applyParamsToProfile with helper methods to reduce
   duplication

Co-authored-by: Claude <noreply@anthropic.com>
- Document CreateLocationParams with examples for string, URL, Blob, and structured formats
- Add documentation for LocationParams union type
- Add JSDoc to resolveLocationValue explaining format handling
- Add examples to createLocationRecord method

Closes hypercerts-sdk-38n
- Add test for attachLocation with simple text string (beta.13+ format)
- Add test for attachLocation with geo: URI string
- Add test for createCollection with simple text location string
- Verify strings get wrapped in URI ref format

Closes hypercerts-sdk-d2e
@aspiers aspiers force-pushed the richtext-uploadBlob branch from 363bf0e to 4348925 Compare January 29, 2026 02:19
Copy link
Copy Markdown

@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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/sdk-core/src/repository/HypercertOperationsImpl.ts (1)

950-982: Wrap uploaded blobs with blobToJsonRef before embedding.

resolveUriOrBlob (line 959) and resolveCollectionImageInput (lines 976–981) embed raw upload results directly without wrapping them. Lexicon blob fields require JsonBlobRef format with { $type: "blob", ref, mimeType, size }. Other methods in this file (lines 174, 705) correctly use this.blobToJsonRef() for this purpose.

🛠️ Suggested fix
-    const uploadResult = await this.blobs.upload(content);
-    return {
-      $type: "org.hypercerts.defs#smallBlob" as const,
-      blob: uploadResult,
-    };
+    const uploadResult = await this.blobs.upload(content);
+    return {
+      $type: "org.hypercerts.defs#smallBlob" as const,
+      blob: this.blobToJsonRef(uploadResult),
+    };
-    const uploadResult = await this.blobs.upload(input);
+    const uploadResult = await this.blobs.upload(input);
     if (isBanner) {
-      return { $type: "org.hypercerts.defs#largeImage" as const, image: uploadResult };
+      return { $type: "org.hypercerts.defs#largeImage" as const, image: this.blobToJsonRef(uploadResult) };
     }

-    return { $type: "org.hypercerts.defs#smallImage" as const, image: uploadResult };
+    return { $type: "org.hypercerts.defs#smallImage" as const, image: this.blobToJsonRef(uploadResult) };
🤖 Fix all issues with AI agents
In @.changeset/add-richtext-utility.md:
- Around line 1-12: Update the changeset to explicitly state the 0.x minor-bump
posture and any compatibility caveats: inside the .changeset entry for
"@hypercerts-org/sdk-core" add a short paragraph whether this minor bump is
intended to be non‑breaking for 0.x or may introduce breaking changes, and list
affected APIs and behavioral notes (mention createFacetsFromText,
createFacetsFromTextSync and the RichText re-export), including any removals,
signature changes, or runtime behavior differences (e.g., async mention
resolution requiring an agent) and recommended upgrade guidance for consumers.

In `@packages/sdk-core/src/lib/rich-text.ts`:
- Around line 40-43: Update the JSDoc for the async function
createFacetsFromText to match the sync variant and tests: replace the incorrect
statement that mentions without an agent will have "empty DIDs" with text
stating that unresolved mentions will use the handle string as the DID (e.g.,
"alice.bsky.social"); ensure the description for the agent parameter and the
return value reflect this behavior consistently with createFacetsFromTextSync
and existing tests.

In `@packages/sdk-core/src/repository/HypercertOperationsImpl.ts`:
- Around line 1880-1885: The doc examples for repo.hypercerts.createCollection
use the old location shape ({ value: ... }) and must be updated to the new
CreateLocationParams shape; locate the example blocks around the
createCollection usage in HypercertOperationsImpl (the doc comments that show
const collection = await repo.hypercerts.createCollection({...})), and replace
the location object with the required fields from CreateLocationParams (e.g.,
include srs and the appropriate lpVersion or locationType plus the descriptive
name/text fields) so the examples match the runtime shape expected by the
CreateLocationParams type.
🧹 Nitpick comments (9)
AGENTS.md (2)

95-118: Consider reversing the preference order to align with established best practices.

The current guidance prefers cd packages/<package> and treats --filter as an alternative. However, based on learnings, the repository's established best practice is to "Use pnpm workspaces with pnpm --filter <package> for package-specific commands." The --filter approach is more robust (works from any directory, clearer in CI/scripts) while cd'ing has trade-offs (requires knowing current directory, uses relative paths).

Consider reframing this section to recommend --filter as the primary approach, with the note that developers may prefer cd'ing into packages for interactive work when they want cleaner output or easier argument passing.

Based on learnings: "Use pnpm workspaces with pnpm --filter <package> for package-specific commands"


486-486: Provide guidance for "File issues for remaining work" step.

Step 1 of the mandatory workflow instructs to "File issues for remaining work" but provides no guidance on:

  • How to create issues (GitHub CLI? Web interface? API?)
  • What information to include in issues
  • How to link issues to the current work
  • Templates or examples to follow

Consider adding a brief example or reference to issue creation tools/process.

packages/sdk-core/src/repository/types.ts (1)

121-143: Add runtime validation for BlobUploadResult before conversion.
This is a public helper; a malformed upload result will silently generate invalid blob refs. Consider a small Zod schema and throw a ValidationError on failure before constructing JsonBlobRef.

As per coding guidelines, Use Zod schemas for runtime validation in TypeScript files; packages/sdk-core/src/**/*: Use error hierarchy from sdk-core/src/core/errors.ts: HypercertsError, ValidationError, AuthenticationError, NetworkError, NotFoundError, SDSRequiredError.

packages/sdk-core/src/lib/rich-text.ts (2)

1-8: Rename utility file to camelCase for consistency.
Consider renaming to richText.ts and updating exports/imports/tests accordingly.

As per coding guidelines, Use camelCase for file names containing utilities and helper functions.


88-104: Validate text input at runtime for this public API.
Consider a minimal Zod schema (and ValidationError on failure) to guard non‑string inputs before constructing RichText.

As per coding guidelines, Use Zod schemas for runtime validation in TypeScript files; packages/sdk-core/src/**/*: Use error hierarchy from sdk-core/src/core/errors.ts: HypercertsError, ValidationError, AuthenticationError, NetworkError, NotFoundError, SDSRequiredError.

packages/sdk-core/src/services/hypercerts/types.ts (1)

284-376: Add UpdateLocationParams to complete the location type set.

You added CreateLocationParams and LocationParams, but the SDK pattern for lexicon entities expects an Update*Params alias as well. This also keeps updates consistent with Partial<Create*>.

♻️ Suggested addition
 export type CreateLocationParams = OverrideProperties<
   SetOptional<HypercertLocation, "$type" | "createdAt">,
   {
     location: string | Blob | HypercertLocation["location"];
   }
 >;

+export type UpdateLocationParams = Partial<CreateLocationParams>;
+
 export type LocationParams = StrongRef | string | CreateLocationParams;

As per coding guidelines: For each lexicon entity, define five types: Main (from lexicon), SDK type, CreateParams, UpdateParams, and union Params type.

packages/sdk-core/tests/repository/BlobOperationsImpl.test.ts (1)

5-13: Align mocks/fixtures with repo test guidelines.

These tests import mocks/constants from tests/utils/mocks.ts. The repo guidelines call for using mocks from src/testing/ and fixtures from tests/utils/fixtures.ts. Consider switching to those sources for consistency.

As per coding guidelines: “Use mocks from src/testing/ for unit tests” and “Use fixtures from tests/utils/fixtures.ts in test files”.

packages/sdk-core/tests/repository/ProfileOperationsImpl.test.ts (1)

5-16: Use standard test mocks/fixtures.

Repo guidelines prefer mocks from src/testing/ and fixtures from tests/utils/fixtures.ts. Consider swapping createMockAgent/createMockBlobOperations and TEST_REPO_DID to those sources if available.

As per coding guidelines: “Use mocks from src/testing/ for unit tests” and “Use fixtures from tests/utils/fixtures.ts in test files”.

packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts (1)

5-6: Use standard test mocks/fixtures.

Repo guidelines prefer mocks from src/testing/ and fixtures from tests/utils/fixtures.ts. Consider switching away from tests/utils/mocks.ts and reusing fixtures for constants like repo DID.

As per coding guidelines: “Use mocks from src/testing/ for unit tests” and “Use fixtures from tests/utils/fixtures.ts in test files”.

Also applies to: 28-38

Comment thread .changeset/add-richtext-utility.md
Comment on lines +40 to +43
* @param text - The plain text to parse for facets
* @param agent - Optional AT Protocol agent for resolving @mentions to DIDs.
* Without an agent, mentions will have empty DIDs (invalid for posting).
* @returns Promise resolving to the text and detected facets
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

head -150 packages/sdk-core/src/lib/rich-text.ts | cat -n

Repository: hypercerts-org/hypercerts-sdk

Length of output: 5580


🏁 Script executed:

find . -type f -name "*rich-text*" | grep -E "\.(test|spec)\.(ts|tsx|js)"

Repository: hypercerts-org/hypercerts-sdk

Length of output: 120


🏁 Script executed:

cat -n ./packages/sdk-core/tests/lib/rich-text.test.ts

Repository: hypercerts-org/hypercerts-sdk

Length of output: 6311


Fix incorrect async function documentation on unresolved mentions.

The createFacetsFromText documentation (line 42) incorrectly states that mentions without an agent will have "empty DIDs." However, the actual behavior—confirmed by tests—is that mentions receive the handle string as the DID (e.g., alice.bsky.social), not an empty value. Align the async function's documentation with the sync function's more accurate description (lines 110-112) and the test expectations (test line 45).

🤖 Prompt for AI Agents
In `@packages/sdk-core/src/lib/rich-text.ts` around lines 40 - 43, Update the
JSDoc for the async function createFacetsFromText to match the sync variant and
tests: replace the incorrect statement that mentions without an agent will have
"empty DIDs" with text stating that unresolved mentions will use the handle
string as the DID (e.g., "alice.bsky.social"); ensure the description for the
agent parameter and the return value reflect this behavior consistently with
createFacetsFromTextSync and existing tests.

Comment on lines +1880 to 1885
* ```typescript
* const collection = await repo.hypercerts.createCollection({
* title: "Amazon Basin Projects",
* items: [...],
* location: { value: "Amazon Rainforest, Brazil", name: "Amazon Basin" },
* });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Location examples still use the old value shape.

The examples show location: { value: ... }, but the new CreateLocationParams expects location plus required fields like srs (and usually lpVersion / locationType). These examples will error at runtime. Please update them to the new shape.

✍️ Example update (apply similarly in all three locations)
-  location: { value: "Amazon Rainforest, Brazil", name: "Amazon Basin" },
+  location: {
+    lpVersion: "1.0.0",
+    srs: "EPSG:4326",
+    locationType: "coordinate-decimal",
+    location: "Amazon Rainforest, Brazil",
+    name: "Amazon Basin",
+  },

Also applies to: 2088-2094, 2355-2359

🤖 Prompt for AI Agents
In `@packages/sdk-core/src/repository/HypercertOperationsImpl.ts` around lines
1880 - 1885, The doc examples for repo.hypercerts.createCollection use the old
location shape ({ value: ... }) and must be updated to the new
CreateLocationParams shape; locate the example blocks around the
createCollection usage in HypercertOperationsImpl (the doc comments that show
const collection = await repo.hypercerts.createCollection({...})), and replace
the location object with the required fields from CreateLocationParams (e.g.,
include srs and the appropriate lpVersion or locationType plus the descriptive
name/text fields) so the examples match the runtime shape expected by the
CreateLocationParams type.

@aspiers aspiers merged commit 3d19806 into develop Jan 29, 2026
4 checks passed
@aspiers aspiers deleted the richtext-uploadBlob branch January 29, 2026 02:50
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.

2 participants