Skip to content

Comments

refactor(sdk-core): extract URI parsing/record helpers, fix blob upload and scope validation#126

Merged
aspiers merged 8 commits intohypercerts-org:developfrom
Kzoeps:location-refactor
Feb 18, 2026
Merged

refactor(sdk-core): extract URI parsing/record helpers, fix blob upload and scope validation#126
aspiers merged 8 commits intohypercerts-org:developfrom
Kzoeps:location-refactor

Conversation

@Kzoeps
Copy link
Contributor

@Kzoeps Kzoeps commented Feb 3, 2026

Changes

  • Extract AT_URI_REGEX as a constant for reuse, and fix the rkey capture group (.+[^/]+) to prevent over-matching across path segments
  • Add fetchRecord<T>() and saveRecord() internal helpers to reduce repeated code; fetchRecord now throws NetworkError when the CID is absent rather than silently defaulting to an empty string
  • Fix saveRecord error construction (previously passed two string args to NetworkError instead of a message + cause)
  • Use repo.blobs.upload() and return a proper BlobRef instance instead of manually constructing a blob-like object — this is a breaking change: the return type of upload() is now BlobRef (from @atproto/lexicon). SDS uploads construct the BlobRef via CID.parse so that ref, mimeType, and size are all correctly populated from the server response
  • Remove dead parseAndValidateUri private method and its unused imports (isValidAtUri, AtUriComponents)
  • Eliminate the double fetchRecord call in updateProject by extracting a private updateCollectionRecord helper that accepts a pre-fetched record
  • Fix misleading test name in permissions.test.ts

Note: PR #125 (on which this was originally dependent) has since been merged.

Summary by CodeRabbit

  • New Features

    • Exported AT_URI_REGEX and new utilities for AT-URI parsing and record fetch/save.
  • Bug Fixes

    • Tightened AT-URI parsing to avoid over-matching.
    • Improved error handling and messaging for missing CIDs and network failures.
  • Breaking Changes

    • Upload now returns a BlobRef object; use its methods (e.g., toString()) to access the reference.
  • Refactor

    • Consolidated URI parsing and removed redundant network calls.
  • Tests

    • Expanded coverage for utilities and error scenarios.

@changeset-bot
Copy link

changeset-bot bot commented Feb 3, 2026

🦋 Changeset detected

Latest commit: 5eda6ee

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 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Centralizes AT-URI parsing and record fetch/save into exported utilities (AT_URI_REGEX, parseAtUri, fetchRecord, saveRecord), refactors HypercertOperationsImpl to use them (removing redundant fetches), changes BlobOperationsImpl.upload to return an AT Protocol BlobRef, and tightens permission-scope regex; tests updated accordingly.

Changes

Cohort / File(s) Summary
AT-URI regex & lexicon utils
packages/sdk-core/src/lexicons/utils.ts, packages/sdk-core/src/index.ts
Add and export AT_URI_REGEX (RegExp) alongside existing lexicon utilities.
Repository blob handling
packages/sdk-core/src/repository/BlobOperationsImpl.ts, packages/sdk-core/tests/repository/BlobOperationsImpl.test.ts
SDS upload now parses blob ref into a CID and returns a BlobRef (from @atproto/lexicon) instead of a plain {ref,mimeType,size} object; tests updated to expect BlobRef.toString() and adjusted error cases.
Core record helpers & Hypercert operations
packages/sdk-core/src/repository/HypercertOperationsImpl.ts, packages/sdk-core/tests/repository/HypercertOperationsImpl.test.ts
Introduce parseUri/fetchRecord<T>/saveRecord helpers; centralize get/putRecord logic across many methods, remove parseAndValidateUri, avoid redundant fetches, standardize ValidationError/NetworkError handling, and update tests/mocks for cid/uri shapes.
Permissions validation
packages/sdk-core/src/auth/permissions.ts, packages/sdk-core/tests/auth/permissions.test.ts
Tighten validateScope regex to anchor atproto and accept repo, blob, rpc prefixes with : or ?; add positive/negative tests for query-style and invalid prefixes.
Tests for new utilities
packages/sdk-core/tests/lexicons/utils.test.ts
Add tests exercising exported AT_URI_REGEX.
Changelog & gitignore
.changeset/refactor-uri-parsing-utilities.md, .beads/.gitignore, package.json
Add changeset documenting the refactor/minor package bump; add Dolt ignore entries to .gitignore.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant HypercertOps as HypercertOperationsImpl
  participant ATProto
  participant SDS as Storage

  Client->>HypercertOps: request using at://... URI (get/update/attach)
  HypercertOps->>HypercertOps: parseAtUri / parseUri (AT_URI_REGEX)
  HypercertOps->>ATProto: getRecord (fetchRecord)
  ATProto-->>HypercertOps: record + cid
  HypercertOps->>HypercertOps: apply updates in-memory
  HypercertOps->>ATProto: putRecord (saveRecord)
  ATProto-->>HypercertOps: result { uri, cid }
  HypercertOps-->>Client: return result

  alt Blob upload via SDS
    Client->>HypercertOps: upload(blob)
    HypercertOps->>SDS: upload request
    SDS-->>HypercertOps: blob response (ref link, mimeType, size)
    HypercertOps->>HypercertOps: CID.parse -> new BlobRef(...)
    HypercertOps-->>Client: BlobRef
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hop through at:// lanes so spry,
Regex snug, helpers tidy and spry.
Blobs now wear refs with proper shine,
Fewer fetches, one clean line.
A rabbit wiggles—code looks fine.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Location refactor' is vague and does not clearly convey the main changes. The PR involves URI parsing refactoring, BlobRef handling, and internal helper consolidation—not primarily location-related operations. Use a more specific title that reflects the core changes, such as 'Refactor URI parsing and consolidate blob/record operations' or 'Extract AT_URI_REGEX and add internal record helpers'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@Kzoeps Kzoeps force-pushed the location-refactor branch from a4f97a6 to 9358d16 Compare February 3, 2026 08:41
@Kzoeps Kzoeps self-assigned this Feb 4, 2026
@Kzoeps Kzoeps force-pushed the profile branch 9 times, most recently from 5fa030d to 1e5e282 Compare February 16, 2026 09:36
@aspiers aspiers changed the base branch from profile to develop February 18, 2026 12:38
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.

