Skip to content

Merge 2026 05 26#5

Merged
Spotchi merged 88 commits into
mainfrom
merge-2026-05-26
May 26, 2026
Merged

Merge 2026 05 26#5
Spotchi merged 88 commits into
mainfrom
merge-2026-05-26

Conversation

@Spotchi

@Spotchi Spotchi commented May 26, 2026

Copy link
Copy Markdown

Merging from upstream Bluesky
Commit: 622d365
Date: Sun May 24 2026 05:51:02 GMT+0200 (Central European Summer Time)

rafaeleyng and others added 30 commits April 22, 2026 10:14
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Fix /account/reset-password route

* changeset

* tidy

* improve error reporting

* messages

* name all providers
* pds: setup entryway mock server for tests, remove stale dep.

* changeset

* tidy test

* pds: tidy mock entryway
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Blur media on account level adult labels

* WIP blur more

* Update defs

* Update tests

* Changeset

* Delete artifacts

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Add AA config for BR

* Changeset
* ✨ Add expiring tags in ozone

* 🧹 Cleanup

* 📝 Add changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Add associatedRecord to external embed record
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* 📝 Initial draft of report based ticketing system

* ✨ Introduce report entity

* cleanup

* ✨ Fix merge conflict

* 🧹 Cleanup

* ✨ Return reporter details in report view

* 🐛 Remove foreign key ref

