@proteinjs/user-server 1.5.3 → 1.6.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 (51) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/dist/generated/index.d.ts +1 -4
  4. package/dist/generated/index.d.ts.map +1 -1
  5. package/dist/generated/index.js +6 -7
  6. package/dist/generated/index.js.map +1 -1
  7. package/dist/src/authentication/DbSessionStore.d.ts +0 -1
  8. package/dist/src/authentication/DbSessionStore.d.ts.map +1 -1
  9. package/dist/src/routes/avatar.d.ts +19 -0
  10. package/dist/src/routes/avatar.d.ts.map +1 -0
  11. package/dist/src/routes/avatar.js +108 -0
  12. package/dist/src/routes/avatar.js.map +1 -0
  13. package/dist/src/routes/devLogin.d.ts.map +1 -1
  14. package/dist/src/routes/devLogin.js +11 -0
  15. package/dist/src/routes/devLogin.js.map +1 -1
  16. package/dist/src/services/UpdateUserInfo.d.ts +40 -2
  17. package/dist/src/services/UpdateUserInfo.d.ts.map +1 -1
  18. package/dist/src/services/UpdateUserInfo.js +183 -12
  19. package/dist/src/services/UpdateUserInfo.js.map +1 -1
  20. package/dist/test/UpdateUserInfoAvatar.test.d.ts +2 -0
  21. package/dist/test/UpdateUserInfoAvatar.test.d.ts.map +1 -0
  22. package/dist/test/UpdateUserInfoAvatar.test.js +337 -0
  23. package/dist/test/UpdateUserInfoAvatar.test.js.map +1 -0
  24. package/dist/test/UpdateUserInfoSessionRefresh.test.d.ts +2 -0
  25. package/dist/test/UpdateUserInfoSessionRefresh.test.d.ts.map +1 -0
  26. package/dist/test/UpdateUserInfoSessionRefresh.test.js +94 -0
  27. package/dist/test/UpdateUserInfoSessionRefresh.test.js.map +1 -0
  28. package/dist/test/UserServerTestEnvironment.d.ts +26 -0
  29. package/dist/test/UserServerTestEnvironment.d.ts.map +1 -0
  30. package/dist/test/UserServerTestEnvironment.js +202 -0
  31. package/dist/test/UserServerTestEnvironment.js.map +1 -0
  32. package/dist/test/avatarRoute.test.d.ts +2 -0
  33. package/dist/test/avatarRoute.test.d.ts.map +1 -0
  34. package/dist/test/avatarRoute.test.js +222 -0
  35. package/dist/test/avatarRoute.test.js.map +1 -0
  36. package/generated/index.ts +21 -25
  37. package/jest.config.js +4 -1
  38. package/package.json +9 -5
  39. package/src/heic-decode.d.ts +13 -0
  40. package/src/routes/avatar.ts +57 -0
  41. package/src/routes/devLogin.ts +5 -0
  42. package/src/services/UpdateUserInfo.ts +124 -13
  43. package/test/UpdateUserInfoAvatar.test.ts +174 -0
  44. package/test/UpdateUserInfoSessionRefresh.test.ts +37 -0
  45. package/test/UserServerTestEnvironment.ts +97 -0
  46. package/test/avatarRoute.test.ts +111 -0
  47. package/test/emulatorLock.globalSetup.js +11 -0
  48. package/test/emulatorLock.globalTeardown.js +8 -0
  49. package/test/emulatorLock.js +124 -0
  50. package/test/fixtures/fixture.heic +0 -0
  51. package/tsconfig.json +1 -0
@@ -1,27 +1,25 @@
1
1
  /** Load Dependency Source Graphs */
2
2
 
3
3
  import '@proteinjs/db';
4
+ import '@proteinjs/db-file';
4
5
  import '@proteinjs/email-server';
5
6
  import '@proteinjs/logger';
6
- import '@proteinjs/reflection';
7
7
  import '@proteinjs/server';
8
8
  import '@proteinjs/server-api';
9
9
  import '@proteinjs/user';
10
- import 'crypto-js';
11
- import 'express-session';
12
- import 'moment';
13
-
14
10
 
15
11
  /** Generate Source Graph */
16
12
 
