64b0039c67
The bundle becomes `{session, appviewClient, pdsClient, chatClient, service}`
and the hooks read those fields directly, so `useAgent` and the three
`agentTo*Client` memo maps go with the agent. The WeakMaps existed only because
clients were derived from a long-lived agent; a bundle that HOLDS its clients
gets identity stability for free.
The clients are now the sole producers of their own headers, which is what lets
`bridge-agent.ts` (442 lines) and `agent.ts` go:
- the appview client passes BLUESKY_PROXY_HEADER as its `service`, so it emits
atproto-proxy itself rather than inheriting it from a configureProxy call
sequenced after the PDS-targeting setup;
- labeler subscriptions go on the appview instance via `setLabelers`, and the
global `;redact` authorities come from the `Client` static alone. Both halves
of the double-emit hazard the previous slice documented are closed by there
being one producer instead of two, so the appview client no longer suppresses
`appLabelers` - only the PDS and chat clients do, because those services take
no moderation authorities at all;
- `configureGlobalAppLabelers` drops its `AtpAgent.configure` half.
PDS routing for the pre-didDoc window is `routeSessionToPds`, a 10-line shim
replacing the manager's dispatchUrl/extractPdsUrl/identity-cache apparatus.
`PasswordSession` resolves each request against `extractPdsUrl(didDoc) ??
service`, so an entryway account with no didDoc yet - the synchronous resume
fast path, i.e. the common cold start - would send every request to the
entryway. The shim absolutizes against the stored url first, which survives
`new URL(path, base)` untouched. It pins that url for the bundle's lifetime
where the manager would have preferred a later didDoc endpoint; that only
differs if the account's PDS moved, and the next cold start pins the new one.
Four cases in clients-test cover this, including the two the old suite could
not express (pinned-vs-didDoc precedence, and didDoc routing with nothing
pinned).
`createPublicSessionBundle` now runs `configureModerationForGuest` itself. That
is a behavior fix, not a refactor: it populates the `Client.appLabelers` static
the public appview client reads, and with no agent left to stamp the header a
logged-out read would otherwise carry no moderation authorities.
finishPreparation, the kill-switch disposal, the redacted logging, refreshSession
and the expiry-rescue path are unchanged - only how the bundle is constructed
moved. Disposal is now just the hook kill: the clients hold no state, and every
request they make goes through the session's injected fetch.
bridge-agent-test.ts is deleted with its subject. clients-test.ts is rewritten
against session-built clients, keeping every header canary (exactly-once on the
appview, none on pds/chat, exact proxy values, the throwing client's cause
chain). The three provider suites and session-core-test are adapted in place -
their constructions change, their assertions do not.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
import {BSKY_LABELER_DID} from '@atproto/api'
|
|
import {type Client} from '@atproto/lex'
|
|
import {type DidString} from '@atproto/syntax'
|
|
|
|
import {IS_TEST_USER} from '#/lib/constants'
|
|
import {com} from '#/lexicons'
|
|
import {account as accountStorage} from '#/storage'
|
|
import {
|
|
configureAdditionalModerationAuthorities,
|
|
configureGlobalAppLabelers,
|
|
} from './additional-moderation-authorities'
|
|
import {type SessionAccount} from './types'
|
|
|
|
/** The moderation surface of a session bundle. */
|
|
type ModerationSession = {appviewClient: Client}
|
|
|
|
/**
|
|
* Cache an account's subscribed labeler DIDs. Called on every preferences
|
|
* fetch, so the cache is eventually consistent with the server.
|
|
*/
|
|
export function saveLabelers(did: string, value: string[]) {
|
|
accountStorage.set([did, 'labelers'], value)
|
|
}
|
|
|
|
/**
|
|
* Read the cached labeler DIDs for an account, or `undefined` if none have
|
|
* been cached yet (first session on this device) or the entry is unreadable.
|
|
*/
|
|
export function readLabelers(did: string): string[] | undefined {
|
|
try {
|
|
return accountStorage.get([did, 'labelers'])
|
|
} catch {
|
|
/* a corrupt entry fails JSON.parse inside Storage.get; treat as no cache */
|
|
return undefined
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply an account's labeler subscriptions to the appview client, without
|
|
* duplicating the globally redacted Bluesky moderation authority.
|
|
*
|
|
* The Bluesky DID is filtered out because it already flows through the global
|
|
* `Client.appLabelers`, which lex emits with a `;redact` suffix. Listing it
|
|
* per-instance would add a second, non-redacting entry for the same authority:
|
|
* lex collects the two lists into a `Set` keyed on the suffixed string, so
|
|
* neither dedupes against the other.
|
|
*
|
|
* Only the appview client takes subscriptions - the PDS and chat clients suppress
|
|
* labelers entirely (see clients.ts).
|
|
*/
|
|
export function applyLabelersToClient(
|
|
client: Client,
|
|
subscribedDids: string[],
|
|
) {
|
|
client.setLabelers(
|
|
subscribedDids.filter(did => did !== BSKY_LABELER_DID) as DidString[],
|
|
)
|
|
}
|
|
|
|
export function configureModerationForGuest() {
|
|
// This global mutation is *only* OK because this code is only relevant for testing.
|
|
// Don't add any other global behavior here!
|
|
switchToBskyAppLabeler()
|
|
configureAdditionalModerationAuthorities()
|
|
}
|
|
|
|
/**
|
|
* Configure global app labelers and the account's cached labeler
|
|
* subscriptions. Fully synchronous so session setup can apply labeler headers
|
|
* in the same tick, before any request goes out.
|
|
*/
|
|
export function configureModerationForAccount(
|
|
bundle: ModerationSession,
|
|
account: SessionAccount,
|
|
) {
|
|
// This global mutation is *only* OK because this code is only relevant for testing.
|
|
// Don't add any other global behavior here!
|
|
switchToBskyAppLabeler()
|
|
if (IS_TEST_USER(account.handle)) {
|
|
// Test accounts may briefly use the production authority while this resolves.
|
|
void trySwitchToTestAppLabeler(bundle.appviewClient)
|
|
}
|
|
|
|
// The code below is actually relevant to production (and isn't global).
|
|
const labelerDids = readLabelers(account.did)
|
|
if (labelerDids) {
|
|
applyLabelersToClient(bundle.appviewClient, labelerDids)
|
|
} else {
|
|
// If there are no headers in the storage, we'll not send them on the initial requests.
|
|
// If we wanted to fix this, we could block on the preferences query here.
|
|
}
|
|
|
|
configureAdditionalModerationAuthorities()
|
|
}
|
|
|
|
function switchToBskyAppLabeler() {
|
|
configureGlobalAppLabelers([BSKY_LABELER_DID])
|
|
}
|
|
|
|
/** Resolve and install the test environment's moderation authority. */
|
|
async function trySwitchToTestAppLabeler(client: Client) {
|
|
const did = (
|
|
await client
|
|
.call(com.atproto.identity.resolveHandle, {
|
|
handle: 'mod-authority.test',
|
|
})
|
|
.catch(_ => undefined)
|
|
)?.did
|
|
if (did) {
|
|
console.warn('USING TEST ENV MODERATION')
|
|
configureGlobalAppLabelers([did])
|
|
}
|
|
}
|