@flowerforce/flowerbase-client 0.1.1-beta.2 → 0.1.1-beta.4

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 (45) hide show
  1. package/dist/app.d.ts +55 -10
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +322 -47
  4. package/dist/credentials.d.ts +1 -0
  5. package/dist/credentials.d.ts.map +1 -1
  6. package/dist/credentials.js +6 -0
  7. package/dist/functions.d.ts +4 -1
  8. package/dist/functions.d.ts.map +1 -1
  9. package/dist/functions.js +18 -1
  10. package/dist/http.d.ts +24 -4
  11. package/dist/http.d.ts.map +1 -1
  12. package/dist/http.js +121 -25
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -1
  16. package/dist/mongo.d.ts +1 -1
  17. package/dist/mongo.d.ts.map +1 -1
  18. package/dist/mongo.js +45 -4
  19. package/dist/session.d.ts +6 -0
  20. package/dist/session.d.ts.map +1 -1
  21. package/dist/session.js +52 -0
  22. package/dist/session.native.d.ts.map +1 -1
  23. package/dist/session.native.js +5 -10
  24. package/dist/types.d.ts +28 -4
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/user.d.ts +24 -4
  27. package/dist/user.d.ts.map +1 -1
  28. package/dist/user.js +103 -8
  29. package/package.json +12 -1
  30. package/src/__tests__/auth.test.ts +49 -0
  31. package/src/__tests__/compat.test.ts +10 -0
  32. package/src/__tests__/functions.test.ts +236 -0
  33. package/src/__tests__/mongo.test.ts +35 -0
  34. package/src/__tests__/session.test.ts +494 -0
  35. package/src/__tests__/watch.test.ts +74 -0
  36. package/src/app.ts +390 -63
  37. package/src/credentials.ts +7 -0
  38. package/src/functions.ts +27 -3
  39. package/src/http.ts +156 -27
  40. package/src/index.ts +1 -0
  41. package/src/mongo.ts +48 -4
  42. package/src/session.native.ts +2 -11
  43. package/src/session.ts +55 -0
  44. package/src/types.ts +34 -4
  45. package/src/user.ts +123 -12
package/src/app.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import { normalizeFunctionResponse } from './functions'
2
- import { requestJson } from './http'
2
+ import { FlowerbaseHttpError, requestJson, requestStream } from './http'
3
3
  import { SessionManager } from './session'
4
4
  import { AppConfig, CredentialsLike, FunctionCallPayload, ProfileData, SessionData } from './types'
5
+ import { Credentials } from './credentials'
5
6
  import { User } from './user'
6
7
 
7
8
  const API_PREFIX = '/api/client/v2.0'
@@ -17,44 +18,191 @@ type SessionResponse = {
17
18
  }
18
19
 
