-
v0.7.0
Stablereleased this
2026-09-28 14:05:19 +00:00 | 15 commits to main since this releaseA large, deliberately breaking release. Identifiers are typed everywhere; sessions, session stores and errors each have one model; Lexicon unions are open and keep data the SDK does not model; the stream consumers share one core with Sync 1.1 verification; the OAuth client is hardened and its hosted login moved into
ATProtoNet.Server; and inbound service auth replaces the bearer handler. The SDK also catches up with the protocol: the Spaces read/write split, group chats, Ozone reports and queues, label signing, Lexicon resolution and the currentapp.bskysurface.Upgrading from 0.6? Start with the migration guide (
docs/migrating-to-0.7.md), which walks through every breaking change below with before-and-after code.Breaking changes
ATProtoNetno longer depends onMicrosoft.Extensions.HttporMicrosoft.Extensions.Options— ASP.NET Core apps are unaffected. Migration: an app outside ASP.NET Core that usedIHttpClientFactory,IOptions<T>orLoggerFactoryonly through the SDK references those packages itself (#110)- The EF Core stores are their own package again:
ATProtoNet.Server.EntityFrameworkCore— the session, replay, space and Sync 1.1 stores leaveATProtoNet.Server; namespace, types and tables are unchanged. Migration: reference the package wherever one of them is used (#136) - The OAuth cookie login moves from
ATProtoNet.BlazortoATProtoNet.Server— so any ASP.NET Core app can use it; the Blazor package still brings it along. Migration:using ATProtoNet.Server.Authentication;for theATProtoNet.Blazorusings, andAddAtProto().WithOAuth()(#136) AddAtProto()returns anIAtProtoBuilder—WithLifetime,WithOAuth,WithClientFactory,With…SessionStoreandWithHealthCheckreplaceAddAtProtoScoped,AddAtProtoAuthentication,AddAtProtoServer,AddAtProto<TStore>andAddAtProtoEfCoreTokenStore. Migration: follow the guide's table (#136)- The client factory's default session store is in memory, with a startup warning —
AddAtProtoServer()wrote tokens to disk unasked. Migration:WithFileSessionStore()for the old behaviour,WithEfCoreSessionStore<TContext>(),WithSessionStore<T>(), orWithInMemorySessionStore()(#136) - One named
HttpClientper registration — everyAtProtoClientofAddAtProto()sends with"ATProtoNet", exposed asIAtProtoBuilder.HttpClient, andServiceCollectionExtensionsisAtProtoServiceCollectionExtensions. Migration: customizeAddAtProto().HttpClient(#136) - Every options class goes through
IOptions<T>and is validated at startup — a bad value failsStartAsyncwith anOptionsValidationException, code callbacks run after configuration binding, and everyAddAtProtoIdentity/AddAtProtoSpacescallback applies. Migration: catch that exception (#136) services.AddAtProtoPdsAdmin(PdsAdminOptions)is removed — it duplicated the other overloads. Migration:services.AddAtProtoPdsAdmin(url, adminPassword), plusservices.Configure<PdsAdminOptions>(…)for the other settings (#180)- Aspire:
AddAtProtoClientreturns theIAtProtoBuilder, andAtProtoClientSettingsis gone — its keys bind toAtProtoClientOptions, andDisableResiliencehas nothing left to disable. Migration:builder.AddAtProtoClient(configure: o => …)forconfigureSettings: s => …(#136) - Aspire hosting: the settings both PDS servers share are generic — one
AtProtoPdsHostingExtensionsmethod each (WithHostname, …), onAtProtoPdsContainerResourceBase, replaces the per-server overloads. Migration: recompile; write an explicit static call aspds.WithHostname(…)(#116, #180) AtProtoClientBuilderis removed: one constructor builds the client —new AtProtoClient(options, httpClient, sessionStore, logger), every argument optional. Migration:With…settings become options or constructor arguments;WithLoggerFactory(f)islogger: f.CreateLogger<AtProtoClient>()(#122)AtProtoClient.PdsUrl/SetPdsUrl(string)areServiceUrl/SetServiceUrl(Uri)— the client holds its service URL instead of writingHttpClient.BaseAddress. Migration:client.SetServiceUrl(new Uri(url));ServiceUrlis aUriwith a trailing slash (#117)- A supplied
HttpClient'sBaseAddressis ignored —AtProtoClientstarts atAtProtoClientOptions.InstanceUrl, andPdsAdminClientsends toPdsAdminOptions.Url. Migration: put the service URL in the options (#117) XrpcClientis internal —MaxRateLimitRetriesmoves toAtProtoClientOptions.RateLimit.MaxRetries, andBaseUrl,IsAuthenticated,UpdateDPoPNonceand the body-lessQueryAsyncare gone. Migration: call custom methods throughclient.QueryAsync/ProcedureAsyncorclient.Transport(#117)SetLabelersis oneparams IEnumerable<string>method — Migration: calls compile unchanged; recompile (#122)AtProtoClientOptions.OAuthandOAuthOptions.DefaultPdsUrlare removed — nothing read either. Migration: delete the assignments; OAuth is configured on theOAuthClient(#132)- Identifier parsing is strict —
Parsethrows andTryParsereturnsfalsefor every value the specs and theatproto-interop-testsfixtures reject, including anyCidbut a base32 CIDv1 (DAG-CBOR or raw, SHA-256), and JSON reads fail alike. Migration: read legacy data withTryParse(#114) - Identifier types are
sealed records —Did,Handle,AtIdentifier,Nsid,Tid,RecordKey,Cid,AtUri,SpaceUriandSpaceRecordUrikeep their source shape, so this is a binary break only. Migration: recompile (#114) AtUricomponents are typed —CollectionisNsid?,RecordKeyisRecordKey?, andAtUri.Createtakes(AtIdentifier, Nsid?, RecordKey?). Migration:AtUri.Create(repo, Nsid.Parse(collection), RecordKey.Parse(rkey));uri.Collection?.Valuewhere a string was used (#114)- Typed identifiers across
com.atproto.*, the client,RecordCollection<T>andPdsAdminClient—Did,Handle,AtUri,Nsid,Cid,RecordKey,Tidand the rest wherever the Lexicon says so. Migration: pass returned values through; parse literals where they enter your code (#119) - Typed identifiers in
app.bsky.*— its models and clients use the identifier types andAtDatetime, andRichTextBuilder.Mentiontakes(Handle, Did). Migration: pass returned values through, and parse literals (AtIdentifier.Parse("alice.bsky.social")) (#119) - Typed identifiers in
chat.bsky.*,tools.ozone.*andsite.standard.*—StandardSiteClientalso takes anAtIdentifierand aRecordKey, and its listings return a typedRecordPage<T>. Migration: parse literals; readentry.Valuedirectly instead of deserializing it (#119) - Typed identifiers across Spaces and the space server — a space is a
SpaceUri, a record in one aSpaceRecordUri, a participant aDid; thestringoverloads andToSpaceUri()/ToRecordUri()are gone. Migration: read.Uri; parse literals once (options.ServiceDid = Did.Parse(…)) (#119) - Typed identifiers in the firehose and Jetstream models — events carry
Did,Cid,Tid,Nsid,RecordKeyandAtDatetime,RepoOp.Actionis theRepoOpActionenum, andRKeyis renamedRkey. Migration: compare withRepoOpAction.Create, not"create";WantedDids = [Did.Parse(…)](#119) - Lexicon
datetimefields areAtDatetime—AtProtoRecord.CreatedAtand every otherdatetimefield of the typed models. Migration: set withAtDatetime.Now(),ParseorFromDateTimeOffset; read.Value,TryGetValueorToString()(#119) AtProtoJsonDefaults.FormatTimestampandNowTimestampare removed —AtDatetimewrites the same text. Migration:AtDatetime.FromDateTime(value).ToString()andAtDatetime.Now().ToString()(#180)- Parameters follow one order: subject, required inputs, filters,
limit,cursor—ListRecordsAsync,RecordCollection<T>.ListAsync,CreateReportAsync,GetTimelineAsync,ListNotificationsAsync, the Spaces listings and others. Migration: reorder positional calls, or name the arguments (#119) - New Lexicon parameters shift positional arguments —
SearchPostsAsynctakestagsfortag, andGetFollowersAsync,GetListsAsync,MuteActorAsync,ListConvosAsync,LoginAsyncand others gain inputs before paging or the token. Migration: name the arguments after the first (#123, #124, #128) ListAllRecordsAsyncisEnumerateRecordsAsync, and enumerators takeint? pageSize = null— the server's default, whereRecordCollection<T>used 100;ListAllSegmentsAsyncisEnumerateSegmentsAsync. Migration: rename; passpageSize: 100to keep the old size (#119)- Cursored responses derive from
CursorPage<T>, replacingICursoredResponse—sealed records with a publicItems(ICursorPage<T>); model lists areIReadOnlyList<T>, andGetListResponse.ItemsisMembers. Migration: copy a list you need to change ([.. page.Records]) (#119, #180) - Single-use request bodies are internal — request types only one method built (
CreateRecordRequest,CreateSessionRequest,SendMessageRequestand many more), and the never-sentDeleteChatAccountRequestis removed. Migration: call the method, which takes their fields as parameters (#119) ProfileViewBasic⊂ProfileView⊂ProfileViewDetailed, andGetSessionResponse⊂SessionResponse, by inheritance — sois ProfileViewalso matches aProfileViewDetailed. Migration: recompile, and put the most specific type first in aswitchor chainedis(#180)atproto-lexgen csharpemits typed identifiers andIReadOnlyList<T>— Lexicon string formats become the SDK's identifier types andAtDatetime, space declarations included, andlexiconmaps them back. Migration: regenerate, then parse literals (#119, #141)- One session model:
AtProtoSession, withPasswordSessionandOAuthSession— immutable records replacingSession,OAuthSessionResultandAtProtoTokenData, with a typed, requiredDidandHandle;client.OAuthSessionis gone. Migration: pattern-matchclient.Session(#119, #124) ApplyOAuthSessionAsyncis replaced byApplySessionAsync(session, oauthClient)— it installs either kind of session and persists to the client's own store;ResumeSessionAsynctakes anAtProtoSession. Migration:ApplySessionAsync(session, oauth)on a client constructed with the store (#124)CompleteAuthorizationAsyncreturns anOAuthAuthorizationResult, andRefreshTokensAsyncisRefreshAsync(session)— the result carries theOAuthSessionandAppState, and owns nothing disposable. Migration:var (session, _) = await oauth.CompleteAuthorizationAsync(…)withoutusing(#124, #180)- One persistence interface:
IAtProtoSessionStore— it replacesISessionStoreandIAtProtoTokenStore, and the stores areInMemoryAtProtoSessionStore,FileAtProtoSessionStoreandEfCoreAtProtoSessionStore<T>, which read 0.6 data. Migration: rename; a custom store implements the new members (#124) ServerClientno longer changes the client's session —CreateSessionAsync/CreateAccountAsyncreturn tokens without installing them, andRefreshSessionAsync/DeleteSessionAsynctake the refresh JWT. Migration:client.LoginAsync,CreateAccountAndLoginAsync,LogoutAsync(#124)AutoRefreshSessionrefreshes on demand, and the refresh timer is opt-in — before a request whose token is about to expire and once after a rejection;BackgroundRefreshadds the old timer. Migration: drop a setting that only turned the timer off, since that now disables refreshing (#124)LogoutAsyncthrows when the service-side sign-out fails — after removing the session locally, and for an OAuth session installed without itsOAuthClient. Migration: catchXrpcException/OAuthExceptionfor a best-effort sign-out (#124)AtProtoOAuthServerOptions.ClaimsFactorytakes anOAuthSession— Migration: readsession.Did.Valueandsession.Handle.Valuewhereresult.Didandresult.Handlewere read (#124)- One XRPC exception:
XrpcExceptionreplacesAtProtoHttpException— it derives fromAtProtoException, notHttpRequestException;Error(wasErrorType) is never null; 429 and 401 have their own subtypes. Migration:catch (XrpcException ex) when (ex.Is(XrpcErrors.X))(#117) - Hosted endpoints throw the core
ATProtoNet.Http.XrpcException—ATProtoNet.Server.Xrpc.XrpcExceptionis gone, and its status (likeSpaceVerificationException's) is anHttpStatusCode. Migration:using ATProtoNet.Http;, andHttpStatusCode.NotFoundforStatusCodes.Status404NotFound(#117) - Every SDK exception derives from
AtProtoException, andOAuthException.ErrorCodeisError— onecatch (AtProtoException)covers every SDK failure. Migration: recompile, and readex.Error(#117) - A response that does not match its type throws
XrpcResponseFormatException— instead of a rawJsonExceptionorInvalidOperationException, from every XRPC call and typed record read. Migration: catchXrpcResponseFormatException(#117) - Calls that need a session throw
XrpcAuthenticationExceptionwithout one — withAuthenticationRequiredand before sending, instead ofInvalidOperationException. Migration:catch (XrpcAuthenticationException ex) when (ex.Is(XrpcErrors.AuthenticationRequired))(#122) - One
RecordRefand oneRecordView<T>for every write and typed read — sealed records built by constructor;CreateRecordResponse,PutRecordResponseandGetRecordResponseleave the public API. Migration: declareRecordRefwhere those were used, and use the constructors (#122) AtProtoRecord.CreatedAtis no longer set on construction —RecordCollection<T>.CreateAsyncstamps it when unset, so a record read withoutcreatedAtis written back without one. Migration: setCreatedAtyourself beforePutAsyncor aRepoClientwrite (#122)- The Bluesky helpers move to
client.Bskyand take typed subjects —PostAsync(RichText, PostOptions?),LikeAsync(StrongRef),RepostAsyncandFollowAsync(Did)return aRecordRef;DeleteRecordAsync(AtUri)replaces the undo methods. Migration:client.Bsky.LikeAsync(posted.ToStrongRef())(#122) UpdateProfileAsyncmoves toclient.Bskyand takes an edit callback —UpdateProfileAsync(Action<ProfileRecord>)replaces the four optional parameters and keeps every field you do not touch. Migration:client.Bsky.UpdateProfileAsync(p => p.DisplayName = "Alice")(#115, #122)RichTextBuilder.Build()returnsRichText— instead of a tuple, with empty rather thannullFacets. Migration: it still deconstructs; testfacets.Countinstead ofnull(#122)Embed.RecordViewis renamedRecordEmbedView— Migration:post.Embed is RecordEmbedView quote(#122)CreatePdsAccountRequestis removed —PdsAdminClient.CreateAccountAsynctakes the Lexicon'sCreateAccountRequest. Migration:new CreateAccountRequest { … }(ATProtoNet.Lexicon.Com.AtProto.Server) with the same properties (#122)BlobLinkis removed:BlobRef.Refis aCidLink— Migration:CidLink.FromCid(cid)(#122)- Custom XRPC takes
XrpcParams, typed procedure inputs andXrpcCallOptions—QueryAsync<TOut>(nsid, XrpcParams?, …)andProcedureAsync<TIn, TOut>(nsid, input, …)replace anonymous-object parameters andProcedureAsync<T>. Migration:new XrpcParams { { "limit", 25 } }; nameoptions:(#117, #122, #180) - Blob, repository and chat-export downloads return
XrpcStreamResponse— the stream with itsContentTypeandContentLength; disposing it releases the response. Migration:await using var blob = await client.Sync.GetBlobAsync(did, cid);and readblob.Content(#117, #123) - XRPC endpoints declare their NSID once, as a static
Nsid—IXrpcEndpoint.Nsidisstatic abstract, and[XrpcEndpoint]is removed. Migration:public static Nsid Nsid { get; } = Nsid.Parse("…");in place of the attribute and the instance property (#121) AddXrpcEndpointsFromAssemblyregisters every class implementingIXrpcEndpoint— and registration rejects a handler with no or several endpoint interfaces, or a second one for an NSID. Migration: keep handlers that must not be mapped out of scanned assemblies (#121)MapXrpcEndpoints()returns the/xrpcRouteGroupBuilder— so.RequireAuthorization()and the like apply to every endpoint. Migration: make aMap…call chained onto its result on the app instead, or it maps under/xrpc(#121)- The SDK's Lexicon unions are open, with an
Unknown*variant — a$typethe SDK does not model reads asUnknownEmbed,UnknownFacetFeatureand so on instead of throwing. Migration: add anUnknown*arm, or a discard, to everyswitchover a union; the compiler does not flag a missing one (#115) - Lexicon models derive from
LexObject— as doesAtProtoRecord, whoseExtensionDatakeeps fields a model does not declare. Migration: recompile; a record type with its own[JsonExtensionData]member drops it (#115) AtProtoJsonDefaults.Optionsis read-only — Migration: register union variants onLexiconTypeRegistry.Instance, or copy the options withnew JsonSerializerOptions(AtProtoJsonDefaults.Options)(#115)LexiconTypeRegistryis reduced to union variants — the record-type registry,CreateOptions(),LoadPluginsFromAssembly,[LexiconPlugin], the constructor andIAtProtoUnionare gone; closed unions refuse variants. Migration:LexiconTypeRegistry.Instance,LoadPlugin<T>(),[AtProtoUnion(…)](#115, #133)- Lexicon unions that were
JsonElementare typed open unions —FeedViewPost.Reason,ThreadgateRecord.Allow,PostgateRecord.EmbeddingRules,RecordEmbedView.Record, the relationships and the preferences. Migration: match the variant types; an unknown one'sRawis the oldJsonElement(#123) - Chat messages, log entries and message embeds are typed unions —
GetMessagesResponse.MessagesandConvoView.LastMessageareConvoMessages, andConvoLogEntryis the base of one type per event. Migration: pattern-match (msg is MessageView m) instead of reading$type(#128) - Lexicon references that were
JsonElementare typed —PostView.Threadgate,BlockedPost.Author,ViewerState.MutedByList,StarterPackView.Feeds,LabelerView.Creator,TeamMember.Profileand others;TeamMemberProfileis removed. Migration: read the typed properties (#123) PostViewwrites its$type— it derives from the newPostEntryunion base, asGeneratorViewand the other union members already did. Migration: none for reading; expect the$typein JSON you write from aPostView(#127)ProfileViewDetailed.AssociatedChatis replaced byAssociated—associatedChatwas never on the wire, so it was alwaysnull. Migration: readprofile.Associated?.Chat?.AllowIncoming(#123)- Chat names follow the Lexicon —
CanConvoisCanChat,ListConvosAsynctakesreadStateforreadOnly,ConvoView.OpenedandAcceptConvoResponse.Convoare removed, andUpdateAllReadAsyncreturns anUpdateAllReadResponse. Migration:CanChat,readState: "unread"(#123) - Ozone's review queue is
QueryStatusesAsync—QuerySubjectsAsyncandQuerySubjectsResponsecalled a method that never existed. Migration:QueryStatusesAsync(new SubjectStatusFilter { … }), and readSubjectStatuses(#123) SignatureClient.SearchAccountsAsynctakes signature values and returns account views —valuesisIEnumerable<string>, results areAccountInfo, andAccountResultis removed. Migration: pass theSigDetail.Values (#123)- Notification methods drop
priorityandseenAt—ListNotificationsAsynctakesreasonsinstead, andGetUnreadCountAsyncdropspriority; upstream ignores one and rejects the other. Migration: remove the arguments (#123) AtProtoScopes.PermissionSetsholds only published permission sets — six constants naming unpublished sets are removed, andManageNotificationsisapp.bsky.authManageNotifications. Migration:DeleteContent,ManageNotifications,ManageModerationorFullApp; recompile (#123)- Ozone models match their Lexicons —
RecordViewDetail.BlobCidsisBlobs, the label and tag event lists andModEventReport.ReportTypearerequired, andModEventMuteReporter.DurationInHoursisint?. Migration: readBlobs[i].Cid; set the lists, empty if unchanged (#123, #131) - One moderation-subject union,
ModerationSubject— inATProtoNet.Lexicon.Com.AtProto.Moderation, replacingReportSubjectand the Ozone copy and typing report and status subjects;RecordSubject.Cidis required. Migration: import the namespace;ModerationSubjectforReportSubject(#122) VideoClient.UploadVideoAsyncuploads in parts and waits for processing — it returns the completedJobStatus, throwsVideoUploadExceptionfor a failed job, and takesVideoUploadOptions?before the token. Migration: drop your polling loop, or callUploadVideoInOneRequestAsync(#127)- One immutable
DidDocument—ATProtoNet.Auth.OAuth.DidDocumentandDidServiceare removed;Identity.DidDocumentis init-only, its entries areDidDocumentServices, andGetHandle()/GetPdsEndpoint()return aHandle?/Uri?. Migration: object initializers;GetPdsEndpoint()?.OriginalString(#120) DidDocument.GetSigningKey/GetVerificationKeyreturnnullfor key material that does not decode — instead of throwingFormatException, as doesSpaceAuthority.GetSigningKey's fallback. Migration:TryGetVerificationKeytells a malformed entry from an absent one (#180)DidResolutionExceptionreplacesPlcExceptionandDidWebException— every resolver throws it, network failures included, with aDidResolutionErrorKind(TombstonedisDeactivated). Migration:catch (DidResolutionException ex) when (ex.Kind == DidResolutionErrorKind.NotFound)(#120)- DID resolvers implement
IDidResolver—ResolveAsync(Did)replacesResolveDidAsync(string), andSpaceCredentialProvidertakes anIDidResolver. Migration:await resolver.ResolveAsync(Did.Parse(did)); pass aCachingDidResolver(#120) PlcClienttakes an explicit directory URL and typed arguments — it no longer readsBaseAddress, methods take aDid,PlcOperationreplacesPlcData, andPlcOperationBuilderis typed too. Migration:new PlcClient(httpClient, new Uri("https://plc.directory"))(#120)AuthorizationServerDiscoveryandOAuthClient.Discoveryare internal —OAuthClientwalks the discovery chain itself, with the resolverOAuthOptions.IdentityResolversupplies. Migration: resolve an identity with anIIdentityResolver(ResolveAsync(AtIdentifier.Parse(handle))) (#120, #180)OAuthClienttakes itsHttpClientfrom the options, and follows the identity fetch policy —OAuthClient(options, logger); withoutOAuthOptions.HttpClient, requests go to public HTTPS addresses only and fail asOAuthException. Migration:AllowPrivateNetworksfor a local PDS (#120, #132)StartAuthorizationAsyncreturns anOAuthAuthorizationRequestand takesOAuthAuthorizationOptions—AuthorizationUrl(aUri),StateandExpiresAtinstead of a tuple, andpdsUrlisServerUrl. Migration:var (url, state, _) = …; redirect tourl.AbsoluteUri(#132)OAuthClient.GetPendingAuthorizationStateandOAuthAuthorizationStateare removed — they exported the PKCE verifier without the DPoP key. Migration: keep pending logins across instances withOAuthOptions.StateStore(#132)AtProtoScopes.RpcandIncluderefuse an audience that is not a service —audmust be a DID with a service fragment, or*forrpc. Migration: append the fragment, for example withAtProtoScopes.BlueskyAppView(#132)IdentityActionandAtProtoScopes.Identity(attr, action)are removed — the permission spec dropped theactionparameter. Migration:AtProtoScopes.Identity(attr)(#132, #180)AtProtoOAuthService's constructor is internal —WithOAuth()builds the service from the container's identity resolver, state store, server and refresh coordinator. Migration: resolve it from dependency injection instead of constructing it (#120, #132, #136, #180)- The hosted login's HTTP and identity settings move to the shared registrations —
AtProtoOAuthServerOptions.HttpClient,HttpClientTimeoutandHandleResolutionTimeoutare removed. Migration:AddAtProtoIdentity(o => …)for the timeout; configure the clientAtProtoOAuthExtensions.HttpClientName(#180) IOAuthClientProvideris removed; the client factory takes theOAuthClient—WithOAuth()registers the login's client as theOAuthClientsingleton, andTryGetClient()is theClientproperty. Migration: with your own OAuth flow, register yourOAuthClientas a singleton (#136)AtProtoOAuthService.CompleteCallbackAsyncreturns anAtProtoOAuthCallbackResult—RedirectUrl,IsRelayandDid, instead of a string the caller had to vet. Migration: redirect toresult.RedirectUrl(#136)- The client factory, the Blazor widgets and sign-out act only for the OAuth login's user — they ignore identities the login did not issue, service auth's included. Migration: a principal you build for the factory needs
AtProtoClaimTypes.AuthMethodset tooauth(#136) - The development loopback client takes its callback from the server's address — the plain HTTP loopback address fixed at startup, not the first request; without one the login fails. Migration: bind an address such as
http://127.0.0.1:5000, or setBaseUrl(#136) - The Blazor components act as the signed-in user, and
FeedView.FeedSourceis aFeedSource— throughAtProtoUserClientAccessor, registered byAddAtProtoBlazor(). Migration: callAddAtProtoBlazor();FeedSource.Author(AtIdentifier.Parse("alice.bsky.social"))for"author:alice.bsky.social"(#136) - The bearer-JWT
AtProtoAuthenticationHandleris removed — withAtProtoAuthenticationOptionsandAuthenticationBuilder.AddAtProto(). Migration: authenticate other services withAddAtProtoServiceAuth(…)and[RequireServiceAuth], and users with the OAuth cookie login (#136) ServiceAuthGeneratortakes aDid, andCreateTokenrequires anNsidlxm— per the 2026 service auth revision;audiencemust be a DID with an optional service fragment. Migration:new ServiceAuthGenerator(Did.Parse(…), key); pass the NSID of the method called (#119, #125)ISpaceReplayStoreis generalized intoIJtiReplayStore—InMemoryJtiReplayStore,EfCoreJtiReplayStore<T>andJtiReplayEntityon tableAtProtoJtiReplayreplace theSpace…Replay…names. Migration: rename, and add an EF migration for the renamed table; dropping the old one loses nothing (#125)VerifiedServiceAuthmoves toATProtoNet.Server.Authentication— a class with init-only properties instead of a positional record, with the token'slxmasMethodand itsKeyId,TokenId,IssuedAtandExpiresAt. Migration: read properties instead of deconstructing (#125)notifyWritemust be signed by its writer, and more strictly —issmust be the account whose repo advanced, and a token withoutjti,lxmoriat, with a fragment iniss, or with akidother than#atprotois refused. Migration: sign as the writer throughISpaceAccountSigner(#125, #133)ISpaceServiceAuthVerifierandSpaceServiceAuthVerifierare removed —notifyWriteverifies its service auth withServiceAuthVerifierunderSpaceServerOptions.ClockSkew. Migration: verify service auth elsewhere withServiceAuthVerifierorAddAtProtoServiceAuth()(#113, #133, #180)- Firehose construction leaves
AtProtoClient—CreateFirehoseClient,CreateFirehoseConsumerandRelayUrl(options, builder and Aspire settings) are removed. Migration:new TypedFirehoseConsumer(new TypedFirehoseConsumerOptions { ServiceUrl = "wss://bsky.network" })(#122) FirehoseClient,FirehoseConsumer,FirehoseFrameandJetstreamClientare removed —TypedFirehoseConsumer,LabelStreamConsumerandJetstreamConsumerread every stream, reconnecting, and are notIDisposable. Migration:ConsumeAsync(cursor)forSubscribeAsync(#126, #180)- Stream consumers share
StreamConsumerOptionsand aStreamReconnectPolicy—ReconnectreplacesReconnectDelayandMaxReconnectAttempts, and the cursor settings sit onCursorStreamConsumerOptions. Migration:Reconnect = new() { MaxAttempts = null }forMaxReconnectAttempts = -1(#126, #180) - A consumer whose reconnect attempts run out throws
EventStreamException— instead of completing, and cancelling the token ends every consumer's enumeration normally. Migration: catchEventStreamException, or setMaxAttempts = nullto reconnect forever (#126) IFirehoseCursorStoreisIStreamCursorStore, withValueTaskmethods — shared by every cursor-keeping consumer;InMemoryFirehoseCursorStoreisInMemoryStreamCursorStore. Migration: rename, and returnValueTaskfrom a custom store (#126)FirehoseEventParserkeepsParse(ReadOnlyMemory<byte>)—Parse(FirehoseFrame)andTryParseare removed, and an error frame throwsEventStreamException. Migration:FirehoseEventParser.Parse(bytes)(#126)- Firehose verification is
RepoSyncVerifier—FirehoseVerifier,VerificationResultand the options'VerifierandVerifySignaturesare removed, andCollectionFilteris anIReadOnlySet<Nsid>. Migration:SyncVerifier = new RepoSyncVerifier();VerifyCidsstill checks CIDs alone (#126, #180) - Firehose messages follow the current
subscribeReposLexicon —HandleEventandTombstoneEventare gone,SeqandTimemove to a newFirehoseEventbase, andTooBig,RebaseandBlobsare[Obsolete]. Migration:(message as FirehoseEvent)?.Seq; handle#identityand#account(#126) - Jetstream uses
RepoOpAction—JetstreamOperationis removed, andJetstreamCommitEvent.Operationis the firehose's enum. Migration: renameJetstreamOperationtoRepoOpAction(#126) - One
JetstreamException— it replacesJetstreamConnectExceptionandJetstreamArchiveExceptionand derives fromEventStreamException, andEventStreamErrorreplacesJetstreamStreamErrorinOnStreamError. Migration: catchJetstreamException(#126) JetstreamDictionaryClientis folded intoJetstreamArchiveClient— asGetZstdDictionaryAsync(id). Migration:new JetstreamArchiveClient(url).GetZstdDictionaryAsync()(#126)JetstreamEventParserkeepsParseFrame(ReadOnlyMemory<byte>, JetstreamProtocol)— the otherParseandParseFrameoverloads are removed. Migration:ParseFrame(Encoding.UTF8.GetBytes(json), protocol).Event(#126)- Unused Jetstream members are removed —
JetstreamPlanStats,JetstreamSnapshotPlan.Stats,JetstreamConsumer.IsConnectedandDispose, andJetstreamReplayConsumer.PinnedTipSeq;JetstreamSegmentReader.DecodeBlockis internal. Migration: decode withDecodeBlockFrame(#126) JetstreamReplayConsumeris configured by its options alone — the(options, archiveClient)constructor andJetstreamArchiveOptions.AfterSeqare removed. Migration: set the archive settings onJetstreamArchiveOptions, and pass the resume position toReplayAsync(afterSeq)(#180)MerkleSearchTreeonly accepts valid MST keys, andCreaterejects duplicates —AddandCreatethrowArgumentExceptionfor a key that is notcollection/rkey, and for a repeated key rather than building a corrupt tree. Migration: key the tree by repo paths (#111)- Unused repository helpers are removed or internal —
CreateFromEntries,DecodeToNode,TryValidate,DagCborEncoder.ComputeCidandCarBlock.DataLengthgo; the MST node types are internal. Migration:MerkleSearchTree.Create,CidComputation.ComputeForDagCbor,Data.Length,Decode(#111, #180) DagCborDecoder.DecodethrowsFormatExceptionfor all malformed input — it threwInvalidOperationExceptionor leakedCborContentException, and so doesMerkleSearchTree.Deserializefor a tree too deep. Migration: catchFormatException(#111)simplespaceseparates read and write policies, andputMemberreplacesaddMember— following upstream's read/write split (bluesky-social/atproto#5496), which 0.6 failed against. Migration: passreadPolicy:andwritePolicy:forpolicy:, andPutMemberAsync(space, did, read: true, write: true)forAddMemberAsync(#113)SimpleSpaceClient.CheckUserAccessAsynctakes a requiredaccess— beforeclientId, as the Lexicon now requires. Migration:CheckUserAccessAsync(space, user, SimpleSpaceAccess.Read, clientId)(#113)- The
simplespacestore and record carry both policies and per-member access —SimpleSpaceRecordtakesReadPolicyandWritePolicy,ISimpleSpaceStorehasPutMemberAsync/GetMemberAsync, andSpaceNsids.AddSimpleSpaceMemberisPutSimpleSpaceMember. Migration: implement both in a custom store (#113) - EF Core
simplespaceschema:Policyis split and members gain access flags —ReadPolicyandWritePolicyreplacePolicy, and members gainReadandWrite. Migration: a migration that copiesPolicyinto both columns and defaults the flags totrue; seedocs/spaces.md(#113) ISpaceAccessPolicyalso decides writes —SpaceAccessRequest.AccessisSpaceAccessKind.ReadorWrite, andISimpleSpaceManagingAppClient.CheckUserAccessAsynctakes the kind too. Migration: a custom policy handlesWritewithout an app perimeter, or it refuses every writer (#113)- Space server endpoint handlers and query-parameter types are internal —
NotifyWriteEndpoint,AddSimpleSpaceMemberEndpointand the other handlers, their bases and*Parameterstypes; theAdd…registrations map them as before. Migration: implement your ownIXrpcEndpoint(#113, #133) - The space server resolves DIDs through an
IDidResolver— a keyedCachingDidResolverset bySpaceServerOptions.DidCachereplacesISpaceDidDocumentResolverandDidDocumentCacheLifetime. Migration: register a custom one underSpaceServerExtensions.DidResolverKey; setDidCache.StaleAfter(#120) SpaceSyncertakes anIDidResolver— instead of a signing-key delegate, andResolveSigningKeyAsyncis removed;SpaceAuthority.GetHostEndpointandGetServiceEndpointreturnUri?. Migration:new SpaceSyncer(space, store, new CachingDidResolver())(#120)- Space server constructors take new optional parameters —
AddSpaceAuthority<T>gainsserviceAuthKey,SpaceCredentialVerifieroptions, the notifier and managing-app client anISpaceAccountSigner;SpaceCredentialIssueris notIDisposable. Migration: name later arguments; dispose the key yourself (#133) ISpaceCredentialIssueris removed — credential signing usesSpaceCredentialIssuerover the in-process key given toAddSpaceAuthority. Migration: resolveSpaceCredentialIssuer; an out-of-process signer has no replacement in 0.7 (#180)- Space server token types carry only what they establish — the
Verified…token records drop unused members,DPoPProofkeeps onlyKeyThumbprint, and bothBuildRequestUrimethods are gone. Migration:auth.Delegation.Space,credential.Space.Authority(#133, #180) - Test-only public members are removed or internal —
SpaceTokens.TryParseandVerify(type, jwt, …),SpaceToken.ToSpaceUri(),DPoPProofValidator.ProofType,ClaimsSpaceCallerResolver.DidClaimTypeandSimpleSpaceAuthorityStore.Inner. Migration:Verify(SpaceTokens.Parse(type, jwt), …),AtProtoClaimTypes.Did(#180) - Space and SimpleSpace models follow the open-union pattern —
SimpleSpaceUserPolicyandSimpleSpaceAppAccessare open, so a newer variant reads asUnknown…instead of failinggetSpace, and the models derive fromLexObject. Migration: add anUnknown…(or_) arm to aswitchover a policy (#133) - The EF Core space and replay stores no longer run on the EF Core in-memory provider — they write with
ExecuteUpdate/ExecuteDelete, which it does not translate. Migration: test on SQLite in memory (Mode=Memory;Cache=Shared, connection held open) (#133) - One startup warning for in-process stores;
WarnOnInMemoryStoresandMaxClientMetadataBytesare removed — it covers the replay storeAddAtProtoSpaces()andAddAtProtoServiceAuth()fall back to. Migration: registerInMemoryJtiReplayStoreyourself to keep it quietly (#133, #180) atproto-lexgen publishpublishes to a PDS, andmigrateis removed —publishwritescom.atproto.lexicon.schemarecords, and its--output,--baseline,--assemblyand--no-bumpoptions and theMigrationstypes are gone. Migration: gate changes withdiff --strict(#130, #180)- Unused
atproto-lexgenhelpers are removed —CSharpEmitter.Emit(LexiconDocument),LexiconEmitter.JsonOptionsand fiveTypeMapperhelpers. Migration:EmitAll([document]),LexiconJson.WriteOptions, andTypeMapper.SplitRefforParseTypeValue(#130)
Added
TidGenerator— mints strictly increasing, never-repeating TIDs per the spec, thread-safe with an injectableTimeProvider;Tid.Next()andRecordKey.NewTid()use a shared instance (#114)- Generic parsing for identifiers — every identifier type implements
IParsable<T>,ISpanParsable<T>andIComparable<T>, so minimal-API binding and genericT.Parsecode accept them;CidgainsCodec,DigestandToBytes()(#114) AtDatetime— the Lexicondatetimetype: it keeps the text it was read with, so a record written back keeps its CID, reads leniently (IsValid,TryGetValue) and is strict when created from code (#119)- Open unions and unknown-field round-tripping for your own Lexicons — mark a union base
[AtProtoUnion(typeof(UnknownX))]and derive models fromLexObject; seedocs/custom-records.md(#115) app.bsky.embed.galleryand new profile record fields —GalleryEmbedandGalleryViewas post andrecordWithMediaembeds, andProfileRecord.Pronouns,Website,LabelsandJoinedViaStarterPack(#115)- Per-call XRPC options —
XrpcCallOptionssetsProxy,AcceptLabelers,Headersand aTimeoutfor one call, so concurrent callers on a shared client no longer race onSetProxy/SetLabelers(#117) AtProtoClientOptions.UserAgentandRateLimit— theUser-Agentsent with every request, andXrpcRateLimitOptions(MaxRetries,MaxDelay) for 429 handling (#117)XrpcErrors— constants for the generic and commoncom.atproto.*XRPC error names, forXrpcException.Is(#117)- XRPC procedures with no input or a binary body —
IXrpcProcedure<TOutput>,IXrpcProcedureVoid, andIXrpcBlobProcedure<TOutput>, which receives the unbuffered body as anXrpcBlobInput(#121) - XRPC handler attributes apply to their endpoint —
[Authorize],[AllowAnonymous],[EnableRateLimiting]and any other attribute on a handler class become its endpoint's metadata (#121) - Enumerators and a public paginator —
Enumerate*walks the timeline, author feed, actor likes, followers, follows, list members, notifications, messages and bookmarks, andPagination.EnumerateAsyncany other cursored method (#119, #127, #180) - AT URI overloads —
RepoClient.GetRecordAsync/DeleteRecordAsync,SpaceClient.GetRecordAsync/DeleteRecordAsyncand theStandardSiteClientgetters also take the record'sAtUriorSpaceRecordUri(#119) - A drift test against the upstream Lexicons — every XRPC call's NSID and method, every model's JSON names and the known-values constants are checked against a pinned snapshot of bluesky-social/atproto and standard.site (#123)
- Current
app.bskyfields — profile views gainPronouns,Associated,VerificationandStatus, and posts, feeds, embeds, notifications, graph views, labelers and video jobs gain the fields their Lexicons added, with newNotificationReasonsandStandardLabelValues(#123) - Current chat,
com.atproto,site.standardand Ozone fields — chat member views and declarations,DescribeServerResponse.BlobUploadLimit,DocumentRecord.Contributors, Ozone event and subject-status details, and the missingqueryEvents,querySetsandlistMembersfilters (#123) - Permission sets —
AtProtoScopes.PermissionSets.DeleteContent,FullChatClient,StandardSiteFullandStandardSiteSocial(#123) AtProtoClient.SessionChanged— raised when a session is created, refreshed, expires or is signed out, with the new and the previous session (#124)- Taken-down accounts can sign in —
LoginAsyncandCreateSessionAsynctakeallowTakendown, for a session limited to migrating or exporting the account (#124) CreateAccountAndLoginAsyncandTryRestoreSessionAsync— create an account and install its session, or install the session the store holds for a DID without a request (#124)- OAuth revocation —
OAuthClient.RevokeAsync(session)revokes a session at its authorization server (RFC 7009) (#124) - Signing in through an entryway lands on the account's PDS — login and refresh follow the
#atproto_pdsendpoint of the account's own DID document when it is HTTPS (#124) - Ozone reports and queues —
client.Ozone.Reportcoverstools.ozone.report(queries, assignment, typed activities, closing, and live and daily statistics, the live ones asQueueStats), andclient.Ozone.Queuecoverstools.ozone.queue; seedocs/ozone.md(#131, #180) - Ozone settings, safelink rules, verifications and account history —
client.Ozone.Setting,Safelink,VerificationandHosting(#131) - Ozone moderation lookups and scheduled actions — batch repo, record and subject lookups returning the open
ModerationSubjectViewunion, account timelines, reporter stats, and scheduling and cancelling actions (#131) - The Ozone event types the SDK lacked — appeals, priority scores, age assurance, credential revocation, scheduled takedowns, and the account, identity and record events Ozone records for every subject (#131)
- Granular report reasons —
ReportReasonsgains thetools.ozone.report.defs#reason*values upstream prefers, and each coarsecom.atprotoreason names its replacement (#131) - Group chats —
client.Chat.Groupcoverschat.bsky.group: groups, members, join links and join requests; conversations and members carry a typedKind, and aJoinLinkEmbedshares a group (#128) - Conversation endpoints and message fields — unread counts, conversation requests, members, locking, the
kindandlockStatusfilters, reactions, replies, typed system messages, and constants for the known values (#128) - Chat status, notification preferences and moderation —
ChatActorClient.GetStatusAsync,client.Chat.Notification, andclient.Chat.Moderationfor moderation services (#128) - Verified record reads —
SyncClient.GetVerifiedRecordAsyncandRecordProof.Verifycheck async.getRecordproof against the repository's signed commit, returning aVerifiedRecordor proof of absence (#129) SyncClient.GetBlocksAsyncandRepoClient.ImportRepoAsync— fetch repository blocks by CID, and upload a repository CAR when migrating an account (#129)AdminClient.SearchAccountsAsyncandUpdateAccountSigningKeyAsync— andPdsAdminClient.SearchAccountsAsyncfor PDSs that serve it, such as Tranquil (#129)AtProtoClient.Temp—com.atproto.temp.*: handle availability with suggestions, the signup queue, scope references, phone verification and credential revocation (#129)site.standard.graph.recommend—RecommendRecordand theStandardSiteClientrecommendation methods (#129)- Bookmarks and drafts —
client.Bsky.Bookmarkandclient.Bsky.Draftmanage the account's private bookmarks and post drafts; a bookmark's item is aPostEntry, the new open union of post views (#127) - Multipart video upload —
VideoClient.StartUploadAsync,UploadPartAsync,FinishUploadAsync,GetUploadStatusAsyncandAbortUploadAsync, whichUploadVideoAsyncdrives (#127) FeedClient.SearchPostsV2Async—app.bsky.feed.searchPostsV2, with every filter in aPostSearchFilters(#127)- Notification preferences and activity subscriptions —
GetPreferencesAsync/PutPreferencesV2Async, activity subscriptions,UnregisterPushAsyncandNotificationDeclarationRecord(#127) - Actor and graph records —
StatusRecord,ContentVisibilityDeclarationRecord,VerificationRecordandReferenceListOptOutRecord(#127) - Graph membership and starter pack search —
GetListsWithMembershipAsync,GetStarterPacksWithMembershipAsyncandSearchStarterPacksV2Async(#127) - Threads, link cards, feed feedback and age assurance —
GetPostThreadV2Async,GetEmbedExternalViewAsync,SendInteractionsAsync(withServiceProxy.BskyFeedGenerator) andclient.Bsky.AgeAssurance(#127) - Record types name their collection — a record implementing
IAtProtoRecorddeclaresstatic Nsid Collection, soclient.GetCollection<PostRecord>()needs no NSID; the SDK's records andatproto-lexgenoutput implement it (#122) RecordCollection<T>.FindAsync/FindFromAsync— the record, ornullonRecordNotFound(#122)- A public XRPC transport:
AtProtoClient.Transport— anIXrpcTransportthat a sub-client for another Lexicon builds on, sharing the client's session, refresh and errors; seedocs/custom-xrpc.md(#122) - Identity resolution API —
IDidResolver,IHandleResolverandIIdentityResolver;ResolveAsync(AtIdentifier)returns a bidirectionally verifiedResolvedIdentity, andIdentityResolver.CreateDefault()builds the stack (#120) CachingDidResolver— a bounded cache for anyIDidResolver, with stale-while-revalidate, remembered failures, one fetch per DID, invalidation and an optionalIDistributedCache(#120)AddAtProtoIdentity()— registers the resolvers as singletons, optionally over the registeredIDistributedCache(#120)resolveIdentity,resolveDidandrefreshIdentity— onIdentityClient, withIdentityInfoand the matchingXrpcErrors(#120)- PLC export —
PlcClient.ExportAsyncpages the directory's/export, andStreamExportAsyncfollows/export/stream(#120, #180) - A configurable DNS-over-HTTPS endpoint —
IdentityResolverOptions.DnsOverHttpsUrl;nullresolves handles over HTTPS only (#120) - Event-stream error frames are a typed error — an
op = -1frame is anEventStreamExceptionwithErrorandIsRetryable, reported toOnStreamError, and consumers reconnect after a retryable one (#126) - Label streams and chat moderation events —
LabelStreamConsumerreadssubscribeLabels, andChatModerationEventConsumerreadschat.bsky.moderation.subscribeModEventsinto typed events (#126) - Jetstream timestamp cursors —
JetstreamCursor.FromTimestampbuilds a v2 cursor that seeks by time, so a stored v1 cursor resumes a v2 consumer (#126) IRecordEvent— one record change from either stream:JetstreamCommitEventimplements it, andCommitEvent.GetRecordEvents()splits a commit into decoded records (#126)OnEventDropped— every consumer reports the events it skips, with aStreamDropReasonand the cursor (#126)- Jetstream archive probes and health check —
ProbeSegmentAsync,ProbeBlockAsyncandGetHealthAsync, andAddHealthChecks().AddJetstream(url)inATProtoNet.Server(#126) - Inbound service auth:
AddAtProtoServiceAuth()andServiceAuthVerifier— services built onATProtoNet.Serveraccept calls carrying service auth tokens, checked as the 2026 spec requires; require it with[RequireServiceAuth], and tune it throughAtProtoServiceAuthOptions.Verifier(#125, #180) XrpcMethodMetadataandJtiReplayDbContext— the NSID each mapped XRPC endpoint serves, for accepting service auth on routes you map yourself, and the replay table on a context of its own (#125)AtProtoClaimTypes— constants for the claims the OAuth login and service auth issue:Did,Handle,HandleVerified,PdsUrl,AuthMethod,LexiconMethodandAudience(#136)- Confidential OAuth clients (
private_key_jwt) —OAuthClientKeyandOAuthOptions.ClientKeyssign every request with a client assertion, which earns longer sessions;OAuthSession.ClientKeyIdsurvives key rotation (#132) - A store for pending OAuth authorizations —
IOAuthStateStore, in memory and bounded per requester by default, orDistributedCacheOAuthStateStore, which requiresProtect/Unprotect(orStoreSecretsUnencrypted), so a login can complete on another instance (#132) OAuthAuthorizationOptions.RequesterIdForandAppState— a requester id from a remote address, IPv6 grouped by its /64, and application data carried through the flow and handed back at completion (#132)OAuthOptions.RequestTimeout— bounds a whole pushed-authorization, token, refresh or revocation exchange, retry and body read included (#132)- A process-wide DPoP nonce cache — every client shares each server's latest nonce, so first requests skip a
use_dpop_nonceround trip (#132) - Scope presets built on permission sets —
AtProtoScopes.Presets, and the service audiencesAtProtoScopes.BlueskyAppViewandBlueskyChat(#132) - OAuth sign-in can start at an entryway — a URL that serves no protected-resource metadata is tried as an authorization server, as upstream atproto#4896 allows (#132)
- Lexicon resolution —
LexiconResolverresolves an NSID through its_lexiconDNS record and verifies the published schema against its commit proof; alsoCachingLexiconResolver,client.Lexicon.ResolveLexiconAsyncandAddAtProtoIdentityregistration (#130) - Permission-set checks —
ResolvePermissionSetAsyncandResolveIncludeScopeAsyncconfirm that the set aninclude:scope names exists before an app requests it (#130) atproto-lexgenpermission sets andlint—csharpemits a class perpermission-setwith anInclude(aud)helper, andlintchecks documents against the Lexicon and permission specs (#130)atproto-lexgen publishandresolveon the network —publishlints, signs in, skips unchanged schemas, refuses breaking changes unless--forceand prints the_lexiconTXT records;resolvefetches and verifies published schemas (#130)- Refresh coordination for clients that share a session store —
ISessionRefreshCoordinator(InProcessSessionRefreshCoordinator,AtProtoClientOptions.RefreshCoordinator) makes clients of one account share a refresh instead of spending the refresh token twice;WithClientFactory()registers one (#136) - Confidential clients in the hosted OAuth login —
AtProtoOAuthServerOptions.ClientKeys, andServeClientMetadatahasMapAtProtoOAuthserve the client metadata and the public key set (#136) PostCardlikes and reposts, andProfileCardloads byActor— as the signed-in user, when no callback or profile is given (#136)- Default styles for the Blazor components — the opt-in stylesheet
_content/ATProtoNet.Blazor/atproto-components.css(#136) AddAtProtoClient(connectionName:)— takes the PDS URL from the connection string an AspireWithReference(pds)supplies (#136)- Label signing and verification —
LabelSignersigns per the label spec, byte for byte like@atproto/ozone, andLabelVerifierchecks against the issuer's#atproto_labelkey (DidDocument.LabelKeyId), reporting aLabelVerificationStatus(#135) - Verified label streams —
LabelStreamConsumerOptions.Verifierverifies every streamed label intoLabelsEvent.Verification(#135) - Serving labels —
QueryLabelsEndpointservesqueryLabelsover yourILabelSource, signing on the way out, andLabelStreamFramesencodessubscribeLabelsframes (#135) - Sync 1.1 firehose verification:
RepoSyncVerifier— runs the sync spec's checklist on#commitand#sync(limits, CIDs, signature, operations inverted ontoprevData), keeping each repository's state in anIRepoSyncStateStore, in memory or EF Core (#134) - Sync 1.1 mode for
TypedFirehoseConsumer—SyncVerifierdelivers only events that verify and chain, and withResynca broken repository is refetched, verified as aRepoSnapshotand delivered as aRepoResyncEvent(#134) - Tap client —
TapClientreads Tap's/channelwith explicit acknowledgements, reconnecting, and calls its admin endpoints (#134) - Tap webhook receiver —
MapTapWebhook(pattern, (evt, ct) => …)receives Tap's webhook deliveries, checks the admin password, and acknowledges an unreadable event with a warning (#134, #180) ISpaceAccountSigner— signs space service auth as the account it speaks for, as the reference authority and managing apps require; an authority with a dedicated#atproto_spacekey passes its#atprotokey toAddSpaceAuthority(#133)SpaceTokens.Verify(SpaceToken, …)— verifies a token already returned bySpaceTokens.Parse, for verifiers that readissandkidfirst (#118)- Public API tracking — every package carries
PublicAPI.Shipped.txt/PublicAPI.Unshipped.txt, checked by the public API analyzers, so an undeclared API change fails the build (#139) - A migration guide for 0.7 —
docs/migrating-to-0.7.mdcovers every breaking change, with compiled before-and-after code (#138)
Changed
- Shared build settings are set once — target framework, nullable, implicit usings and package metadata come from
Directory.Build.props; published metadata is unchanged apart from the core package's dependencies (#110) - Aspire hosting: one base for both PDS container resources —
AtProtoPdsContainerResourceandAtProtoTranquilPdsContainerResourcederive fromAtProtoPdsContainerResourceBase, so one AppHost helper covers either; environment and manifest are unchanged (#116) MerkleSearchTree.SerializeProofreturns the reference covering proof — including the nodes on the paths to each key's neighbours, which relays need to invert a#commit(#111)MerkleSearchTree.Validatechecks that a loaded tree is canonical — it rebuilds the tree and throwsInvalidOperationExceptionwhen the root differs (#111)- Faster DAG-CBOR, BLAKE3,
LtHashanddid:keyverification — the decoder writes JSON directly, BLAKE3 andLtHashuse hardware vectors, andAtProtoCrypto.VerifySignaturecaches parsed keys; output is unchanged (#111, #112) - Identifier conversions are null-safe, and
SpaceUri/SpaceRecordUriserialize as strings — anullidentifier converts to anullstring, and the space URIs round-trip through JSON like every other identifier (#114) - A union variant always writes its
$type— also outside union position, as the data model allows, so a$typeread there is written back (#115) - Minted JWTs allocate far less and share one
jtiformat — DPoP proofs, service auth and space tokens are written straight to UTF-8, and each carries a randomjtifrom the system CSPRNG in the same hex form (#118) - Compressed responses and bounded connection lifetimes — every
HttpClientthe SDK creates or registers decompresses responses, recycles pooled connections so DNS changes are picked up, and bounds connecting (#117) - JSON responses are deserialized as they arrive — a large page no longer allocates a large-object-heap buffer, and
RecordCollection<T>reads each record straight intoT(#117, #122) - Rate-limit waits are capped — a 429 asking for longer than
RateLimit.MaxDelaythrowsXrpcRateLimitExceptionat once,Retry-Afterdates are read, and retries carry jitter (#117) atproto-proxyandatproto-accept-labelersare sent without a session too — and the session calls themselves are never proxied (#117)- The SDK sets
User-Agentper request — the Aspire client and the OAuth login no longer add one to theHttpClient, andSpaceReaders share their provider'sHttpClient(#117) - Service base URLs with a query or fragment are refused —
InstanceUrl,SetServiceUrl,PdsAdminOptions.Url, the PLC directory and space host URLs throwArgumentException(#117) - Unmatched
/xrpc/{nsid}requests answer with an XRPC error —501 MethodNotImplementedfor an unserved NSID,405withAllow, and400 InvalidRequestfor a malformed one, as the referencexrpc-serverdoes (#121) - Stream and space readers check identifiers where they read them — a frame, commit, index entry, token or query parameter whose identifier does not parse is dropped or refused (#119)
- Deprecated upstream, now
[Obsolete]—SyncClient.NotifyOfUpdateAsync,ListNotificationsResponse.Priority,GetSuggestedFollowsByActorResponse.IsFallback, andStandardLabelValues.Gore,ContentWarningandNotAvailable(#123) DisposeAsyncis the primary way to disposeAtProtoClient— it waits for a token exchange under way to be stored,Disposereturns at once, and the client disposes only what it created (#124)- A session store that fails to write no longer discards a successful refresh — the refreshed session stays installed, and the error is logged (#124)
- Signing out of the hosted login revokes the OAuth session —
/atproto/logoutrevokes it at the authorization server before removing it (#124) - Identity resolution is cached wherever it repeats —
RepoSyncVerifier,SpaceCredentialProviderand the space server resolve through aCachingDidResolver, refetching once when a cached key fails, as the sync spec requires (#120) - Handle resolution fails closed on a conflict, and OAuth login asks only a handle's own authorities — two DIDs in DNS, or DNS and HTTPS disagreeing, fail, and the
bsky.socialAppView is no longer asked (#120) - Streaming spends less CPU per event — filtered consumers skip decoding dropped commits, a verified commit is parsed once, and the Jetstream archive filters rows before decoding them (#126)
- Cursors are saved in the background — every
CursorPersistIntervalevents, off the read loop (#126) - A v2
JetstreamConsumerskips the replay of its start cursor — the server replays?cursor=Ninclusively (#126) - Service auth signatures are checked as the reference implementation checks them —
algmust match the key, high-S signatures are accepted, and a key a cached DID document lacks is found by one refresh (#125) - Space endpoints that authenticate their own callers opt out of host authorization — a group-wide
RequireAuthorization()orRequireServiceAuth()no longer blocks the credential, repo-host andnotifyWriteendpoints (#125) - An OAuth session's
ServiceEndpointis the account's PDS — also for a login started from a server URL, and a refresh follows a PDS move (#132) - OAuth sign-in fetches less — metadata is cached briefly, the callback reuses the identity resolved at the start, and a PDS's protected-resource metadata is read once (#132, #136)
- The transitional scopes are documented as legacy —
transition:*remains the default for now (#132) - The client factory caches each account's imported DPoP key — per-request clients are much cheaper, and a signed-out session takes its key with it (#136)
FileAtProtoSessionStorereads without a lock and writes under a lock per account (#136)ATProtoNet.Blazorno longer declares browser support — it runs on the server, on top ofATProtoNet.Server(#136)ATProtoNet.Serverdepends on nothing beyond the ASP.NET Core shared framework — EF Core moved to its own package, andStackExchange.Redisand the resilience handler are gone (#136)- The client factory resolves the login's
OAuthClientwhen a client first needs it — resolving the factory before the server starts no longer builds the loopback client (#136) - The hosted login's
HttpClientcomes fromIHttpClientFactory— the named clientAtProtoOAuthExtensions.HttpClientName, under the identity fetch policy, so logging and telemetry handlers see its requests (#136) - Per-user clients take
UserAgentandRateLimitfromAddAtProto(o => …)— they were created with default options (#136) atproto-lexgen: thenulltype and spacecollections—csharpreports anulldefinition as removed from the language, and leaves non-NSID space collections out with a warning (#130)atproto-lexgen lexiconanddiff --assemblyload the assembly in its own context — bound to its own dependencies, anddiffreports unreadable files instead of skipping them (#130)- Space credentials and client attestation keys are cached — a repo host verifies a credential's signature once until it expires, and attestation keys are cached per
client_idwith rate-limited refetches (#133) SpaceCredentialProvidermints one credential per space at a time — concurrent callers share one mint, and replaced credentials release their keys (#133)- The EF Core space stores update in one statement, and the replay stores sweep in the background (#133)
SpaceSyncerrecovery buffers the repo once, and the space verifiers are activated by the container — so a registeredTimeProviderreaches them (#133)- Integration tests use one enum-driven
[RequiresFact]and provision their own accounts —ATPROTO_REQUIRE_INTEGRATION=1covers every requirement, and CI runs the PDS suites on every pull request (#137) - Every await in the libraries calls
ConfigureAwait(false)— enforced by CA2007, which matters for hosts with a synchronization context (#139) - The documentation's C# samples are compiled —
tests/ATProtoNet.DocSnippetsfails the build at the Markdown line of a sample that drifts from the API (#138) CidComputation.DecodeCidStringandTryDecodeCidStringaccept only canonical base32 — asCid.Parsedoes (#180)- One loop reads every stream — Jetstream and the Tap channel reconnect through the shared loop and send the SDK's
User-Agent, and an abnormal close fails with anEventStreamExceptioncarrying its reason (#180) - Bodies from untrusted senders go through one bounded reader — the Tap webhook, record proofs, repository downloads and space recovery (#180)
- Signature checks against a DID document share one refetch-once path — and
SpaceCommitVerifier.Verifyreturnsfalsefor a curve the platform lacks (#180) - Every bounded cache evicts the least recently used entry (#180)
- Faster JSON models, XRPC calls, Jetstream parsing and CAR reading — identifiers and datetimes are validated from UTF-8 without regular expressions, and their parts are built on first use; what is accepted and produced is unchanged (#190)
Fixed
- Rate-limit back-off follows the client's clock — 429 retry delays ran on the system clock rather than the clock the client's session refresh uses (#187)
- A scoped
IJtiReplayStoreno longer stops the host under scope validation — the startup warning reads the registrations instead of resolving the store (#180) atproto-lexgen --versionreports the tool's real version — instead of a hard-coded1.0.0(#110)- The managing-app check follows the Lexicon —
checkUserAccesssendsuserandaccess(it sentdid, so everyManagingAppPolicyspace refused everyone) and addresses the app's full service identifier (#113) notifyWriteadmitted any writer — the authority now evaluates the write policy, and answers 403 to a refused writer, which it neither records nor forwards (#113)- Write notifications reach their subscribers — an accepted
notifyWriteis forwarded to the services registered withregisterNotify, each addressed by the identifier it registered (#113) - Write notifications to an authority on an ordinary PDS were dropped —
#atproto_space_hostfalls back to#atproto_pds, and inbound notifications accept every way the authority is addressed (#113) - Malformed
simplespaceandnotifyWriteinput is refused up front — arevthat is not a TID, a missinghash, or anullpolicy (#113) docs/testing-spaces.mdclaimed there is no spaces container image — the alpha isghcr.io/bluesky-social/atproto:pds-spaces-alpha, pinned by digest on the page (#113)- MST root CIDs now match every other implementation — null links, empty subtrees and incremental edits were encoded differently, so repos written with
MerkleSearchTreewere rejected elsewhere; roots are pinned against the reference vectors (#111) MerkleSearchTree.Deserializereads real repositories — thenulllinks other implementations write threw (#111)- DAG-CBOR sorts non-ASCII keys by their UTF-8 bytes — keys of equal length were compared in UTF-16 order, which differs for characters above U+FFFF (#111)
SpaceRepoCar.Verifyreports a malformed index block asSpaceRepoVerificationException(#111)Tid.Next()is monotonic and unique — values minted in the same millisecond came out of order and could collide,RecordKey.NewTid()included (#114)- Identifier types accepted invalid values — every line of the interop syntax fixtures now parses or fails as specified, and an invalid identifier in JSON fails as a
JsonExceptionwith its path (#114) - One unknown embed or union variant no longer fails a whole page — a single gallery post made timeline, feed, thread and search calls throw for the entire page (#115)
- Signed labels deserialize —
Label.Sigdid not read the{"$bytes": …}form (#115) UpdateProfileAsyncno longer deletes profile data — it rebuilt the record from four fields; it now edits the stored record and writes withswapRecord(#115)- Unknown record fields survive a read-modify-write (#115)
- Registered union variants take effect —
AtProtoJsonDefaults.Optionsconsults the registry (#115) $bytesis unpadded base64, as the data model specifies — Jetstreamsyncevents and archive rows accepted only padded input, andDagCborDecoderwrote padding (#115, #126)- Malformed keys fail consistently — a
did:keyor multikey that is not a compressed point throwsFormatException, and an undefinedKeyCurvethrows rather than meaning K-256 (#112) - DPoP proofs are ES256 and carry no userinfo — a K-256 key is refused rather than labelled ES256, and the generator and validator normalize
htuthe same way (#118) - Changing the service URL or re-applying a session on a used
HttpClientworks — both wroteBaseAddress, which .NET forbids after the first request (#117) User-Agentno longer grows on a sharedHttpClient— it is set per request (#117)- Query parameters and stamped timestamps are culture-invariant — dates go out as ISO 8601 UTC and enums by their JSON names, and
createdAtno longer follows the current culture's calendar (#117) - Uploads send from the stream's current position — instead of rewinding it, and a non-seekable stream that needs a retry fails clearly (#117)
RecordCollection<T>.ExistsAsyncanswersfalseonly forRecordNotFound(#117)- A failed blob or repository download no longer leaks its HTTP response (#117)
- XRPC query parameters bind by their declared type — a single value binds to a collection, and identifier parameters are validated by their parsers (#121)
- Every XRPC endpoint failure answers with the XRPC error envelope —
500 InternalServerErrorwithout the exception's message,413 PayloadTooLarge, andInvalidRequestfor a wrongContent-Type(#121) - Enumerators stop on a repeated cursor — a server repeating its cursor drove an endless loop in the record, Spaces and Jetstream archive enumerators (#119)
- Properties under names the Lexicons never used were always empty —
canConvo,associatedChat,blobCidsand thereadOnlyfilter (#123) - Calls that did not match their Lexicons — the Ozone review queue called a nonexistent method,
SearchAccountsAsyncPOSTed a query, andSearchPostsAsyncandListNotificationsAsyncsent parameters the service refuses (#123) - Two procedures sent no body —
DeactivateAccountAsyncandUpdateAllReadAsyncnow send their JSON input (#123) - Five chat methods and the chat export read the wrong response shape (#123)
- Seven permission-set constants named sets nobody published (#123)
- Sign-out revokes the session —
LogoutAsyncsentdeleteSessionthe access JWT, which the Lexicon refuses, and never revoked an OAuth session (#124) - A failed OAuth refresh is no longer stored as a token — an error response became an empty access token (#124)
ResumeSessionAsyncrefreshes an expired session on a fresh client (#124)- Installing a session replaces all of the previous one — a password login after an OAuth session kept refreshing through OAuth, and raw
ServerClientcalls changed the transport's session (#124) - The client refreshes after
ExpiredTokenor a DPoPinvalid_tokenchallenge — and resends once, only as the same account (#124) - Concurrent refreshes share one exchange — single-use OAuth refresh tokens were spent more than once (#124)
- App-password sessions refresh by the token's
exp— not on a fixed timer (#124) Disposeno longer blocks waiting for a timer refresh (#124)- A permanent reporter mute no longer fails a page of Ozone events (#131)
- DID resolution failures stay within the Spaces error contracts — they escaped as
PlcException,JsonExceptionorHttpRequestException, and one failing subscriber could stopdeleteSpace(#120) - DID documents that DID Core allows parse — an
@contextgiven as a string, and a structuredserviceEndpoint(#120) - Collection-filtered firehose consumers no longer replay history after a restart — every event moves the cursor, and the final cursor is saved when enumeration ends (#126)
- A Jetstream frame with a non-numeric
time_usorseqis skipped — instead of throwing out of the parser (#126) - Malformed
exp,iatorjtianswer 401, not 500 — tokens and DPoP proofs with out-of-range times or unusablejtis are refused as malformed (#125) - The OAuth callback completes after a restart or on another instance — it no longer needs the process that started the login (#132)
- Stored OAuth sessions refresh after a restart — the client factory gets the login's
OAuthClientfrom the options alone (#136) FeedView,PostCard,ProfileCardandComposePostrender — they injected a client nobody registered;FeedViewreloads only when needed, andComposePostcounts graphemes (#136)- OAuth requests dispose their responses and read errors correctly — a pushed request is retried only on
use_dpop_nonce, and a non-JSON error is anOAuthException(#132) - The 0.4.0 notes claimed "Sync v1.1 support" — they now say "Sync v1.1 message fields"; this release adds the verification (#134)
- Indefinite-length CBOR is refused in CAR headers, commits and MST nodes (#134)
SpaceSyncerverifies commits against the author's#atprotokey — not a#atproto_spacekey (#133)- Space type declarations refuse a wildcard collection clearly — as proposal 0016 requires (#133)
services.AddAtProto()registers one configuredAtProtoClient— it handed out a new client forbsky.socialon every resolution (#136)- The PDS health check reports its registered failure status, and registering it twice no longer breaks
/health(#136)
Removed
samples/BlazorOAuthSample— folded intosamples/ServerIntegrationSample, which runs the same OAuth login and adds server-side access (#110)RedisSpaceReplayStoreandAddAtProtoRedisSpaceReplayStore(), and with them theStackExchange.Redisdependency —EfCoreJtiReplayStore<T>remains the shared store. Migration: copy the shortRedisJtiReplayStorefromdocs/spaces.mdand register it as theIJtiReplayStore(#136)Microsoft.Extensions.Http.Resilienceand the Aspire client's resilience handler — a retrying handler below the SDK resendsPOSTs and spent DPoP proofs, and the SDK retries 429 itself. Migration:RemoveAllResilienceHandlers()on the SDK's clients under Aspire service defaults (#136)
Security
- The GitHub ↔ Forgejo sync workflows no longer run event text as shell — issue, PR and comment fields reached
run:scripts through${{ }}, so anyone on the public mirror could run commands with the sync tokens; they now pass throughenv:. RotateFORGEJO_TOKENandGH_MIRROR_TOKEN(#108) - Mirrored pull requests can no longer reach repository secrets —
sync-prs.ymlfetches a PR into a bare repository instead of checking it out, and refuses to mirror a PR that changes.forgejo/or.gitea/(#108) - Crafted CAR and DAG-CBOR input no longer crashes the process — a deeply nested CAR header overflowed the stack; nesting is capped and parsing uses
CborReader(#111) - MST prefix lengths are range-checked — a crafted node drove a huge allocation before any check (#111)
CarReadervalidates every length and CID it reads — and rejects CIDv0 blocks, which bypassed the codec check (#111)MerkleSearchTree.Deserializerejects malformed trees — invalid or out-of-order keys, shared subtrees and empty inner nodes (#111)- JWT and JWK base64url decoding is strict — only the URL-safe alphabet, correct padding and zero trailing bits, so a token has one spelling (#118)
- XRPC error bodies are read up to a cap — a service could answer an error with a body of any size (#117)
- One SSRF policy for every identity fetch —
did:web, PLC, handle and DNS-over-HTTPS requests are HTTPS-only, refuse private addresses after DNS, follow no redirects, cap bodies and time out;IdentityResolverOptions.AllowPrivateNetworksopts out (#120) - OAuth and the space server's outbound requests no longer reach private addresses — metadata, pushed-authorization, token and revocation requests follow the fetch policy, as does the space server's client, and a URL breaking its rules is refused before sending (#120, #132)
- Single-use space tokens could be replayed just after expiry — every verifier now keeps the
jtiuntil the token stops being accepted (#125) - Identifiers no longer parse with a trailing line break —
$-anchored patterns accepteddid:plc:abcfollowed by a line feed (#125) - A
did:webdocument must name exactly the DID it was fetched for — a case-insensitive comparison let one signer present several identities (#125) - OAuth refresh and code-exchange responses are checked against a freshly resolved issuer — and the expected DID, the
atprotoscope and DPoP binding; tokens failing a check are revoked (#132) - Authorization server metadata is held to the AT Protocol profile — exact issuer, HTTPS endpoints, required PAR and
iss, and a PDS and authorization server that name each other (#132) - Anyone could block every OAuth login — pending logins are limited per requester, with a total bound that displaces rather than refuses (#132)
- Reading an authorization server's response is bounded —
OAuthOptions.RequestTimeoutcovers the whole exchange, and connection failures areOAuthExceptions (#132) - A login could be completed by anyone holding the callback URL —
/atproto/loginbinds the pending authorization to an HttpOnly cookie that the callback requires, andreturnUrlmust be a local path (#132) - A failed login no longer puts exception messages in the login URL — the redirect carries an error code. Migration: a login page of your own reads
erroras a code (#136) - A service auth caller could drive an account's stored OAuth session — the client factory and sign-out now honour only the OAuth login's identity (#136)
- The OAuth callback re-checks the stored login state — a return URL that is not local is dropped, and a malformed origin fails the login (#136)
PostCardlinks onlyhttpandhttpsURLs — ajavascript:URL in an external embed ran script in the application's origin (#136)- An unredeemed relayed login is revoked — tokens waiting on a loopback relay code are revoked when it expires or is evicted (#136)
- The hosted login's own
HttpClientnever uses a proxy — so the address the fetch policy checks is the real target; behind an egress proxy, configure the named clientAtProtoOAuthExtensions.HttpClientName(#132) - Malformed
#atproto_spaceand#atproto_space_hostentries are errors, not fallbacks — per proposal 0016, instead of silently using#atproto/#atproto_pds(#133) - Space tokens must name their key, and only that key is tried — Migration: an authority signing with a dedicated key sets
SpaceServerOptions.CredentialKeyId = "#atproto_space"(#133) - Space DPoP proofs are
ES256only, and the exchange's proof carries noath— as proposal 0016 and the reference require (#133) SpaceRepoCar.Verifychecks what the reference checks — exact index keys, DAG-CBOR links, and strict DAG-CBOR record blocks, so each value has one byte form (#133)SpaceSyncer.RecoverAsyncbounds a repo download —SpaceSyncer.MaxRepoSizerefuses a larger repository before anything is replaced (#133)- A signed commit with a malformed link no longer ends a Sync 1.1 consumer — it is an invalid event (#180)
Downloads
-
Source code (ZIP)
0 downloads
-
Source code (TAR.GZ)
0 downloads