@atproto/lex-password-session 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +413 -0
  2. package/dist/error.d.ts +8 -0
  3. package/dist/error.d.ts.map +1 -0
  4. package/dist/error.js +14 -0
  5. package/dist/error.js.map +1 -0
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +6 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/lexicons/com/atproto/server/createAccount.d.ts +3 -0
  11. package/dist/lexicons/com/atproto/server/createAccount.d.ts.map +1 -0
  12. package/dist/lexicons/com/atproto/server/createAccount.defs.d.ts +57 -0
  13. package/dist/lexicons/com/atproto/server/createAccount.defs.d.ts.map +1 -0
  14. package/dist/lexicons/com/atproto/server/createAccount.defs.js +43 -0
  15. package/dist/lexicons/com/atproto/server/createAccount.defs.js.map +1 -0
  16. package/dist/lexicons/com/atproto/server/createAccount.js +10 -0
  17. package/dist/lexicons/com/atproto/server/createAccount.js.map +1 -0
  18. package/dist/lexicons/com/atproto/server/createSession.d.ts +3 -0
  19. package/dist/lexicons/com/atproto/server/createSession.d.ts.map +1 -0
  20. package/dist/lexicons/com/atproto/server/createSession.defs.d.ts +53 -0
  21. package/dist/lexicons/com/atproto/server/createSession.defs.d.ts.map +1 -0
  22. package/dist/lexicons/com/atproto/server/createSession.defs.js +35 -0
  23. package/dist/lexicons/com/atproto/server/createSession.defs.js.map +1 -0
  24. package/dist/lexicons/com/atproto/server/createSession.js +10 -0
  25. package/dist/lexicons/com/atproto/server/createSession.js.map +1 -0
  26. package/dist/lexicons/com/atproto/server/deleteSession.d.ts +3 -0
  27. package/dist/lexicons/com/atproto/server/deleteSession.d.ts.map +1 -0
  28. package/dist/lexicons/com/atproto/server/deleteSession.defs.d.ts +13 -0
  29. package/dist/lexicons/com/atproto/server/deleteSession.defs.d.ts.map +1 -0
  30. package/dist/lexicons/com/atproto/server/deleteSession.defs.js +19 -0
  31. package/dist/lexicons/com/atproto/server/deleteSession.defs.js.map +1 -0
  32. package/dist/lexicons/com/atproto/server/deleteSession.js +10 -0
  33. package/dist/lexicons/com/atproto/server/deleteSession.js.map +1 -0
  34. package/dist/lexicons/com/atproto/server/getSession.d.ts +3 -0
  35. package/dist/lexicons/com/atproto/server/getSession.d.ts.map +1 -0
  36. package/dist/lexicons/com/atproto/server/getSession.defs.d.ts +37 -0
  37. package/dist/lexicons/com/atproto/server/getSession.defs.d.ts.map +1 -0
  38. package/dist/lexicons/com/atproto/server/getSession.defs.js +27 -0
  39. package/dist/lexicons/com/atproto/server/getSession.defs.js.map +1 -0
  40. package/dist/lexicons/com/atproto/server/getSession.js +10 -0
  41. package/dist/lexicons/com/atproto/server/getSession.js.map +1 -0
  42. package/dist/lexicons/com/atproto/server/refreshSession.d.ts +3 -0
  43. package/dist/lexicons/com/atproto/server/refreshSession.d.ts.map +1 -0
  44. package/dist/lexicons/com/atproto/server/refreshSession.defs.d.ts +43 -0
  45. package/dist/lexicons/com/atproto/server/refreshSession.defs.d.ts.map +1 -0
  46. package/dist/lexicons/com/atproto/server/refreshSession.defs.js +30 -0
  47. package/dist/lexicons/com/atproto/server/refreshSession.defs.js.map +1 -0
  48. package/dist/lexicons/com/atproto/server/refreshSession.js +10 -0
  49. package/dist/lexicons/com/atproto/server/refreshSession.js.map +1 -0
  50. package/dist/lexicons/com/atproto/server.d.ts +6 -0
  51. package/dist/lexicons/com/atproto/server.d.ts.map +1 -0
  52. package/dist/lexicons/com/atproto/server.js +13 -0
  53. package/dist/lexicons/com/atproto/server.js.map +1 -0
  54. package/dist/lexicons/com/atproto.d.ts +2 -0
  55. package/dist/lexicons/com/atproto.d.ts.map +1 -0
  56. package/dist/lexicons/com/atproto.js +9 -0
  57. package/dist/lexicons/com/atproto.js.map +1 -0
  58. package/dist/lexicons/com.d.ts +2 -0
  59. package/dist/lexicons/com.d.ts.map +1 -0
  60. package/dist/lexicons/com.js +9 -0
  61. package/dist/lexicons/com.js.map +1 -0
  62. package/dist/lexicons/index.d.ts +2 -0
  63. package/dist/lexicons/index.d.ts.map +1 -0
  64. package/dist/lexicons/index.js +9 -0
  65. package/dist/lexicons/index.js.map +1 -0
  66. package/dist/password-session.d.ts +127 -0
  67. package/dist/password-session.d.ts.map +1 -0
  68. package/dist/password-session.js +242 -0
  69. package/dist/password-session.js.map +1 -0
  70. package/dist/util.d.ts +5 -0
  71. package/dist/util.d.ts.map +1 -0
  72. package/dist/util.js +46 -0
  73. package/dist/util.js.map +1 -0
  74. package/package.json +52 -0
  75. package/src/error.ts +14 -0
  76. package/src/index.ts +2 -0
  77. package/src/lexicons/com/atproto/server/createAccount.defs.ts +56 -0
  78. package/src/lexicons/com/atproto/server/createAccount.ts +6 -0
  79. package/src/lexicons/com/atproto/server/createSession.defs.ts +48 -0
  80. package/src/lexicons/com/atproto/server/createSession.ts +6 -0
  81. package/src/lexicons/com/atproto/server/deleteSession.defs.ts +32 -0
  82. package/src/lexicons/com/atproto/server/deleteSession.ts +6 -0
  83. package/src/lexicons/com/atproto/server/getSession.defs.ts +36 -0
  84. package/src/lexicons/com/atproto/server/getSession.ts +6 -0
  85. package/src/lexicons/com/atproto/server/refreshSession.defs.ts +43 -0
  86. package/src/lexicons/com/atproto/server/refreshSession.ts +6 -0
  87. package/src/lexicons/com/atproto/server.ts +9 -0
  88. package/src/lexicons/com/atproto.ts +5 -0
  89. package/src/lexicons/com.ts +5 -0
  90. package/src/lexicons/index.ts +5 -0
  91. package/src/password-session-utils.test.ts +177 -0
  92. package/src/password-session.test.ts +416 -0
  93. package/src/password-session.ts +404 -0
  94. package/src/util.ts +61 -0
  95. package/tsconfig.build.json +12 -0
  96. package/tsconfig.json +7 -0
  97. package/tsconfig.tests.json +9 -0