19
20
  export class App {
21
+ private static readonly appCache: Record<string, App> = {}
22
+ static readonly Credentials = Credentials
23
+
20
24
  readonly id: string
21
25
  readonly baseUrl: string
22
26
  readonly timeout: number
23
27
  private readonly sessionManager: SessionManager
24
- currentUser: User | null = null
28
+ private readonly usersById = new Map<string, User>()
29
+ private readonly sessionsByUserId = new Map<string, SessionData>()
30
+ private usersOrder: string[] = []
31
+ private readonly profilesByUserId = new Map<string, ProfileData>()
25
32
  private readonly sessionBootstrapPromise: Promise<void>
33
+ private readonly listeners = new Set<() => void>()
26
34
 
27
35
  emailPasswordAuth: {
28
36
  registerUser: (input: { email: string; password: string }) => Promise<unknown>
29
- sendResetPasswordEmail: (email: string) => Promise<unknown>
30
- callResetPasswordFunction: (email: string, password: string, ...args: unknown[]) => Promise<unknown>
37
+ confirmUser: (input: { token: string; tokenId: string }) => Promise<unknown>
38
+ resendConfirmationEmail: (input: { email: string }) => Promise<unknown>
39
+ retryCustomConfirmation: (input: { email: string }) => Promise<unknown>
40
+ sendResetPasswordEmail: (input: { email: string } | string) => Promise<unknown>
41
+ callResetPasswordFunction: (
42
+ input: { email: string; password: string } | string,
43
+ passwordOrArg?: string,
44
+ ...args: unknown[]
45
+ ) => Promise<unknown>
31
46
  resetPassword: (input: { token: string; tokenId: string; password: string }) => Promise<unknown>
32
47
  }
33
48
 
34
- constructor(config: AppConfig) {
49
+ constructor(idOrConfig: string | AppConfig) {
50
+ const config = typeof idOrConfig === 'string' ? { id: idOrConfig } : idOrConfig
35
51
  this.id = config.id
36
- this.baseUrl = config.baseUrl.replace(/\/$/, '')
52
+ this.baseUrl = (config.baseUrl ?? '').replace(/\/$/, '')
37
53
  this.timeout = config.timeout ?? 10000
38
54
  this.sessionManager = new SessionManager(this.id)
55
+ const persistedSessionsByUser = this.sessionManager.getSessionsByUser()
56
+ for (const [userId, session] of Object.entries(persistedSessionsByUser)) {
57
+ this.sessionsByUserId.set(userId, session)
58
+ }
59
+ this.usersOrder = this.sessionManager.getUsersOrder()
60
+ for (const userId of this.sessionsByUserId.keys()) {
61
+ if (!this.usersOrder.includes(userId)) {
62
+ this.usersOrder.push(userId)
63
+ }
64
+ }
65
+ for (const userId of this.usersOrder) {
66
+ this.getOrCreateUser(userId)
67
+ }
39
68
 
40
- const session = this.sessionManager.get()
41
- if (session?.userId) {
42
- this.currentUser = new User(this, session.userId)
69
+ const currentSession = this.sessionManager.get()
70
+ if (currentSession?.userId) {
71
+ this.sessionsByUserId.set(currentSession.userId, currentSession)
72
+ this.getOrCreateUser(currentSession.userId)
73
+ this.touchUser(currentSession.userId)
74
+ this.persistSessionsByUser()
75
+ } else {
76
+ this.setCurrentSessionFromOrder()
43
77
  }
44
78
  this.sessionBootstrapPromise = this.bootstrapSessionOnLoad()
45
79
 
46
80
  this.emailPasswordAuth = {
47
81
  registerUser: ({ email, password }) =>
48
82
  this.postProvider('/local-userpass/register', { email, password }),
49
- sendResetPasswordEmail: (email) =>
50
- this.postProvider('/local-userpass/reset/send', { email }),
51
- callResetPasswordFunction: (email, password, ...args) =>
52
- this.postProvider('/local-userpass/reset/call', { email, password, arguments: args }),
83
+ confirmUser: ({ token, tokenId }) =>
84
+ this.postProvider('/local-userpass/confirm', { token, tokenId }),
85
+ resendConfirmationEmail: ({ email }) =>
86
+ this.postProvider('/local-userpass/confirm/send', { email }),
87
+ retryCustomConfirmation: ({ email }) =>
88
+ this.postProvider('/local-userpass/confirm/call', { email }),
89
+ sendResetPasswordEmail: (input) =>
90
+ this.postProvider('/local-userpass/reset/send', {
91
+ email: typeof input === 'string' ? input : input.email
92
+ }),
93
+ callResetPasswordFunction: (input, passwordOrArg, ...args) => {
94
+ if (typeof input === 'string') {
95
+ return this.postProvider('/local-userpass/reset/call', {
96
+ email: input,
97
+ password: passwordOrArg,
98
+ arguments: args
99
+ })
100
+ }
101
+
102
+ return this.postProvider('/local-userpass/reset/call', {
103
+ email: input.email,
104
+ password: input.password,
105
+ arguments: [passwordOrArg, ...args].filter((value) => value !== undefined)
106
+ })
107
+ },
53
108
  resetPassword: ({ token, tokenId, password }) =>
54
109
  this.postProvider('/local-userpass/reset', { token, tokenId, password })
55
110
  }
56
111
  }
57
112
 
113
+ static getApp(appIdOrConfig: string | AppConfig) {
114
+ const appId = typeof appIdOrConfig === 'string' ? appIdOrConfig : appIdOrConfig.id
115
+ if (appId in App.appCache) {
116
+ return App.appCache[appId]
117
+ }
118
+ const app = new App(appIdOrConfig)
119
+ App.appCache[appId] = app
120
+ return app
121
+ }
122
+
123
+ get currentUser() {
124
+ for (const userId of this.usersOrder) {
125
+ const user = this.usersById.get(userId)
126
+ if (user?.state === 'active') {
127
+ return user
128
+ }
129
+ }
130
+ return null
131
+ }
132
+
133
+ get allUsers(): Readonly<Record<string, User>> {
134
+ const activeUsers: string[] = []
135
+ const loggedOutUsers: string[] = []
136
+ for (const userId of this.usersOrder) {
137
+ const user = this.usersById.get(userId)
138
+ if (!user) continue
139
+ if (user.state === 'active') {
140
+ activeUsers.push(userId)
141
+ } else if (user.state === 'logged-out') {
142
+ loggedOutUsers.push(userId)
143
+ }
144
+ }
145
+
146
+ const users = Object.fromEntries(
147
+ [...activeUsers, ...loggedOutUsers].map((userId) => [userId, this.usersById.get(userId)!])
148
+ )
149
+ return users
150
+ }
151
+
152
+ private persistSessionsByUser() {
153
+ this.sessionManager.setSessionsByUser(Object.fromEntries(this.sessionsByUserId.entries()))
154
+ }
155
+
156
+ private persistUsersOrder() {
157
+ this.sessionManager.setUsersOrder(this.usersOrder)
158
+ }
159
+
160
+ private touchUser(userId: string) {
161
+ this.usersOrder = [userId, ...this.usersOrder.filter((id) => id !== userId)]
162
+ this.persistUsersOrder()
163
+ }
164
+
165
+ private removeUserFromOrder(userId: string) {
166
+ this.usersOrder = this.usersOrder.filter((id) => id !== userId)
167
+ this.persistUsersOrder()
168
+ }
169
+
170
+ private setSessionForUser(session: SessionData) {
171
+ this.sessionsByUserId.set(session.userId, session)
172
+ this.sessionManager.set(session)
173
+ this.persistSessionsByUser()
174
+ }
175
+
176
+ private clearSessionForUser(userId: string) {
177
+ this.sessionsByUserId.delete(userId)
178
+ this.persistSessionsByUser()
179
+ }
180
+
181
+ private setCurrentSessionFromOrder() {
182
+ for (const userId of this.usersOrder) {
183
+ const session = this.sessionsByUserId.get(userId)
184
+ if (session) {
185
+ this.sessionManager.set(session)
186
+ return
187
+ }
188
+ }
189
+ this.sessionManager.clear()
190
+ }
191
+
192
+ private notifyListeners(userId?: string) {
193
+ for (const callback of Array.from(this.listeners)) {
194
+ try {
195
+ callback()
196
+ } catch {
197
+ // Listener failures should not break auth/session lifecycle.
198
+ }
199
+ }
200
+
201
+ if (userId) {
202
+ this.usersById.get(userId)?.notifyListeners()
203
+ }
204
+ }
205
+
58
206
  private providerUrl(path: string) {
59
207
  return `${this.baseUrl}${API_PREFIX}/app/${this.id}/auth/providers${path}`
60
208
  }
@@ -84,13 +232,13 @@ export class App {
84
232
 
85
233
  try {
86
234
  const result = await this.createSession(session.refreshToken)
87
- this.sessionManager.set({
235
+ this.setSessionForUser({
88
236
  ...session,
89
237
  accessToken: result.access_token
90
238
  })
91
239
  } catch {
92
- this.sessionManager.clear()
93
- this.currentUser = null
240
+ this.clearSessionForUser(session.userId)
241
+ this.setCurrentSessionFromOrder()
94
242
  }
95
243
  }
96
244
 
@@ -98,19 +246,36 @@ export class App {
98
246
  await this.sessionBootstrapPromise
99
247
  }
100
248
 
101
- private async setLoggedInUser(data: LoginResponse, profileEmail?: string) {
249
+ private async setLoggedInUser(
250
+ data: LoginResponse,
251
+ providerType: CredentialsLike['provider'],
252
+ profileEmail?: string
253
+ ) {
102
254
  const sessionResult = await this.createSession(data.refresh_token)
103
255
  const session: SessionData = {
104
256
  accessToken: sessionResult.access_token,
105
257
  refreshToken: data.refresh_token,
106
258
  userId: data.user_id
107
259
  }
108
- this.sessionManager.set(session)
109
- this.currentUser = new User(this, data.user_id)
260
+ this.setSessionForUser(session)
261
+ const user = this.getOrCreateUser(data.user_id)
262
+ user.setProviderType(providerType)
263
+ this.touchUser(data.user_id)
110
264
  if (profileEmail) {
111
- this.currentUser.profile = { email: profileEmail }
265
+ user.profile = { email: profileEmail }
266
+ }
267
+ this.notifyListeners(data.user_id)
268
+ return user
269
+ }
270
+
271
+ private getOrCreateUser(userId: string) {
272
+ const existing = this.usersById.get(userId)
273
+ if (existing) {
274
+ return existing
112
275
  }
113
- return this.currentUser
276
+ const user = new User(this, userId)
277
+ this.usersById.set(userId, user)
278
+ return user
114
279
  }
115
280
 
116
281
  async logIn(credentials: CredentialsLike) {
@@ -119,26 +284,94 @@ export class App {
119
284
  username: credentials.email,
120
285
  password: credentials.password
121
286
  })
122
- return this.setLoggedInUser(result, credentials.email)
287
+ return this.setLoggedInUser(result, 'local-userpass', credentials.email)
123
288
  }
124
289
 
125
290
  if (credentials.provider === 'anon-user') {
126
291
  const result = await this.postProvider<LoginResponse>('/anon-user/login', {})
127
- return this.setLoggedInUser(result)
292
+ return this.setLoggedInUser(result, 'anon-user')
293
+ }
294
+
295
+ if (credentials.provider === 'custom-function') {
296
+ const result = await this.postProvider<LoginResponse>('/custom-function/login', credentials.payload)
297
+ return this.setLoggedInUser(result, 'custom-function')
298
+ }
299
+
300
+ if (credentials.provider === 'custom-token') {
301
+ const result = await this.postProvider<LoginResponse>('/custom-token/login', { token: credentials.token })
302
+ return this.setLoggedInUser(result, 'custom-token')
128
303
  }
129
304
 
130
- const result = await this.postProvider<LoginResponse>('/custom-function/login', credentials.payload)
131
- return this.setLoggedInUser(result)
305
+ const unsupportedProvider: never = credentials
306
+ throw new Error(`Unsupported credentials provider: ${JSON.stringify(unsupportedProvider)}`)
132
307
  }
133
308
 
134
- getSessionOrThrow() {
135
- const session = this.sessionManager.get()
309
+ switchUser(nextUser: User) {
310
+ const knownUser = this.usersById.get(nextUser.id)
311
+ if (!knownUser) {
312
+ throw new Error('The user was never logged into this app')
313
+ }
314
+ this.touchUser(nextUser.id)
315
+ const session = this.sessionsByUserId.get(nextUser.id)
316
+ if (session) {
317
+ this.sessionManager.set(session)
318
+ }
319
+ this.notifyListeners(nextUser.id)
320
+ }
321
+
322
+ async removeUser(user: User) {
323
+ const knownUser = this.usersById.get(user.id)
324
+ if (!knownUser) {
325
+ throw new Error('The user was never logged into this app')
326
+ }
327
+ if (this.sessionsByUserId.has(user.id)) {
328
+ await this.logoutUser(user.id)
329
+ }
330
+ this.usersById.delete(user.id)
331
+ this.removeUserFromOrder(user.id)
332
+ this.profilesByUserId.delete(user.id)
333
+ this.clearSessionForUser(user.id)
334
+ this.setCurrentSessionFromOrder()
335
+ this.notifyListeners(user.id)
336
+ }
337
+
338
+ async deleteUser(user: User) {
339
+ await this.requestWithAccessToken((accessToken) =>
340
+ requestJson({
341
+ url: this.authUrl('/delete'),
342
+ method: 'DELETE',
343
+ bearerToken: accessToken,
344
+ timeout: this.timeout
345
+ }),
346
+ user.id
347
+ )
348
+ await this.removeUser(user)
349
+ }
350
+
351
+ getSessionOrThrow(userId?: string) {
352
+ const targetUserId = userId ?? this.currentUser?.id
353
+ const session = targetUserId ? this.sessionsByUserId.get(targetUserId) : this.sessionManager.get()
136
354
  if (!session) {
137
355
  throw new Error('User is not authenticated')
138
356
  }
139
357
  return session
140
358
  }
141
359
 
360
+ getSession(userId?: string) {
361
+ if (userId) {
362
+ return this.sessionsByUserId.get(userId) ?? null
363
+ }
364
+ return this.sessionManager.get()
365
+ }
366
+
367
+ hasUser(userId: string) {
368
+ return this.usersById.has(userId)
369
+ }
370
+
371
+ getProfileSnapshot(userId: string) {
372
+ return this.profilesByUserId.get(userId)
373
+ }
374
+
142
375
  async postProvider<T = unknown>(path: string, body: unknown): Promise<T> {
143
376
  return requestJson<T>({
144
377
  url: this.providerUrl(path),
@@ -148,76 +381,155 @@ export class App {
148
381
  })
149
382
  }
150
383
 
151
- async callFunction(name: string, args: unknown[]) {
384
+ private async requestWithAccessToken<T>(operation: (accessToken: string) => Promise<T>, userId?: string) {
385
+ const firstSession = this.getSessionOrThrow(userId)
386
+ try {
387
+ return await operation(firstSession.accessToken)
388
+ } catch (error) {
389
+ if (!(error instanceof FlowerbaseHttpError) || error.status !== 401) {
390
+ throw error
391
+ }
392
+ await this.refreshAccessToken(userId)
393
+ const refreshedSession = this.getSessionOrThrow(userId)
394
+ return operation(refreshedSession.accessToken)
395
+ }
396
+ }
397
+
398
+ async callFunction(name: string, args: unknown[], userId?: string) {
152
399
  await this.ensureSessionBootstrapped()
153
- const session = this.getSessionOrThrow()
154
400
  const payload: FunctionCallPayload = {
155
401
  name,
156
402
  arguments: args
157
403
  }
158
404
 
159
- const result = await requestJson<unknown>({
160
- url: this.functionsUrl('/call'),
161
- method: 'POST',
162
- body: payload,
163
- bearerToken: session.accessToken,
164
- timeout: this.timeout
165
- })
405
+ const result = await this.requestWithAccessToken((accessToken) =>
406
+ requestJson<unknown>({
407
+ url: this.functionsUrl('/call'),
408
+ method: 'POST',
409
+ body: payload,
410
+ bearerToken: accessToken,
411
+ timeout: this.timeout
412
+ }),
413
+ userId
414
+ )
166
415
 
167
416
  return normalizeFunctionResponse(result)
168
417
  }
169
418
 
170
- async callService(name: string, args: unknown[]) {
419
+ async callFunctionStreaming(name: string, args: unknown[], userId?: string): Promise<AsyncIterable<Uint8Array>> {
171
420
  await this.ensureSessionBootstrapped()
172
- const session = this.getSessionOrThrow()
173
421
  const payload: FunctionCallPayload = {
174
422
  name,
175
- service: 'mongodb-atlas',
176
423
  arguments: args
177
424
  }
425
+ const resolveSession = () => this.getSessionOrThrow(userId)
426
+ const refreshSession = () => this.refreshAccessToken(userId)
427
+ const timeout = this.timeout
428
+ const url = this.functionsUrl('/call')
429
+
430
+ return {
431
+ async *[Symbol.asyncIterator]() {
432
+ let didRefresh = false
433
+ while (true) {
434
+ const session = resolveSession()
435
+ let stream: AsyncIterable<Uint8Array>
436
+
437
+ try {
438
+ stream = await requestStream({
439
+ url,
440
+ method: 'POST',
441
+ body: payload,
442
+ bearerToken: session.accessToken,
443
+ timeout
444
+ })
445
+ } catch (error) {
446
+ if (!didRefresh && error instanceof FlowerbaseHttpError && error.status === 401) {
447
+ await refreshSession()
448
+ didRefresh = true
449
+ continue
450
+ }
451
+ throw error
452
+ }
453
+
454
+ try {
455
+ for await (const chunk of stream) {
456
+ yield chunk
457
+ }
458
+ return
459
+ } catch (error) {
460
+ if (!didRefresh && error instanceof FlowerbaseHttpError && error.status === 401) {
461
+ await refreshSession()
462
+ didRefresh = true
463
+ continue
464
+ }
465
+ throw error
466
+ }
467
+ }
468
+ }
469
+ }
470
+ }
178
471
 
179
- return requestJson<unknown>({
180
- url: this.functionsUrl('/call'),
181
- method: 'POST',
182
- body: payload,
183
- bearerToken: session.accessToken,
184
- timeout: this.timeout
185
- })
472
+ async callService(name: string, args: unknown[], service = 'mongodb-atlas', userId?: string) {
473
+ await this.ensureSessionBootstrapped()
474
+ const payload: FunctionCallPayload = {
475
+ name,
476
+ service,
477
+ arguments: args
478
+ }
479
+
480
+ return this.requestWithAccessToken((accessToken) =>
481
+ requestJson<unknown>({
482
+ url: this.functionsUrl('/call'),
483
+ method: 'POST',
484
+ body: payload,
485
+ bearerToken: accessToken,
486
+ timeout: this.timeout
487
+ }),
488
+ userId
489
+ )
186
490
  }
187
491
 
188
- async getProfile(): Promise<ProfileData> {
492
+ async getProfile(userId?: string): Promise<ProfileData> {
189
493
  await this.ensureSessionBootstrapped()
190
- const session = this.getSessionOrThrow()
191
- return requestJson<ProfileData>({
192
- url: this.authUrl('/profile'),
193
- method: 'GET',
194
- bearerToken: session.accessToken,
195
- timeout: this.timeout
196
- })
494
+ const profile = await this.requestWithAccessToken((accessToken) =>
495
+ requestJson<ProfileData>({
496
+ url: this.authUrl('/profile'),
497
+ method: 'GET',
498
+ bearerToken: accessToken,
499
+ timeout: this.timeout
500
+ }),
501
+ userId
502
+ )
503
+ const session = this.getSessionOrThrow(userId)
504
+ this.profilesByUserId.set(session.userId, profile)
505
+ return profile
197
506
  }
198
507
 
199
- async refreshAccessToken() {
508
+ async refreshAccessToken(userId?: string) {
200
509
  await this.ensureSessionBootstrapped()
201
- const session = this.getSessionOrThrow()
510
+ const session = this.getSessionOrThrow(userId)
202
511
 
203
512
  try {
204
513
  const result = await this.createSession(session.refreshToken)
205
514
 
206
- this.sessionManager.set({
515
+ this.setSessionForUser({
207
516
  ...session,
208
517
  accessToken: result.access_token
209
518
  })
519
+ this.touchUser(session.userId)
520
+ this.notifyListeners(session.userId)
210
521
 
211
522
  return result.access_token
212
523
  } catch (error) {
213
- this.sessionManager.clear()
214
- this.currentUser = null
524
+ this.clearSessionForUser(session.userId)
525
+ this.setCurrentSessionFromOrder()
526
+ this.notifyListeners(session.userId)
215
527
  throw error
216
528
  }
217
529
  }
218
530
 
219
- async logoutUser() {
220
- const session = this.sessionManager.get()
531
+ async logoutUser(userId?: string) {
532
+ const session = this.getSession(userId ?? this.currentUser?.id)
221
533
  try {
222
534
  if (session) {
223
535
  await requestJson({
@@ -228,8 +540,23 @@ export class App {
228
540
  })
229
541
  }
230
542
  } finally {
231
- this.sessionManager.clear()
232
- this.currentUser = null
543
+ if (session) {
544
+ this.clearSessionForUser(session.userId)
545
+ this.notifyListeners(session.userId)
546
+ }
547
+ this.setCurrentSessionFromOrder()
233
548
  }
234
549
  }
550
+
551
+ addListener(callback: () => void) {
552
+ this.listeners.add(callback)
553
+ }
554
+
555
+ removeListener(callback: () => void) {
556
+ this.listeners.delete(callback)
557
+ }
558
+
559
+ removeAllListeners() {
560
+ this.listeners.clear()
561
+ }
235
562
  }
@@ -21,4 +21,11 @@ export class Credentials {
21
21
  payload
22
22
  }
23
23
  }
24
+
25
+ static jwt(token: string): CredentialsLike {
26
+ return {
27
+ provider: 'custom-token',
28
+ token
29
+ }
30
+ }
24
31
  }
package/src/functions.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  import { EJSON } from './bson'
2
2
 
3
+ const RESERVED_PROXY_KEYS = new Set([
4
+ 'toJSON',
5
+ 'then',
6
+ 'catch',
7
+ 'finally',
8
+ 'constructor',
9
+ '__proto__',
10
+ 'prototype'
11
+ ])
12
+
3
13
  const deserialize = <T>(value: T): T => {
4
14
  if (!value || typeof value !== 'object') return value
5
15
  return EJSON.deserialize(value as Record<string, unknown>) as T
@@ -19,14 +29,28 @@ export const normalizeFunctionResponse = (value: unknown) => {
19
29
  }
20
30
 
21
31
  export const createFunctionsProxy = (
22
- callFunction: (name: string, args: unknown[]) => Promise<unknown>
23
- ): Record<string, (...args: unknown[]) => Promise<unknown>> =>
32
+ callFunction: (name: string, args: unknown[]) => Promise<unknown>,
33
+ callFunctionStreaming: (name: string, args: unknown[]) => Promise<AsyncIterable<Uint8Array>>
34
+ ): Record<string, (...args: unknown[]) => Promise<unknown>> & {
35
+ callFunction: (name: string, ...args: unknown[]) => Promise<unknown>
36
+ callFunctionStreaming: (name: string, ...args: unknown[]) => Promise<AsyncIterable<Uint8Array>>
37
+ } =>
24
38
  new Proxy(
25
39
  {},
26
40
  {
27
41
  get: (_, key) => {
28
42
  if (typeof key !== 'string') return undefined
43
+ if (RESERVED_PROXY_KEYS.has(key)) return undefined
44
+ if (key === 'callFunction') {
45
+ return (name: string, ...args: unknown[]) => callFunction(name, args)
46
+ }
47
+ if (key === 'callFunctionStreaming') {
48
+ return (name: string, ...args: unknown[]) => callFunctionStreaming(name, args)
49
+ }
29
50
  return (...args: unknown[]) => callFunction(key, args)
30
51
  }
31
52
  }
32
- ) as Record<string, (...args: unknown[]) => Promise<unknown>>
53
+ ) as Record<string, (...args: unknown[]) => Promise<unknown>> & {
54
+ callFunction: (name: string, ...args: unknown[]) => Promise<unknown>
55
+ callFunctionStreaming: (name: string, ...args: unknown[]) => Promise<AsyncIterable<Uint8Array>>
56
+ }