* ❇️ Action reports via events (bluesky-social#4681)

* ✨ Allow associating actions to reports via events

* 🐛 Rollback subject type

* ✨ Add queue management lexicons

* ❇️ Ozone queue management (bluesky-social#4686)

* ✨ Add queue management lexicons

* :rotating_lights: Fix lint issue

* 📝 Update lexicons

* ❇️ Ozone queue routing (bluesky-social#4687)

* ✨ Add queue routing for for reports

* ✨ Add queue delete and migration

* ✨ Add validation for subject type

* 🧹 Cleanup

* Ozone User Assignments (bluesky-social#4663)

* moderator_assignment table

* queue assignment endpoints

* cleanup schema

* report assignment endpoints

* register handlers

* allow queue unassign

* rename tools.ozone.queue.assign to tools.ozone.queue.assignModerator

* remove unique constraint since expired assignments arent removed

* remove queue assignment report ID

* Realtime assignments (bluesky-social#4682)

* add ws types

* basic web socket

* add getAssignments to AssignmentService

* add multiple mods to queue test case

* add triage test cases

* add realtime test case

* add backfill logic

* cleanup

* websocket server restructure

* keep assignment queue id if not provided

* fix test: subscription receives updates

* fix test: new subscription receives snapshot

* add error handling

* fix test count

* ws: basic auth

* report ended test

* move assignment wss inside assignment service

* allow realtime updates to be disabled

* add assignment duration env vars

* cleanup

* default to 5 minutes

* rename claimReport to assignModerator

* add test to check dids parameter

* remove subject param from queue.getAssignments

* cleanup

* only throw AlreadyAssigned if active assignment to another user is attempted

* remove queue.assignModerator assign input

* add more queue tests

* tools.ozone.report.getAssignments

* qualify function for clarity

* cleanup wording

* dont call broadcast within wss since the service handles it

* throw error in either assign case

* handle unassignment case properly

* adjust assignment change test case

* cleanup

* cleanup

* split getAssignments into 2 methods. update downstream endpoints

* cleanup

* support token query param

* add queue:assign server message

* attaach did to queue:assign message

* refactor realtime tests to use websockets for updates

* use http for creating queue assignments

* cleanup

* create view methods

* cleanup

* cleanup

* remove assignment websocket server usage

* delete assignment wss

* remove line to keep diff clean

* add db indexes

* use varchar timestamps

* remove redundant types

* add pagination to queue.getAssignments

* add pagination to report.getAssignments

* hydrate queueView when reading/writing queue assignments

* hydrate queue when working with report assignments

* rename test

* cleanup

* remove queue service since it wont be needed

* update description

* (forgot to run codegen)

* default to true

* update description

* cleanup

* cleanup

* use deterministic IDs

* remove ws types

* rename queueView to queue

* create getAuthDid

* create queueFromJoined to be able to reuse queueService.view

* Ozone assignment refactors (bluesky-social#4707)

* add invalid queue error

* add InvalidAssignment error when assigning report

* add invalid assignment tests

* update report assignment tests to seed reports

* create tools.ozone.report.unassignModerator

* require DID when assigning to queue

* only check queue collection match if routing a record

* add description to report_queue

* add description to queue API

* add queue description to test

* codegen

* Ozone enable broad queue record matching (bluesky-social#4724)

dont match collection if it is null

* ♻️ Refactor report view lexicons and introduce single report query (bluesky-social#4727)

* ♻️ Refactor report view lexicons and introduce single report query

* 🧹 Cleanup

* 🧹 Cleanup queueView

* 🐛 Fix typo

* 🐛 Fix remaining broken test

* ✨ Enrich moderator team member for assignments (bluesky-social#4737)

* ✨ Enrich moderator team member for assignments

* 🧹 Cleanup

* ✨ Return only permanent assignments

* 🐛 Fix did assignment

* Ozone report rerouting (bluesky-social#4738)

* create stub endpoint

* cleanup

* lift queue router logic up into service layer

* remove report routing tests from this file

* test for regression bug

* use assignReportBatch in routeReports

* cleanup tests

* create tools.ozone.queue.getLatest

* rename to tools.ozone.report.getLatestReport

* cleanup

* fix limit bug

* properly group OR clauses

* require limit when assigning reports to queues for more robust limit enforcement

* cleanup

* update description

* cleanup lint issues

* allow mods to route reports

* add not found error to single report queries

* create reusable query for report service

* hotfix: update to new function name

* ❇️ Report activity and status change log (bluesky-social#4752)

* ✨ Add report activity entity

* ✨ Insert report activity when queueing and actioning

* ✨ Only create report activity on permanent assignment

* ✨ Move to object based activity schema

* ✨ Adjust event types

* ✨ Add member details to listActivities and cleanup note

* 🧹 Revert lex-cbor package changes

* ✨ Allow admins to assign mods to reports

* 🐛 Fix issues pointed out in review

* :rotating_lights: Fix lint issue

* ♻️ Refactor report status update path

* ❇️ Muted report support and better indexing/denormalizing of report data (bluesky-social#4812)

* ✨ Add muted report support

* ⚡ Improve indexing for reports and bring over subject message ref

* 🐛 Fix open report action association

* ✨ Add assignedTo filter param for queryReports

* :rightwards_twisted_arrows: Untangle botched merge

* 🐛 Fix client instantiation

* ✨ Cleanup assignment normalization

* Ozone report stats (bluesky-social#4789)

* create report_stat table

* create report stats service

* create tools.ozone.queue.getLiveStats lexicon

* implement endpoint

* simplify service method

* attach stats computation to daemon

* add stats tests

* implement queue stats

* cleanup

* fix index to only enforce uniqueness for live stats

* switch to mode + timeframe schema

* overhaul report stat service to use materialzation patterns

* add force param. fix bugs

* use -1 to make conflict detection easier

* cleanup

* fix update bug

* simpliofy

* add report table index for stats

* cleanup

* remove unused stats

* use for loop to prevent hitting connection limit

* log errors

* change pending to be windowed

* create StatsComputer

* create isGroupFresh

* cleanup

* optimize computeGroup

* create idx_report_stat_lookup

* docs

* cleanup

* cleanup

* actually start the stats computer :)

* lint

* clean up stats fields

* lint

* re-use viewing logic

* destroy stats computer

* add moderatorDid to report_stat

* create stats for aggregate, queues, and moderator

* add description

* add avgHandlingTimeSec

* create AHT test

* allow assigned reports to be closed via reportAction

* add computeStats

* update test now that stats can be undefined

* cleanup

* refactor test

* cleanup

* fix bug

* cleanup

* create const for live stats ttl

* rename fixed to historical

* change pendingCount to be all time

* change pendingCount to be all-time

* put live stats ahead of historical when computing stats

* fix

* dont reference moderation_event for mod stats

* add mod assignment index for stats

* allow stats for unqueued reports by making queueId nullable

* add test for unqueued report stats

* cleanup

* cleanup

* cleanup

* add report type stats

* cleanup

* fix failing report type tests

* create single getLiveStats endpoint

* cleanup

* create getHistoricalStats

* cleanup

* make historical stats paginated

* group report types

* linting

* cleanup

* fix bug

* ✨ Generate weekly stats, refactor, add locking

* 🔨 Refactor live and historical stats to use day based snapshots

* 🐛 Bring back datetime format check

* ✅ Pass date string for test

* stats: make computer interval configurable

* cleanup

---------

Co-authored-by: Foysal Ahamed <foysal@blueskyweb.xyz>

* Ozone ticketing system fixes (bluesky-social#4884)

* use defined report reason type

* lint fix

* Various fixes (bluesky-social#4882)

* 🐛 Include actions in response in getReport response

* ✨ Implement reassign queue endpoint

* :rotating_lights: Fix lint issue

* 🐛 Fix various issues found during testing queue system (bluesky-social#4910)

* 🐛 Add status change on report unassignment

* ✨ Add queue unassignment endpoint

* ✨ Make queue assignments permanent by default

* ⚡ Add better indexes, make status required

* ✨ Add did filter param to queryReports

* ⚡ 🔨 Move report creation to queue router (bluesky-social#4917)

* ⚡ 🔨 Move report creation to queue router

* fix tests for new required status and immediate report routing

* cleanup

---------

Co-authored-by: gcwill70 <gcwill70@gmail.com>

* 🧹 Cleanup reviewedBy filter

* 📝 Add changeset

* :rotating_lights: Fix lint issue

---------

Co-authored-by: Geoff Cramer <gcwill70@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix impossible comparison in service auth

* add changeset

---------

Co-authored-by: Matthieu Sieben <matthieu.sieben@gmail.com>
* update chat lexicons

* codegen
* update chat lexicons

* changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Add devEngines config for node and pnpm

* Update GitHub Actions

* Feedback
github-actions Bot and others added 27 commits May 20, 2026 15:51
…edExternalView` (bluesky-social#4915)

* Add rich metadata fields to app.bsky.embed.external#viewExternal

* Hook up hydration

* Update lexicons to match new changes

* Hydrate site.standard records by ref on the read path

Replaces the placeholder GetSiteStandardDocumentsWithPublication RPC with
GetSiteStandardRecordsByURI and GetSiteStandardRecordsByRef to match what
tango exposes. Hydration uses the ByRef variant so embedded link cards
freeze to the exact record versions present at post time.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Plumb hydration state into externalEmbed view

Threads HydrationState through externalEmbed call sites and adds a
lookup helper that resolves associatedRecords against the hydrated
site.standard documents and publications by their "<uri>@<cid>" keys.
Field extraction is left as a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Implement getEmbedExternalView for compose-time link cards

Switches the lex query to take an array of site.standard.* URIs and
return strongRefs alongside an optional viewExternal. The handler
resolves URIs through GetSiteStandardRecordsByURI and emits strongRefs
suitable for embedding into a post's external.associatedRecords.
viewExternal field decoration and verification are left as follow-ups.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Update getEmbedExternalView lex, update code to use new lexicons

* Integrate new protos and response shapes

* Update lexicons

* Hydrate labels for site.standard.* URIs and gate on takedown

Appends site.standard record URIs to the post-hydration label fetch so
state.labels carries entries for every SS doc/publication referenced by
an embed. Adds actionSiteStandardTakedownLabels to drop hydrated SS
records whose labels include a takedown from a redacting labeler.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Decorate viewExternal with site.standard records

externalEmbed now layers a site.standard-derived overlay on top of the
post's embed values, so hydrated document/publication fields override
title/description/thumb/createdAt/updatedAt/labels/source while uri and
associatedRefs always come from the post. The overlay is exposed as
externalEmbedFromStandardSite for reuse.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Wire getEmbedExternalView through site.standard hydration

Adds Hydrator.hydrateEmbedExternalViewFromUris which filters input URIs to
site.standard.*, hydrates the latest record versions plus labels, and
gates taken-down records. The handler runs that hydration, builds
associatedRefs/Records from the surviving entries, and produces a view
via Views.externalEmbedFromStandardSite. Adds a required `url` param to
the lex so the returned viewExternal can carry the user's pasted share
URL as its `uri`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Refactor getEmbedExternalView to createPipeline

Adopts the skeleton/hydration/rules/presentation pattern used by other
query handlers (searchPosts, getFeedGenerators) for parity.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* PR review nits

- Clarify in the getEmbedExternalView lex that `view` is optional
- Simplify the no-fields-set branch in buildExternalEmbedSourceTheme
- Make externalEmbed's overlay spread order robust to future overlay
  fields by re-setting uri and associatedRefs after the spread

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Extract site.standard hydrator into hydration/external.ts

Splits the SS record types, key helpers, AssociatedSiteStandardRecord
type, and the two getSiteStandardRecords RPC wrappers out of feed.ts
and the SITE_STANDARD_NSID_PREFIX const out of hydrator.ts into a new
ExternalHydrator class. The Hydrator gains an `external` field
mirroring how actor/feed/graph/label are mounted, and call sites move
from this.feed.* to this.external.*.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* PR review nits

- Refresh siteStandardRefsFromPosts JSDoc to reflect uri@cid map keys
- Note in the getEmbedExternalView handler why we pass over the
  hydration maps twice
- Move AssociatedSiteStandardRecord back to views/index.ts since it's
  internal to that module's view-building helpers

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Thread includeTakedowns through read-path SS hydration

The compose-time hydrator method already forwards ctx.includeTakedowns;
mirror it on the read path so parseRecord respects the same flag and
takedown gating stays consistent across the two entry points.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Fix var shadowing

* Update changeset

* Update lex, update codepaths

* Merge labels

* Format

* Remove comment

* Prune publications not referenced by any hydrated document

The dataplane auto-resolves a document's `site` field, so a request can
return a publication the caller never asked for. If the caller also
passed a publication URI that doesn't match any document's `site`, that
publication is dropped to keep the response strictly tied to the
documents we returned. Publication-only resolution (no documents
hydrated) skips the prune.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Format

* Estimate reading time from document.textContent

Uses a coarse 200wpm word-count heuristic to populate viewExternal's
readingTime when a document has plaintext content. Factored into a
helper so swapping in a library later is a one-function change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Move publication pruning into the getEmbedExternalView handler

The hydrator method goes back to a generic "fetch what was requested"
shape. Request-shape semantics — i.e. dropping publications the
dataplane auto-resolved that the caller didn't actually ask for — now
live in presentation, alongside the rest of the response assembly.
collectAllowedPublicationUris is exported from hydration/external for
reuse.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Extract site.standard helpers into util/standard-site.ts

Moves SITE_STANDARD_NSID_PREFIX, the (uri, cid) key helpers,
collectAllowedPublicationUris, AssociatedSiteStandardRecord,
lookupAssociatedSiteStandardRecords, and estimateReadingTimeMinutes out
of hydration/external and views/index into a single util module. The
util keeps no hydration imports — collectAllowedPublicationUris and
lookupAssociatedSiteStandardRecords are typed structurally over the
record-info shapes they read.

Also tightens the getEmbedExternalView handler: empty maps return early,
publications get pruned only when documents are present, and the view
is always returned with the request's `url` and overlay-derived
title/description (falling back to empty strings) when any record was
hydrated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Move SS hydration-state lookup back to hydration/external

The util-level helper went generic over the doc/publication info shape,
which lost the documented hydration-map types (and their `uri@cid` key
docblocks) at the call boundary. Move it next to
buildSiteStandardRecordsHydrationMaps under the more discoverable name
getSiteStandardRecordsFromHydrationMaps and type it concretely against
SiteStandardDocuments/Publications.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Mirror takedowns across site.standard doc/pub pairs

Fold the per-record null sweep and the cross-pair sweep into a single
actionSiteStandardTakedownLabels(documents, publications, labels) so
neither read- nor compose-path callers can forget the second half. If
either side of a doc/publication pair (discovered via doc.record.site)
is taken down, mirror the null onto the other side; orphan docs and
orphan publications still get the per-record sweep.

Also: in the view layer, treat the document as authoritative for
title/description as a unit — don't blend a doc's title with a
publication's description.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Split externalEmbedFromStandardSite into ref- and record-based variants

externalEmbedFromStandardSite stays as the read-path entry point,
resolving doc/publication via the by-ref lookup against state, then
delegates to externalEmbedFromStandardSiteRecords which owns the
overlay-building body. The compose-path handler now captures the first
hydrated doc/publication during its existing pass over the SS maps and
calls the records variant directly, eliminating the redundant second
walk.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Dispatch presentation by hydrated record type

`presentation` is now a thin dispatcher that hands off to a
`standardSitePresentation` helper when site.standard records were
hydrated, returning `{}` otherwise. Future record types get their own
branch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Document empty-string contract on getEmbedExternalView output

When the resolved records don't supply title/description, those fields
land as empty strings on the returned view rather than the view being
omitted. Spell that out in the lex description and at the build site
so clients know to treat empty values as 'no enrichment' rather than
'titleless content.'

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Inline gate for publication allow-set

Collapse the `prunePublications` flag into the allow-set itself: when no
docs were hydrated, `allowedPublicationUris` is `undefined` and the
publication loop allows everything; an empty set still means
"constraint exists, nothing matches" and drops every publication.
Removes the second variable that encoded the same rule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Validate site.standard records against the assumed URL

Both view-layer entry points now take an `assumedUrl` (the post's
external.uri on the read path, the request's `url` on compose) and
pass it through to validateStandardSiteForUrl. The validator mirrors
the spec's rule tree:

- Doc + publication: doc.site must resolve to the hydrated publication,
  and publication.url + document.path must equal assumedUrl.
- Loose doc: doc.site must be HTTP and doc.site + document.path must
  equal assumedUrl.
- Publication only: publication.url must equal assumedUrl.

URL comparison normalizes via the WHATWG URL constructor (resolving
path against base, lowercase host, dropping query/fragment, stripping
trailing slash). On failure, the overlay is dropped and clients fall
back to the bare embed.

Adds parameterized tests covering the rule tree and the URL
normalization cases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Reject misconstructed doc/pub pairings during ref lookup

When `associatedRefs` carries both a document and a publication, the
hydration-map lookup now requires the doc's `site` field to equal the
publication ref's URI; mismatches return both slots as `undefined`
rather than silently rendering enrichment from records that don't
agree. Doc-only and publication-only refs pass through unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Symmetric SS lookup contracts across read and compose paths

Splits the SS hydration-map lookup into two named sister functions:

- getSiteStandardRecordsFromHydrationMapsByRefs (read path) trusts
  the post's strongRefs and rejects pairs that disagree, including
  the case where a doc declares an at-uri `site` but no matching
  publication ref was pinned.
- getSiteStandardRecordsFromHydrationMapsByDocumentUri (compose path)
  takes the first hydrated doc and pairs it with the publication its
  `site` field points at, rejecting the whole pairing when the
  declared publication wasn't auto-resolved.

The handler now uses the compose-path variant directly, so the
firstDoc/firstPub capture, the allowed-publications prune, and
collectAllowedPublicationUris all go away.

By the time externalEmbedFromStandardSiteRecords runs, both paths
guarantee the same contract: doc+pub agree on URI, doc-only means
loose doc, pub-only means publication-only flow, otherwise both slots
are undefined.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Test the SS hydration-map lookups

Covers both byRefs (read-path strict) and byDocumentUri (compose-path
doc-first) variants, including the doc/pub pair-match invariant, the
loose-doc and publication-only branches, the bail-out when a doc
declares an at-uri publication that wasn't hydrated, and version-exact
lookup misses.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Tighten validation and the empty-overlay contract

- validateStandardSiteForUrl is now URL-match only; the structural
  pair-agreement and orphan-doc checks live exclusively in the
  hydration-map lookups (`hydration/external.ts`). Function and test
  comments cross-reference where each layer enforces what.
- externalEmbedFromStandardSiteRecords now bails out and returns
  undefined when the chosen pair can't supply both `title` and
  `description`. The handler treats an undefined overlay the same as
  "nothing hydrated" and returns `{}` so Cardy falls back to its own
  card render and skips writing strongRefs to the post.
- Lex description on getEmbedExternalView updated: empty-string
  fallbacks for title/description are gone; the response is empty when
  records are missing or validation fails.
- Lookup JSDocs cross-reference each other: same observable contract,
  different upstream guarantee.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Drop unused collectAllowedPublicationUris

The handler-side prune got obsoleted by the doc-first lookup
(getSiteStandardRecordsFromHydrationMapsByDocumentUri), which only
emits the publication a hydrated doc points at. No callers remain.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Remove untracked new codegen files

* Remove untracked new codegen files for Ozone

* Update comments

* Refresh comments on the SS external-embed view methods

The overlay is now a fully-populated `viewExternal['external']` (uri,
title, description always set), not a partial. Update the JSDocs and
the read-path spread comment to match: drop the "partial overlay" /
"uri intentionally excluded" framing, explain the new return shape,
and note that the spread on the read path layers SS-derived fields
over post-author-supplied ones with `associatedRefs` re-set last
because the post is authoritative.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Stick to convention

* Fmt

* Untrack generated files

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Improve bundling size of lex-client

* Improve bundle size of oauth-client-browser-example
…luesky-social#4981)

* Export `lexiconMessageSchema` (and type) for subscription messages

* Remove need for inference when computing XRPC method derived types

* tidy
…esky-social#4983)

* Make the generation of the `$defs` namespace optional by default

* remove un-necessary use of `$defs` accessor
…social#4984)

* Add `default` export of "main" schema in the namespace file

* update readme

* fix table of contents
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
)

* bump json size limit on pds record methods

* changeset

* fix test
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* steering readmes toward lex client

* fix lex package link in main readme

* fixed CI

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* switch main readme links from npm to npmx

* prettier pass

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* Add validation logging

* Fix subpath joining in validateStandardSiteForUrl

`new URL(path, base)` does relative-URL resolution, so a leading slash
on `path` swallows the base's pathname:

  new URL('/indexing-standard-site', 'https://atproto.com/blog').href
  // = 'https://atproto.com/indexing-standard-site'  -- wrong

We want path-append semantics. Replace the `new URL(path, base)` call
with an explicit `joinPath(base, path)` helper that trims one slash
from each side and joins with exactly one `/`. The joined string is
still piped through `canonicalizeHttpUrl` for normalization.

Adds a parameterized test matrix covering all four slash placements ×
root-domain vs subpath bases, including the regression case from the
real-world atproto.com/blog publication that surfaced this bug.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Make fmt

* Changeset

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ed on external view only for SS hydrations (bluesky-social#5001)

* Add some more logs

* Build associatedProfiles inside externalEmbedFromStandardSiteRecords

Hoists the doc/pub profile lookup into the shared SS overlay builder so
both the read-path externalEmbed and the compose-path handler emit
associatedProfiles from the same source of truth, derived from the
selected document/publication refs rather than the post's full
associatedRefs array.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Changeset

* Format

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
…es (bluesky-social#4992)

* feat(did): add AtprotoDidRefAbsolute, DidRefRelative, AtprotoScopeAud, AtprotoTokenAud

Renames AtprotoAudience to AtprotoDidRefAbsolute (kept as deprecated alias),
adds DidRefRelative + isDidRefRelative for #fragment-only references, and
introduces use-site unions: AtprotoScopeAud (always combined, for OAuth
scopes) and AtprotoTokenAud (bare DID or combined, for service-auth JWT
audience claims). Lays the groundwork for Phase 1 of proposal 0014.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(did): cover new DID-reference predicates

Tightens isAtprotoDidRefAbsolute to reject empty fragments
(`did:plc:abc#`), aligning it with isDidRefRelative which already
rejects bare `#`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(oauth-scopes): use AtprotoScopeAud / isAtprotoDidRefAbsolute

Switches from the legacy AtprotoAudience names to the renamed canonical
forms. Behavior unchanged; the deprecated alias from @atproto/did keeps
external consumers compiling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(lexicons): loosen getServiceAuth.aud to accept did or did#serviceId

The 'format: did' constraint excluded the combined did#serviceId syntax
that OAuth-scoped credentials grant via 'rpc:lxm?aud=did#service'.
Loosens to a plain string with a maxLength cap; handler-side validation
will enforce that the value is either a bare atproto DID or an
AtprotoDidRefAbsolute.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(lexicons): tighten getServiceAuth.aud description and maxLength

Drops the internal "Phase 1" reference from the user-facing description
and bumps maxLength from 1024 to 2048 to accommodate the spec maximum
DID length plus a service-id fragment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(pds): accept did#serviceId aud in getServiceAuth handler

Validates that the caller-supplied aud is a valid atproto DID or
AtprotoDidRefAbsolute, then passes the original string straight through
to permissions.assertRpc and createServiceJwt. Combined-aud OAuth scopes
now match (resolves part of bluesky-social#4850); password-session callers continue to
use bare DID.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(pds): pass combined did#serviceId aud to pipethrough scope check

parseProxyInfo and parseProxyHeader now return the serviceId alongside
the did and url. The proxyHandler uses the combined did#serviceId form
when calling permissions.assertRpc, so OAuth credentials with
'rpc:lxm?aud=did#service' scopes succeed (resolves the proxy half of
bluesky-social#4850). The outbound service-auth JWT keeps bare-DID aud — Slice D adds
the operator-visible flag to flip that.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(pds): rename scopeAud/tokenAud to clarify Phase 1 asymmetry

Names the Phase 1 audience asymmetry structurally rather than relying
on a comment to document it. The scope check uses the combined
did#serviceId form; the outbound JWT keeps bare-DID aud.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(pds): assert parseProxyHeader returns serviceId

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(oauth-scopes): cover combined-aud assertRpc match (issue bluesky-social#4850)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(pds): validate getServiceAuth aud for all caller types

Moves the aud predicate check from the OAuth-only authorize callback into
the shared handler body, so app-password and session callers also get
syntactic validation — restoring the coverage that the lexicon's
format: did constraint provided before Slice A loosened it to a plain
string with maxLength.

Also updates app-passwords tests to use valid atproto DIDs instead of
did:example:test, which now correctly fails the predicate check.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Address review feedback for service-auth oauth fix

- Annotate non-self-documenting checks in isAtprotoDidRefAbsolute and
  isDidRefRelative with terse case-name comments.
- Spell out "Phase 1 of service auth updates" in the proxyHandler
  asymmetry comment.
- Drop the issue-number reference from the assertRpc combined-aud
  describe block.
- Mirror PR bluesky-social#4602's createReport pattern: use scopeAud/tokenAud naming
  to make the bare-DID JWT audience structurally clear.
- Add a proxyHandler regression test that fails when scope-check aud
  reverts to bare DID.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(pds): use bare-DID aud in get/putPreferences proxy fallback

When getPreferences/putPreferences proxy to a non-default appview, they
were sending the combined did#serviceId form as the JWT audience. This
contradicts the Phase 1 convention used elsewhere (proxyHandler,
pipethrough(), createReport) where the scope check sees the combined
form but the outbound service-auth JWT keeps bare-DID aud.

Adds a small bareDidFromProxyTo() helper in pipethrough.ts and uses it
at both call sites.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(pds): use real AppContext + override only authorization

Replaces the proxy-oauth-aud test's stub-AppContext with a real
TestNetworkNoAppView. Only AuthVerifier.authorization is overridden to
drive the OAuth path off an x-test-scope header — every other ctx field
stays real and real-typed. The override's signature is constrained by
the real method type, so future shape changes (renames, output type
restructures) break the test at compile time rather than silently
diverging.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(pds): tighten proxy-oauth-aud cases

Drops the lastAud capture (a 200 already implies combined-form aud
reached the scope check) and replaces the bare-DID-aud negative case
with a different-service-id negative case. Bare-DID aud isn't a valid
rpc scope — isAudParam rejects it at parse time — so the prior negative
case wasn't actually exercising audience matching. The new case
(`upstream.did#atproto_other`) parses valid and proves the runtime
aud's service id is being compared.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: add changesets for service-auth oauth fix

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(pds): cover getServiceAuth aud handling

Adds five integration cases: bare-DID and combined did#serviceId aud
both produce a token whose aud claim equals the input; malformed,
non-atproto, and empty-fragment aud values are rejected.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: pnpm style:fix

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(pds): revert scopeAud/tokenAud rename in handlers without both

The naming pair was meant to disambiguate when scope-check and JWT
audiences appear in the same scope. In get/putPreferences and
createReport only one of the two is in scope at a time, so the rename
adds noise without clarifying anything. proxyHandler keeps both names
since they sit side-by-side there.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(did): drop OAuth-flavored types; remove deprecated AtprotoAudience

Removes AtprotoScopeAud, AtprotoTokenAud, AtprotoAudience, and
isAtprotoAudience from @atproto/did. The only consumer (oauth-scopes)
now uses AtprotoDidRefAbsolute / isAtprotoDidRefAbsolute directly via
its own AudParam alias. Keeps @atproto/did focused on DID-level
primitives without OAuth or service-auth concepts. The
AtprotoAudience/isAtprotoAudience deprecated aliases are dropped
outright (breaking) — migration is a single import swap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(did): split did-ref.ts into atproto-agnostic primitives

did-ref.ts now hosts generic DidRefAbsolute / DidRefRelative
primitives (and their predicates) parameterized on a DID method.
The atproto-flavored AtprotoDidRefAbsolute moves to atproto.ts
alongside the other Atproto* helpers, defined as
DidRefAbsolute<AtprotoIdentityDidMethods>. Public API surface is
unchanged (both still flow through the package barrel).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(did): use String.includes for double-hash check

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: changeset for oauth-scopes breaking re-export change

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(oauth-scopes): tighten scopeNeededFor casts back to concrete types

The casts `as AudParam`/`as LxmParam` widened to include `'*'`, which
is wrong inside scopeNeededFor — it's constructing the specific scope
for a single call, never a wildcard. Restore the original
AtprotoDidRefAbsolute / Nsid casts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore: lead BREAKING-change changesets with **BREAKING:** marker

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
@nicolo-ribaudo

Copy link
Copy Markdown

The failing tests seems to be a timeout, I restarted it.

@Spotchi
Spotchi merged commit 8eefe1c into main May 26, 2026
10 of 11 checks passed
@Spotchi
Spotchi deleted the merge-2026-05-26 branch June 22, 2026 09:58
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.