@atproto/api 0.0.8 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -6
- package/dist/agent.d.ts +22 -0
- package/dist/client/index.d.ts +63 -61
- package/dist/client/lexicons.d.ts +237 -23
- package/dist/client/types/app/bsky/actor/getProfiles.d.ts +19 -0
- package/dist/client/types/app/bsky/actor/getSuggestions.d.ts +0 -7
- package/dist/client/types/app/bsky/actor/profile.d.ts +25 -0
- package/dist/client/types/com/atproto/admin/blob.d.ts +37 -0
- package/dist/client/types/com/atproto/admin/moderationAction.d.ts +13 -2
- package/dist/client/types/com/atproto/admin/record.d.ts +5 -2
- package/dist/client/types/com/atproto/admin/repo.d.ts +2 -2
- package/dist/client/types/com/atproto/admin/takeModerationAction.d.ts +5 -1
- package/dist/client/types/com/atproto/sync/getRepo.d.ts +2 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +623 -253
- package/dist/index.js.map +4 -4
- package/dist/types.d.ts +33 -0
- package/package.json +1 -1
- package/src/agent.ts +305 -0
- package/src/client/index.ts +75 -63
- package/src/client/lexicons.ts +281 -38
- package/src/client/types/app/bsky/actor/getProfiles.ts +35 -0
- package/src/client/types/app/bsky/actor/getSuggestions.ts +0 -18
- package/src/client/types/app/bsky/actor/profile.ts +45 -0
- package/src/client/types/com/atproto/admin/blob.ts +84 -0
- package/src/client/types/com/atproto/admin/moderationAction.ts +29 -10
- package/src/client/types/com/atproto/admin/record.ts +5 -2
- package/src/client/types/com/atproto/admin/repo.ts +2 -2
- package/src/client/types/com/atproto/admin/takeModerationAction.ts +8 -0
- package/src/client/types/com/atproto/sync/getRepo.ts +4 -2
- package/src/index.ts +3 -3
- package/src/types.ts +71 -0
- package/tests/_util.ts +26 -0
- package/tests/agent.test.ts +391 -0
- package/tests/errors.test.ts +4 -8
- package/tsconfig.build.tsbuildinfo +1 -1
- package/src/session.ts +0 -194
- package/tests/session.test.ts +0 -239
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export declare type AtpSessionEvent = 'create' | 'create-failed' | 'update' | 'expired';
|
|
2
|
+
export interface AtpSessionData {
|
|
3
|
+
refreshJwt: string;
|
|
4
|
+
accessJwt: string;
|
|
5
|
+
handle: string;
|
|
6
|
+
did: string;
|
|
7
|
+
}
|
|
8
|
+
export declare type AtpPersistSessionHandler = (evt: AtpSessionEvent, session: AtpSessionData | undefined) => void | Promise<void>;
|
|
9
|
+
export interface AtpAgentOpts {
|
|
10
|
+
service: string | URL;
|
|
11
|
+
persistSession?: AtpPersistSessionHandler;
|
|
12
|
+
}
|
|
13
|
+
export interface AtpAgentCreateAccountOpts {
|
|
14
|
+
email: string;
|
|
15
|
+
password: string;
|
|
16
|
+
handle: string;
|
|
17
|
+
inviteCode?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface AtpAgentLoginOpts {
|
|
20
|
+
identifier: string;
|
|
21
|
+
password: string;
|
|
22
|
+
}
|
|
23
|
+
declare type AtpAgentFetchHeaders = Record<string, string>;
|
|
24
|
+
export interface AtpAgentFetchHandlerResponse {
|
|
25
|
+
status: number;
|
|
26
|
+
headers: Record<string, string>;
|
|
27
|
+
body: any;
|
|
28
|
+
}
|
|
29
|
+
export declare type AptAgentFetchHandler = (httpUri: string, httpMethod: string, httpHeaders: AtpAgentFetchHeaders, httpReqBody: any) => Promise<AtpAgentFetchHandlerResponse>;
|
|
30
|
+
export interface AtpAgentGlobalOpts {
|
|
31
|
+
fetch: AptAgentFetchHandler;
|
|
32
|
+
}
|
|
33
|
+
export {};
|
package/package.json
CHANGED
package/src/agent.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { ErrorResponseBody, errorResponseBody } from '@atproto/xrpc'
|
|
2
|
+
import { defaultFetchHandler } from '@atproto/xrpc'
|
|
3
|
+
import {
|
|
4
|
+
AtpBaseClient,
|
|
5
|
+
AtpServiceClient,
|
|
6
|
+
ComAtprotoAccountCreate,
|
|
7
|
+
ComAtprotoSessionCreate,
|
|
8
|
+
ComAtprotoSessionGet,
|
|
9
|
+
ComAtprotoSessionRefresh,
|
|
10
|
+
} from './client'
|
|
11
|
+
import {
|
|
12
|
+
AtpSessionData,
|
|
13
|
+
AtpAgentCreateAccountOpts,
|
|
14
|
+
AtpAgentLoginOpts,
|
|
15
|
+
AptAgentFetchHandler,
|
|
16
|
+
AtpAgentFetchHandlerResponse,
|
|
17
|
+
AtpAgentGlobalOpts,
|
|
18
|
+
AtpPersistSessionHandler,
|
|
19
|
+
AtpAgentOpts,
|
|
20
|
+
} from './types'
|
|
21
|
+
|
|
22
|
+
const REFRESH_SESSION = 'com.atproto.session.refresh'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* An ATP "Agent"
|
|
26
|
+
* Manages session token lifecycles and provides convenience methods.
|
|
27
|
+
*/
|
|
28
|
+
export class AtpAgent {
|
|
29
|
+
service: URL
|
|
30
|
+
api: AtpServiceClient
|
|
31
|
+
session?: AtpSessionData
|
|
32
|
+
|
|
33
|
+
private _baseClient: AtpBaseClient
|
|
34
|
+
private _persistSession?: AtpPersistSessionHandler
|
|
35
|
+
private _refreshSessionPromise: Promise<void> | undefined
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The `fetch` implementation; must be implemented for your platform.
|
|
39
|
+
*/
|
|
40
|
+
static fetch: AptAgentFetchHandler | undefined = defaultFetchHandler
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Configures the API globally.
|
|
44
|
+
*/
|
|
45
|
+
static configure(opts: AtpAgentGlobalOpts) {
|
|
46
|
+
AtpAgent.fetch = opts.fetch
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
constructor(opts: AtpAgentOpts) {
|
|
50
|
+
this.service =
|
|
51
|
+
opts.service instanceof URL ? opts.service : new URL(opts.service)
|
|
52
|
+
this._persistSession = opts.persistSession
|
|
53
|
+
|
|
54
|
+
// create an ATP client instance for this agent
|
|
55
|
+
this._baseClient = new AtpBaseClient()
|
|
56
|
+
this._baseClient.xrpc.fetch = this._fetch.bind(this) // patch its fetch implementation
|
|
57
|
+
this.api = this._baseClient.service(opts.service)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Is there any active session?
|
|
62
|
+
*/
|
|
63
|
+
get hasSession() {
|
|
64
|
+
return !!this.session
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Sets the "Persist Session" method which can be used to store access tokens
|
|
69
|
+
* as they change.
|
|
70
|
+
*/
|
|
71
|
+
setPersistSessionHandler(handler?: AtpPersistSessionHandler) {
|
|
72
|
+
this._persistSession = handler
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a new account and hydrate its session in this agent.
|
|
77
|
+
*/
|
|
78
|
+
async createAccount(
|
|
79
|
+
opts: AtpAgentCreateAccountOpts,
|
|
80
|
+
): Promise<ComAtprotoAccountCreate.Response> {
|
|
81
|
+
try {
|
|
82
|
+
const res = await this.api.com.atproto.account.create({
|
|
83
|
+
handle: opts.handle,
|
|
84
|
+
password: opts.password,
|
|
85
|
+
email: opts.email,
|
|
86
|
+
inviteCode: opts.inviteCode,
|
|
87
|
+
})
|
|
88
|
+
this.session = {
|
|
89
|
+
accessJwt: res.data.accessJwt,
|
|
90
|
+
refreshJwt: res.data.refreshJwt,
|
|
91
|
+
handle: res.data.handle,
|
|
92
|
+
did: res.data.did,
|
|
93
|
+
}
|
|
94
|
+
return res
|
|
95
|
+
} catch (e) {
|
|
96
|
+
this.session = undefined
|
|
97
|
+
throw e
|
|
98
|
+
} finally {
|
|
99
|
+
if (this.session) {
|
|
100
|
+
this._persistSession?.('create', this.session)
|
|
101
|
+
} else {
|
|
102
|
+
this._persistSession?.('create-failed', undefined)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Start a new session with this agent.
|
|
109
|
+
*/
|
|
110
|
+
async login(
|
|
111
|
+
opts: AtpAgentLoginOpts,
|
|
112
|
+
): Promise<ComAtprotoSessionCreate.Response> {
|
|
113
|
+
try {
|
|
114
|
+
const res = await this.api.com.atproto.session.create({
|
|
115
|
+
identifier: opts.identifier,
|
|
116
|
+
password: opts.password,
|
|
117
|
+
})
|
|
118
|
+
this.session = {
|
|
119
|
+
accessJwt: res.data.accessJwt,
|
|
120
|
+
refreshJwt: res.data.refreshJwt,
|
|
121
|
+
handle: res.data.handle,
|
|
122
|
+
did: res.data.did,
|
|
123
|
+
}
|
|
124
|
+
return res
|
|
125
|
+
} catch (e) {
|
|
126
|
+
this.session = undefined
|
|
127
|
+
throw e
|
|
128
|
+
} finally {
|
|
129
|
+
if (this.session) {
|
|
130
|
+
this._persistSession?.('create', this.session)
|
|
131
|
+
} else {
|
|
132
|
+
this._persistSession?.('create-failed', undefined)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resume a pre-existing session with this agent.
|
|
139
|
+
*/
|
|
140
|
+
async resumeSession(
|
|
141
|
+
session: AtpSessionData,
|
|
142
|
+
): Promise<ComAtprotoSessionGet.Response> {
|
|
143
|
+
try {
|
|
144
|
+
this.session = session
|
|
145
|
+
const res = await this.api.com.atproto.session.get()
|
|
146
|
+
if (!res.success || res.data.did !== this.session.did) {
|
|
147
|
+
throw new Error('Invalid session')
|
|
148
|
+
}
|
|
149
|
+
return res
|
|
150
|
+
} catch (e) {
|
|
151
|
+
this.session = undefined
|
|
152
|
+
throw e
|
|
153
|
+
} finally {
|
|
154
|
+
if (this.session) {
|
|
155
|
+
this._persistSession?.('create', this.session)
|
|
156
|
+
} else {
|
|
157
|
+
this._persistSession?.('create-failed', undefined)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Internal helper to add authorization headers to requests.
|
|
164
|
+
*/
|
|
165
|
+
private _addAuthHeader(reqHeaders: Record<string, string>) {
|
|
166
|
+
if (!reqHeaders.authorization && this.session?.accessJwt) {
|
|
167
|
+
return {
|
|
168
|
+
...reqHeaders,
|
|
169
|
+
authorization: `Bearer ${this.session.accessJwt}`,
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return reqHeaders
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Internal fetch handler which adds access-token management
|
|
177
|
+
*/
|
|
178
|
+
private async _fetch(
|
|
179
|
+
reqUri: string,
|
|
180
|
+
reqMethod: string,
|
|
181
|
+
reqHeaders: Record<string, string>,
|
|
182
|
+
reqBody: any,
|
|
183
|
+
): Promise<AtpAgentFetchHandlerResponse> {
|
|
184
|
+
if (!AtpAgent.fetch) {
|
|
185
|
+
throw new Error('AtpAgent fetch() method not configured')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// wait for any active session-refreshes to finish
|
|
189
|
+
await this._refreshSessionPromise
|
|
190
|
+
|
|
191
|
+
// send the request
|
|
192
|
+
let res = await AtpAgent.fetch(
|
|
193
|
+
reqUri,
|
|
194
|
+
reqMethod,
|
|
195
|
+
this._addAuthHeader(reqHeaders),
|
|
196
|
+
reqBody,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
// handle session-refreshes as needed
|
|
200
|
+
if (isErrorResponse(res, ['ExpiredToken']) && this.session?.refreshJwt) {
|
|
201
|
+
// attempt refresh
|
|
202
|
+
await this._refreshSession()
|
|
203
|
+
|
|
204
|
+
// resend the request with the new access token
|
|
205
|
+
res = await AtpAgent.fetch(
|
|
206
|
+
reqUri,
|
|
207
|
+
reqMethod,
|
|
208
|
+
this._addAuthHeader(reqHeaders),
|
|
209
|
+
reqBody,
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return res
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Internal helper to refresh sessions
|
|
218
|
+
* - Wraps the actual implementation in a promise-guard to ensure only
|
|
219
|
+
* one refresh is attempted at a time.
|
|
220
|
+
*/
|
|
221
|
+
private async _refreshSession() {
|
|
222
|
+
if (this._refreshSessionPromise) {
|
|
223
|
+
return this._refreshSessionPromise
|
|
224
|
+
}
|
|
225
|
+
this._refreshSessionPromise = this._refreshSessionInner()
|
|
226
|
+
try {
|
|
227
|
+
await this._refreshSessionPromise
|
|
228
|
+
} finally {
|
|
229
|
+
this._refreshSessionPromise = undefined
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Internal helper to refresh sessions (actual behavior)
|
|
235
|
+
*/
|
|
236
|
+
private async _refreshSessionInner() {
|
|
237
|
+
if (!AtpAgent.fetch) {
|
|
238
|
+
throw new Error('AtpAgent fetch() method not configured')
|
|
239
|
+
}
|
|
240
|
+
if (!this.session?.refreshJwt) {
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// send the refresh request
|
|
245
|
+
const url = new URL(this.service.origin)
|
|
246
|
+
url.pathname = `/xrpc/${REFRESH_SESSION}`
|
|
247
|
+
const res = await AtpAgent.fetch(
|
|
248
|
+
url.toString(),
|
|
249
|
+
'POST',
|
|
250
|
+
{
|
|
251
|
+
authorization: `Bearer ${this.session.refreshJwt}`,
|
|
252
|
+
},
|
|
253
|
+
undefined,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if (isErrorResponse(res, ['ExpiredToken', 'InvalidToken'])) {
|
|
257
|
+
// failed due to a bad refresh token
|
|
258
|
+
this.session = undefined
|
|
259
|
+
this._persistSession?.('expired', undefined)
|
|
260
|
+
} else if (isNewSessionObject(this._baseClient, res.body)) {
|
|
261
|
+
// succeeded, update the session
|
|
262
|
+
this.session = {
|
|
263
|
+
accessJwt: res.body.accessJwt,
|
|
264
|
+
refreshJwt: res.body.refreshJwt,
|
|
265
|
+
handle: res.body.handle,
|
|
266
|
+
did: res.body.did,
|
|
267
|
+
}
|
|
268
|
+
this._persistSession?.('update', this.session)
|
|
269
|
+
}
|
|
270
|
+
// else: other failures should be ignored - the issue will
|
|
271
|
+
// propagate in the _fetch() handler's second attempt to run
|
|
272
|
+
// the request
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isErrorObject(v: unknown): v is ErrorResponseBody {
|
|
277
|
+
return errorResponseBody.safeParse(v).success
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function isErrorResponse(
|
|
281
|
+
res: AtpAgentFetchHandlerResponse,
|
|
282
|
+
errorNames: string[],
|
|
283
|
+
): boolean {
|
|
284
|
+
if (res.status !== 400) {
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
if (!isErrorObject(res.body)) {
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
return (
|
|
291
|
+
typeof res.body.error === 'string' && errorNames.includes(res.body.error)
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isNewSessionObject(
|
|
296
|
+
client: AtpBaseClient,
|
|
297
|
+
v: unknown,
|
|
298
|
+
): v is ComAtprotoSessionRefresh.OutputSchema {
|
|
299
|
+
try {
|
|
300
|
+
client.xrpc.lex.assertValidXrpcOutput('com.atproto.session.refresh', v)
|
|
301
|
+
return true
|
|
302
|
+
} catch {
|
|
303
|
+
return false
|
|
304
|
+
}
|
|
305
|
+
}
|