@atcute/client 3.0.1 → 4.0.0
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 +72 -13
- package/dist/client.d.ts +166 -0
- package/dist/client.js +192 -0
- package/dist/client.js.map +1 -0
- package/dist/credential-manager.d.ts +52 -43
- package/dist/credential-manager.js +51 -39
- package/dist/credential-manager.js.map +1 -1
- package/dist/fetch-handler.d.ts +2 -2
- package/dist/fetch-handler.js +1 -1
- package/dist/fetch-handler.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/lib/client.ts +461 -0
- package/lib/credential-manager.ts +129 -96
- package/lib/env.d.ts +1 -0
- package/lib/fetch-handler.ts +3 -3
- package/lib/index.ts +1 -1
- package/package.json +12 -11
- package/dist/lexicons.d.ts +0 -2199
- package/dist/lexicons.js +0 -4
- package/dist/lexicons.js.map +0 -1
- package/dist/rpc.d.ts +0 -96
- package/dist/rpc.js +0 -141
- package/dist/rpc.js.map +0 -1
- package/dist/utils/did.d.ts +0 -38
- package/dist/utils/did.js +0 -44
- package/dist/utils/did.js.map +0 -1
- package/dist/utils/http.d.ts +0 -7
- package/dist/utils/http.js +0 -20
- package/dist/utils/http.js.map +0 -1
- package/lib/lexicons.ts +0 -2204
- package/lib/rpc.ts +0 -262
- package/lib/utils/did.ts +0 -73
- package/lib/utils/http.ts +0 -27
|
@@ -1,65 +1,74 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { getPdsEndpoint, type DidDocument } from '@atcute/identity';
|
|
2
|
+
import type { Did } from '@atcute/lexicons';
|
|
3
|
+
import type { InferXRPCBodyOutput } from '@atcute/lexicons/validations';
|
|
2
4
|
|
|
5
|
+
import type { ComAtprotoServerCreateSession } from '@atcute/atproto';
|
|
6
|
+
|
|
7
|
+
import { Client, ClientResponseError, isXRPCErrorPayload, ok } from './client.js';
|
|
3
8
|
import { simpleFetchHandler, type FetchHandlerObject } from './fetch-handler.js';
|
|
4
|
-
import { XRPC, XRPCError } from './rpc.js';
|
|
5
9
|
|
|
6
|
-
import { getPdsEndpoint, type DidDocument } from './utils/did.js';
|
|
7
10
|
import { decodeJwt } from './utils/jwt.js';
|
|
8
11
|
|
|
9
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* represents the decoded access token, for convenience
|
|
14
|
+
* @deprecated
|
|
15
|
+
*/
|
|
10
16
|
export interface AtpAccessJwt {
|
|
11
|
-
/**
|
|
17
|
+
/** access token scope */
|
|
12
18
|
scope:
|
|
13
19
|
| 'com.atproto.access'
|
|
14
20
|
| 'com.atproto.appPass'
|
|
15
21
|
| 'com.atproto.appPassPrivileged'
|
|
16
22
|
| 'com.atproto.signupQueued'
|
|
17
23
|
| 'com.atproto.takendown';
|
|
18
|
-
/**
|
|
19
|
-
sub:
|
|
20
|
-
/**
|
|
24
|
+
/** account DID */
|
|
25
|
+
sub: Did;
|
|
26
|
+
/** expiration time in Unix seconds */
|
|
21
27
|
exp: number;
|
|
22
|
-
/**
|
|
28
|
+
/** token issued time in Unix seconds */
|
|
23
29
|
iat: number;
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* represents the the decoded refresh token, for convenience
|
|
34
|
+
* @deprecated
|
|
35
|
+
*/
|
|
27
36
|
export interface AtpRefreshJwt {
|
|
28
|
-
/**
|
|
37
|
+
/** refresh token scope */
|
|
29
38
|
scope: 'com.atproto.refresh';
|
|
30
|
-
/**
|
|
39
|
+
/** unique identifier for this session */
|
|
31
40
|
jti: string;
|
|
32
|
-
/**
|
|
33
|
-
sub:
|
|
34
|
-
/**
|
|
35
|
-
aud:
|
|
36
|
-
/**
|
|
41
|
+
/** account DID */
|
|
42
|
+
sub: Did;
|
|
43
|
+
/** intended audience of this refresh token, in DID */
|
|
44
|
+
aud: Did;
|
|
45
|
+
/** token expiration time in seconds */
|
|
37
46
|
exp: number;
|
|
38
|
-
/**
|
|
47
|
+
/** token issued time in seconds */
|
|
39
48
|
iat: number;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
/**
|
|
51
|
+
/** session data, can be persisted and reused */
|
|
43
52
|
export interface AtpSessionData {
|
|
44
|
-
/**
|
|
53
|
+
/** refresh token */
|
|
45
54
|
refreshJwt: string;
|
|
46
|
-
/**
|
|
55
|
+
/** access token */
|
|
47
56
|
accessJwt: string;
|
|
48
|
-
/**
|
|
57
|
+
/** account handle */
|
|
49
58
|
handle: string;
|
|
50
|
-
/**
|
|
51
|
-
did:
|
|
59
|
+
/** account DID */
|
|
60
|
+
did: Did;
|
|
52
61
|
/** PDS endpoint found in the DID document, this will be used as the service URI if provided */
|
|
53
62
|
pdsUri?: string;
|
|
54
|
-
/**
|
|
63
|
+
/** email address of the account, might not be available if on app password */
|
|
55
64
|
email?: string;
|
|
56
|
-
/**
|
|
65
|
+
/** whether the email address has been confirmed or not */
|
|
57
66
|
emailConfirmed?: boolean;
|
|
58
|
-
/**
|
|
67
|
+
/** whether the account has email-based two-factor authentication enabled */
|
|
59
68
|
emailAuthFactor?: boolean;
|
|
60
|
-
/**
|
|
69
|
+
/** whether the account is active (not deactivated, taken down, or suspended) */
|
|
61
70
|
active: boolean;
|
|
62
|
-
/**
|
|
71
|
+
/** possible reason for why the account is inactive */
|
|
63
72
|
inactiveStatus?: string;
|
|
64
73
|
}
|
|
65
74
|
|
|
@@ -67,29 +76,36 @@ export interface CredentialManagerOptions {
|
|
|
67
76
|
/** PDS server URL */
|
|
68
77
|
service: string;
|
|
69
78
|
|
|
70
|
-
/**
|
|
71
|
-
fetch?: typeof
|
|
79
|
+
/** custom fetch function */
|
|
80
|
+
fetch?: typeof fetch;
|
|
72
81
|
|
|
73
|
-
/**
|
|
82
|
+
/** function called when the session expires and can't be refreshed */
|
|
74
83
|
onExpired?: (session: AtpSessionData) => void;
|
|
75
|
-
/**
|
|
84
|
+
/** function called after a successful session refresh */
|
|
76
85
|
onRefresh?: (session: AtpSessionData) => void;
|
|
77
|
-
/**
|
|
86
|
+
/** function called whenever the session object is updated (login, resume, refresh) */
|
|
78
87
|
onSessionUpdate?: (session: AtpSessionData) => void;
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
export class CredentialManager implements FetchHandlerObject {
|
|
91
|
+
/** service URL to make authentication requests with */
|
|
82
92
|
readonly serviceUrl: string;
|
|
93
|
+
/** fetch implementation */
|
|
83
94
|
fetch: typeof fetch;
|
|
84
95
|
|
|
85
|
-
|
|
96
|
+
/** internal client instance for making authentication requests */
|
|
97
|
+
#server: Client;
|
|
98
|
+
/** holds a promise for the current refresh operation, used for debouncing */
|
|
86
99
|
#refreshSessionPromise: Promise<void> | undefined;
|
|
87
100
|
|
|
101
|
+
/** callback for session expiration */
|
|
88
102
|
#onExpired: CredentialManagerOptions['onExpired'];
|
|
103
|
+
/** callback for successful session refresh */
|
|
89
104
|
#onRefresh: CredentialManagerOptions['onRefresh'];
|
|
105
|
+
/** callback for session updates */
|
|
90
106
|
#onSessionUpdate: CredentialManagerOptions['onSessionUpdate'];
|
|
91
107
|
|
|
92
|
-
/**
|
|
108
|
+
/** current active session, undefined if not authenticated */
|
|
93
109
|
session?: AtpSessionData;
|
|
94
110
|
|
|
95
111
|
constructor({
|
|
@@ -102,13 +118,14 @@ export class CredentialManager implements FetchHandlerObject {
|
|
|
102
118
|
this.serviceUrl = service;
|
|
103
119
|
this.fetch = _fetch;
|
|
104
120
|
|
|
105
|
-
this.#server = new
|
|
121
|
+
this.#server = new Client({ handler: simpleFetchHandler({ service, fetch: _fetch }) });
|
|
106
122
|
|
|
107
123
|
this.#onRefresh = onRefresh;
|
|
108
124
|
this.#onExpired = onExpired;
|
|
109
125
|
this.#onSessionUpdate = onSessionUpdate;
|
|
110
126
|
}
|
|
111
127
|
|
|
128
|
+
/** service URL to make actual API requests with */
|
|
112
129
|
get dispatchUrl() {
|
|
113
130
|
return this.session?.pdsUri ?? this.serviceUrl;
|
|
114
131
|
}
|
|
@@ -138,13 +155,14 @@ export class CredentialManager implements FetchHandlerObject {
|
|
|
138
155
|
return initialResponse;
|
|
139
156
|
}
|
|
140
157
|
|
|
141
|
-
//
|
|
142
|
-
// - refreshSession
|
|
143
|
-
// -
|
|
158
|
+
// return initial response if:
|
|
159
|
+
// - the above refreshSession failed and cleared the session
|
|
160
|
+
// - provided request body was a stream, which can't be resent once consumed
|
|
144
161
|
if (!this.session || init.body instanceof ReadableStream) {
|
|
145
162
|
return initialResponse;
|
|
146
163
|
}
|
|
147
164
|
|
|
165
|
+
// set the new token and retry the request
|
|
148
166
|
headers.set('authorization', `Bearer ${this.session.accessJwt}`);
|
|
149
167
|
|
|
150
168
|
return await (0, this.fetch)(url, { ...init, headers });
|
|
@@ -158,33 +176,36 @@ export class CredentialManager implements FetchHandlerObject {
|
|
|
158
176
|
|
|
159
177
|
async #refreshSessionInner(): Promise<void> {
|
|
160
178
|
const currentSession = this.session;
|
|
161
|
-
|
|
162
179
|
if (!currentSession) {
|
|
163
180
|
return;
|
|
164
181
|
}
|
|
165
182
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
this.#updateSession({ ...currentSession, ...data });
|
|
174
|
-
this.#onRefresh?.(this.session!);
|
|
175
|
-
} catch (err) {
|
|
176
|
-
if (err instanceof XRPCError) {
|
|
177
|
-
const kind = err.kind;
|
|
183
|
+
const response = await this.#server.post('com.atproto.server.refreshSession', {
|
|
184
|
+
headers: {
|
|
185
|
+
authorization: `Bearer ${currentSession.refreshJwt}`,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
178
188
|
|
|
179
|
-
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
switch (response.data.error) {
|
|
191
|
+
case 'ExpiredToken':
|
|
192
|
+
case 'InvalidToken': {
|
|
180
193
|
this.session = undefined;
|
|
181
194
|
this.#onExpired?.(currentSession);
|
|
195
|
+
break;
|
|
182
196
|
}
|
|
183
197
|
}
|
|
198
|
+
|
|
199
|
+
throw new ClientResponseError(response);
|
|
184
200
|
}
|
|
201
|
+
|
|
202
|
+
this.#updateSession({ ...currentSession, ...response.data });
|
|
203
|
+
this.#onRefresh?.(this.session!);
|
|
185
204
|
}
|
|
186
205
|
|
|
187
|
-
#updateSession(
|
|
206
|
+
#updateSession(
|
|
207
|
+
raw: InferXRPCBodyOutput<ComAtprotoServerCreateSession.mainSchema['output']>,
|
|
208
|
+
): AtpSessionData {
|
|
188
209
|
const didDoc = raw.didDoc as DidDocument | undefined;
|
|
189
210
|
|
|
190
211
|
let pdsUri: string | undefined;
|
|
@@ -192,7 +213,7 @@ export class CredentialManager implements FetchHandlerObject {
|
|
|
192
213
|
pdsUri = getPdsEndpoint(didDoc);
|
|
193
214
|
}
|
|
194
215
|
|
|
195
|
-
const newSession = {
|
|
216
|
+
const newSession: AtpSessionData = {
|
|
196
217
|
accessJwt: raw.accessJwt,
|
|
197
218
|
refreshJwt: raw.refreshJwt,
|
|
198
219
|
handle: raw.handle,
|
|
@@ -200,7 +221,7 @@ export class CredentialManager implements FetchHandlerObject {
|
|
|
200
221
|
pdsUri: pdsUri,
|
|
201
222
|
email: raw.email,
|
|
202
223
|
emailConfirmed: raw.emailConfirmed,
|
|
203
|
-
emailAuthFactor: raw.
|
|
224
|
+
emailAuthFactor: raw.emailAuthFactor,
|
|
204
225
|
active: raw.active ?? true,
|
|
205
226
|
inactiveStatus: raw.status,
|
|
206
227
|
};
|
|
@@ -212,16 +233,16 @@ export class CredentialManager implements FetchHandlerObject {
|
|
|
212
233
|
}
|
|
213
234
|
|
|
214
235
|
/**
|
|
215
|
-
*
|
|
216
|
-
* @param session
|
|
236
|
+
* resume from a persisted session
|
|
237
|
+
* @param session session data, taken from `AtpAuth#session` after login
|
|
217
238
|
*/
|
|
218
239
|
async resume(session: AtpSessionData): Promise<AtpSessionData> {
|
|
219
|
-
const now = Date.now() /
|
|
240
|
+
const now = Date.now() / 1_000 + 60 * 5;
|
|
220
241
|
|
|
221
242
|
const refreshToken = decodeJwt(session.refreshJwt) as AtpRefreshJwt;
|
|
222
243
|
|
|
223
244
|
if (now >= refreshToken.exp) {
|
|
224
|
-
throw new
|
|
245
|
+
throw new ClientResponseError({ status: 401, data: { error: 'InvalidToken' } });
|
|
225
246
|
}
|
|
226
247
|
|
|
227
248
|
const accessToken = decodeJwt(session.accessJwt) as AtpAccessJwt;
|
|
@@ -230,62 +251,69 @@ export class CredentialManager implements FetchHandlerObject {
|
|
|
230
251
|
if (now >= accessToken.exp) {
|
|
231
252
|
await this.#refreshSession();
|
|
232
253
|
} else {
|
|
233
|
-
const promise =
|
|
234
|
-
|
|
235
|
-
|
|
254
|
+
const promise = ok(
|
|
255
|
+
this.#server.get('com.atproto.server.getSession', {
|
|
256
|
+
headers: {
|
|
257
|
+
authorization: `Bearer ${session.accessJwt}`,
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
promise.then(
|
|
263
|
+
(next) => {
|
|
264
|
+
const existing = this.session;
|
|
265
|
+
if (!existing || existing.did !== next.did) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this.#updateSession({ ...existing, ...next });
|
|
236
270
|
},
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const next = response.data;
|
|
242
|
-
|
|
243
|
-
if (!existing) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
this.#updateSession({ ...existing, ...next });
|
|
248
|
-
});
|
|
271
|
+
(_err) => {
|
|
272
|
+
// ignore error
|
|
273
|
+
},
|
|
274
|
+
);
|
|
249
275
|
}
|
|
250
276
|
|
|
251
277
|
if (!this.session) {
|
|
252
|
-
throw new
|
|
278
|
+
throw new ClientResponseError({ status: 401, data: { error: 'InvalidToken' } });
|
|
253
279
|
}
|
|
254
280
|
|
|
255
281
|
return this.session;
|
|
256
282
|
}
|
|
257
283
|
|
|
258
284
|
/**
|
|
259
|
-
*
|
|
260
|
-
* @param options
|
|
261
|
-
* @returns
|
|
285
|
+
* sign in to an account
|
|
286
|
+
* @param options credential options
|
|
287
|
+
* @returns session data
|
|
262
288
|
*/
|
|
263
289
|
async login(options: AuthLoginOptions): Promise<AtpSessionData> {
|
|
264
290
|
// Reset the session
|
|
265
291
|
this.session = undefined;
|
|
266
292
|
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
293
|
+
const session = await ok(
|
|
294
|
+
this.#server.post('com.atproto.server.createSession', {
|
|
295
|
+
input: {
|
|
296
|
+
identifier: options.identifier,
|
|
297
|
+
password: options.password,
|
|
298
|
+
authFactorToken: options.code,
|
|
299
|
+
allowTakendown: options.allowTakendown,
|
|
300
|
+
},
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
275
303
|
|
|
276
|
-
return this.#updateSession(
|
|
304
|
+
return this.#updateSession(session);
|
|
277
305
|
}
|
|
278
306
|
}
|
|
279
307
|
|
|
280
|
-
/**
|
|
308
|
+
/** credentials */
|
|
281
309
|
export interface AuthLoginOptions {
|
|
282
|
-
/**
|
|
310
|
+
/** what account to login as, this could be domain handle, DID, or email address */
|
|
283
311
|
identifier: string;
|
|
284
|
-
/**
|
|
312
|
+
/** account password */
|
|
285
313
|
password: string;
|
|
286
|
-
/**
|
|
314
|
+
/** two-factor authentication code, if email TOTP is enabled */
|
|
287
315
|
code?: string;
|
|
288
|
-
/**
|
|
316
|
+
/** allow signing in even if the account has been taken down */
|
|
289
317
|
allowTakendown?: boolean;
|
|
290
318
|
}
|
|
291
319
|
|
|
@@ -298,6 +326,9 @@ const isExpiredTokenResponse = async (response: Response): Promise<boolean> => {
|
|
|
298
326
|
return false;
|
|
299
327
|
}
|
|
300
328
|
|
|
329
|
+
// this is nasty as it relies heavily on what the PDS returns, but avoiding
|
|
330
|
+
// cloning and reading the request as much as possible is better.
|
|
331
|
+
|
|
301
332
|
// {"error":"ExpiredToken","message":"Token has expired"}
|
|
302
333
|
// {"error":"ExpiredToken","message":"Token is expired"}
|
|
303
334
|
if (extractContentLength(response.headers) > 54 * 1.5) {
|
|
@@ -305,8 +336,10 @@ const isExpiredTokenResponse = async (response: Response): Promise<boolean> => {
|
|
|
305
336
|
}
|
|
306
337
|
|
|
307
338
|
try {
|
|
308
|
-
const
|
|
309
|
-
|
|
339
|
+
const data = await response.clone().json();
|
|
340
|
+
if (isXRPCErrorPayload(data)) {
|
|
341
|
+
return data.error === 'ExpiredToken';
|
|
342
|
+
}
|
|
310
343
|
} catch {}
|
|
311
344
|
|
|
312
345
|
return false;
|
package/lib/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import type {} from '@atcute/atproto';
|
package/lib/fetch-handler.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** fetch handler function */
|
|
2
2
|
export type FetchHandler = (pathname: string, init: RequestInit) => Promise<Response>;
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/** fetch handler in an object */
|
|
5
5
|
export interface FetchHandlerObject {
|
|
6
6
|
handle(this: FetchHandlerObject, pathname: string, init: RequestInit): Promise<Response>;
|
|
7
7
|
}
|
|
@@ -25,6 +25,6 @@ export const simpleFetchHandler = ({
|
|
|
25
25
|
}: SimpleFetchHandlerOptions): FetchHandler => {
|
|
26
26
|
return async (pathname, init) => {
|
|
27
27
|
const url = new URL(pathname, service);
|
|
28
|
-
return _fetch(url, init);
|
|
28
|
+
return await _fetch(url, init);
|
|
29
29
|
};
|
|
30
30
|
};
|
package/lib/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@atcute/client",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.0.0",
|
|
5
5
|
"description": "lightweight and cute API client for AT Protocol",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -15,22 +15,23 @@
|
|
|
15
15
|
"!lib/**/*.test.ts"
|
|
16
16
|
],
|
|
17
17
|
"exports": {
|
|
18
|
-
".": "./dist/index.js"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
22
|
-
"
|
|
18
|
+
".": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@atcute/identity": "^1.0.0",
|
|
22
|
+
"@atcute/lexicons": "^1.0.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@vitest/coverage-v8": "^3.
|
|
26
|
-
"vitest": "^3.
|
|
27
|
-
"@atcute/
|
|
28
|
-
"@atcute/
|
|
25
|
+
"@vitest/coverage-v8": "^3.1.3",
|
|
26
|
+
"vitest": "^3.1.3",
|
|
27
|
+
"@atcute/bluesky": "^3.0.0",
|
|
28
|
+
"@atcute/atproto": "^3.0.0",
|
|
29
|
+
"@atcute/internal-dev-env": "^1.0.1"
|
|
29
30
|
},
|
|
30
31
|
"scripts": {
|
|
31
32
|
"build": "tsc --project tsconfig.build.json",
|
|
33
|
+
"postbuild": "node ./scripts/ensure-files.js",
|
|
32
34
|
"test": "vitest run --coverage",
|
|
33
|
-
"generate": "./scripts/generate-lexicons.sh",
|
|
34
35
|
"prepublish": "rm -rf dist; pnpm run build"
|
|
35
36
|
}
|
|
36
37
|
}
|