@atcute/client 2.0.3 → 2.0.5
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 +12 -5
- package/dist/lexicons.d.ts +3 -0
- package/dist/rpc.d.ts +1 -1
- package/dist/rpc.js +2 -2
- package/dist/rpc.js.map +1 -1
- package/lib/credential-manager.ts +312 -0
- package/lib/fetch-handler.ts +30 -0
- package/lib/index.ts +3 -0
- package/lib/lexicons.ts +1841 -0
- package/lib/rpc.ts +262 -0
- package/lib/utils/did.ts +73 -0
- package/lib/utils/http.ts +27 -0
- package/lib/utils/jwt.ts +76 -0
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
lightweight and cute API client for AT Protocol.
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
This package only contains the base AT Protocol lexicons and endpoints, along with an authentication middleware.
|
|
9
|
-
For Bluesky-related lexicons, see `@atcute/bluesky` package.
|
|
5
|
+
- **small**, the bare minimum is ~1 kB gzipped with the full package at ~2.4 kB gzipped.
|
|
6
|
+
- **no runtime validation**, type definitions match actual HTTP responses, the server is assumed to
|
|
7
|
+
be trusted in returning valid responses.
|
|
10
8
|
|
|
11
9
|
```ts
|
|
12
10
|
import { XRPC, CredentialManager } from '@atcute/client';
|
|
@@ -28,3 +26,12 @@ const { data } = await rpc.get('com.atproto.identity.resolveHandle', {
|
|
|
28
26
|
console.log(data.did);
|
|
29
27
|
// -> did:plc:ragtjsm2j2vknwkz3zp4oxrd
|
|
30
28
|
```
|
|
29
|
+
|
|
30
|
+
by default, the API client only ships with the base AT Protocol (`com.atproto.*`) lexicons and
|
|
31
|
+
endpoints , along with a middleware for doing a (legacy) authentication with a PDS. you can extend
|
|
32
|
+
these with optional definition packages:
|
|
33
|
+
|
|
34
|
+
- [`@atcute/bluemoji`](../../definitions/bluemoji): adds `blue.moji.*` definitions
|
|
35
|
+
- [`@atcute/bluesky`](../../definitions/bluesky): adds `app.bsky.*` and `chat.bsky.*` definitions
|
|
36
|
+
- [`@atcute/ozone`](../../definitions/ozone): adds `tools.ozone.*` definitions
|
|
37
|
+
- [`@atcute/whitewind`](../../definitions/whitewind): adds `com.whtwnd.*` definitions
|
package/dist/lexicons.d.ts
CHANGED
|
@@ -627,6 +627,9 @@ export declare namespace ComAtprotoRepoGetRecord {
|
|
|
627
627
|
value: unknown;
|
|
628
628
|
cid?: At.CID;
|
|
629
629
|
}
|
|
630
|
+
interface Errors {
|
|
631
|
+
RecordNotFound: {};
|
|
632
|
+
}
|
|
630
633
|
}
|
|
631
634
|
/** Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set. */
|
|
632
635
|
export declare namespace ComAtprotoRepoImportRepo {
|
package/dist/rpc.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export declare class XRPCError extends Error {
|
|
|
24
24
|
kind?: string;
|
|
25
25
|
/** Error description */
|
|
26
26
|
description?: string;
|
|
27
|
-
constructor(status: number, { kind, description, headers, cause }?: XRPCErrorOptions);
|
|
27
|
+
constructor(status: number, { kind, description, headers, cause, }?: XRPCErrorOptions);
|
|
28
28
|
}
|
|
29
29
|
/** Service proxy options */
|
|
30
30
|
export interface XRPCProxyOptions {
|
package/dist/rpc.js
CHANGED
|
@@ -2,8 +2,8 @@ import { buildFetchHandler } from './fetch-handler.js';
|
|
|
2
2
|
import { mergeHeaders } from './utils/http.js';
|
|
3
3
|
/** Error coming from the XRPC service */
|
|
4
4
|
export class XRPCError extends Error {
|
|
5
|
-
constructor(status, { kind
|
|
6
|
-
super(`${kind
|
|
5
|
+
constructor(status, { kind = `HTTP error ${status}`, description = `Unspecified error description`, headers, cause, } = {}) {
|
|
6
|
+
super(`${kind} > ${description}`, { cause });
|
|
7
7
|
this.name = 'XRPCError';
|
|
8
8
|
this.status = status;
|
|
9
9
|
this.kind = kind;
|
package/dist/rpc.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc.js","sourceRoot":"","sources":["../lib/rpc.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAA8C,MAAM,oBAAoB,CAAC;AACnG,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAkB/C,yCAAyC;AACzC,MAAM,OAAO,SAAU,SAAQ,KAAK;IAYnC,
|
|
1
|
+
{"version":3,"file":"rpc.js","sourceRoot":"","sources":["../lib/rpc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type { At, ComAtprotoServerCreateSession } from './lexicons.js';
|
|
2
|
+
|
|
3
|
+
import { simpleFetchHandler, type FetchHandlerObject } from './fetch-handler.js';
|
|
4
|
+
import { XRPC, XRPCError } from './rpc.js';
|
|
5
|
+
|
|
6
|
+
import { getPdsEndpoint, type DidDocument } from './utils/did.js';
|
|
7
|
+
import { decodeJwt } from './utils/jwt.js';
|
|
8
|
+
|
|
9
|
+
/** Interface for the decoded access token, for convenience */
|
|
10
|
+
export interface AtpAccessJwt {
|
|
11
|
+
/** Access token scope, app password returns a different scope. */
|
|
12
|
+
scope: 'com.atproto.access' | 'com.atproto.appPass' | 'com.atproto.appPassPrivileged';
|
|
13
|
+
/** Account DID */
|
|
14
|
+
sub: At.DID;
|
|
15
|
+
/** Expiration time */
|
|
16
|
+
exp: number;
|
|
17
|
+
/** Creation/issued time */
|
|
18
|
+
iat: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Interface for the decoded refresh token, for convenience */
|
|
22
|
+
export interface AtpRefreshJwt {
|
|
23
|
+
/** Refresh token scope */
|
|
24
|
+
scope: 'com.atproto.refresh';
|
|
25
|
+
/** ID of this refresh token */
|
|
26
|
+
jti: string;
|
|
27
|
+
/** Account DID */
|
|
28
|
+
sub: At.DID;
|
|
29
|
+
/** Intended audience of this refresh token, in DID */
|
|
30
|
+
aud: At.DID;
|
|
31
|
+
/** Expiration time */
|
|
32
|
+
exp: number;
|
|
33
|
+
/** Creation/issued time */
|
|
34
|
+
iat: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Saved session data, this can be reused again for next time. */
|
|
38
|
+
export interface AtpSessionData {
|
|
39
|
+
/** Refresh token */
|
|
40
|
+
refreshJwt: string;
|
|
41
|
+
/** Access token */
|
|
42
|
+
accessJwt: string;
|
|
43
|
+
/** Account handle */
|
|
44
|
+
handle: string;
|
|
45
|
+
/** Account DID */
|
|
46
|
+
did: At.DID;
|
|
47
|
+
/** PDS endpoint found in the DID document, this will be used as the service URI if provided */
|
|
48
|
+
pdsUri?: string;
|
|
49
|
+
/** Email address of the account, might not be available if on app password */
|
|
50
|
+
email?: string;
|
|
51
|
+
/** If the email address has been confirmed or not */
|
|
52
|
+
emailConfirmed?: boolean;
|
|
53
|
+
/** If the account has email-based two-factor authentication enabled */
|
|
54
|
+
emailAuthFactor?: boolean;
|
|
55
|
+
/** Whether the account is active (not deactivated, taken down, or suspended) */
|
|
56
|
+
active: boolean;
|
|
57
|
+
/** Possible reason for why the account is inactive */
|
|
58
|
+
inactiveStatus?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface CredentialManagerOptions {
|
|
62
|
+
/** PDS server URL */
|
|
63
|
+
service: string;
|
|
64
|
+
|
|
65
|
+
/** Custom fetch function */
|
|
66
|
+
fetch?: typeof globalThis.fetch;
|
|
67
|
+
|
|
68
|
+
/** Function that gets called if the session turned out to have expired during an XRPC request */
|
|
69
|
+
onExpired?: (session: AtpSessionData) => void;
|
|
70
|
+
/** Function that gets called if the session has been refreshed during an XRPC request */
|
|
71
|
+
onRefresh?: (session: AtpSessionData) => void;
|
|
72
|
+
/** Function that gets called if the session object has been refreshed */
|
|
73
|
+
onSessionUpdate?: (session: AtpSessionData) => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class CredentialManager implements FetchHandlerObject {
|
|
77
|
+
readonly serviceUrl: string;
|
|
78
|
+
fetch: typeof fetch;
|
|
79
|
+
|
|
80
|
+
#server: XRPC;
|
|
81
|
+
#refreshSessionPromise: Promise<void> | undefined;
|
|
82
|
+
|
|
83
|
+
#onExpired: CredentialManagerOptions['onExpired'];
|
|
84
|
+
#onRefresh: CredentialManagerOptions['onRefresh'];
|
|
85
|
+
#onSessionUpdate: CredentialManagerOptions['onSessionUpdate'];
|
|
86
|
+
|
|
87
|
+
/** Current session state */
|
|
88
|
+
session?: AtpSessionData;
|
|
89
|
+
|
|
90
|
+
constructor({
|
|
91
|
+
service,
|
|
92
|
+
onExpired,
|
|
93
|
+
onRefresh,
|
|
94
|
+
onSessionUpdate,
|
|
95
|
+
fetch: _fetch = fetch,
|
|
96
|
+
}: CredentialManagerOptions) {
|
|
97
|
+
this.serviceUrl = service;
|
|
98
|
+
this.fetch = _fetch;
|
|
99
|
+
|
|
100
|
+
this.#server = new XRPC({ handler: simpleFetchHandler({ service: service, fetch: _fetch }) });
|
|
101
|
+
|
|
102
|
+
this.#onRefresh = onRefresh;
|
|
103
|
+
this.#onExpired = onExpired;
|
|
104
|
+
this.#onSessionUpdate = onSessionUpdate;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get dispatchUrl() {
|
|
108
|
+
return this.session?.pdsUri ?? this.serviceUrl;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async handle(pathname: string, init: RequestInit): Promise<Response> {
|
|
112
|
+
await this.#refreshSessionPromise;
|
|
113
|
+
|
|
114
|
+
const url = new URL(pathname, this.dispatchUrl);
|
|
115
|
+
const headers = new Headers(init.headers);
|
|
116
|
+
|
|
117
|
+
if (!this.session || headers.has('authorization')) {
|
|
118
|
+
return (0, this.fetch)(url, init);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
headers.set('authorization', `Bearer ${this.session.accessJwt}`);
|
|
122
|
+
|
|
123
|
+
const initialResponse = await (0, this.fetch)(url, { ...init, headers });
|
|
124
|
+
const isExpired = await isExpiredTokenResponse(initialResponse);
|
|
125
|
+
|
|
126
|
+
if (!isExpired) {
|
|
127
|
+
return initialResponse;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await this.#refreshSession();
|
|
132
|
+
} catch {
|
|
133
|
+
return initialResponse;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Return initial response if:
|
|
137
|
+
// - refreshSession returns expired
|
|
138
|
+
// - Body stream has been consumed
|
|
139
|
+
if (!this.session || init.body instanceof ReadableStream) {
|
|
140
|
+
return initialResponse;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
headers.set('authorization', `Bearer ${this.session.accessJwt}`);
|
|
144
|
+
|
|
145
|
+
return await (0, this.fetch)(url, { ...init, headers });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
#refreshSession() {
|
|
149
|
+
return (this.#refreshSessionPromise ||= this.#refreshSessionInner().finally(
|
|
150
|
+
() => (this.#refreshSessionPromise = undefined),
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async #refreshSessionInner(): Promise<void> {
|
|
155
|
+
const currentSession = this.session;
|
|
156
|
+
|
|
157
|
+
if (!currentSession) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const { data } = await this.#server.call('com.atproto.server.refreshSession', {
|
|
163
|
+
headers: {
|
|
164
|
+
authorization: `Bearer ${currentSession.refreshJwt}`,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
this.#updateSession({ ...currentSession, ...data });
|
|
169
|
+
this.#onRefresh?.(this.session!);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err instanceof XRPCError) {
|
|
172
|
+
const kind = err.kind;
|
|
173
|
+
|
|
174
|
+
if (kind === 'ExpiredToken' || kind === 'InvalidToken') {
|
|
175
|
+
this.session = undefined;
|
|
176
|
+
this.#onExpired?.(currentSession);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#updateSession(raw: ComAtprotoServerCreateSession.Output): AtpSessionData {
|
|
183
|
+
const didDoc = raw.didDoc as DidDocument | undefined;
|
|
184
|
+
|
|
185
|
+
let pdsUri: string | undefined;
|
|
186
|
+
if (didDoc) {
|
|
187
|
+
pdsUri = getPdsEndpoint(didDoc);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const newSession = {
|
|
191
|
+
accessJwt: raw.accessJwt,
|
|
192
|
+
refreshJwt: raw.refreshJwt,
|
|
193
|
+
handle: raw.handle,
|
|
194
|
+
did: raw.did,
|
|
195
|
+
pdsUri: pdsUri,
|
|
196
|
+
email: raw.email,
|
|
197
|
+
emailConfirmed: raw.emailConfirmed,
|
|
198
|
+
emailAuthFactor: raw.emailConfirmed,
|
|
199
|
+
active: raw.active ?? true,
|
|
200
|
+
inactiveStatus: raw.status,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
this.session = newSession;
|
|
204
|
+
this.#onSessionUpdate?.(newSession);
|
|
205
|
+
|
|
206
|
+
return newSession;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Resume a saved session
|
|
211
|
+
* @param session Session information, taken from `AtpAuth#session` after login
|
|
212
|
+
*/
|
|
213
|
+
async resume(session: AtpSessionData): Promise<AtpSessionData> {
|
|
214
|
+
const now = Date.now() / 1000 + 60 * 5;
|
|
215
|
+
|
|
216
|
+
const refreshToken = decodeJwt(session.refreshJwt) as AtpRefreshJwt;
|
|
217
|
+
|
|
218
|
+
if (now >= refreshToken.exp) {
|
|
219
|
+
throw new XRPCError(401, { kind: 'InvalidToken' });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const accessToken = decodeJwt(session.accessJwt) as AtpAccessJwt;
|
|
223
|
+
this.session = session;
|
|
224
|
+
|
|
225
|
+
if (now >= accessToken.exp) {
|
|
226
|
+
await this.#refreshSession();
|
|
227
|
+
} else {
|
|
228
|
+
const promise = this.#server.get('com.atproto.server.getSession', {
|
|
229
|
+
headers: {
|
|
230
|
+
authorization: `Bearer ${session.accessJwt}`,
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
promise.then((response) => {
|
|
235
|
+
const existing = this.session;
|
|
236
|
+
const next = response.data;
|
|
237
|
+
|
|
238
|
+
if (!existing) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.#updateSession({ ...existing, ...next });
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (!this.session) {
|
|
247
|
+
throw new XRPCError(401, { kind: 'InvalidToken' });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return this.session;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Perform a login operation
|
|
255
|
+
* @param options Login options
|
|
256
|
+
* @returns Session data that can be saved for later
|
|
257
|
+
*/
|
|
258
|
+
async login(options: AuthLoginOptions): Promise<AtpSessionData> {
|
|
259
|
+
// Reset the session
|
|
260
|
+
this.session = undefined;
|
|
261
|
+
|
|
262
|
+
const res = await this.#server.call('com.atproto.server.createSession', {
|
|
263
|
+
data: {
|
|
264
|
+
identifier: options.identifier,
|
|
265
|
+
password: options.password,
|
|
266
|
+
authFactorToken: options.code,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return this.#updateSession(res.data);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Login options */
|
|
275
|
+
export interface AuthLoginOptions {
|
|
276
|
+
/** What account to login as, this could be domain handle, DID, or email address */
|
|
277
|
+
identifier: string;
|
|
278
|
+
/** Account password */
|
|
279
|
+
password: string;
|
|
280
|
+
/** Two-factor authentication code */
|
|
281
|
+
code?: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const isExpiredTokenResponse = async (response: Response): Promise<boolean> => {
|
|
285
|
+
if (response.status !== 400) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (extractContentType(response.headers) !== 'application/json') {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// {"error":"ExpiredToken","message":"Token has expired"}
|
|
294
|
+
// {"error":"ExpiredToken","message":"Token is expired"}
|
|
295
|
+
if (extractContentLength(response.headers) > 54 * 1.5) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const { error, message } = await response.clone().json();
|
|
301
|
+
return error === 'ExpiredToken' && (typeof message === 'string' || message === undefined);
|
|
302
|
+
} catch {}
|
|
303
|
+
|
|
304
|
+
return false;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const extractContentType = (headers: Headers) => {
|
|
308
|
+
return headers.get('content-type')?.split(';')[0]?.trim();
|
|
309
|
+
};
|
|
310
|
+
const extractContentLength = (headers: Headers) => {
|
|
311
|
+
return Number(headers.get('content-length') ?? ';');
|
|
312
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Fetch handler function */
|
|
2
|
+
export type FetchHandler = (pathname: string, init: RequestInit) => Promise<Response>;
|
|
3
|
+
|
|
4
|
+
/** Fetch handler in an object */
|
|
5
|
+
export interface FetchHandlerObject {
|
|
6
|
+
handle(this: FetchHandlerObject, pathname: string, init: RequestInit): Promise<Response>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const buildFetchHandler = (handler: FetchHandler | FetchHandlerObject): FetchHandler => {
|
|
10
|
+
if (typeof handler === 'object') {
|
|
11
|
+
return handler.handle.bind(handler);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return handler;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface SimpleFetchHandlerOptions {
|
|
18
|
+
service: string | URL;
|
|
19
|
+
fetch?: typeof globalThis.fetch;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const simpleFetchHandler = ({
|
|
23
|
+
service,
|
|
24
|
+
fetch: _fetch = fetch,
|
|
25
|
+
}: SimpleFetchHandlerOptions): FetchHandler => {
|
|
26
|
+
return async (pathname, init) => {
|
|
27
|
+
const url = new URL(pathname, service);
|
|
28
|
+
return _fetch(url, init);
|
|
29
|
+
};
|
|
30
|
+
};
|
package/lib/index.ts
ADDED