@atproto/oauth-client-node 0.0.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/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
+ }