@atproto/oauth-client-node 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
package/LICENSE.txt ADDED
@@ -0,0 +1,7 @@
1
+ Dual MIT/Apache-2.0 License
2
+
3
+ Copyright (c) 2022-2024 Bluesky PBC, and Contributors
4
+
5
+ Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
6
+
7
+ Downstream projects and end users may chose either license individually, or both together, at their discretion. The motivation for this dual-licensing is the additional software patent assurance provided by Apache 2.0.
package/README.md ADDED
@@ -0,0 +1,392 @@
1
+ # ATPROTO OAuth Client for NodeJS
2
+
3
+ This package implements all the OAuth features required by [ATPROTO] (PKCE,
4
+ etc.) to run in a NodeJS based environment (Election APP or Backend).
5
+
6
+ ## Setup
7
+
8
+ ### Client configuration
9
+
10
+ The `client_id` is what identifies your application to the OAuth server. It is
11
+ used to fetch the client metadata, and to initiate the OAuth flow. The
12
+ `client_id` must be a URL that points to the client metadata.
13
+
14
+ Your OAuth client metadata should be hosted at a URL that corresponds to the
15
+ `client_id` of your application. This URL should return a JSON object with the
16
+ client metadata. The client metadata should be configured according to the
17
+ needs of your application, and must respect the [ATPROTO].
18
+
19
+ #### From a backend service
20
+
21
+ The `client_metadata` object will typically be built by the backend at startup.
22
+
23
+ ```ts
24
+ import { NodeOAuthClientOptions } from '@atproto/oauth-client-node'
25
+
26
+ const client = new NodeOAuthClientOptions({
27
+ // This object will be used to build the payload of the /client-metadata.json
28
+ // endpoint metadata, exposing the client metadata to the OAuth server.
29
+ clientMetadata: {
30
+ // Must be a URL that will be exposing this metadata
31
+ client_id: 'https://my-app.com/client-metadata.json',
32
+ client_name: 'My App',
33
+ client_uri: 'https://my-app.com',
34
+ logo_uri: 'https://my-app.com/logo.png',
35
+ tos_uri: 'https://my-app.com/tos',
36
+ policy_uri: 'https://my-app.com/policy',
37
+ redirect_uris: ['https://my-app.com/callback'],
38
+ scope: 'profile email offline_access',
39
+ grant_types: ['authorization_code', 'refresh_token'],
40
+ response_types: ['code'],
41
+ application_type: 'web',
42
+ token_endpoint_auth_method: 'client_secret_jwt',
43
+ dpop_bound_access_tokens: true,
44
+ jwks_uri: 'https://my-app.com/jwks.json',
45
+ },
46
+
47
+ // Used to authenticate the client to the token endpoint. Will be used to
48
+ // build the jwks object to be exposed on the "jwks_uri" endpoint.
49
+ keyset: await Promise.all([
50
+ JoseKey.fromImportable(process.env.PRIVATE_KEY_1),
51
+ JoseKey.fromImportable(process.env.PRIVATE_KEY_2),
52
+ JoseKey.fromImportable(process.env.PRIVATE_KEY_3),
53
+ ]),
54
+
55
+ // Interface to store authorization state data (during authorization flows)
56
+ stateStore: {
57
+ set(key: string, internalState: NodeSavedState): Promise<void> {},
58
+ get(key: string): Promise<NodeSavedState | undefined> {},
59
+ del(key: string): Promise<void> {},
60
+ },
61
+
62
+ // Interface to store authenticated session data
63
+ sessionStore: {
64
+ set(sub: string, session: Session): Promise<void> {},
65
+ get(sub: string): Promise<Session | undefined> {},
66
+ del(sub: string): Promise<void> {},
67
+ },
68
+
69
+ // A lock to prevent concurrent access to the session store. Optional if only one instance is running.
70
+ requestLock,
71
+ })
72
+
73
+ const app = express()
74
+
75
+ // Expose the metadata and jwks
76
+ app.get('client-metadata.json', (req, res) => res.json(client.clientMetadata))
77
+ app.get('jwks.json', (req, res) => res.json(client.jwks))
78
+
79
+ // Create an endpoint to initiate the OAuth flow
80
+ app.get('/login', async (req, res, next) => {
81
+ try {
82
+ const handle = 'some-handle.bsky.social' // eg. from query string
83
+ const state = '434321'
84
+
85
+ // Revoke any pending authentication requests if the connection is closed (optional)
86
+ const ac = new AbortController()
87
+ req.on('close', () => ac.abort())
88
+
89
+ const url = await client.authorize(handle, {
90
+ signal: ac.signal,
91
+ state,
92
+ // Only supported if OAuth server is openid-compliant
93
+ ui_locales: 'fr-CA fr en',
94
+ })
95
+
96
+ res.redirect(url)
97
+ } catch (err) {
98
+ next(err)
99
+ }
100
+ })
101
+
102
+ // Create an endpoint to handle the OAuth callback
103
+ app.get('/atproto-oauth-callback', async (req, res, next) => {
104
+ try {
105
+ const params = new URLSearchParams(req.url.split('?')[1])
106
+
107
+ const { agent, state } = await client.callback(params)
108
+
109
+ // Process successful authentication here
110
+ console.log('authorize() was called with state:', state)
111
+
112
+ console.log('User authenticated as:', agent.did)
113
+
114
+ // Make Authenticated API calls
115
+ const profile = await agent.getProfile({ actor: agent.did })
116
+ console.log('Bsky profile:', profile.data)
117
+
118
+ res.json({ ok: true })
119
+ } catch (err) {
120
+ next(err)
121
+ }
122
+ })
123
+
124
+ // Whenever needed, restore a user's session
125
+ async function worker() {
126
+ const userDid = 'did:plc:123'
127
+
128
+ const agent = await client.restore(userDid)
129
+
130
+ // Note: If the current access_token is expired, the agent will automatically
131
+ // (and transparently) refresh it. The new token set will be saved though
132
+ // the client's session store.
133
+
134
+ // Make Authenticated API calls
135
+ const profile = await agent.getProfile({ actor: agent.did })
136
+ console.log('Bsky profile:', profile.data)
137
+ }
138
+ ```
139
+
140
+ #### From a native application
141
+
142
+ This applies to mobile apps, desktop apps, etc. based on NodeJS (e.g. Electron).
143
+
144
+ The client metadata must be hosted on an internet-accessible URL owned by you.
145
+ The client metadata will typically contain:
146
+
147
+ ```json
148
+ {
149
+ "client_id": "https://my-app.com/client-metadata.json",
150
+ "client_name": "My App",
151
+ "client_uri": "https://my-app.com",
152
+ "logo_uri": "https://my-app.com/logo.png",
153
+ "tos_uri": "https://my-app.com/tos",
154
+ "policy_uri": "https://my-app.com/policy",
155
+ "redirect_uris": ["https://my-app.com/atproto-oauth-callback"],
156
+ "scope": "profile email offline_access",
157
+ "grant_types": ["authorization_code", "refresh_token"],
158
+ "response_types": ["code"],
159
+ "application_type": "native",
160
+ "token_endpoint_auth_method": "none",
161
+ "dpop_bound_access_tokens": true
162
+ }
163
+ ```
164
+
165
+ Instead of hard-coding the client metadata in your app, you can fetch it when
166
+ the app starts:
167
+
168
+ ```ts
169
+ import { NodeOAuthClientOptions } from '@atproto/oauth-client-node'
170
+
171
+ const client = await NodeOAuthClientOptions.fromClientId({
172
+ clientId: 'https://my-app.com/client-metadata.json',
173
+
174
+ stateStore: {
175
+ set(key: string, internalState: NodeSavedState): Promise<void> {},
176
+ get(key: string): Promise<NodeSavedState | undefined> {},
177
+ del(key: string): Promise<void> {},
178
+ },
179
+
180
+ sessionStore: {
181
+ set(sub: string, session: Session): Promise<void> {},
182
+ get(sub: string): Promise<Session | undefined> {},
183
+ del(sub: string): Promise<void> {},
184
+ },
185
+
186
+ // A lock to prevent concurrent access to the session store. Optional if only one instance is running.
187
+ requestLock,
188
+ })
189
+ ```
190
+
191
+ > [!NOTE]
192
+ >
193
+ > There is no `keyset` in this instance. This is due to the fact that app
194
+ > clients cannot safely store a private key. The `token_endpoint_auth_method` is
195
+ > set to `none` in the client metadata, which means that the client will not be
196
+ > authenticating itself to the token endpoint. This will cause sessions to have
197
+ > a shorter lifetime. You can circumvent this by providing a "BFF" (Backend for
198
+ > Frontend) that will perform an authenticated OAuth flow and use a session id
199
+ > based mechanism to authenticate the client.
200
+
201
+ ### Common configuration options
202
+
203
+ The `OAuthClient` and `OAuthAgent` classes will manage and refresh OAuth tokens
204
+ transparently. They are also responsible to properly format the HTTP requests
205
+ payload, using DPoP, and transparently retrying requests when the access token
206
+ expires.
207
+
208
+ For this to work, the client must be configured with the following options:
209
+
210
+ #### `sessionStore`
211
+
212
+ A simple key-value store to save the OAuth session data. This is used to save
213
+ the access token, refresh token, and other session data.
214
+
215
+ ```ts
216
+ const sessionStore: NodeSavedSessionStore = {
217
+ async set(sub: string, sessionData: NodeSavedSession) {
218
+ // Insert or update the session data in your database
219
+ await saveSessionDataToDb(sub, sessionData)
220
+ },
221
+
222
+ async get(sub: string) {
223
+ // Retrieve the session data from your database
224
+ const sessionData = await getSessionDataFromDb(sub)
225
+ if (!sessionData) return undefined
226
+
227
+ return sessionData
228
+ },
229
+
230
+ async del(sub: string) {
231
+ // Delete the session data from your database
232
+ await deleteSessionDataFromDb(sub)
233
+ },
234
+ }
235
+ ```
236
+
237
+ #### `stateStore`
238
+
239
+ A simple key-value store to save the state of the OAuth
240
+ authorization flow. This is used to prevent CSRF attacks.
241
+
242
+ The implementation of the `StateStore` is similar to the
243
+ [`sessionStore`](#sessionstore).
244
+
245
+ ```ts
246
+ interface NodeSavedStateStore {
247
+ set: (key: string, internalState: NodeSavedState) => Promise<void>
248
+ get: (key: string) => Promise<NodeSavedState | undefined>
249
+ del: (key: string) => Promise<void>
250
+ }
251
+ ```
252
+
253
+ One notable exception is that state store items can (and should) be deleted
254
+ after a short period of time (one hour should be more than enough).
255
+
256
+ #### `requestLock`
257
+
258
+ When multiple instances of the client are running, this lock will prevent
259
+ concurrent refreshes of the same session.
260
+
261
+ Here is an example implementation based on [`redlock`](https://www.npmjs.com/package/redlock):
262
+
263
+ ```ts
264
+ import { RuntimeLock } from '@atproto/oauth-client-node'
265
+ import Redis from 'ioredis'
266
+ import Redlock from 'redlock'
267
+
268
+ const redisClients = new Redis()
269
+ const redlock = new Redlock(redisClients)
270
+
271
+ const requestLock: RuntimeLock = async (key, fn) => {
272
+ // 30 seconds should be enough. Since we will be using one lock per user id
273
+ // we can be quite liberal with the lock duration here.
274
+ const lock = await redlock.lock(key, 45e3)
275
+ try {
276
+ return await fn()
277
+ } finally {
278
+ await redlock.unlock(lock)
279
+ }
280
+ }
281
+ ```
282
+
283
+ ## Advances use-cases
284
+
285
+ ### Listening for session updates and deletion
286
+
287
+ The `OAuthClient` will emit events whenever a session is updated or deleted.
288
+
289
+ ```ts
290
+ import {
291
+ Session,
292
+ TokenRefreshError,
293
+ TokenRevokedError,
294
+ } from '@atproto/oauth-client-node'
295
+
296
+ client.addEventListener('updated', (event: CustomEvent<Session>) => {
297
+ console.log('Refreshed tokens were saved in the store:', event.detail)
298
+ })
299
+
300
+ client.addEventListener(
301
+ 'deleted',
302
+ (
303
+ event: CustomEvent<{
304
+ sub: string
305
+ cause: TokenRefreshError | TokenRevokedError | unknown
306
+ }>,
307
+ ) => {
308
+ console.log('Session was deleted from the session store:', event.detail)
309
+
310
+ const { cause } = event.detail
311
+
312
+ if (cause instanceof TokenRefreshError) {
313
+ // - refresh_token unavailable or expired
314
+ // - oauth response error (`cause.cause instanceof OAuthResponseError`)
315
+ // - session data does not match expected values returned by the OAuth server
316
+ } else if (cause instanceof TokenRevokedError) {
317
+ // Session was revoked through:
318
+ // - agent.signOut()
319
+ // - client.revoke(sub)
320
+ } else {
321
+ // An unexpected error occurred, causing the session to be deleted
322
+ }
323
+ },
324
+ )
325
+ ```
326
+
327
+ ### Silent Sign-In
328
+
329
+ Using silent sign-in requires to handle retries on the callback endpoint.
330
+
331
+ ```ts
332
+ app.get('/login', async (req, res) => {
333
+ const handle = 'some-handle.bsky.social' // eg. from query string
334
+ const user = req.user.id
335
+
336
+ const url = await client.authorize(handle, {
337
+ // Use "prompt=none" to attempt silent sign-in
338
+ prompt: 'none',
339
+
340
+ // Build an internal state to map the login request to the user, and allow retries
341
+ state: JSON.stringify({
342
+ user,
343
+ handle,
344
+ }),
345
+ })
346
+
347
+ res.redirect(url)
348
+ })
349
+
350
+ app.get('/atproto-oauth-callback', async (req, res) => {
351
+ const params = new URLSearchParams(req.url.split('?')[1])
352
+ try {
353
+ try {
354
+ const { agent, state } = await client.callback(params)
355
+
356
+ // Process successful authentication here
357
+ } catch (err) {
358
+ // Silent sign-in failed, retry without prompt=none
359
+ if (
360
+ err instanceof OAuthCallbackError &&
361
+ ['login_required', 'consent_required'].includes(err.params.get('error'))
362
+ ) {
363
+ // Parse previous state
364
+ const { user, handle } = JSON.parse(err.state)
365
+
366
+ const url = await client.authorize(handle, {
367
+ // Note that we omit the prompt parameter here. Setting "prompt=none"
368
+ // here would result in an infinite redirect loop.
369
+
370
+ // Build a new state (or re-use the previous one)
371
+ state: JSON.stringify({
372
+ user,
373
+ handle,
374
+ }),
375
+ })
376
+
377
+ // redirect to new URL
378
+ res.redirect(url)
379
+
380
+ return
381
+ }
382
+
383
+ throw err
384
+ }
385
+ } catch (err) {
386
+ next(err)
387
+ }
388
+ })
389
+ ```
390
+
391
+ [ATPROTO]: https://atproto.com/ 'AT Protocol'
392
+ [API]: ../../api/README.md
@@ -0,0 +1,5 @@
1
+ export * from '@atproto-labs/handle-resolver-node';
2
+ export * from '@atproto/jwk-webcrypto';
3
+ export * from '@atproto/oauth-client';
4
+ export * from './node-oauth-client.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oCAAoC,CAAA;AAClD,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AAErC,cAAc,wBAAwB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("@atproto-labs/handle-resolver-node"), exports);
18
+ __exportStar(require("@atproto/jwk-webcrypto"), exports);
19
+ __exportStar(require("@atproto/oauth-client"), exports);
20
+ __exportStar(require("./node-oauth-client.js"), exports);
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,qEAAkD;AAClD,yDAAsC;AACtC,wDAAqC;AAErC,yDAAsC"}
@@ -0,0 +1,21 @@
1
+ import { SimpleStore } from '@atproto-labs/simple-store';
2
+ import { Jwk, Key } from '@atproto/jwk';
3
+ import { InternalStateData, Session } from '@atproto/oauth-client';
4
+ type ToDpopJwkValue<V extends {
5
+ dpopKey: Key;
6
+ }> = Omit<V, 'dpopKey'> & {
7
+ dpopJwk: Jwk;
8
+ };
9
+ /**
10
+ * Utility function that allows to simplify the store interface by exposing a
11
+ * JWK (JSON) instead of a Key instance.
12
+ */
13
+ export declare function toDpopKeyStore<K extends string, V extends {
14
+ dpopKey: Key;
15
+ }>(store: SimpleStore<K, ToDpopJwkValue<V>>): SimpleStore<K, V>;
16
+ export type NodeSavedState = ToDpopJwkValue<InternalStateData>;
17
+ export type NodeSavedStateStore = SimpleStore<string, NodeSavedState>;
18
+ export type NodeSavedSession = ToDpopJwkValue<Session>;
19
+ export type NodeSavedSessionStore = SimpleStore<string, NodeSavedSession>;
20
+ export {};
21
+ //# sourceMappingURL=node-dpop-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-dpop-store.d.ts","sourceRoot":"","sources":["../src/node-dpop-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AACxD,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AAEvC,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAElE,KAAK,cAAc,CAAC,CAAC,SAAS;IAAE,OAAO,EAAE,GAAG,CAAA;CAAE,IAAI,IAAI,CAAC,CAAC,EAAE,SAAS,CAAC,GAAG;IACrE,OAAO,EAAE,GAAG,CAAA;CACb,CAAA;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS;IAAE,OAAO,EAAE,GAAG,CAAA;CAAE,EACzE,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,GACvC,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAqBnB;AAED,MAAM,MAAM,cAAc,GAAG,cAAc,CAAC,iBAAiB,CAAC,CAAA;AAC9D,MAAM,MAAM,mBAAmB,GAAG,WAAW,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;AAErE,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;AACtD,MAAM,MAAM,qBAAqB,GAAG,WAAW,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA"}
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toDpopKeyStore = void 0;
4
+ const jwk_jose_1 = require("@atproto/jwk-jose");
5
+ /**
6
+ * Utility function that allows to simplify the store interface by exposing a
7
+ * JWK (JSON) instead of a Key instance.
8
+ */
9
+ function toDpopKeyStore(store) {
10
+ return {
11
+ async set(sub, { dpopKey, ...data }) {
12
+ const dpopJwk = dpopKey.privateJwk;
13
+ if (!dpopJwk)
14
+ throw new Error('Private DPoP JWK is missing.');
15
+ await store.set(sub, { ...data, dpopJwk });
16
+ },
17
+ async get(sub) {
18
+ const result = await store.get(sub);
19
+ if (!result)
20
+ return undefined;
21
+ const { dpopJwk, ...data } = result;
22
+ const dpopKey = await jwk_jose_1.JoseKey.fromJWK(dpopJwk);
23
+ return { ...data, dpopKey };
24
+ },
25
+ del: store.del.bind(store),
26
+ clear: store.clear?.bind(store),
27
+ };
28
+ }
29
+ exports.toDpopKeyStore = toDpopKeyStore;
30
+ //# sourceMappingURL=node-dpop-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-dpop-store.js","sourceRoot":"","sources":["../src/node-dpop-store.ts"],"names":[],"mappings":";;;AAEA,gDAA2C;AAO3C;;;GAGG;AACH,SAAgB,cAAc,CAC5B,KAAwC;IAExC,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAM,EAAE,EAAE,OAAO,EAAE,GAAG,IAAI,EAAK;YACvC,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAA;YAClC,IAAI,CAAC,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAA;YAE7D,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAA;QAC5C,CAAC;QAED,KAAK,CAAC,GAAG,CAAC,GAAM;YACd,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACnC,IAAI,CAAC,MAAM;gBAAE,OAAO,SAAS,CAAA;YAE7B,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAA;YACnC,MAAM,OAAO,GAAG,MAAM,kBAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;YAC9C,OAAO,EAAE,GAAG,IAAI,EAAE,OAAO,EAAkB,CAAA;QAC7C,CAAC;QAED,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;QAC1B,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;KAChC,CAAA;AACH,CAAC;AAvBD,wCAuBC"}
@@ -0,0 +1,19 @@
1
+ import { AtprotoHandleResolverNodeOptions } from '@atproto-labs/handle-resolver-node';
2
+ import { OAuthClient, OAuthClientFetchMetadataOptions, OAuthClientOptions, RuntimeLock } from '@atproto/oauth-client';
3
+ import { OAuthResponseMode } from '@atproto/oauth-types';
4
+ import { NodeSavedSessionStore, NodeSavedStateStore } from './node-dpop-store.js';
5
+ export type * from './node-dpop-store.js';
6
+ export type { OAuthClientOptions, OAuthResponseMode, RuntimeLock };
7
+ export type NodeOAuthClientOptions = Omit<OAuthClientOptions, 'responseMode' | 'runtimeImplementation' | 'handleResolver' | 'sessionStore' | 'stateStore'> & {
8
+ fallbackNameservers?: AtprotoHandleResolverNodeOptions['fallbackNameservers'];
9
+ responseMode?: OAuthResponseMode;
10
+ stateStore: NodeSavedStateStore;
11
+ sessionStore: NodeSavedSessionStore;
12
+ requestLock?: RuntimeLock;
13
+ };
14
+ export type NodeOAuthClientFromMetadataOptions = OAuthClientFetchMetadataOptions & Omit<NodeOAuthClientOptions, 'clientMetadata'>;
15
+ export declare class NodeOAuthClient extends OAuthClient {
16
+ static fromClientId(options: NodeOAuthClientFromMetadataOptions): Promise<NodeOAuthClient>;
17
+ constructor({ fetch, responseMode, fallbackNameservers, stateStore, sessionStore, requestLock, ...options }: NodeOAuthClientOptions);
18
+ }
19
+ //# sourceMappingURL=node-oauth-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-oauth-client.d.ts","sourceRoot":"","sources":["../src/node-oauth-client.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,gCAAgC,EACjC,MAAM,oCAAoC,CAAA;AAE3C,OAAO,EACL,WAAW,EACX,+BAA+B,EAC/B,kBAAkB,EAClB,WAAW,EACZ,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAExD,OAAO,EACL,qBAAqB,EACrB,mBAAmB,EAEpB,MAAM,sBAAsB,CAAA;AAE7B,mBAAmB,sBAAsB,CAAA;AACzC,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,CAAA;AAElE,MAAM,MAAM,sBAAsB,GAAG,IAAI,CACvC,kBAAkB,EAChB,cAAc,GACd,uBAAuB,GACvB,gBAAgB,GAChB,cAAc,GACd,YAAY,CACf,GAAG;IACF,mBAAmB,CAAC,EAAE,gCAAgC,CAAC,qBAAqB,CAAC,CAAA;IAC7E,YAAY,CAAC,EAAE,iBAAiB,CAAA;IAEhC,UAAU,EAAE,mBAAmB,CAAA;IAC/B,YAAY,EAAE,qBAAqB,CAAA;IACnC,WAAW,CAAC,EAAE,WAAW,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,kCAAkC,GAC5C,+BAA+B,GAC7B,IAAI,CAAC,sBAAsB,EAAE,gBAAgB,CAAC,CAAA;AAElD,qBAAa,eAAgB,SAAQ,WAAW;WACjC,YAAY,CAAC,OAAO,EAAE,kCAAkC;gBAKzD,EACV,KAAK,EACL,YAAsB,EACtB,mBAAmB,EAEnB,UAAU,EACV,YAAY,EACZ,WAAuB,EAEvB,GAAG,OAAO,EACX,EAAE,sBAAsB;CA2B1B"}
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NodeOAuthClient = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const handle_resolver_node_1 = require("@atproto-labs/handle-resolver-node");
6
+ const jwk_jose_1 = require("@atproto/jwk-jose");
7
+ const oauth_client_1 = require("@atproto/oauth-client");
8
+ const node_dpop_store_js_1 = require("./node-dpop-store.js");
9
+ class NodeOAuthClient extends oauth_client_1.OAuthClient {
10
+ static async fromClientId(options) {
11
+ const clientMetadata = await oauth_client_1.OAuthClient.fetchMetadata(options);
12
+ return new NodeOAuthClient({ ...options, clientMetadata });
13
+ }
14
+ constructor({ fetch, responseMode = 'query', fallbackNameservers, stateStore, sessionStore, requestLock = undefined, ...options }) {
15
+ if (!requestLock) {
16
+ // Ok if only one instance of the client is running at a time.
17
+ console.warn('No lock mechanism provided. Credentials might get revoked.');
18
+ }
19
+ super({
20
+ fetch,
21
+ responseMode,
22
+ handleResolver: new handle_resolver_node_1.AtprotoHandleResolverNode({
23
+ fetch,
24
+ fallbackNameservers,
25
+ }),
26
+ runtimeImplementation: {
27
+ requestLock,
28
+ createKey: (algs) => jwk_jose_1.JoseKey.generate(algs),
29
+ getRandomValues: node_crypto_1.randomBytes,
30
+ digest: (bytes, algorithm) => (0, node_crypto_1.createHash)(algorithm.name).update(bytes).digest(),
31
+ },
32
+ stateStore: (0, node_dpop_store_js_1.toDpopKeyStore)(stateStore),
33
+ sessionStore: (0, node_dpop_store_js_1.toDpopKeyStore)(sessionStore),
34
+ ...options,
35
+ });
36
+ }
37
+ }
38
+ exports.NodeOAuthClient = NodeOAuthClient;
39
+ //# sourceMappingURL=node-oauth-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-oauth-client.js","sourceRoot":"","sources":["../src/node-oauth-client.ts"],"names":[],"mappings":";;;AAAA,6CAAqD;AAErD,6EAG2C;AAC3C,gDAA2C;AAC3C,wDAK8B;AAG9B,6DAI6B;AAyB7B,MAAa,eAAgB,SAAQ,0BAAW;IAC9C,MAAM,CAAC,KAAK,CAAC,YAAY,CAAC,OAA2C;QACnE,MAAM,cAAc,GAAG,MAAM,0BAAW,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;QAC/D,OAAO,IAAI,eAAe,CAAC,EAAE,GAAG,OAAO,EAAE,cAAc,EAAE,CAAC,CAAA;IAC5D,CAAC;IAED,YAAY,EACV,KAAK,EACL,YAAY,GAAG,OAAO,EACtB,mBAAmB,EAEnB,UAAU,EACV,YAAY,EACZ,WAAW,GAAG,SAAS,EAEvB,GAAG,OAAO,EACa;QACvB,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,8DAA8D;YAC9D,OAAO,CAAC,IAAI,CAAC,4DAA4D,CAAC,CAAA;QAC5E,CAAC;QAED,KAAK,CAAC;YACJ,KAAK;YACL,YAAY;YACZ,cAAc,EAAE,IAAI,gDAAyB,CAAC;gBAC5C,KAAK;gBACL,mBAAmB;aACpB,CAAC;YACF,qBAAqB,EAAE;gBACrB,WAAW;gBACX,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,kBAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAC3C,eAAe,EAAE,yBAAW;gBAC5B,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,CAC3B,IAAA,wBAAU,EAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE;aACpD;YAED,UAAU,EAAE,IAAA,mCAAc,EAAC,UAAU,CAAC;YACtC,YAAY,EAAE,IAAA,mCAAc,EAAC,YAAY,CAAC;YAE1C,GAAG,OAAO;SACX,CAAC,CAAA;IACJ,CAAC;CACF;AA3CD,0CA2CC"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@atproto/oauth-client-node",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "description": "ATPROTO OAuth client for the NodeJS",
6
+ "keywords": [
7
+ "atproto",
8
+ "oauth",
9
+ "client",
10
+ "node"
11
+ ],
12
+ "homepage": "https://atproto.com",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/bluesky-social/atproto",
16
+ "directory": "packages/oauth/oauth-client-node"
17
+ },
18
+ "type": "commonjs",
19
+ "main": "dist/index.js",
20
+ "types": "dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "dependencies": {
31
+ "@atproto-labs/did-resolver": "0.1.1",
32
+ "@atproto-labs/handle-resolver-node": "0.1.1",
33
+ "@atproto-labs/simple-store": "0.1.1",
34
+ "@atproto/did": "0.1.0",
35
+ "@atproto/jwk": "0.1.1",
36
+ "@atproto/jwk-jose": "0.1.1",
37
+ "@atproto/jwk-webcrypto": "0.1.1",
38
+ "@atproto/oauth-client": "0.1.1",
39
+ "@atproto/oauth-types": "0.1.1"
40
+ },
41
+ "devDependencies": {
42
+ "typescript": "^5.3.3",
43
+ "@atproto/api": "0.12.24"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc --build tsconfig.build.json"
47
+ }
48
+ }