@atproto/oauth-client-node 0.0.0-spaces-alpha-20260818163953
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/CHANGELOG.md +724 -0
- package/LICENSE.txt +7 -0
- package/README.md +469 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/node-dpop-store.d.ts +22 -0
- package/dist/node-dpop-store.d.ts.map +1 -0
- package/dist/node-dpop-store.js +26 -0
- package/dist/node-dpop-store.js.map +1 -0
- package/dist/node-oauth-client.d.ts +30 -0
- package/dist/node-oauth-client.d.ts.map +1 -0
- package/dist/node-oauth-client.js +31 -0
- package/dist/node-oauth-client.js.map +1 -0
- package/dist/util.d.ts +5 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +2 -0
- package/dist/util.js.map +1 -0
- package/package.json +47 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Dual MIT/Apache-2.0 License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2026 Bluesky Social 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,469 @@
|
|
|
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 such as desktop apps built with
|
|
5
|
+
Electron or traditional web app backends built with frameworks like Express.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
### Client configuration
|
|
10
|
+
|
|
11
|
+
The `client_id` is what identifies your application to the OAuth server. It is
|
|
12
|
+
used to fetch the client metadata, and to initiate the OAuth flow. The
|
|
13
|
+
`client_id` must be a URL that points to the client metadata.
|
|
14
|
+
|
|
15
|
+
Your OAuth client metadata should be hosted at a URL that corresponds to the
|
|
16
|
+
`client_id` of your application. This URL should return a JSON object with the
|
|
17
|
+
client metadata. The client metadata should be configured according to the
|
|
18
|
+
needs of your application, and must respect the [ATPROTO].
|
|
19
|
+
|
|
20
|
+
#### From a backend service
|
|
21
|
+
|
|
22
|
+
The `client_metadata` object will typically be built by the backend at startup.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { NodeOAuthClient, Session } from '@atproto/oauth-client-node'
|
|
26
|
+
import { JoseKey } from '@atproto/jwk-jose'
|
|
27
|
+
|
|
28
|
+
const client = new NodeOAuthClient({
|
|
29
|
+
// This object will be used to build the payload of the /client-metadata.json
|
|
30
|
+
// endpoint metadata, exposing the client metadata to the OAuth server.
|
|
31
|
+
clientMetadata: {
|
|
32
|
+
// Must be a URL that will be exposing this metadata
|
|
33
|
+
client_id: 'https://my-app.com/client-metadata.json',
|
|
34
|
+
client_name: 'My App',
|
|
35
|
+
client_uri: 'https://my-app.com',
|
|
36
|
+
logo_uri: 'https://my-app.com/logo.png',
|
|
37
|
+
tos_uri: 'https://my-app.com/tos',
|
|
38
|
+
policy_uri: 'https://my-app.com/policy',
|
|
39
|
+
redirect_uris: ['https://my-app.com/callback'],
|
|
40
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
41
|
+
scope: 'atproto transition:generic',
|
|
42
|
+
response_types: ['code'],
|
|
43
|
+
application_type: 'web',
|
|
44
|
+
token_endpoint_auth_method: 'private_key_jwt',
|
|
45
|
+
token_endpoint_auth_signing_alg: 'RS256',
|
|
46
|
+
dpop_bound_access_tokens: true,
|
|
47
|
+
jwks_uri: 'https://my-app.com/jwks.json',
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Used to authenticate the client to the token endpoint. Will be used to
|
|
51
|
+
// build the jwks object to be exposed on the "jwks_uri" endpoint.
|
|
52
|
+
keyset: await Promise.all([
|
|
53
|
+
JoseKey.fromImportable(process.env.PRIVATE_KEY_1, 'key1'),
|
|
54
|
+
JoseKey.fromImportable(process.env.PRIVATE_KEY_2, 'key2'),
|
|
55
|
+
JoseKey.fromImportable(process.env.PRIVATE_KEY_3, 'key3'),
|
|
56
|
+
]),
|
|
57
|
+
|
|
58
|
+
// Interface to store authorization state data (during authorization flows)
|
|
59
|
+
stateStore: {
|
|
60
|
+
async set(key: string, internalState: NodeSavedState): Promise<void> {},
|
|
61
|
+
async get(key: string): Promise<NodeSavedState | undefined> {},
|
|
62
|
+
async del(key: string): Promise<void> {},
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Interface to store authenticated session data
|
|
66
|
+
sessionStore: {
|
|
67
|
+
async set(sub: string, session: Session): Promise<void> {},
|
|
68
|
+
async get(sub: string): Promise<Session | undefined> {},
|
|
69
|
+
async del(sub: string): Promise<void> {},
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// A lock to prevent concurrent access to the session store. Optional if only one instance is running.
|
|
73
|
+
requestLock,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const app = express()
|
|
77
|
+
|
|
78
|
+
// Expose the metadata and jwks
|
|
79
|
+
app.get('client-metadata.json', (req, res) => res.json(client.clientMetadata))
|
|
80
|
+
app.get('jwks.json', (req, res) => res.json(client.jwks))
|
|
81
|
+
|
|
82
|
+
// Create an endpoint to initiate the OAuth flow
|
|
83
|
+
app.get('/login', async (req, res, next) => {
|
|
84
|
+
try {
|
|
85
|
+
const handle = 'some-handle.bsky.social' // eg. from query string
|
|
86
|
+
const state = '434321'
|
|
87
|
+
|
|
88
|
+
// Revoke any pending authentication requests if the connection is closed (optional)
|
|
89
|
+
const ac = new AbortController()
|
|
90
|
+
req.on('close', () => ac.abort())
|
|
91
|
+
|
|
92
|
+
const url = await client.authorize(handle, {
|
|
93
|
+
signal: ac.signal,
|
|
94
|
+
state,
|
|
95
|
+
// Only supported if OAuth server is openid-compliant
|
|
96
|
+
ui_locales: 'fr-CA fr en',
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
res.redirect(url)
|
|
100
|
+
} catch (err) {
|
|
101
|
+
next(err)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Create an endpoint to handle the OAuth callback
|
|
106
|
+
app.get('/atproto-oauth-callback', async (req, res, next) => {
|
|
107
|
+
try {
|
|
108
|
+
const params = new URLSearchParams(req.url.split('?')[1])
|
|
109
|
+
|
|
110
|
+
const { session, state } = await client.callback(params)
|
|
111
|
+
|
|
112
|
+
// Process successful authentication here
|
|
113
|
+
console.log('authorize() was called with state:', state)
|
|
114
|
+
|
|
115
|
+
console.log('User authenticated as:', session.did)
|
|
116
|
+
|
|
117
|
+
const agent = new Agent(session)
|
|
118
|
+
|
|
119
|
+
// Make Authenticated API calls
|
|
120
|
+
const profile = await agent.getProfile({ actor: agent.did })
|
|
121
|
+
console.log('Bsky profile:', profile.data)
|
|
122
|
+
|
|
123
|
+
res.json({ ok: true })
|
|
124
|
+
} catch (err) {
|
|
125
|
+
next(err)
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Whenever needed, restore a user's session
|
|
130
|
+
async function worker() {
|
|
131
|
+
const userDid = 'did:plc:123'
|
|
132
|
+
|
|
133
|
+
const oauthSession = await client.restore(userDid)
|
|
134
|
+
|
|
135
|
+
// Note: If the current access_token is expired, the session will automatically
|
|
136
|
+
// (and transparently) refresh it. The new token set will be saved though
|
|
137
|
+
// the client's session store.
|
|
138
|
+
|
|
139
|
+
const agent = new Agent(oauthSession)
|
|
140
|
+
|
|
141
|
+
// Make Authenticated API calls
|
|
142
|
+
const profile = await agent.getProfile({ actor: agent.did })
|
|
143
|
+
console.log('Bsky profile:', profile.data)
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### From a native application
|
|
148
|
+
|
|
149
|
+
This applies to mobile apps, desktop apps, etc. based on NodeJS (e.g. Electron).
|
|
150
|
+
|
|
151
|
+
The client metadata must be hosted on an internet-accessible URL owned by you.
|
|
152
|
+
The client metadata will typically contain:
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{
|
|
156
|
+
"client_id": "https://my-app.com/client-metadata.json",
|
|
157
|
+
"client_name": "My App",
|
|
158
|
+
"client_uri": "https://my-app.com",
|
|
159
|
+
"logo_uri": "https://my-app.com/logo.png",
|
|
160
|
+
"tos_uri": "https://my-app.com/tos",
|
|
161
|
+
"policy_uri": "https://my-app.com/policy",
|
|
162
|
+
"redirect_uris": ["https://my-app.com/atproto-oauth-callback"],
|
|
163
|
+
"scope": "atproto",
|
|
164
|
+
"grant_types": ["authorization_code", "refresh_token"],
|
|
165
|
+
"response_types": ["code"],
|
|
166
|
+
"application_type": "native",
|
|
167
|
+
"token_endpoint_auth_method": "none",
|
|
168
|
+
"dpop_bound_access_tokens": true
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Instead of hard-coding the client metadata in your app, you can fetch it when
|
|
173
|
+
the app starts:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { NodeOAuthClient } from '@atproto/oauth-client-node'
|
|
177
|
+
|
|
178
|
+
const client = await NodeOAuthClient.fromClientId({
|
|
179
|
+
clientId: 'https://my-app.com/client-metadata.json',
|
|
180
|
+
|
|
181
|
+
stateStore: {
|
|
182
|
+
async set(key: string, internalState: NodeSavedState): Promise<void> {},
|
|
183
|
+
async get(key: string): Promise<NodeSavedState | undefined> {},
|
|
184
|
+
async del(key: string): Promise<void> {},
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
sessionStore: {
|
|
188
|
+
async set(sub: string, session: Session): Promise<void> {},
|
|
189
|
+
async get(sub: string): Promise<Session | undefined> {},
|
|
190
|
+
async del(sub: string): Promise<void> {},
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// A lock to prevent concurrent access to the session store. Optional if only one instance is running.
|
|
194
|
+
requestLock,
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
> [!NOTE]
|
|
199
|
+
>
|
|
200
|
+
> There is no `keyset` in this instance. This is due to the fact that app
|
|
201
|
+
> clients cannot safely store a private key. The `token_endpoint_auth_method` is
|
|
202
|
+
> set to `none` in the client metadata, which means that the client will not be
|
|
203
|
+
> authenticating itself to the token endpoint. This will cause sessions to have
|
|
204
|
+
> a shorter lifetime. You can circumvent this by providing a "BFF" (Backend for
|
|
205
|
+
> Frontend) that will perform an authenticated OAuth flow and use a session id
|
|
206
|
+
> based mechanism to authenticate the client.
|
|
207
|
+
|
|
208
|
+
### Common configuration options
|
|
209
|
+
|
|
210
|
+
The `OAuthClient` and `OAuthAgent` classes will manage and refresh OAuth tokens
|
|
211
|
+
transparently. They are also responsible to properly format the HTTP requests
|
|
212
|
+
payload, using DPoP, and transparently retrying requests when the access token
|
|
213
|
+
expires.
|
|
214
|
+
|
|
215
|
+
For this to work, the client must be configured with the following options:
|
|
216
|
+
|
|
217
|
+
#### `sessionStore`
|
|
218
|
+
|
|
219
|
+
A simple key-value store to save the OAuth session data. This is used to save
|
|
220
|
+
the access token, refresh token, and other session data.
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
const sessionStore: NodeSavedSessionStore = {
|
|
224
|
+
async set(sub: string, sessionData: NodeSavedSession) {
|
|
225
|
+
// Insert or update the session data in your database
|
|
226
|
+
await saveSessionDataToDb(sub, sessionData)
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
async get(sub: string) {
|
|
230
|
+
// Retrieve the session data from your database
|
|
231
|
+
const sessionData = await getSessionDataFromDb(sub)
|
|
232
|
+
if (!sessionData) return undefined
|
|
233
|
+
|
|
234
|
+
return sessionData
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
async del(sub: string) {
|
|
238
|
+
// Delete the session data from your database
|
|
239
|
+
await deleteSessionDataFromDb(sub)
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
#### `stateStore`
|
|
245
|
+
|
|
246
|
+
A simple key-value store to save the state of the OAuth
|
|
247
|
+
authorization flow. This is used to prevent CSRF attacks.
|
|
248
|
+
|
|
249
|
+
The implementation of the `StateStore` is similar to the
|
|
250
|
+
[`sessionStore`](#sessionstore).
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
interface NodeSavedStateStore {
|
|
254
|
+
set: (key: string, internalState: NodeSavedState) => Promise<void>
|
|
255
|
+
get: (key: string) => Promise<NodeSavedState | undefined>
|
|
256
|
+
del: (key: string) => Promise<void>
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
One notable exception is that state store items can (and should) be deleted
|
|
261
|
+
after a short period of time (one hour should be more than enough).
|
|
262
|
+
|
|
263
|
+
#### `requestLock`
|
|
264
|
+
|
|
265
|
+
When multiple instances of the client are running, this lock will prevent
|
|
266
|
+
concurrent refreshes of the same session. If the lock fails to be acquired an
|
|
267
|
+
error should be thrown.
|
|
268
|
+
|
|
269
|
+
Here is an example implementation based on [`redlock`](https://www.npmjs.com/package/redlock):
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { RuntimeLock } from '@atproto/oauth-client-node'
|
|
273
|
+
import Redis from 'ioredis'
|
|
274
|
+
import Redlock from 'redlock'
|
|
275
|
+
|
|
276
|
+
const redisClients = new Redis()
|
|
277
|
+
const redlock = new Redlock(redisClients)
|
|
278
|
+
|
|
279
|
+
const requestLock: RuntimeLock = async (key, fn) => {
|
|
280
|
+
// 30 seconds should be enough. Since we will be using one lock per user id
|
|
281
|
+
// we can be quite liberal with the lock duration here.
|
|
282
|
+
const lock = await redlock.lock(key, 45e3)
|
|
283
|
+
try {
|
|
284
|
+
return await fn()
|
|
285
|
+
} finally {
|
|
286
|
+
await redlock.unlock(lock)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Usage with `@atproto/api`
|
|
292
|
+
|
|
293
|
+
`@atproto/oauth-client-*` packages all return an `ApiClient` instance upon
|
|
294
|
+
successful authentication. This instance can be used to make authenticated
|
|
295
|
+
requests using all the `ApiClient` methods defined in [[API]] (non exhaustive
|
|
296
|
+
list of examples below). Any refresh of the credentials will happen under the
|
|
297
|
+
hood, and the new tokens will be saved in the session store.
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
const session = await client.restore('did:plc:123')
|
|
301
|
+
const agent = new Agent(session)
|
|
302
|
+
|
|
303
|
+
// Feeds and content
|
|
304
|
+
await agent.getTimeline(params, opts)
|
|
305
|
+
await agent.getAuthorFeed(params, opts)
|
|
306
|
+
await agent.getPostThread(params, opts)
|
|
307
|
+
await agent.getPost(params)
|
|
308
|
+
await agent.getPosts(params, opts)
|
|
309
|
+
await agent.getLikes(params, opts)
|
|
310
|
+
await agent.getRepostedBy(params, opts)
|
|
311
|
+
await agent.post(record)
|
|
312
|
+
await agent.deletePost(postUri)
|
|
313
|
+
await agent.like(uri, cid)
|
|
314
|
+
await agent.deleteLike(likeUri)
|
|
315
|
+
await agent.repost(uri, cid)
|
|
316
|
+
await agent.deleteRepost(repostUri)
|
|
317
|
+
await agent.uploadBlob(data, opts)
|
|
318
|
+
|
|
319
|
+
// Social graph
|
|
320
|
+
await agent.getFollows(params, opts)
|
|
321
|
+
await agent.getFollowers(params, opts)
|
|
322
|
+
await agent.follow(did)
|
|
323
|
+
await agent.deleteFollow(followUri)
|
|
324
|
+
|
|
325
|
+
// Actors
|
|
326
|
+
await agent.getProfile(params, opts)
|
|
327
|
+
await agent.upsertProfile(updateFn)
|
|
328
|
+
await agent.getProfiles(params, opts)
|
|
329
|
+
await agent.getSuggestions(params, opts)
|
|
330
|
+
await agent.searchActors(params, opts)
|
|
331
|
+
await agent.searchActorsTypeahead(params, opts)
|
|
332
|
+
await agent.mute(did)
|
|
333
|
+
await agent.unmute(did)
|
|
334
|
+
await agent.muteModList(listUri)
|
|
335
|
+
await agent.unmuteModList(listUri)
|
|
336
|
+
await agent.blockModList(listUri)
|
|
337
|
+
await agent.unblockModList(listUri)
|
|
338
|
+
|
|
339
|
+
// Notifications
|
|
340
|
+
await agent.listNotifications(params, opts)
|
|
341
|
+
await agent.countUnreadNotifications(params, opts)
|
|
342
|
+
await agent.updateSeenNotifications()
|
|
343
|
+
|
|
344
|
+
// Identity
|
|
345
|
+
await agent.resolveHandle(params, opts)
|
|
346
|
+
await agent.updateHandle(params, opts)
|
|
347
|
+
|
|
348
|
+
// etc.
|
|
349
|
+
|
|
350
|
+
// Always remember to revoke the credentials when you are done
|
|
351
|
+
await session.signOut()
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Advances use-cases
|
|
355
|
+
|
|
356
|
+
### Listening for session updates and deletion
|
|
357
|
+
|
|
358
|
+
The `OAuthClient` will emit events whenever a session is updated or deleted.
|
|
359
|
+
|
|
360
|
+
```ts
|
|
361
|
+
import {
|
|
362
|
+
Session,
|
|
363
|
+
TokenRefreshError,
|
|
364
|
+
TokenRevokedError,
|
|
365
|
+
} from '@atproto/oauth-client-node'
|
|
366
|
+
|
|
367
|
+
client.addEventListener('updated', (event: CustomEvent<Session>) => {
|
|
368
|
+
console.log('Refreshed tokens were saved in the store:', event.detail)
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
client.addEventListener(
|
|
372
|
+
'deleted',
|
|
373
|
+
(
|
|
374
|
+
event: CustomEvent<{
|
|
375
|
+
sub: string
|
|
376
|
+
cause: TokenRefreshError | TokenRevokedError | unknown
|
|
377
|
+
}>,
|
|
378
|
+
) => {
|
|
379
|
+
console.log('Session was deleted from the session store:', event.detail)
|
|
380
|
+
|
|
381
|
+
const { cause } = event.detail
|
|
382
|
+
|
|
383
|
+
if (cause instanceof TokenRefreshError) {
|
|
384
|
+
// - refresh_token unavailable or expired
|
|
385
|
+
// - oauth response error (`cause.cause instanceof OAuthResponseError`)
|
|
386
|
+
// - session data does not match expected values returned by the OAuth server
|
|
387
|
+
} else if (cause instanceof TokenRevokedError) {
|
|
388
|
+
// Session was revoked through:
|
|
389
|
+
// - session.signOut()
|
|
390
|
+
// - client.revoke(sub)
|
|
391
|
+
} else {
|
|
392
|
+
// An unexpected error occurred, causing the session to be deleted
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Silent Sign-In
|
|
399
|
+
|
|
400
|
+
Using silent sign-in requires to handle retries on the callback endpoint.
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
app.get('/login', async (req, res) => {
|
|
404
|
+
const handle = 'some-handle.bsky.social' // eg. from query string
|
|
405
|
+
const user = req.user.id
|
|
406
|
+
|
|
407
|
+
const url = await client.authorize(handle, {
|
|
408
|
+
// Use "prompt=none" to attempt silent sign-in
|
|
409
|
+
prompt: 'none',
|
|
410
|
+
|
|
411
|
+
// Build an internal state to map the login request to the user, and allow retries
|
|
412
|
+
state: JSON.stringify({
|
|
413
|
+
user,
|
|
414
|
+
handle,
|
|
415
|
+
}),
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
res.redirect(url)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
app.get('/atproto-oauth-callback', async (req, res) => {
|
|
422
|
+
const params = new URLSearchParams(req.url.split('?')[1])
|
|
423
|
+
try {
|
|
424
|
+
try {
|
|
425
|
+
const { session, state } = await client.callback(params)
|
|
426
|
+
|
|
427
|
+
// Process successful authentication here. For example:
|
|
428
|
+
|
|
429
|
+
const agent = new Agent(session)
|
|
430
|
+
|
|
431
|
+
const profile = await agent.getProfile({ actor: agent.did })
|
|
432
|
+
|
|
433
|
+
console.log('Bsky profile:', profile.data)
|
|
434
|
+
} catch (err) {
|
|
435
|
+
// Silent sign-in failed, retry without prompt=none
|
|
436
|
+
if (
|
|
437
|
+
err instanceof OAuthCallbackError &&
|
|
438
|
+
['login_required', 'consent_required'].includes(err.params.get('error'))
|
|
439
|
+
) {
|
|
440
|
+
// Parse previous state
|
|
441
|
+
const { user, handle } = JSON.parse(err.state)
|
|
442
|
+
|
|
443
|
+
const url = await client.authorize(handle, {
|
|
444
|
+
// Note that we omit the prompt parameter here. Setting "prompt=none"
|
|
445
|
+
// here would result in an infinite redirect loop.
|
|
446
|
+
|
|
447
|
+
// Build a new state (or re-use the previous one)
|
|
448
|
+
state: JSON.stringify({
|
|
449
|
+
user,
|
|
450
|
+
handle,
|
|
451
|
+
}),
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
// redirect to new URL
|
|
455
|
+
res.redirect(url)
|
|
456
|
+
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
throw err
|
|
461
|
+
}
|
|
462
|
+
} catch (err) {
|
|
463
|
+
next(err)
|
|
464
|
+
}
|
|
465
|
+
})
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
[ATPROTO]: https://atproto.com/ 'AT Protocol'
|
|
469
|
+
[API]: ../../api/README.md
|
package/dist/index.d.ts
ADDED
|
@@ -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;AACrC,cAAc,mBAAmB,CAAA;AAEjC,cAAc,wBAAwB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oCAAoC,CAAA;AAClD,cAAc,wBAAwB,CAAA;AACtC,cAAc,uBAAuB,CAAA;AACrC,cAAc,mBAAmB,CAAA;AAEjC,cAAc,wBAAwB,CAAA","sourcesContent":["export * from '@atproto-labs/handle-resolver-node'\nexport * from '@atproto/jwk-webcrypto'\nexport * from '@atproto/oauth-client'\nexport * from '@atproto/jwk-jose'\n\nexport * from './node-oauth-client.js'\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Jwk, Key } from '@atproto/jwk';
|
|
2
|
+
import type { InternalStateData, Session } from '@atproto/oauth-client';
|
|
3
|
+
import type { SimpleStore } from '@atproto-labs/simple-store';
|
|
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
|
+
dpopJwk?: never;
|
|
16
|
+
}>(store: SimpleStore<K, ToDpopJwkValue<V>>): SimpleStore<K, V>;
|
|
17
|
+
export type NodeSavedState = ToDpopJwkValue<InternalStateData>;
|
|
18
|
+
export type NodeSavedStateStore = SimpleStore<string, NodeSavedState>;
|
|
19
|
+
export type NodeSavedSession = ToDpopJwkValue<Session>;
|
|
20
|
+
export type NodeSavedSessionStore = SimpleStore<string, NodeSavedSession>;
|
|
21
|
+
export {};
|
|
22
|
+
//# 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,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,cAAc,CAAA;AAE5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAE7D,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,CAC5B,CAAC,SAAS,MAAM,EAChB,CAAC,SAAS;IAAE,OAAO,EAAE,GAAG,CAAC;IAAC,OAAO,CAAC,EAAE,KAAK,CAAA;CAAE,EAC3C,KAAK,EAAE,WAAW,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC,CAqB7D;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,26 @@
|
|
|
1
|
+
import { JoseKey } from '@atproto/jwk-jose';
|
|
2
|
+
/**
|
|
3
|
+
* Utility function that allows to simplify the store interface by exposing a
|
|
4
|
+
* JWK (JSON) instead of a Key instance.
|
|
5
|
+
*/
|
|
6
|
+
export function toDpopKeyStore(store) {
|
|
7
|
+
return {
|
|
8
|
+
async set(sub, { dpopKey, ...data }) {
|
|
9
|
+
const dpopJwk = dpopKey.privateJwk;
|
|
10
|
+
if (!dpopJwk)
|
|
11
|
+
throw new Error('Private DPoP JWK is missing.');
|
|
12
|
+
await store.set(sub, { ...data, dpopJwk });
|
|
13
|
+
},
|
|
14
|
+
async get(sub) {
|
|
15
|
+
const result = await store.get(sub);
|
|
16
|
+
if (!result)
|
|
17
|
+
return undefined;
|
|
18
|
+
const { dpopJwk, ...data } = result;
|
|
19
|
+
const dpopKey = await JoseKey.fromJWK(dpopJwk);
|
|
20
|
+
return { ...data, dpopKey };
|
|
21
|
+
},
|
|
22
|
+
del: store.del.bind(store),
|
|
23
|
+
clear: store.clear?.bind(store),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# 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":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAQ3C;;;GAGG;AACH,MAAM,UAAU,cAAc,CAG5B,KAAwC;IACxC,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,OAAO,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","sourcesContent":["import type { Jwk, Key } from '@atproto/jwk'\nimport { JoseKey } from '@atproto/jwk-jose'\nimport type { InternalStateData, Session } from '@atproto/oauth-client'\nimport type { SimpleStore } from '@atproto-labs/simple-store'\n\ntype ToDpopJwkValue<V extends { dpopKey: Key }> = Omit<V, 'dpopKey'> & {\n dpopJwk: Jwk\n}\n\n/**\n * Utility function that allows to simplify the store interface by exposing a\n * JWK (JSON) instead of a Key instance.\n */\nexport function toDpopKeyStore<\n K extends string,\n V extends { dpopKey: Key; dpopJwk?: never },\n>(store: SimpleStore<K, ToDpopJwkValue<V>>): SimpleStore<K, V> {\n return {\n async set(sub: K, { dpopKey, ...data }: V) {\n const dpopJwk = dpopKey.privateJwk\n if (!dpopJwk) throw new Error('Private DPoP JWK is missing.')\n\n await store.set(sub, { ...data, dpopJwk })\n },\n\n async get(sub: K) {\n const result = await store.get(sub)\n if (!result) return undefined\n\n const { dpopJwk, ...data } = result\n const dpopKey = await JoseKey.fromJWK(dpopJwk)\n return { ...data, dpopKey } as unknown as V\n },\n\n del: store.del.bind(store),\n clear: store.clear?.bind(store),\n }\n}\n\nexport type NodeSavedState = ToDpopJwkValue<InternalStateData>\nexport type NodeSavedStateStore = SimpleStore<string, NodeSavedState>\n\nexport type NodeSavedSession = ToDpopJwkValue<Session>\nexport type NodeSavedSessionStore = SimpleStore<string, NodeSavedSession>\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type HandleResolver, OAuthClient, type OAuthClientFetchMetadataOptions, type OAuthClientOptions, type RuntimeImplementation, type RuntimeLock } from '@atproto/oauth-client';
|
|
2
|
+
import type { OAuthResponseMode } from '@atproto/oauth-types';
|
|
3
|
+
import { type AtprotoHandleResolverNodeOptions } from '@atproto-labs/handle-resolver-node';
|
|
4
|
+
import { type NodeSavedSessionStore, type NodeSavedStateStore } from './node-dpop-store.js';
|
|
5
|
+
import type { Override } from './util.js';
|
|
6
|
+
export type * from './node-dpop-store.js';
|
|
7
|
+
export type { OAuthClientOptions, OAuthResponseMode, RuntimeLock };
|
|
8
|
+
export type NodeOAuthClientOptions = Override<OAuthClientOptions, {
|
|
9
|
+
responseMode?: Exclude<OAuthResponseMode, 'fragment'>;
|
|
10
|
+
stateStore: NodeSavedStateStore;
|
|
11
|
+
sessionStore: NodeSavedSessionStore;
|
|
12
|
+
/**
|
|
13
|
+
* Used to build a {@link NodeOAuthClientOptions.handleResolver} if none is
|
|
14
|
+
* provided.
|
|
15
|
+
*/
|
|
16
|
+
fallbackNameservers?: AtprotoHandleResolverNodeOptions['fallbackNameservers'];
|
|
17
|
+
handleResolver?: HandleResolver | string | URL;
|
|
18
|
+
/**
|
|
19
|
+
* Used to build a {@link NodeOAuthClientOptions.runtimeImplementation} if
|
|
20
|
+
* none is provided. Pass in `requestLocalLock` from `@atproto/oauth-client`
|
|
21
|
+
* to mute warning.
|
|
22
|
+
*/
|
|
23
|
+
requestLock?: RuntimeLock;
|
|
24
|
+
runtimeImplementation?: RuntimeImplementation;
|
|
25
|
+
}>;
|
|
26
|
+
export type NodeOAuthClientFromMetadataOptions = OAuthClientFetchMetadataOptions & Omit<NodeOAuthClientOptions, 'clientMetadata'>;
|
|
27
|
+
export declare class NodeOAuthClient extends OAuthClient {
|
|
28
|
+
constructor({ requestLock, fallbackNameservers, fetch, responseMode, stateStore, sessionStore, handleResolver, runtimeImplementation, ...options }: NodeOAuthClientOptions);
|
|
29
|
+
}
|
|
30
|
+
//# 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,EACL,KAAK,cAAc,EACnB,WAAW,EACX,KAAK,+BAA+B,EACpC,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAC7D,OAAO,EAEL,KAAK,gCAAgC,EACtC,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EAEzB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAEzC,mBAAmB,sBAAsB,CAAA;AACzC,YAAY,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,CAAA;AAElE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAC3C,kBAAkB,EAClB;IACE,YAAY,CAAC,EAAE,OAAO,CAAC,iBAAiB,EAAE,UAAU,CAAC,CAAA;IAErD,UAAU,EAAE,mBAAmB,CAAA;IAC/B,YAAY,EAAE,qBAAqB,CAAA;IAEnC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,gCAAgC,CAAC,qBAAqB,CAAC,CAAA;IAE7E,cAAc,CAAC,EAAE,cAAc,GAAG,MAAM,GAAG,GAAG,CAAA;IAE9C;;;;OAIG;IACH,WAAW,CAAC,EAAE,WAAW,CAAA;IAEzB,qBAAqB,CAAC,EAAE,qBAAqB,CAAA;CAC9C,CACF,CAAA;AAED,MAAM,MAAM,kCAAkC,GAC5C,+BAA+B,GAC7B,IAAI,CAAC,sBAAsB,EAAE,gBAAgB,CAAC,CAAA;AAElD,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,YAAY,EACV,WAAuB,EACvB,mBAA+B,EAE/B,KAAK,EACL,YAAsB,EAEtB,UAAU,EACV,YAAY,EAEZ,cAGE,EAEF,qBAMC,EAED,GAAG,OAAO,EACX,EAAE,sBAAsB,EAiBxB;CACF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { JoseKey } from '@atproto/jwk-jose';
|
|
3
|
+
import { OAuthClient, } from '@atproto/oauth-client';
|
|
4
|
+
import { AtprotoHandleResolverNode, } from '@atproto-labs/handle-resolver-node';
|
|
5
|
+
import { toDpopKeyStore, } from './node-dpop-store.js';
|
|
6
|
+
export class NodeOAuthClient extends OAuthClient {
|
|
7
|
+
constructor({ requestLock = undefined, fallbackNameservers = undefined, fetch, responseMode = 'query', stateStore, sessionStore, handleResolver = new AtprotoHandleResolverNode({
|
|
8
|
+
fetch,
|
|
9
|
+
fallbackNameservers,
|
|
10
|
+
}), runtimeImplementation = {
|
|
11
|
+
requestLock,
|
|
12
|
+
createKey: (algs) => JoseKey.generate(algs),
|
|
13
|
+
getRandomValues: randomBytes,
|
|
14
|
+
digest: (bytes, algorithm) => createHash(algorithm.name).update(bytes).digest(),
|
|
15
|
+
}, ...options }) {
|
|
16
|
+
if (!runtimeImplementation.requestLock) {
|
|
17
|
+
// Ok if only one instance of the client is running at a time.
|
|
18
|
+
console.warn('No lock mechanism provided. Credentials might get revoked.');
|
|
19
|
+
}
|
|
20
|
+
super({
|
|
21
|
+
...options,
|
|
22
|
+
fetch,
|
|
23
|
+
responseMode,
|
|
24
|
+
handleResolver,
|
|
25
|
+
runtimeImplementation,
|
|
26
|
+
stateStore: toDpopKeyStore(stateStore),
|
|
27
|
+
sessionStore: toDpopKeyStore(sessionStore),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
//# 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,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAA;AAC3C,OAAO,EAEL,WAAW,GAKZ,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EACL,yBAAyB,GAE1B,MAAM,oCAAoC,CAAA;AAC3C,OAAO,EAGL,cAAc,GACf,MAAM,sBAAsB,CAAA;AAqC7B,MAAM,OAAO,eAAgB,SAAQ,WAAW;IAC9C,YAAY,EACV,WAAW,GAAG,SAAS,EACvB,mBAAmB,GAAG,SAAS,EAE/B,KAAK,EACL,YAAY,GAAG,OAAO,EAEtB,UAAU,EACV,YAAY,EAEZ,cAAc,GAAG,IAAI,yBAAyB,CAAC;QAC7C,KAAK;QACL,mBAAmB;KACpB,CAAC,EAEF,qBAAqB,GAAG;QACtB,WAAW;QACX,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;QAC3C,eAAe,EAAE,WAAW;QAC5B,MAAM,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,CAC3B,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE;KACpD,EAED,GAAG,OAAO,EACa;QACvB,IAAI,CAAC,qBAAqB,CAAC,WAAW,EAAE,CAAC;YACvC,8DAA8D;YAC9D,OAAO,CAAC,IAAI,CAAC,4DAA4D,CAAC,CAAA;QAC5E,CAAC;QAED,KAAK,CAAC;YACJ,GAAG,OAAO;YAEV,KAAK;YACL,YAAY;YACZ,cAAc;YACd,qBAAqB;YAErB,UAAU,EAAE,cAAc,CAAC,UAAU,CAAC;YACtC,YAAY,EAAE,cAAc,CAAC,YAAY,CAAC;SAC3C,CAAC,CAAA;IACJ,CAAC;CACF","sourcesContent":["import { createHash, randomBytes } from 'node:crypto'\nimport { JoseKey } from '@atproto/jwk-jose'\nimport {\n type HandleResolver,\n OAuthClient,\n type OAuthClientFetchMetadataOptions,\n type OAuthClientOptions,\n type RuntimeImplementation,\n type RuntimeLock,\n} from '@atproto/oauth-client'\nimport type { OAuthResponseMode } from '@atproto/oauth-types'\nimport {\n AtprotoHandleResolverNode,\n type AtprotoHandleResolverNodeOptions,\n} from '@atproto-labs/handle-resolver-node'\nimport {\n type NodeSavedSessionStore,\n type NodeSavedStateStore,\n toDpopKeyStore,\n} from './node-dpop-store.js'\nimport type { Override } from './util.js'\n\nexport type * from './node-dpop-store.js'\nexport type { OAuthClientOptions, OAuthResponseMode, RuntimeLock }\n\nexport type NodeOAuthClientOptions = Override<\n OAuthClientOptions,\n {\n responseMode?: Exclude<OAuthResponseMode, 'fragment'>\n\n stateStore: NodeSavedStateStore\n sessionStore: NodeSavedSessionStore\n\n /**\n * Used to build a {@link NodeOAuthClientOptions.handleResolver} if none is\n * provided.\n */\n fallbackNameservers?: AtprotoHandleResolverNodeOptions['fallbackNameservers']\n\n handleResolver?: HandleResolver | string | URL\n\n /**\n * Used to build a {@link NodeOAuthClientOptions.runtimeImplementation} if\n * none is provided. Pass in `requestLocalLock` from `@atproto/oauth-client`\n * to mute warning.\n */\n requestLock?: RuntimeLock\n\n runtimeImplementation?: RuntimeImplementation\n }\n>\n\nexport type NodeOAuthClientFromMetadataOptions =\n OAuthClientFetchMetadataOptions &\n Omit<NodeOAuthClientOptions, 'clientMetadata'>\n\nexport class NodeOAuthClient extends OAuthClient {\n constructor({\n requestLock = undefined,\n fallbackNameservers = undefined,\n\n fetch,\n responseMode = 'query',\n\n stateStore,\n sessionStore,\n\n handleResolver = new AtprotoHandleResolverNode({\n fetch,\n fallbackNameservers,\n }),\n\n runtimeImplementation = {\n requestLock,\n createKey: (algs) => JoseKey.generate(algs),\n getRandomValues: randomBytes,\n digest: (bytes, algorithm) =>\n createHash(algorithm.name).update(bytes).digest(),\n },\n\n ...options\n }: NodeOAuthClientOptions) {\n if (!runtimeImplementation.requestLock) {\n // Ok if only one instance of the client is running at a time.\n console.warn('No lock mechanism provided. Credentials might get revoked.')\n }\n\n super({\n ...options,\n\n fetch,\n responseMode,\n handleResolver,\n runtimeImplementation,\n\n stateStore: toDpopKeyStore(stateStore),\n sessionStore: toDpopKeyStore(sessionStore),\n })\n }\n}\n"]}
|
package/dist/util.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,EAAE,CAAA;AACvD,MAAM,MAAM,QAAQ,CAAC,CAAC,EAAE,CAAC,IAAI,QAAQ,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA"}
|
package/dist/util.js
ADDED
package/dist/util.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"","sourcesContent":["export type Simplify<T> = { [K in keyof T]: T[K] } & {}\nexport type Override<T, V> = Simplify<V & Omit<T, keyof V>>\n"]}
|