@@ -0,0 +1,404 @@
1
+ import {
2
+ Agent,
3
+ LexRpcError,
4
+ LexRpcFailure,
5
+ buildAgent,
6
+ xrpcSafe,
7
+ } from '@atproto/lex-client'
8
+ import { LexAuthFactorError } from './error.js'
9
+ import { com } from './lexicons/index.js'
10
+ import { extractLexRpcErrorCode, extractPdsUrl, noop } from './util.js'
11
+
12
+ export type RefreshFailure = LexRpcFailure<
13
+ typeof com.atproto.server.refreshSession.main
14
+ >
15
+
16
+ export type DeleteFailure = LexRpcFailure<
17
+ typeof com.atproto.server.deleteSession.main
18
+ >
19
+
20
+ export type SessionData = com.atproto.server.createSession.OutputBody & {
21
+ service: string
22
+ }
23
+
24
+ export type PasswordSessionOptions = {
25
+ /**
26
+ * Custom fetch implementation to use for network requests
27
+ */
28
+ fetch?: typeof globalThis.fetch
29
+
30
+ /**
31
+ * Called whenever the session is successfully created/refreshed, and new
32
+ * credentials have been obtained. Use this hook to persist the updated
33
+ * session information.
34
+ *
35
+ * If this callback returns a promise, this function will never be called
36
+ * again (on the same process) until the promise resolves.
37
+ *
38
+ * @note this function **must** not throw
39
+ */
40
+ onUpdated: (this: PasswordSession, data: SessionData) => void | Promise<void>
41
+
42
+ /**
43
+ * Called whenever the session update fails due to an expected error, such as
44
+ * a network issue or server unavailability. This function can be used to log
45
+ * the error or notify the user, but should not assume that the session is
46
+ * invalid.
47
+ *
48
+ * @note this function **must** not throw
49
+ */
50
+ onUpdateFailure?: (
51
+ this: PasswordSession,
52
+ data: SessionData,
53
+ err: RefreshFailure,
54
+ ) => void | Promise<void>
55
+
56
+ /**
57
+ * Called whenever the session is deleted, either due to an explicit logout or
58
+ * because the refresh operation indicated that the session is no longer
59
+ * valid. Use this hook to clean up any persisted session information and
60
+ * update the application state accordingly.
61
+ *
62
+ * @note this function **must** not throw
63
+ */
64
+ onDeleted: (this: PasswordSession, data: SessionData) => void | Promise<void>
65
+
66
+ /**
67
+ * Called whenever a session deletion fails due to an unexpected error, such
68
+ * as a network issue or server unavailability. This function can be used to
69
+ * log the error or notify the user. When this function is called, the session
70
+ * might still be valid on the server. It is up to the implementation to
71
+ * decide whether to retry the deletion or keep the session active. Ignoring
72
+ * these errors is not recommended as it can lead to orphaned sessions on the
73
+ * server, or security issues if the user believes they have logged out when a
74
+ * bad actor is still using the session. The implementation should consider
75
+ * keeping track of failed deletions and retrying them later, until they
76
+ * succeed.
77
+ *
78
+ * @note this function **must** not throw
79
+ */
80
+ onDeleteFailure?: (
81
+ this: PasswordSession,
82
+ data: SessionData,
83
+ err: DeleteFailure,
84
+ ) => void | Promise<void>
85
+ }
86
+
87
+ export class PasswordSession implements Agent {
88
+ /**
89
+ * Internal {@link Agent} used for session management towards the
90
+ * authentication service only.
91
+ */
92
+ #serviceAgent: Agent
93
+
94
+ #sessionData: null | SessionData
95
+ #sessionPromise: Promise<SessionData>
96
+
97
+ constructor(
98
+ sessionData: SessionData,
99
+ protected readonly options: PasswordSessionOptions,
100
+ ) {
101
+ this.#serviceAgent = buildAgent({
102
+ service: sessionData.service,
103
+ fetch: options.fetch,
104
+ })
105
+
106
+ this.#sessionData = sessionData
107
+ this.#sessionPromise = Promise.resolve(this.#sessionData)
108
+ }
109
+
110
+ get did() {
111
+ return this.session.did
112
+ }
113
+
114
+ get handle() {
115
+ return this.session.handle
116
+ }
117
+
118
+ get session() {
119
+ if (this.#sessionData) return this.#sessionData
120
+ throw new LexRpcError('AuthenticationRequired', 'Logged out')
121
+ }
122
+
123
+ get destroyed(): boolean {
124
+ return this.#sessionData === null
125
+ }
126
+
127
+ async fetchHandler(path: string, init: RequestInit): Promise<Response> {
128
+ const headers = new Headers(init.headers)
129
+ if (headers.has('authorization')) {
130
+ throw new TypeError("Unexpected 'authorization' header set")
131
+ }
132
+
133
+ const sessionPromise = this.#sessionPromise
134
+ const sessionData = await sessionPromise
135
+
136
+ const fetch = this.options.fetch ?? globalThis.fetch
137
+
138
+ headers.set('authorization', `Bearer ${sessionData.accessJwt}`)
139
+ const initialRes = await fetch(fetchUrl(sessionData, path), {
140
+ ...init,
141
+ headers,
142
+ })
143
+
144
+ const refreshNeeded =
145
+ initialRes.status === 401 ||
146
+ (initialRes.status === 400 &&
147
+ (await extractLexRpcErrorCode(initialRes)) === 'ExpiredToken')
148
+
149
+ if (!refreshNeeded) {
150
+ return initialRes
151
+ }
152
+
153
+ // Refresh session (unless it was already refreshed in the meantime)
154
+ const newSessionPromise =
155
+ this.#sessionPromise === sessionPromise
156
+ ? this.refresh()
157
+ : this.#sessionPromise
158
+
159
+ // Error should have been propagated through hooks
160
+ const newSessionData = await newSessionPromise.catch((_err) => null)
161
+ if (!newSessionData) {
162
+ return initialRes
163
+ }
164
+
165
+ // refresh silently failed, no point in retrying.
166
+ if (newSessionData.accessJwt === sessionData.accessJwt) {
167
+ return initialRes
168
+ }
169
+
170
+ if (init?.signal?.aborted) {
171
+ return initialRes
172
+ }
173
+
174
+ // The stream was already consumed. We cannot retry the request. A solution
175
+ // would be to tee() the input stream but that would bufferize the entire
176
+ // stream in memory which can lead to memory starvation. Instead, we will
177
+ // return the original response and let the calling code handle retries.
178
+ if (ReadableStream && init?.body instanceof ReadableStream) {
179
+ return initialRes
180
+ }
181
+
182
+ // Make sure the initial request is cancelled to avoid leaking resources
183
+ // (NodeJS 👀): https://undici.nodejs.org/#/?id=garbage-collection
184
+ if (!initialRes.bodyUsed) {
185
+ await initialRes.body?.cancel()
186
+ }
187
+
188
+ // Finally, retry the request with the new access token
189
+ headers.set('authorization', `Bearer ${newSessionData.accessJwt}`)
190
+ return fetch(fetchUrl(newSessionData, path), { ...init, headers })
191
+ }
192
+
193
+ async refresh(): Promise<SessionData> {
194
+ this.#sessionPromise = this.#sessionPromise.then(async (sessionData) => {
195
+ const response = await xrpcSafe(
196
+ this.#serviceAgent,
197
+ com.atproto.server.refreshSession.main,
198
+ { headers: { Authorization: `Bearer ${sessionData.refreshJwt}` } },
199
+ )
200
+
201
+ if (!response.success && response.matchesSchema()) {
202
+ // Expected errors that indicate the session is no longer valid
203
+ await this.options.onDeleted.call(this, sessionData)
204
+
205
+ // Update the session promise to a rejected state
206
+ this.#sessionData = null
207
+ throw response
208
+ }
209
+
210
+ if (!response.success) {
211
+ // We failed to refresh the token, assume the session might still be
212
+ // valid by returning the existing session.
213
+ await this.options.onUpdateFailure?.call(this, sessionData, response)
214
+
215
+ return sessionData
216
+ }
217
+
218
+ const data = response.body
219
+
220
+ // Historically, refreshSession did not return all the fields from
221
+ // getSession. In particular, emailConfirmed and didDoc were missing.
222
+ // Similarly, some servers might not return the didDoc in refreshSession.
223
+ // We fetch them via getSession if missing, allowing to ensure that we are
224
+ // always talking with the right PDS.
225
+ if (data.emailConfirmed == null || data.didDoc == null) {
226
+ const extraData = await xrpcSafe(
227
+ this.#serviceAgent,
228
+ com.atproto.server.getSession.main,
229
+ { headers: { Authorization: `Bearer ${data.accessJwt}` } },
230
+ )
231
+ if (extraData.success && extraData.body.did === data.did) {
232
+ Object.assign(data, extraData.body)
233
+ }
234
+ }
235
+
236
+ const newSession: SessionData = {
237
+ ...data,
238
+ service: sessionData.service,
239
+ }
240
+
241
+ await this.options.onUpdated.call(this, newSession)
242
+
243
+ return (this.#sessionData = newSession)
244
+ })
245
+
246
+ return this.#sessionPromise
247
+ }
248
+
249
+ async logout(): Promise<void> {
250
+ let reason: DeleteFailure | null = null
251
+
252
+ this.#sessionPromise = this.#sessionPromise.then(async (sessionData) => {
253
+ const result = await xrpcSafe(
254
+ this.#serviceAgent,
255
+ com.atproto.server.deleteSession.main,
256
+ { headers: { Authorization: `Bearer ${sessionData.refreshJwt}` } },
257
+ )
258
+
259
+ if (result.success || result.matchesSchema()) {
260
+ await this.options.onDeleted.call(this, sessionData)
261
+
262
+ // Update the session promise to a rejected state
263
+ this.#sessionData = null
264
+ throw new LexRpcError('AuthenticationRequired', 'Logged out')
265
+ } else {
266
+ // Capture the reason for the failure to re-throw in the outer promise
267
+ reason = result
268
+
269
+ // An unknown/unexpected error occurred (network, server down, etc)
270
+ await this.options.onDeleteFailure?.call(this, sessionData, result)
271
+
272
+ // Keep the session in an active state
273
+ return sessionData
274
+ }
275
+ })
276
+
277
+ return this.#sessionPromise.then(
278
+ (_session) => {
279
+ // If the promise above resolved, then logout failed. Re-throw the
280
+ // reason captured earlier.
281
+ throw reason!
282
+ },
283
+ (_err) => {
284
+ // Successful logout
285
+ },
286
+ )
287
+ }
288
+
289
+ /**
290
+ * @note It is **not** recommended to use {@link PasswordSession} with main
291
+ * account credentials. Instead, it is strongly advised to use OAuth based
292
+ * authentication for main username/password credentials and use
293
+ * {@link PasswordSession} with an app-password, for bots, scripts, or similar
294
+ * use-cases.
295
+ *
296
+ * @throws If unable to create a session. In particular, if the server
297
+ * requires a 2FA token, a {@link LexRpcResponseError} with the
298
+ * `AuthFactorTokenRequired` error code will be thrown.
299
+ *
300
+ *
301
+ * @example Handling 2FA errors
302
+ *
303
+ * ```ts
304
+ * try {
305
+ * const session = await PasswordSession.create({
306
+ * service: 'https://example.com',
307
+ * identifier: 'alice',
308
+ * password: 'correct horse battery staple',
309
+ * })
310
+ * } catch (err) {
311
+ * if (err instanceof LexRpcResponseError && err.error === 'AuthFactorTokenRequired') {
312
+ * // Prompt user for 2FA token and re-attempt session creation
313
+ * }
314
+ * }
315
+ * ```
316
+ */
317
+ static async create({
318
+ service,
319
+ identifier,
320
+ password,
321
+ allowTakendown,
322
+ authFactorToken,
323
+ ...options
324
+ }: PasswordSessionOptions & {
325
+ service: string | URL
326
+ identifier: string
327
+ password: string
328
+ allowTakendown?: boolean
329
+ authFactorToken?: string
330
+ }): Promise<PasswordSession> {
331
+ const xrpcAgent = buildAgent({
332
+ service,
333
+ fetch: options.fetch,
334
+ })
335
+
336
+ const response = await xrpcSafe(
337
+ xrpcAgent,
338
+ com.atproto.server.createSession.main,
339
+ { body: { identifier, password, allowTakendown, authFactorToken } },
340
+ )
341
+
342
+ if (!response.success) {
343
+ if (response.error === 'AuthFactorTokenRequired') {
344
+ throw new LexAuthFactorError(response)
345
+ }
346
+ throw response.reason
347
+ }
348
+
349
+ const data: SessionData = {
350
+ ...response.body,
351
+ service: String(service),
352
+ }
353
+
354
+ const agent = new PasswordSession(data, options)
355
+ await options.onUpdated.call(agent, data)
356
+ return agent
357
+ }
358
+
359
+ /**
360
+ * Resume an existing session, ensuring it is still valid by refreshing it.
361
+ * Any error thrown here indicates that the session is definitely no longer
362
+ * valid. Network errors will be propagated through the
363
+ * {@link PasswordSessionOptions.onUpdateFailure} hook, and not re-thrown
364
+ * here. This means that a resolved promise does not necessarily indicate a
365
+ * valid session, only that it's refresh did not definitively fail.
366
+ *
367
+ * This is the same as calling {@link PasswordSession.refresh} after
368
+ * constructing the {@link PasswordSession} manually.
369
+ *
370
+ * @throws If, and only if, the session is definitely no longer valid.
371
+ */
372
+ static async resume(
373
+ data: SessionData,
374
+ options: PasswordSessionOptions,
375
+ ): Promise<PasswordSession> {
376
+ const agent = new PasswordSession(data, options)
377
+ await agent.refresh()
378
+ return agent
379
+ }
380
+
381
+ /**
382
+ * Delete a session without having to {@link resume resume()} it first, or
383
+ * provide hooks.
384
+ *
385
+ * @throws In case of unexpected error (network issue, server down, etc)
386
+ * meaning that the session may still be valid.
387
+ */
388
+ static async delete(
389
+ data: SessionData,
390
+ options?: Partial<PasswordSessionOptions>,
391
+ ): Promise<void> {
392
+ const agent = new PasswordSession(data, {
393
+ ...options,
394
+ onUpdated: options?.onUpdated ?? noop,
395
+ onDeleted: options?.onDeleted ?? noop,
396
+ })
397
+ await agent.logout()
398
+ }
399
+ }
400
+
401
+ function fetchUrl(sessionData: SessionData, path: string): URL {
402
+ const pdsUrl = extractPdsUrl(sessionData.didDoc)
403
+ return new URL(path, pdsUrl ?? sessionData.service)
404
+ }
package/src/util.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { LexMap, LexValue } from '@atproto/lex-client'
2
+ import { l } from '@atproto/lex-schema'
3
+
4
+ export const noop = () => {}
5
+
6
+ export async function extractLexRpcErrorCode(
7
+ response: Response,
8
+ ): Promise<string | null> {
9
+ const json = await peekJson(response, 10 * 1024) // Avoid reading large bodies
10
+ if (json === undefined) return null
11
+ if (!l.lexErrorData.matches(json)) return null
12
+ return json.error
13
+ }
14
+
15
+ async function peekJson(
16
+ response: Response,
17
+ maxSize = Infinity,
18
+ ): Promise<undefined | LexValue> {
19
+ const type = extractType(response)
20
+ if (type !== 'application/json') return undefined
21
+ const length = extractLength(response)
22
+ if (length != null && length > maxSize) return undefined
23
+
24
+ try {
25
+ return (await response.clone().json()) as Promise<LexValue>
26
+ } catch {
27
+ return undefined
28
+ }
29
+ }
30
+
31
+ function extractLength({ headers }: Response) {
32
+ return headers.get('Content-Length')
33
+ ? Number(headers.get('Content-Length'))
34
+ : undefined
35
+ }
36
+
37
+ function extractType({ headers }: Response) {
38
+ return headers.get('Content-Type')?.split(';')[0]?.trim().toLowerCase()
39
+ }
40
+
41
+ export function extractPdsUrl(didDoc?: LexMap): string | null {
42
+ const pdsService = ifArray(didDoc?.service)?.find((service) =>
43
+ ifString((service as any)?.id)?.endsWith('#atproto_pds'),
44
+ )
45
+ const pdsEndpoint = ifString((pdsService as any)?.serviceEndpoint)
46
+ return pdsEndpoint && URL.canParse(pdsEndpoint) ? pdsEndpoint : null
47
+ }
48
+
49
+ const ifString = <T>(v: T) =>
50
+ (typeof v === 'string' ? v : undefined) as unknown extends T
51
+ ? undefined | string
52
+ : T extends string
53
+ ? string
54
+ : undefined
55
+
56
+ const ifArray = <T>(v: T) =>
57
+ (Array.isArray(v) ? v : undefined) as unknown extends T
58
+ ? undefined | unknown[]
59
+ : T extends unknown[]
60
+ ? Extract<T, unknown[]>
61
+ : undefined
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": ["../../../tsconfig/isomorphic.json"],
3
+ "include": ["./src"],
4
+ "exclude": ["**/*.test.ts"],
5
+ "compilerOptions": {
6
+ "noImplicitAny": true,
7
+ "importHelpers": true,
8
+ "target": "ES2023",
9
+ "rootDir": "./src",
10
+ "outDir": "./dist"
11
+ }
12
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "include": [],
3
+ "references": [
4
+ { "path": "./tsconfig.build.json" },
5
+ { "path": "./tsconfig.tests.json" }
6
+ ]
7
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../../tsconfig/vitest.json",
3
+ "include": ["./tests", "./src/**/*.test.ts"],
4
+ "compilerOptions": {
5
+ "noImplicitAny": true,
6
+ "rootDir": "./",
7
+ "baseUrl": "./"
8
+ }
9
+ }