Actionable comments posted: 4

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)

266-271: 🛠️ Refactor suggestion | 🟠 Major

Remove parseAndValidateUri method — it is dead code.

No callers remain after migration to parseUri. The method performs redundant validation before parsing, while parseUri (active at lines 167, 852, 1043, 1795, 2428) serves the same purpose more efficiently.

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

In `@packages/sdk-core/src/repository/HypercertOperationsImpl.ts` around lines 266
- 271, Remove the dead method parseAndValidateUri from class
HypercertOperationsImpl: delete the private parseAndValidateUri(uri: string):
AtUriComponents { ... } implementation and any leftover references or imports
solely used by it, and ensure all callers use the existing parseUri (the active
function referenced at lines near usages) instead; verify there are no remaining
references to parseAndValidateUri and run tests/type-checks to confirm clean
removal.
🧹 Nitpick comments (4)
.changeset/refactor-uri-parsing-utilities.md (1)

2-2: patch undersells a public API breaking change — consider minor with explicit breakage note.

The upload() return type changes from a plain { ref: { $link: string }, mimeType, size } object to a BlobRef class instance where ref is a CID object (.toString()) rather than a { $link: string } plain object. Any consumer that accessed uploadResult.ref.$link will silently break at runtime. For a 0.x pre-release package, this warrants at least a minor bump with an explicit compatibility note per this project's changeset conventions.

♻️ Suggested update
-"@hypercerts-org/sdk-core": patch
+"@hypercerts-org/sdk-core": minor

And add to the bullet list:

+- **BREAKING**: `BlobOperationsImpl.upload()` now returns a `BlobRef` class instance; the `ref` field is a CID object (use `.ref.toString()` instead of `.ref.$link`)

Based on learnings, for pre-1.0 libraries (0.x), minor version bumps can introduce breaking changes and should clearly document any API changes, removal or behavioral changes.

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

In @.changeset/refactor-uri-parsing-utilities.md at line 2, Update the changeset
from "patch" to "minor" and add an explicit breaking-change note describing the
upload() return-type change: explain that upload() now returns a BlobRef
instance where ref is a CID object (string via .toString()) instead of a plain {
$link: string } object, and that any consumer accessing uploadResult.ref.$link
will break; include a clear migration hint (e.g., use
uploadResult.ref.toString() or unwrap BlobRef) and add this bullet to the list
of changes in the changeset file so release tooling and consumers are aware of
the API break.
packages/sdk-core/src/repository/HypercertOperationsImpl.ts (2)

1042-1050: Variable record shadows the concept of "record data" — rename to fetchResult for clarity.

const record = await this.fetchRecord(uri) holds a { uri, cid, record, collection, rkey } result object. Accessing record.cid on line 1045/1049 is the result object's cid, not the record field's cid. The name record collides with the inner record field of the same object, making the code confusing to reason about.