17
- const sourceGraph = "{\"options\":{\"directed\":true,\"multigraph\":false,\"compound\":false},\"nodes\":[{\"v\":\"@proteinjs/user-server/DbSessionStore\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"DbSessionStore\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/authentication/DbSessionStore.ts\",\"qualifiedName\":\"@proteinjs/user-server/DbSessionStore\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"TWELVE_HOURS\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"TOUCH_INTERVAL\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\"},{\"name\":\"logger\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\"},{\"name\":\"lastTouchMs\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\"},{\"name\":\"get\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"},{\"name\":\"set\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"},{\"name\":\"touch\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"},{\"name\":\"destroy\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[{\"name\":\"isTransactionAbort\",\"returnType\":{\"packageName\":\"\",\"name\":\"boolean\",\"filePath\":null,\"qualifiedName\":\"/boolean\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":true,\"visibility\":\"private\",\"parameters\":[{\"name\":\"error\",\"type\":{\"packageName\":\"\",\"name\":\"any\",\"filePath\":null,\"qualifiedName\":\"/any\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"insertOrUpdate\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[{\"name\":\"sessionId\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"session\",\"type\":{\"packageName\":\"\",\"name\":\"Express.SessionData\",\"filePath\":null,\"qualifiedName\":\"/Express.SessionData\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"cb\",\"type\":{\"packageName\":\"\",\"name\":\"(error?: any) => void\",\"filePath\":null,\"qualifiedName\":\"/(error?: any) => void\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"sweep\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[]}],\"typeParameters\":[],\"directParentInterfaces\":[],\"directParentClasses\":[{\"packageName\":\"express-session\",\"name\":\"Store\",\"filePath\":null,\"qualifiedName\":\"express-session/Store\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParentInterfaces\":[],\"directParentClasses\":[]}],\"sourceType\":2}},{\"v\":\"express-session/Store\"},{\"v\":\"@proteinjs/user-server/SocketIOSessionWatcher\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"SocketIOSessionWatcher\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/authentication/SocketIOSessionWatcher.ts\",\"qualifiedName\":\"@proteinjs/user-server/SocketIOSessionWatcher\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[],\"methods\":[{\"name\":\"name\",\"returnType\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]},{\"name\":\"table\",\"returnType\":{\"packageName\":\"\",\"name\":\"Table<Session>\",\"filePath\":null,\"qualifiedName\":\"/Table<Session>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]},{\"name\":\"afterDelete\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"recordDeleteCount\",\"type\":{\"packageName\":\"\",\"name\":\"number\",\"filePath\":null,\"qualifiedName\":\"/number\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"deletedRecords\",\"type\":{\"packageName\":\"\",\"name\":\"T[]\",\"filePath\":null,\"qualifiedName\":\"/T[]\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"initialQb\",\"type\":{\"packageName\":\"\",\"name\":\"QueryBuilder<T>\",\"filePath\":null,\"qualifiedName\":\"/QueryBuilder<T>\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"deleteQb\",\"type\":{\"packageName\":\"\",\"name\":\"QueryBuilder<T>\",\"filePath\":null,\"qualifiedName\":\"/QueryBuilder<T>\",\"typeParameters\":null,\"directParents\":null}}]}],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/db\",\"name\":\"TableWatcher\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db/TableWatcher\",\"properties\":[],\"methods\":[],\"typeParameters\":[\"@proteinjs/user/Session\"],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/db/TableWatcher\"},{\"v\":\"@proteinjs/user-server/userCache\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"userCache\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/authorization/userCache.ts\",\"qualifiedName\":\"@proteinjs/user-server/userCache\",\"type\":{\"packageName\":\"\",\"name\":\"SessionDataCache<User>\",\"filePath\":null,\"qualifiedName\":\"/SessionDataCache<User>\",\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"SessionDataCache\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/SessionDataCache\",\"typeParameters\":[\"@proteinjs/user/User\"],\"directParents\":null}]},\"isExported\":true,\"isConst\":true,\"sourceType\":0}},{\"v\":\"@proteinjs/server-api/SessionDataCache\"},{\"v\":\"@proteinjs/user-server/devLogin\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"devLogin\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/routes/devLogin.ts\",\"qualifiedName\":\"@proteinjs/user-server/devLogin\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":null}]},\"isExported\":true,\"isConst\":true,\"sourceType\":0}},{\"v\":\"@proteinjs/server-api/Route\"},{\"v\":\"@proteinjs/user-server/executePasswordReset\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"executePasswordReset\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/routes/executePasswordReset.ts\",\"qualifiedName\":\"@proteinjs/user-server/executePasswordReset\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":null}]},\"isExported\":true,\"isConst\":true,\"sourceType\":0}},{\"v\":\"@proteinjs/user-server/initiatePasswordReset\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"initiatePasswordReset\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/routes/initiatePasswordReset.ts\",\"qualifiedName\":\"@proteinjs/user-server/initiatePasswordReset\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":null}]},\"isExported\":true,\"isConst\":true,\"sourceType\":0}},{\"v\":\"@proteinjs/user-server/login\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"login\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/routes/login.ts\",\"qualifiedName\":\"@proteinjs/user-server/login\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":null}]},\"isExported\":true,\"isConst\":true,\"sourceType\":0}},{\"v\":\"@proteinjs/user-server/logout\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"logout\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/routes/logout.ts\",\"qualifiedName\":\"@proteinjs/user-server/logout\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":null}]},\"isExported\":true,\"isConst\":true,\"sourceType\":0}},{\"v\":\"@proteinjs/user-server/validateResetPasswordToken\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"validateResetPasswordToken\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/routes/validateResetPasswordToken.ts\",\"qualifiedName\":\"@proteinjs/user-server/validateResetPasswordToken\",\"type\":{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/server-api\",\"name\":\"Route\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/server-api/Route\",\"typeParameters\":[],\"directParents\":null}]},\"isExported\":true,\"isConst\":true,\"sourceType\":0}},{\"v\":\"@proteinjs/user-server/AccessInvite\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"AccessInvite\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/services/AccessInvite.ts\",\"qualifiedName\":\"@proteinjs/user-server/AccessInvite\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"serviceMetadata\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[{\"name\":\"createAccessInvite\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<CreateAccessInviteResponse>\",\"filePath\":null,\"qualifiedName\":\"/Promise<CreateAccessInviteResponse>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"params\",\"type\":{\"packageName\":\"@proteinjs/user\",\"name\":\"CreateAccessInviteParams\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/user/CreateAccessInviteParams\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"acceptAccessInvite\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<T | undefined>\",\"filePath\":null,\"qualifiedName\":\"/Promise<T | undefined>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"token\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]}],\"typeParameters\":[\"/T extends Record\"],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/user\",\"name\":\"AccessInviteService\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/user/AccessInviteService\",\"properties\":[],\"methods\":[],\"typeParameters\":[\"/T\"],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/user/AccessInviteService\"},{\"v\":\"@proteinjs/user-server/Roles\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"Roles\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/services/Roles.ts\",\"qualifiedName\":\"@proteinjs/user-server/Roles\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"serviceMetadata\",\"type\":{\"packageName\":\"\",\"name\":\"Service['serviceMetadata']\",\"filePath\":null,\"qualifiedName\":\"/Service['serviceMetadata']\",\"typeParameters\":null,\"directParents\":null},\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[{\"name\":\"grantRole\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"userId\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"role\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"revokeRole\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"userId\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"role\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"changeRole\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[{\"name\":\"userId\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"role\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"action\",\"type\":{\"packageName\":\"\",\"name\":\"'grant' | 'revoke'\",\"filePath\":null,\"qualifiedName\":\"/'grant' | 'revoke'\",\"typeParameters\":null,\"directParents\":null}}]}],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/user\",\"name\":\"RolesService\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/user/RolesService\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/user/RolesService\"},{\"v\":\"@proteinjs/user-server/DefaultInviteConfigFactory\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"DefaultInviteConfigFactory\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/services/Signup.ts\",\"qualifiedName\":\"@proteinjs/user-server/DefaultInviteConfigFactory\",\"properties\":[],\"methods\":[{\"name\":\"getConfig\",\"returnType\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"InviteConfig\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/user-server/InviteConfig\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":false,\"isOptional\":false,\"isAbstract\":true,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[]}],\"typeParameters\":[],\"directParents\":[{\"packageName\":\"@proteinjs/reflection\",\"name\":\"Loadable\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/reflection/Loadable\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"sourceType\":3}},{\"v\":\"@proteinjs/reflection/Loadable\"},{\"v\":\"@proteinjs/user-server/Signup\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"Signup\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/services/Signup.ts\",\"qualifiedName\":\"@proteinjs/user-server/Signup\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"serviceMetadata\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[{\"name\":\"createUser\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"user\",\"type\":{\"packageName\":\"@proteinjs/user\",\"name\":\"UserSignup\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/user/UserSignup\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"token\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"sendInvite\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<SendInviteResponse>\",\"filePath\":null,\"qualifiedName\":\"/Promise<SendInviteResponse>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"email\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"revokeInvite\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"email\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"initializeSignup\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<InitializeSignupResponse>\",\"filePath\":null,\"qualifiedName\":\"/Promise<InitializeSignupResponse>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"inviteToken\",\"type\":{\"packageName\":\"\",\"name\":\"string | undefined\",\"filePath\":null,\"qualifiedName\":\"/string | undefined\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"lookupInvite\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<InviteLookup>\",\"filePath\":null,\"qualifiedName\":\"/Promise<InviteLookup>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"private\",\"parameters\":[{\"name\":\"token\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]}],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/user\",\"name\":\"SignupService\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/user/SignupService\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/user/SignupService\"},{\"v\":\"@proteinjs/user-server/UpdateUserInfo\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"UpdateUserInfo\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/services/UpdateUserInfo.ts\",\"qualifiedName\":\"@proteinjs/user-server/UpdateUserInfo\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"serviceMetadata\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[{\"name\":\"updateName\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<void>\",\"filePath\":null,\"qualifiedName\":\"/Promise<void>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"name\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]},{\"name\":\"updatePassword\",\"returnType\":{\"packageName\":\"\",\"name\":\"Promise<UpdatePasswordResponse>\",\"filePath\":null,\"qualifiedName\":\"/Promise<UpdatePasswordResponse>\",\"typeParameters\":null,\"directParents\":null},\"isAsync\":true,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"parameters\":[{\"name\":\"currentPassword\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}},{\"name\":\"newPassword\",\"type\":{\"packageName\":\"\",\"name\":\"string\",\"filePath\":null,\"qualifiedName\":\"/string\",\"typeParameters\":null,\"directParents\":null}}]}],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/user\",\"name\":\"UpdateUserInfoService\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/user/UpdateUserInfoService\",\"properties\":[],\"methods\":[],\"typeParameters\":[],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/user/UpdateUserInfoService\"},{\"v\":\"@proteinjs/user-server/BackfillUserRolesArray\",\"value\":{\"packageName\":\"@proteinjs/user-server\",\"name\":\"BackfillUserRolesArray\",\"filePath\":\"/Users/brentbahry/repos/farm/n3xa2/packages/proteinjs/packages/user/packages/server/src/migrations/BackfillUserRolesArray.ts\",\"qualifiedName\":\"@proteinjs/user-server/BackfillUserRolesArray\",\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\",\"properties\":[{\"name\":\"table\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"},{\"name\":\"record\",\"type\":null,\"isOptional\":false,\"isAbstract\":false,\"isStatic\":false,\"visibility\":\"public\"}],\"methods\":[],\"typeParameters\":[],\"directParentInterfaces\":[{\"packageName\":\"@proteinjs/db\",\"name\":\"SourceRecordLoader\",\"filePath\":null,\"qualifiedName\":\"@proteinjs/db/SourceRecordLoader\",\"properties\":[],\"methods\":[],\"typeParameters\":[\"@proteinjs/db/Migration\"],\"directParents\":[]}],\"directParentClasses\":[],\"sourceType\":2}},{\"v\":\"@proteinjs/db/SourceRecordLoader\"}],\"edges\":[{\"v\":\"@proteinjs/user-server/DbSessionStore\",\"w\":\"express-session/Store\",\"value\":\"extends class\"},{\"v\":\"@proteinjs/user-server/SocketIOSessionWatcher\",\"w\":\"@proteinjs/db/TableWatcher\",\"value\":\"implements interface\"},{\"v\":\"@proteinjs/user-server/userCache\",\"w\":\"@proteinjs/server-api/SessionDataCache\",\"value\":\"has type\"},{\"v\":\"@proteinjs/user-server/devLogin\",\"w\":\"@proteinjs/server-api/Route\",\"value\":\"has type\"},{\"v\":\"@proteinjs/user-server/executePasswordReset\",\"w\":\"@proteinjs/server-api/Route\",\"value\":\"has type\"},{\"v\":\"@proteinjs/user-server/initiatePasswordReset\",\"w\":\"@proteinjs/server-api/Route\",\"value\":\"has type\"},{\"v\":\"@proteinjs/user-server/login\",\"w\":\"@proteinjs/server-api/Route\",\"value\":\"has type\"},{\"v\":\"@proteinjs/user-server/logout\",\"w\":\"@proteinjs/server-api/Route\",\"value\":\"has type\"},{\"v\":\"@proteinjs/user-server/validateResetPasswordToken\",\"w\":\"@proteinjs/server-api/Route\",\"value\":\"has type\"},{\"v\":\"@proteinjs/user-server/AccessInvite\",\"w\":\"@proteinjs/user/AccessInviteService\",\"value\":\"implements interface\"},{\"v\":\"@proteinjs/user-server/Roles\",\"w\":\"@proteinjs/user/RolesService\",\"value\":\"implements interface\"},{\"v\":\"@proteinjs/user-server/DefaultInviteConfigFactory\",\"w\":\"@proteinjs/reflection/Loadable\",\"value\":\"extends interface\"},{\"v\":\"@proteinjs/user-server/Signup\",\"w\":\"@proteinjs/user/SignupService\",\"value\":\"implements interface\"},{\"v\":\"@proteinjs/user-server/UpdateUserInfo\",\"w\":\"@proteinjs/user/UpdateUserInfoService\",\"value\":\"implements interface\"},{\"v\":\"@proteinjs/user-server/BackfillUserRolesArray\",\"w\":\"@proteinjs/db/SourceRecordLoader\",\"value\":\"implements interface\"}]}";
18
-
13
+ const sourceGraph =
14
+ '{"options":{"directed":true,"multigraph":false,"compound":false},"nodes":[{"v":"@proteinjs/user-server/DbSessionStore","value":{"packageName":"@proteinjs/user-server","name":"DbSessionStore","filePath":"/home/runner/work/user/user/packages/server/src/authentication/DbSessionStore.ts","qualifiedName":"@proteinjs/user-server/DbSessionStore","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"TWELVE_HOURS","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"TOUCH_INTERVAL","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"logger","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private"},{"name":"lastTouchMs","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private"},{"name":"get","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"},{"name":"set","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"},{"name":"touch","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"},{"name":"destroy","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[{"name":"isTransactionAbort","returnType":{"packageName":"","name":"boolean","filePath":null,"qualifiedName":"/boolean","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private","parameters":[{"name":"error","type":{"packageName":"","name":"any","filePath":null,"qualifiedName":"/any","typeParameters":null,"directParents":null}}]},{"name":"insertOrUpdate","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"sessionId","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"session","type":{"packageName":"","name":"Express.SessionData","filePath":null,"qualifiedName":"/Express.SessionData","typeParameters":null,"directParents":null}},{"name":"cb","type":{"packageName":"","name":"(error?: any) => void","filePath":null,"qualifiedName":"/(error?: any) => void","typeParameters":null,"directParents":null}}]},{"name":"sweep","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[]}],"typeParameters":[],"directParentInterfaces":[],"directParentClasses":[{"packageName":"express-session","name":"Store","filePath":null,"qualifiedName":"express-session/Store","isAbstract":false,"isStatic":false,"visibility":"public","properties":[],"methods":[],"typeParameters":[],"directParentInterfaces":[],"directParentClasses":[]}],"sourceType":2}},{"v":"express-session/Store"},{"v":"@proteinjs/user-server/SocketIOSessionWatcher","value":{"packageName":"@proteinjs/user-server","name":"SocketIOSessionWatcher","filePath":"/home/runner/work/user/user/packages/server/src/authentication/SocketIOSessionWatcher.ts","qualifiedName":"@proteinjs/user-server/SocketIOSessionWatcher","isAbstract":false,"isStatic":false,"visibility":"public","properties":[],"methods":[{"name":"name","returnType":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"table","returnType":{"packageName":"","name":"Table<Session>","filePath":null,"qualifiedName":"/Table<Session>","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"afterDelete","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"recordDeleteCount","type":{"packageName":"","name":"number","filePath":null,"qualifiedName":"/number","typeParameters":null,"directParents":null}},{"name":"deletedRecords","type":{"packageName":"","name":"T[]","filePath":null,"qualifiedName":"/T[]","typeParameters":null,"directParents":null}},{"name":"initialQb","type":{"packageName":"","name":"QueryBuilder<T>","filePath":null,"qualifiedName":"/QueryBuilder<T>","typeParameters":null,"directParents":null}},{"name":"deleteQb","type":{"packageName":"","name":"QueryBuilder<T>","filePath":null,"qualifiedName":"/QueryBuilder<T>","typeParameters":null,"directParents":null}}]}],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/db","name":"TableWatcher","filePath":null,"qualifiedName":"@proteinjs/db/TableWatcher","properties":[],"methods":[],"typeParameters":["@proteinjs/user/Session"],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/db/TableWatcher"},{"v":"@proteinjs/user-server/userCache","value":{"packageName":"@proteinjs/user-server","name":"userCache","filePath":"/home/runner/work/user/user/packages/server/src/authorization/userCache.ts","qualifiedName":"@proteinjs/user-server/userCache","type":{"packageName":"","name":"SessionDataCache<User>","filePath":null,"qualifiedName":"/SessionDataCache<User>","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"SessionDataCache","filePath":null,"qualifiedName":"@proteinjs/server-api/SessionDataCache","typeParameters":["@proteinjs/user/User"],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/server-api/SessionDataCache"},{"v":"@proteinjs/user-server/BackfillUserRolesArray","value":{"packageName":"@proteinjs/user-server","name":"BackfillUserRolesArray","filePath":"/home/runner/work/user/user/packages/server/src/migrations/BackfillUserRolesArray.ts","qualifiedName":"@proteinjs/user-server/BackfillUserRolesArray","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"table","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"},{"name":"record","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/db","name":"SourceRecordLoader","filePath":null,"qualifiedName":"@proteinjs/db/SourceRecordLoader","properties":[],"methods":[],"typeParameters":["@proteinjs/db/Migration"],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/db/SourceRecordLoader"},{"v":"@proteinjs/user-server/avatar","value":{"packageName":"@proteinjs/user-server","name":"avatar","filePath":"/home/runner/work/user/user/packages/server/src/routes/avatar.ts","qualifiedName":"@proteinjs/user-server/avatar","type":{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/server-api/Route"},{"v":"@proteinjs/user-server/devLogin","value":{"packageName":"@proteinjs/user-server","name":"devLogin","filePath":"/home/runner/work/user/user/packages/server/src/routes/devLogin.ts","qualifiedName":"@proteinjs/user-server/devLogin","type":{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/user-server/executePasswordReset","value":{"packageName":"@proteinjs/user-server","name":"executePasswordReset","filePath":"/home/runner/work/user/user/packages/server/src/routes/executePasswordReset.ts","qualifiedName":"@proteinjs/user-server/executePasswordReset","type":{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/user-server/initiatePasswordReset","value":{"packageName":"@proteinjs/user-server","name":"initiatePasswordReset","filePath":"/home/runner/work/user/user/packages/server/src/routes/initiatePasswordReset.ts","qualifiedName":"@proteinjs/user-server/initiatePasswordReset","type":{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/user-server/login","value":{"packageName":"@proteinjs/user-server","name":"login","filePath":"/home/runner/work/user/user/packages/server/src/routes/login.ts","qualifiedName":"@proteinjs/user-server/login","type":{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/user-server/logout","value":{"packageName":"@proteinjs/user-server","name":"logout","filePath":"/home/runner/work/user/user/packages/server/src/routes/logout.ts","qualifiedName":"@proteinjs/user-server/logout","type":{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/user-server/validateResetPasswordToken","value":{"packageName":"@proteinjs/user-server","name":"validateResetPasswordToken","filePath":"/home/runner/work/user/user/packages/server/src/routes/validateResetPasswordToken.ts","qualifiedName":"@proteinjs/user-server/validateResetPasswordToken","type":{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":[{"packageName":"@proteinjs/server-api","name":"Route","filePath":null,"qualifiedName":"@proteinjs/server-api/Route","typeParameters":[],"directParents":null}]},"isExported":true,"isConst":true,"sourceType":0}},{"v":"@proteinjs/user-server/AccessInvite","value":{"packageName":"@proteinjs/user-server","name":"AccessInvite","filePath":"/home/runner/work/user/user/packages/server/src/services/AccessInvite.ts","qualifiedName":"@proteinjs/user-server/AccessInvite","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"serviceMetadata","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[{"name":"createAccessInvite","returnType":{"packageName":"","name":"Promise<CreateAccessInviteResponse>","filePath":null,"qualifiedName":"/Promise<CreateAccessInviteResponse>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"params","type":{"packageName":"@proteinjs/user","name":"CreateAccessInviteParams","filePath":null,"qualifiedName":"@proteinjs/user/CreateAccessInviteParams","typeParameters":null,"directParents":null}}]},{"name":"acceptAccessInvite","returnType":{"packageName":"","name":"Promise<T | undefined>","filePath":null,"qualifiedName":"/Promise<T | undefined>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"token","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]}],"typeParameters":["/T extends Record"],"directParentInterfaces":[{"packageName":"@proteinjs/user","name":"AccessInviteService","filePath":null,"qualifiedName":"@proteinjs/user/AccessInviteService","properties":[],"methods":[],"typeParameters":["/T"],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/user/AccessInviteService"},{"v":"@proteinjs/user-server/Roles","value":{"packageName":"@proteinjs/user-server","name":"Roles","filePath":"/home/runner/work/user/user/packages/server/src/services/Roles.ts","qualifiedName":"@proteinjs/user-server/Roles","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"serviceMetadata","type":{"packageName":"","name":"Service[\'serviceMetadata\']","filePath":null,"qualifiedName":"/Service[\'serviceMetadata\']","typeParameters":null,"directParents":null},"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[{"name":"grantRole","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"userId","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"role","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"revokeRole","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"userId","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"role","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"changeRole","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"userId","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"role","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"action","type":{"packageName":"","name":"\'grant\' | \'revoke\'","filePath":null,"qualifiedName":"/\'grant\' | \'revoke\'","typeParameters":null,"directParents":null}}]}],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/user","name":"RolesService","filePath":null,"qualifiedName":"@proteinjs/user/RolesService","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/user/RolesService"},{"v":"@proteinjs/user-server/DefaultInviteConfigFactory","value":{"packageName":"@proteinjs/user-server","name":"DefaultInviteConfigFactory","filePath":"/home/runner/work/user/user/packages/server/src/services/Signup.ts","qualifiedName":"@proteinjs/user-server/DefaultInviteConfigFactory","properties":[],"methods":[{"name":"getConfig","returnType":{"packageName":"@proteinjs/user-server","name":"InviteConfig","filePath":null,"qualifiedName":"@proteinjs/user-server/InviteConfig","typeParameters":null,"directParents":null},"isAsync":false,"isOptional":false,"isAbstract":true,"isStatic":false,"visibility":"public","parameters":[]}],"typeParameters":[],"directParents":[{"packageName":"@proteinjs/reflection","name":"Loadable","filePath":null,"qualifiedName":"@proteinjs/reflection/Loadable","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"sourceType":3}},{"v":"@proteinjs/reflection/Loadable"},{"v":"@proteinjs/user-server/Signup","value":{"packageName":"@proteinjs/user-server","name":"Signup","filePath":"/home/runner/work/user/user/packages/server/src/services/Signup.ts","qualifiedName":"@proteinjs/user-server/Signup","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"serviceMetadata","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[{"name":"createUser","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"user","type":{"packageName":"@proteinjs/user","name":"UserSignup","filePath":null,"qualifiedName":"@proteinjs/user/UserSignup","typeParameters":null,"directParents":null}},{"name":"token","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"sendInvite","returnType":{"packageName":"","name":"Promise<SendInviteResponse>","filePath":null,"qualifiedName":"/Promise<SendInviteResponse>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"email","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"revokeInvite","returnType":{"packageName":"","name":"Promise<void>","filePath":null,"qualifiedName":"/Promise<void>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"email","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"initializeSignup","returnType":{"packageName":"","name":"Promise<InitializeSignupResponse>","filePath":null,"qualifiedName":"/Promise<InitializeSignupResponse>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"inviteToken","type":{"packageName":"","name":"string | undefined","filePath":null,"qualifiedName":"/string | undefined","typeParameters":null,"directParents":null}}]},{"name":"lookupInvite","returnType":{"packageName":"","name":"Promise<InviteLookup>","filePath":null,"qualifiedName":"/Promise<InviteLookup>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"token","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]}],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/user","name":"SignupService","filePath":null,"qualifiedName":"@proteinjs/user/SignupService","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/user/SignupService"},{"v":"@proteinjs/user-server/UpdateUserInfo","value":{"packageName":"@proteinjs/user-server","name":"UpdateUserInfo","filePath":"/home/runner/work/user/user/packages/server/src/services/UpdateUserInfo.ts","qualifiedName":"@proteinjs/user-server/UpdateUserInfo","isAbstract":false,"isStatic":false,"visibility":"public","properties":[{"name":"AVATAR_SIZE","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"AVATAR_JPEG_QUALITY","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"MAX_AVATAR_PHOTO_BYTES","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"ACCEPTED_AVATAR_MIME_TYPES","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"MAX_AVATAR_EMOJI_LENGTH","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"EMOJI_PATTERN","type":null,"isOptional":false,"isAbstract":false,"isStatic":true,"visibility":"private"},{"name":"serviceMetadata","type":null,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public"}],"methods":[{"name":"updateName","returnType":{"packageName":"","name":"Promise<UpdatedUser>","filePath":null,"qualifiedName":"/Promise<UpdatedUser>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"name","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"updatePassword","returnType":{"packageName":"","name":"Promise<UpdatePasswordResponse>","filePath":null,"qualifiedName":"/Promise<UpdatePasswordResponse>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"currentPassword","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"newPassword","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"updateAvatarPhoto","returnType":{"packageName":"","name":"Promise<UpdatedUser>","filePath":null,"qualifiedName":"/Promise<UpdatedUser>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"fileData","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}},{"name":"mimeType","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"updateAvatarEmoji","returnType":{"packageName":"","name":"Promise<UpdatedUser>","filePath":null,"qualifiedName":"/Promise<UpdatedUser>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[{"name":"emoji","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"clearAvatar","returnType":{"packageName":"","name":"Promise<UpdatedUser>","filePath":null,"qualifiedName":"/Promise<UpdatedUser>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"public","parameters":[]},{"name":"setAvatar","returnType":{"packageName":"","name":"Promise<UpdatedUser>","filePath":null,"qualifiedName":"/Promise<UpdatedUser>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"avatar","type":{"packageName":"@proteinjs/user-server","name":"{ avatarEmoji: string | null; avatarFileId: string | null }","filePath":null,"qualifiedName":"@proteinjs/user-server/{ avatarEmoji: string | null; avatarFileId: string | null }","typeParameters":null,"directParents":null}}]},{"name":"saveUserInfo","returnType":{"packageName":"","name":"Promise<UpdatedUser>","filePath":null,"qualifiedName":"/Promise<UpdatedUser>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"changes","type":{"packageName":"","name":"Partial<User>","filePath":null,"qualifiedName":"/Partial<User>","typeParameters":null,"directParents":null}}]},{"name":"toAvatarJpeg","returnType":{"packageName":"","name":"Promise<Buffer>","filePath":null,"qualifiedName":"/Promise<Buffer>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"imageBytes","type":{"packageName":"","name":"Buffer","filePath":null,"qualifiedName":"/Buffer","typeParameters":null,"directParents":null}},{"name":"mimeType","type":{"packageName":"","name":"string","filePath":null,"qualifiedName":"/string","typeParameters":null,"directParents":null}}]},{"name":"decodeHeic","returnType":{"packageName":"","name":"Promise<sharp.Sharp>","filePath":null,"qualifiedName":"/Promise<sharp.Sharp>","typeParameters":null,"directParents":null},"isAsync":true,"isOptional":false,"isAbstract":false,"isStatic":false,"visibility":"private","parameters":[{"name":"imageBytes","type":{"packageName":"","name":"Buffer","filePath":null,"qualifiedName":"/Buffer","typeParameters":null,"directParents":null}}]}],"typeParameters":[],"directParentInterfaces":[{"packageName":"@proteinjs/user","name":"UpdateUserInfoService","filePath":null,"qualifiedName":"@proteinjs/user/UpdateUserInfoService","properties":[],"methods":[],"typeParameters":[],"directParents":[]}],"directParentClasses":[],"sourceType":2}},{"v":"@proteinjs/user/UpdateUserInfoService"}],"edges":[{"v":"@proteinjs/user-server/DbSessionStore","w":"express-session/Store","value":"extends class"},{"v":"@proteinjs/user-server/SocketIOSessionWatcher","w":"@proteinjs/db/TableWatcher","value":"implements interface"},{"v":"@proteinjs/user-server/userCache","w":"@proteinjs/server-api/SessionDataCache","value":"has type"},{"v":"@proteinjs/user-server/BackfillUserRolesArray","w":"@proteinjs/db/SourceRecordLoader","value":"implements interface"},{"v":"@proteinjs/user-server/avatar","w":"@proteinjs/server-api/Route","value":"has type"},{"v":"@proteinjs/user-server/devLogin","w":"@proteinjs/server-api/Route","value":"has type"},{"v":"@proteinjs/user-server/executePasswordReset","w":"@proteinjs/server-api/Route","value":"has type"},{"v":"@proteinjs/user-server/initiatePasswordReset","w":"@proteinjs/server-api/Route","value":"has type"},{"v":"@proteinjs/user-server/login","w":"@proteinjs/server-api/Route","value":"has type"},{"v":"@proteinjs/user-server/logout","w":"@proteinjs/server-api/Route","value":"has type"},{"v":"@proteinjs/user-server/validateResetPasswordToken","w":"@proteinjs/server-api/Route","value":"has type"},{"v":"@proteinjs/user-server/AccessInvite","w":"@proteinjs/user/AccessInviteService","value":"implements interface"},{"v":"@proteinjs/user-server/Roles","w":"@proteinjs/user/RolesService","value":"implements interface"},{"v":"@proteinjs/user-server/DefaultInviteConfigFactory","w":"@proteinjs/reflection/Loadable","value":"extends interface"},{"v":"@proteinjs/user-server/Signup","w":"@proteinjs/user/SignupService","value":"implements interface"},{"v":"@proteinjs/user-server/UpdateUserInfo","w":"@proteinjs/user/UpdateUserInfoService","value":"implements interface"}]}';
19
15
 
20
16
  /** Generate Source Links */
21
17
 
22
18
  import { DbSessionStore } from '../src/authentication/DbSessionStore';
23
19
  import { SocketIOSessionWatcher } from '../src/authentication/SocketIOSessionWatcher';
24
20
  import { userCache } from '../src/authorization/userCache';
21
+ import { BackfillUserRolesArray } from '../src/migrations/BackfillUserRolesArray';
22
+ import { avatar } from '../src/routes/avatar';
25
23
  import { devLogin } from '../src/routes/devLogin';
26
24
  import { executePasswordReset } from '../src/routes/executePasswordReset';
27
25
  import { initiatePasswordReset } from '../src/routes/initiatePasswordReset';
@@ -32,30 +30,28 @@ import { AccessInvite } from '../src/services/AccessInvite';
32
30
  import { Roles } from '../src/services/Roles';
33
31
  import { Signup } from '../src/services/Signup';
34
32
  import { UpdateUserInfo } from '../src/services/UpdateUserInfo';
35
- import { BackfillUserRolesArray } from '../src/migrations/BackfillUserRolesArray';
36
33
 
37
34
  const sourceLinks = {
38
- '@proteinjs/user-server/DbSessionStore': DbSessionStore,
39
- '@proteinjs/user-server/SocketIOSessionWatcher': SocketIOSessionWatcher,
40
- '@proteinjs/user-server/userCache': userCache,
41
- '@proteinjs/user-server/devLogin': devLogin,
42
- '@proteinjs/user-server/executePasswordReset': executePasswordReset,
43
- '@proteinjs/user-server/initiatePasswordReset': initiatePasswordReset,
44
- '@proteinjs/user-server/login': login,
45
- '@proteinjs/user-server/logout': logout,
46
- '@proteinjs/user-server/validateResetPasswordToken': validateResetPasswordToken,
47
- '@proteinjs/user-server/AccessInvite': AccessInvite,
48
- '@proteinjs/user-server/Roles': Roles,
49
- '@proteinjs/user-server/Signup': Signup,
50
- '@proteinjs/user-server/UpdateUserInfo': UpdateUserInfo,
51
- '@proteinjs/user-server/BackfillUserRolesArray': BackfillUserRolesArray,
35
+ '@proteinjs/user-server/DbSessionStore': DbSessionStore,
36
+ '@proteinjs/user-server/SocketIOSessionWatcher': SocketIOSessionWatcher,
37
+ '@proteinjs/user-server/userCache': userCache,
38
+ '@proteinjs/user-server/BackfillUserRolesArray': BackfillUserRolesArray,
39
+ '@proteinjs/user-server/avatar': avatar,
40
+ '@proteinjs/user-server/devLogin': devLogin,
41
+ '@proteinjs/user-server/executePasswordReset': executePasswordReset,
42
+ '@proteinjs/user-server/initiatePasswordReset': initiatePasswordReset,
43
+ '@proteinjs/user-server/login': login,
44
+ '@proteinjs/user-server/logout': logout,
45
+ '@proteinjs/user-server/validateResetPasswordToken': validateResetPasswordToken,
46
+ '@proteinjs/user-server/AccessInvite': AccessInvite,
47
+ '@proteinjs/user-server/Roles': Roles,
48
+ '@proteinjs/user-server/Signup': Signup,
49
+ '@proteinjs/user-server/UpdateUserInfo': UpdateUserInfo,
52
50
  };
53
51
 
54
-
55
52
  /** Load Source Graph and Links */
56
53
 
57
54
  import { SourceRepository } from '@proteinjs/reflection';
58
55
  SourceRepository.merge(sourceGraph, sourceLinks);
59
56
 
60
-
61
- export * from '../index';
57
+ export * from '../index';
package/jest.config.js CHANGED
@@ -1,10 +1,13 @@
1
1
  module.exports = {
2
2
  roots: ['<rootDir>/test'],
3
- setupFiles: ['<rootDir>/test/setup.js'],
4
3
  transform: {
5
4
  '^.+\\.tsx?$': 'ts-jest',
6
5
  },
7
6
  testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$',
8
7
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
9
8
  testEnvironment: 'node',
9
+ testTimeout: 60000,
10
+ setupFiles: ['./test/setup'],
11
+ globalSetup: './test/emulatorLock.globalSetup.js',
12
+ globalTeardown: './test/emulatorLock.globalTeardown.js',
10
13
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/user-server",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "User server components",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -23,18 +23,21 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@proteinjs/db": "^1.28.1",
26
+ "@proteinjs/db-file": "^1.4.5",
26
27
  "@proteinjs/email-server": "^1.3.0",
27
28
  "@proteinjs/logger": "^1.0.21",
28
29
  "@proteinjs/reflection": "^1.1.12",
29
30
  "@proteinjs/server": "^3.2.1",
30
31
  "@proteinjs/server-api": "^3.0.11",
31
- "@proteinjs/user": "^1.7.1",
32
+ "@proteinjs/user": "^1.8.0",
32
33
  "crypto-js": "4.0.0",
33
34
  "express-session": "1.17.1",
34
- "moment": "2.29.4"
35
+ "heic-decode": "^2.1.0",
36
+ "moment": "2.29.4",
37
+ "sharp": "0.33.4"
35
38
  },
36
39
  "devDependencies": {
37
- "@proteinjs/db-driver-spanner": "^1.14.4",
40
+ "@proteinjs/db-driver-spanner": "^1.16.0",
38
41
  "@proteinjs/reflection-build": "^1.4.10",
39
42
  "@types/crypto-js": "4.0.1",
40
43
  "@types/express-session": "1.17.0",
@@ -50,5 +53,6 @@
50
53
  "typescript": "5.2.2"
51
54
  },
52
55
  "main": "./dist/generated/index.js",
53
- "types": "./dist/generated/index.d.ts"
56
+ "types": "./dist/generated/index.d.ts",
57
+ "gitHead": "ca5406c5d08120454950abcb42a54bf77531b3e0"
54
58
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ambient types for `heic-decode` (ships no TypeScript types): WASM libheif + libde265, so it
3
+ * decodes HEVC-encoded HEIC — which sharp's prebuilt libvips cannot — on any platform.
4
+ */
5
+ declare module 'heic-decode' {
6
+ function decode(input: { buffer: Buffer | Uint8Array }): Promise<{
7
+ width: number;
8
+ height: number;
9
+ /** RGBA pixel data, `width * height * 4` bytes. */
10
+ data: Uint8ClampedArray;
11
+ }>;
12
+ export = decode;
13
+ }
@@ -0,0 +1,57 @@
1
+ import { Route } from '@proteinjs/server-api';
2
+ import { getDbAsSystem } from '@proteinjs/db';
3
+ import { tables, UserAuth, getScopedDbAsSystem } from '@proteinjs/user';
4
+ import { FileStorage, File, tables as fileTables } from '@proteinjs/db-file';
5
+ import { Logger } from '@proteinjs/logger';
6
+
7
+ const logger = new Logger({ name: 'avatarRoute' });
8
+
9
+ /**
10
+ * Shared avatar serving: `GET /avatar/:userId?v=<avatarFileId>` — any logged-in user can render
11
+ * any user's avatar photo (attribution in shared spaces). File rows are ScopedRecords and
12
+ * `GET /file/:id` reads through the OWNER's scope, so other users cannot fetch an avatar through
13
+ * it; this route is the deliberate, narrow door instead.
14
+ *
15
+ * System-read surface is exactly: the user row (for `avatarFileId`), that one file row (for the
16
+ * Content-Type), and its bytes. The file id is never taken from the request — only the id stored
17
+ * on the user row is served, so this can never read arbitrary files. `:userId` is a keyed lookup
18
+ * (no path/filesystem interpretation).
19
+ *
20
+ * Cache contract (mirrored by `avatarRoute` in @proteinjs/user routes.ts): the path is stable per
21
+ * user, so responses carry `max-age=86400, immutable`; clients bust with `?v=<avatarFileId>`,
22
+ * which changes on every photo update. Users with an emoji avatar or no avatar answer 404 — the
23
+ * client renders emoji/initials from the user record; this route serves photos only.
24
+ */
25
+ export const avatar: Route = {
26
+ path: '/avatar/:userId',
27
+ method: 'get',
28
+ onRequest: async (request, response): Promise<void> => {
29
+ if (!UserAuth.isLoggedIn()) {
30
+ response.status(401).send('User not logged in');
31
+ return;
32
+ }
33
+
34
+ try {
35
+ const userId = request.params.userId;
36
+ const user = await getDbAsSystem().get(tables.User, { id: userId });
37
+ if (!user || !user.avatarFileId) {
38
+ response.status(404).send('No avatar photo');
39
+ return;
40
+ }
41
+
42
+ const file = await getScopedDbAsSystem<File>().get(fileTables.File, { id: user.avatarFileId });
43
+ if (!file) {
44
+ response.status(404).send('No avatar photo');
45
+ return;
46
+ }
47
+
48
+ const fileDataBase64 = await new FileStorage().getFileData(file.id);
49
+ response.setHeader('Content-Type', file.type);
50
+ response.setHeader('Cache-Control', 'public, max-age=86400, immutable');
51
+ response.send(Buffer.from(fileDataBase64, 'base64'));
52
+ } catch (error: any) {
53
+ logger.error({ message: 'Failed to serve avatar', error });
54
+ response.status(500).send('Internal Server Error');
55
+ }
56
+ },
57
+ };
@@ -24,6 +24,11 @@ export const devLogin: Route = {
24
24
  }
25
25
 
26
26
  await new Promise((resolve) => request.login(email, resolve));
27
+ // Explicit save before redirecting: with a DB-backed session store, save-on-response-end
28
+ // races the redirected GET / — the follow-up request can read the session row before the
29
+ // write commits and render the login page (observed: first /dev/login load lands on /login,
30
+ // second succeeds). Awaiting the store write closes the race.
31
+ await new Promise((resolve) => request.session.save(resolve));
27
32
  logger.info({ message: 'Dev auto-login session established', obj: { email } });
28
33
  response.redirect('/');
29
34
  },
@@ -1,19 +1,36 @@
1
+ import sharp from 'sharp';
2
+ import heicDecode from 'heic-decode';
3
+ import sha256 from 'crypto-js/sha256';
1
4
  import { getDbAsSystem } from '@proteinjs/db';
2
- import { tables, UserRepo, UpdatePasswordResponse, UpdateUserInfoService } from '@proteinjs/user';
5
+ import { FileStorage } from '@proteinjs/db-file';
6
+ import { tables, User, UserRepo, UpdatePasswordResponse, UpdatedUser, UpdateUserInfoService } from '@proteinjs/user';
3
7
  import { EmailSender, getDefaultPasswordUpdatedEmailConfigFactory } from '@proteinjs/email-server';
4
- import sha256 from 'crypto-js/sha256';
5
8
 
6
9
  export class UpdateUserInfo implements UpdateUserInfoService {
10
+ /** Stored avatar photos are exactly this: cover-cropped square JPEG. */
11
+ private static readonly AVATAR_SIZE = 512;
12
+ private static readonly AVATAR_JPEG_QUALITY = 85;
13
+ private static readonly MAX_AVATAR_PHOTO_BYTES = 10 * 1024 * 1024;
14
+ private static readonly ACCEPTED_AVATAR_MIME_TYPES = [
15
+ 'image/jpeg',
16
+ 'image/png',
17
+ 'image/webp',
18
+ 'image/heic',
19
+ 'image/heif',
20
+ ];
21
+ /** One emoji (possibly a ZWJ/flag/keycap sequence) — a handful of code units, never a sentence. */
22
+ private static readonly MAX_AVATAR_EMOJI_LENGTH = 16;
23
+ /** Constructed (not a literal) because tsconfig targets es5; runs on Node, which supports `\p`. */
24
+ private static readonly EMOJI_PATTERN = new RegExp('[\\p{Extended_Pictographic}\\p{Regional_Indicator}\\u20E3]', 'u');
25
+
7
26
  public serviceMetadata = {
8
27
  auth: {
9
28
  allUsers: true,
10
29
  },
11
30
  };
12
31
 
13
- async updateName(name: string): Promise<void> {
14
- const db = getDbAsSystem();
15
- const user = new UserRepo().getUser();
16
- await db.update(tables.User, { name }, { id: user.id });
32
+ async updateName(name: string): Promise<UpdatedUser> {
33
+ return await this.saveUserInfo({ name });
17
34
  }
18
35
 
19
36
  async updatePassword(currentPassword: string, newPassword: string): Promise<UpdatePasswordResponse> {
@@ -54,13 +71,7 @@ export class UpdateUserInfo implements UpdateUserInfoService {
54
71
  // hash and store new password
55
72
  try {
56
73
  const hashedNewPassword = sha256(newPassword).toString();
57
- await db.update(
58
- tables.User,
59
- {
60
- password: hashedNewPassword,
61
- },
62
- { id: user.id }
63
- );
74
+ await this.saveUserInfo({ password: hashedNewPassword });
64
75
  } catch (error: any) {
65
76
  return {
66
77
  updated: false,
@@ -72,4 +83,104 @@ export class UpdateUserInfo implements UpdateUserInfoService {
72
83
  updated: true,
73
84
  };
74
85
  }
86
+
87
+ async updateAvatarPhoto(fileData: string, mimeType: string): Promise<UpdatedUser> {
88
+ if (!UpdateUserInfo.ACCEPTED_AVATAR_MIME_TYPES.includes(mimeType)) {
89
+ throw new Error(
90
+ `Unsupported avatar image type '${mimeType}'. Accepted: ${UpdateUserInfo.ACCEPTED_AVATAR_MIME_TYPES.join(', ')}`
91
+ );
92
+ }
93
+
94
+ const imageBytes = Buffer.from(fileData, 'base64');
95
+ if (imageBytes.length > UpdateUserInfo.MAX_AVATAR_PHOTO_BYTES) {
96
+ throw new Error(`Avatar photo is too large (max 10MB)`);
97
+ }
98
+
99
+ const jpeg = await this.toAvatarJpeg(imageBytes, mimeType);
100
+ const file = await new FileStorage().createFile(
101
+ { name: 'avatar.jpg', type: 'image/jpeg', size: jpeg.length },
102
+ jpeg.toString('base64')
103
+ );
104
+ return await this.setAvatar({ avatarEmoji: null, avatarFileId: file.id });
105
+ }
106
+
107
+ async updateAvatarEmoji(emoji: string): Promise<UpdatedUser> {
108
+ const trimmed = emoji.trim();
109
+ if (
110
+ !trimmed ||
111
+ trimmed.length > UpdateUserInfo.MAX_AVATAR_EMOJI_LENGTH ||
112
+ /\s/.test(trimmed) ||
113
+ !UpdateUserInfo.EMOJI_PATTERN.test(trimmed)
114
+ ) {
115
+ throw new Error(`Avatar emoji must be a single short emoji`);
116
+ }
117
+
118
+ return await this.setAvatar({ avatarEmoji: trimmed, avatarFileId: null });
119
+ }
120
+
121
+ async clearAvatar(): Promise<UpdatedUser> {
122
+ return await this.setAvatar({ avatarEmoji: null, avatarFileId: null });
123
+ }
124
+
125
+ /**
126
+ * The single door to the avatar columns — every mutation assigns BOTH, which is what enforces
127
+ * the exactly-one-active invariant (photo OR emoji, never both). Also the previous photo's
128
+ * cleanup point: an avatar file the user row no longer points to is unreachable (the /avatar
129
+ * route serves only the id on the user row), so it is deleted here, after the row moves off it.
130
+ */
131
+ private async setAvatar(avatar: { avatarEmoji: string | null; avatarFileId: string | null }): Promise<UpdatedUser> {
132
+ const userId = new UserRepo().getUser().id;
133
+ const previousAvatarFileId = (await getDbAsSystem().get(tables.User, { id: userId })).avatarFileId;
134
+ const updated = await this.saveUserInfo(avatar);
135
+ if (previousAvatarFileId && previousAvatarFileId !== avatar.avatarFileId) {
136
+ await new FileStorage().deleteFile(previousAvatarFileId);
137
+ }
138
+
139
+ return updated;
140
+ }
141
+
142
+ /**
143
+ * The one path every user-info mutation persists through: writes the changes AND refreshes the
144
+ * server session cache in the same stroke. userCache serializes the user into session data at
145
+ * request start; a bare DB write leaves every read through UserRepo in the same context — and
146
+ * long-lived contexts that keep their session data (sockets) — serving the STALE user until
147
+ * re-login (the rename wart). Fusing persist + refresh here means no future mutation can
148
+ * reintroduce that class of staleness.
149
+ */
150
+ private async saveUserInfo(changes: Partial<User>): Promise<UpdatedUser> {
151
+ const userRepo = new UserRepo();
152
+ const userId = userRepo.getUser().id;
153
+ const db = getDbAsSystem();
154
+ await db.update(tables.User, changes, { id: userId });
155
+ const updated = await db.get(tables.User, { id: userId });
156
+ delete (updated as Partial<User>).password; // the same password-less shape userCache caches
157
+ userRepo.setUser(updated);
158
+ return updated;
159
+ }
160
+
161
+ /**
162
+ * Minimal avatar pipeline: decode, auto-orient from EXIF, cover-crop square, re-encode JPEG.
163
+ * sharp drops metadata (EXIF orientation/GPS) on re-encode by default — the stored avatar
164
+ * carries no camera metadata. HEIC/HEIF (default iPhone camera format) is decoded with WASM
165
+ * libheif (`heic-decode`) because sharp's prebuilt libvips has no HEVC decoder. NOTE: this
166
+ * duplicates the HEIC-normalization approach of chat-server's FileTransformer, which this
167
+ * package cannot import (layering) — future extraction target: a shared image-transform seam.
168
+ */
169
+ private async toAvatarJpeg(imageBytes: Buffer, mimeType: string): Promise<Buffer> {
170
+ const pipeline =
171
+ mimeType === 'image/heic' || mimeType === 'image/heif'
172
+ ? await this.decodeHeic(imageBytes)
173
+ : sharp(imageBytes).rotate();
174
+ return await pipeline
175
+ .resize(UpdateUserInfo.AVATAR_SIZE, UpdateUserInfo.AVATAR_SIZE, { fit: 'cover' })
176
+ .jpeg({ quality: UpdateUserInfo.AVATAR_JPEG_QUALITY })
177
+ .toBuffer();
178
+ }
179
+
180
+ private async decodeHeic(imageBytes: Buffer): Promise<sharp.Sharp> {
181
+ const { width, height, data } = await heicDecode({ buffer: imageBytes });
182
+ return sharp(Buffer.from(data.buffer, data.byteOffset, data.byteLength), {
183
+ raw: { width, height, channels: 4 },
184
+ });
185
+ }
75
186
  }