♻️ Proposed rename
-    const record = await this.fetchRecord(uri);
-    if (!record.cid) {
-      throw new NetworkError(`Record missing CID for repo=${did}, collection=${collection}, rkey=${rkey}`);
-    }
-    return { $type: "com.atproto.repo.strongRef" as const, uri, cid: record.cid };
+    const fetchResult = await this.fetchRecord(uri);
+    if (!fetchResult.cid) {
+      throw new NetworkError(`Record missing CID for repo=${did}, collection=${collection}, rkey=${rkey}`);
+    }
+    return { $type: "com.atproto.repo.strongRef" as const, uri, cid: fetchResult.cid };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk-core/src/repository/HypercertOperationsImpl.ts` around lines
1042 - 1050, In resolveStrongRefFromUri, rename the local variable returned by
this.fetchRecord(uri) from record to fetchResult to avoid shadowing the inner
record field; update the subsequent references (the existence check and the
returned cid) to use fetchResult.cid and return { $type: ..., uri, cid:
fetchResult.cid }; keep parseUri(uri) and the NetworkError throw logic unchanged
aside from using fetchResult in the checks.

2262-2274: updateProject fetches the record twice — once to verify the type and once inside updateCollection.

fetchRecord<HypercertCollection>(uri) at line 2264 incurs a network round-trip solely to check record.type !== "project". Then updateCollection (line 2271) calls fetchRecord again at line 2343 for the actual update. This is two round-trips for what could be one.

Consider passing the pre-fetched record into updateCollection, or performing the type check inside updateCollection via the already-fetched data:

♻️ Suggested approach — pass pre-fetched record

Refactor updateCollection to accept an optional existing record to skip the second fetch, or extract a private _updateCollectionWithRecord(existingRecord, uri, updates) helper that both paths can call after a single fetch.

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

In `@packages/sdk-core/src/repository/HypercertOperationsImpl.ts` around lines
2262 - 2274, updateProject currently calls fetchRecord(uri) to validate the type
and then calls updateCollection(uri, updates) which fetches the same record
again; refactor so the pre-fetched record is reused: modify updateCollection to
accept an optional existing record (e.g., updateCollection(uri, updates,
existingRecord?: HypercertCollection)) or add a private helper like
_updateCollectionWithRecord(existingRecord, uri, updates), change updateProject
to pass the fetched record to that new parameter/helper, and ensure the type
check remains (use record.type === "project") and that only one fetchRecord call
occurs per update flow.
packages/sdk-core/tests/auth/permissions.test.ts (1)

1087-1102: Test name is misleading — most entries in the array do have colon separators.

Items 3–6 in the queryParamScopes array ("repo:...", "identity:*", "identity:*?", "include:...") all contain colons, so "without colon separator" is incorrect for the overall test. Additionally, "identity:*?" is a valid prefix match but not a well-defined scope per the ATProto permission spec; it's worth checking whether such a value should actually be considered valid.

♻️ Suggested rename
-    it("should validate query-param style permissions without colon separator", () => {
+    it("should validate mixed-format permissions including query-param style", () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/sdk-core/tests/auth/permissions.test.ts` around lines 1087 - 1102,
Rename the test "should validate query-param style permissions without colon
separator" and/or update its inputs so the name matches intent: either change
the test name to reflect that most entries contain colon separators (e.g.,
"should validate various query-param and colon-separated permissions") or
replace/remove the colon-containing entries in the queryParamScopes array; also
review the `"identity:*?"` entry and decide whether `validateScope` should treat
that wildcard-with-question-mark as valid per ATProto spec—if not, update the
test expectation or the input accordingly; locate the test and the array by the
test description string and the call to validateScope to apply the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/sdk-core/src/lexicons/utils.ts`:
- Line 43: The AT_URI_REGEX currently uses (.+) for the rkey which allows
slashes and is inconsistent with parseAtUri and isValidAtUri; update the regex
AT_URI_REGEX to use ([^/]+) for the rkey so it disallows slashes and matches the
same validation logic used in parseAtUri/isValidAtUri, ensuring callers get
consistent results between AT_URI_REGEX.test, isValidAtUri, and parseAtUri.

In `@packages/sdk-core/src/repository/BlobOperationsImpl.ts`:
- Around line 171-175: The SDS path in BlobOperationsImpl currently casts
response.json() to a BlobRef but returns a plain object which lacks BlobRef
methods needed by the lexicon serializer; replace the raw return of result.blob
with a proper deserialization using BlobRef.fromJsonRef(result.blob) so callers
like putRecord/createRecord receive a real BlobRef instance with .json()/.ipld()
methods.

In `@packages/sdk-core/src/repository/HypercertOperationsImpl.ts`:
- Around line 206-208: The thrown NetworkError is mis-constructed: remove the
stray trailing colon and embed the collection:rkey location in the error
message, and pass an actual Error/unknown as the cause instead of a plain
string; locate the throw inside HypercertOperationsImpl where result.success is
checked and change the throw to include a clear message like "Failed to save
record <collection>:<rkey>" and supply a proper cause (e.g., new Error or the
original error) as the second argument to NetworkError rather than the raw
string `${collection}:${rkey}`.
- Line 181: Replace the silent fallback that sets cid: result.data.cid ?? ""
with an explicit error path: detect when result.data.cid is null/undefined and
throw a descriptive error (e.g., "Missing CID in response") instead of returning
an empty string so callers like resolveStrongRefFromUri and StrongRef
construction cannot produce a cid: "" value; locate the assignment of cid in
HypercertOperationsImpl (the result.data.cid usage) and change it to validate
presence and throw if absent, ensuring downstream callers receive only valid
CIDs.

---

Outside diff comments:
In `@packages/sdk-core/src/repository/HypercertOperationsImpl.ts`:
- Around line 266-271: Remove the dead method parseAndValidateUri from class
HypercertOperationsImpl: delete the private parseAndValidateUri(uri: string):
AtUriComponents { ... } implementation and any leftover references or imports
solely used by it, and ensure all callers use the existing parseUri (the active
function referenced at lines near usages) instead; verify there are no remaining
references to parseAndValidateUri and run tests/type-checks to confirm clean
removal.

---

Nitpick comments:
In @.changeset/refactor-uri-parsing-utilities.md:
- Line 2: Update the changeset from "patch" to "minor" and add an explicit
breaking-change note describing the upload() return-type change: explain that
upload() now returns a BlobRef instance where ref is a CID object (string via
.toString()) instead of a plain { $link: string } object, and that any consumer
accessing uploadResult.ref.$link will break; include a clear migration hint
(e.g., use uploadResult.ref.toString() or unwrap BlobRef) and add this bullet to
the list of changes in the changeset file so release tooling and consumers are
aware of the API break.

In `@packages/sdk-core/src/repository/HypercertOperationsImpl.ts`:
- Around line 1042-1050: In resolveStrongRefFromUri, rename the local variable
returned by this.fetchRecord(uri) from record to fetchResult to avoid shadowing
the inner record field; update the subsequent references (the existence check
and the returned cid) to use fetchResult.cid and return { $type: ..., uri, cid:
fetchResult.cid }; keep parseUri(uri) and the NetworkError throw logic unchanged
aside from using fetchResult in the checks.
- Around line 2262-2274: updateProject currently calls fetchRecord(uri) to
validate the type and then calls updateCollection(uri, updates) which fetches
the same record again; refactor so the pre-fetched record is reused: modify
updateCollection to accept an optional existing record (e.g.,
updateCollection(uri, updates, existingRecord?: HypercertCollection)) or add a
private helper like _updateCollectionWithRecord(existingRecord, uri, updates),
change updateProject to pass the fetched record to that new parameter/helper,
and ensure the type check remains (use record.type === "project") and that only
one fetchRecord call occurs per update flow.

In `@packages/sdk-core/tests/auth/permissions.test.ts`:
- Around line 1087-1102: Rename the test "should validate query-param style
permissions without colon separator" and/or update its inputs so the name
matches intent: either change the test name to reflect that most entries contain
colon separators (e.g., "should validate various query-param and colon-separated
permissions") or replace/remove the colon-containing entries in the
queryParamScopes array; also review the `"identity:*?"` entry and decide whether
`validateScope` should treat that wildcard-with-question-mark as valid per
ATProto spec—if not, update the test expectation or the input accordingly;
locate the test and the array by the test description string and the call to
validateScope to apply the change.

- Fix SDS blob upload to return proper BlobRef instance using BlobRef.asBlobRef
- Fix AT_URI_REGEX rkey capture group (.+ → [^/]+) to prevent over-matching
- Fix fetchRecord to throw NetworkError when CID is absent
- Fix saveRecord error message (two-arg → template literal)
- Remove dead parseAndValidateUri method and unused imports
- Eliminate double-fetch in updateProject via new updateCollectionRecord helper
- Rename record → fetchResult in resolveStrongRefFromUri for clarity
- Rename misleading test description in permissions.test.ts
- Update changeset to minor with breaking change note for BlobRef return type
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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/sdk-core/tests/repository/BlobOperationsImpl.test.ts`:
- Around line 119-123: The test is constructing a BlobRef via BlobRef.asBlobRef
but drops the SDS-provided size (result.blob.size), causing size to default to
-1; update the construction to include the actual size from the SDS
response—either pass the typed blob shape { $type: 'blob', ref: result.ref,
mimeType: result.mimeType, size: result.blob.size } to BlobRef.asBlobRef or
instantiate the BlobRef with the size parameter so the resulting BlobRef
(created in the test/path using BlobRef.asBlobRef) has a positive non-zero size
instead of -1.

@aspiers aspiers changed the title Location refactor refactor(sdk-core): extract URI parsing/record helpers, fix blob upload and scope validation Feb 18, 2026
@aspiers aspiers merged commit 935769e into hypercerts-org:develop Feb 18, 2026
3 checks passed
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